openMSX
OggReader.cc
Go to the documentation of this file.
1 #include "OggReader.hh"
2 #include "MSXException.hh"
3 #include "yuv2rgb.hh"
4 #include "likely.hh"
5 #include "CliComm.hh"
6 #include "MemoryOps.hh"
7 #include "one_of.hh"
8 #include "ranges.hh"
9 #include "stl.hh"
10 #include "stringsp.hh" // for strncasecmp
11 #include "view.hh"
12 #include <cstring> // for memcpy, memcmp
13 #include <cstdlib> // for atoi
14 #include <cctype> // for isspace
15 #include <memory>
16 
17 // TODO
18 // - Improve error handling
19 // - When an non-ogg file is passed, the entire file is scanned
20 // - Clean up this mess!
21 namespace openmsx {
22 
23 Frame::Frame(const th_ycbcr_buffer& yuv)
24 {
25  unsigned y_size = yuv[0].height * yuv[0].stride;
26  unsigned uv_size = yuv[1].height * yuv[1].stride;
27 
28  buffer[0] = yuv[0];
29  buffer[0].data = static_cast<unsigned char*>(
30  MemoryOps::mallocAligned(16, y_size));
31  buffer[1] = yuv[1];
32  buffer[1].data = static_cast<unsigned char*>(
33  MemoryOps::mallocAligned(16, uv_size));
34  buffer[2] = yuv[2];
35  buffer[2].data = static_cast<unsigned char*>(
36  MemoryOps::mallocAligned(16, uv_size));
37 }
38 
40 {
44 }
45 
46 
48  : cli(cli_)
49  , file(filename)
50 {
51  audioSerial = -1;
52  videoSerial = -1;
53  skeletonSerial = -1;
54  audioHeaders = 3;
55  keyFrame = size_t(-1);
56  currentSample = 0;
57  currentFrame = 1;
58  vorbisPos = 0;
59 
60  th_info ti;
61  th_comment tc;
62  th_setup_info* tsi = nullptr;
63 
64  th_info_init(&ti);
65  th_comment_init(&tc);
66  theora = nullptr;
67 
68  vorbis_info_init(&vi);
69  vorbis_comment_init(&vc);
70 
71  ogg_sync_init(&sync);
72 
73  state = PLAYING;
74  fileOffset = 0;
75  fileSize = file.getSize();
76 
77  ogg_page page;
78 
79  try {
80  while ((audioHeaders || !theora) && nextPage(&page)) {
81  int serial = ogg_page_serialno(&page);
82 
83  if (serial == audioSerial) {
84  vorbisHeaderPage(&page);
85  continue;
86  } else if (serial == videoSerial) {
87  theoraHeaderPage(&page, ti, tc, tsi);
88  continue;
89  } else if (serial == skeletonSerial) {
90  continue;
91  }
92 
93  if (!ogg_page_bos(&page)) {
94  if (videoSerial == -1) {
95  throw MSXException("No video track found");
96  }
97 
98  if (audioSerial == -1) {
99  throw MSXException("No audio track found");
100  }
101 
102  // This should be unreachable, right?
103  continue;
104  }
105 
106  ogg_stream_state stream;
107  ogg_packet packet;
108 
109  ogg_stream_init(&stream, serial);
110  ogg_stream_pagein(&stream, &page);
111  if (ogg_stream_packetout(&stream, &packet) <= 0) {
112  ogg_stream_clear(&stream);
113  throw MSXException("Invalid header");
114  }
115 
116  if (packet.bytes < 8) {
117  ogg_stream_clear(&stream);
118  throw MSXException("Header to small");
119  }
120 
121  if (memcmp(packet.packet, "\x01vorbis", 7) == 0) {
122  if (audioSerial != -1) {
123  ogg_stream_clear(&stream);
124  throw MSXException("Duplicate audio stream");
125  }
126 
127  audioSerial = serial;
128  ogg_stream_init(&vorbisStream, serial);
129 
130  vorbisHeaderPage(&page);
131 
132  } else if (memcmp(packet.packet, "\x80theora", 7) == 0) {
133  if (videoSerial != -1) {
134  ogg_stream_clear(&stream);
135  throw MSXException("Duplicate video stream");
136  }
137 
138  videoSerial = serial;
139  ogg_stream_init(&theoraStream, serial);
140 
141  theoraHeaderPage(&page, ti, tc, tsi);
142 
143  } else if (memcmp(packet.packet, "fishead", 8) == 0) {
144  skeletonSerial = serial;
145 
146  } else if (memcmp(packet.packet, "BBCD", 4) == 0) {
147  ogg_stream_clear(&stream);
148  throw MSXException("DIRAC not supported");
149 
150  } else if (memcmp(packet.packet, "\177FLAC", 5) == 0) {
151  ogg_stream_clear(&stream);
152  throw MSXException("FLAC not supported");
153 
154  } else {
155  ogg_stream_clear(&stream);
156  throw MSXException("Unknown stream in ogg file");
157  }
158 
159  ogg_stream_clear(&stream);
160  }
161 
162  if (videoSerial == -1) {
163  throw MSXException("No video track found");
164  }
165 
166  if (audioSerial == -1) {
167  throw MSXException("No audio track found");
168  }
169 
170  if (vi.channels != 2) {
171  throw MSXException("Audio must be stereo");
172  }
173 
174  if (ti.frame_width != 640 || ti.frame_height != 480) {
175  throw MSXException("Video must be size 640x480");
176  }
177 
178  if (ti.fps_numerator == 30000 && ti.fps_denominator == 1001) {
179  frameRate = 30;
180  } else if (ti.fps_numerator == 60000 && ti.fps_denominator == 1001) {
181  frameRate = 60;
182  } else {
183  throw MSXException("Video frame rate must be 59.94Hz or 29.97Hz");
184  }
185 
186  // FIXME: Support YUV444 before release
187  // It would be much better to use YUV444, however the existing
188  // captures are in YUV420 format. yuv2rgb will have to be
189  // updated too.
190  if (ti.pixel_fmt != TH_PF_420) {
191  throw MSXException("Video must be YUV420");
192  }
193  }
194  catch (MSXException&) {
195  th_setup_free(tsi);
196  th_info_clear(&ti);
197  th_comment_clear(&tc);
198  cleanup();
199  throw;
200  }
201 
202  th_setup_free(tsi);
203  th_info_clear(&ti);
204  th_comment_clear(&tc);
205 }
206 
207 void OggReader::cleanup()
208 {
209  if (audioHeaders == 0) {
210  vorbis_dsp_clear(&vd);
211  vorbis_block_clear(&vb);
212  }
213 
214  th_decode_free(theora);
215 
216  vorbis_info_clear(&vi);
217  vorbis_comment_clear(&vc);
218  if (audioSerial != -1) {
219  ogg_stream_clear(&vorbisStream);
220  }
221 
222  if (videoSerial != -1) {
223  ogg_stream_clear(&theoraStream);
224  }
225 
226  ogg_sync_clear(&sync);
227 }
228 
230 {
231  cleanup();
232 }
233 
239 void OggReader::vorbisFoundPosition()
240 {
241  auto last = vorbisPos;
242  for (auto& audioFrag : view::reverse(audioList)) {
243  last -= audioFrag->length;
244  audioFrag->position = last;
245  }
246 
247  // last is now the first vorbis audio decoded
248  if (last > currentSample) {
249  cli.printWarning("missing part of audio stream");
250  }
251 
252  if (vorbisPos > currentSample) {
253  currentSample = vorbisPos;
254  }
255 }
256 
257 
258 void OggReader::vorbisHeaderPage(ogg_page* page)
259 {
260  ogg_stream_pagein(&vorbisStream, page);
261 
262  while (true) {
263  ogg_packet packet;
264 
265  int res = ogg_stream_packetout(&vorbisStream, &packet);
266  if (res < 0) {
267  throw MSXException("error in vorbis stream");
268  }
269  if (res == 0) break;
270 
271  if (audioHeaders == 0) {
272  // ignore, we'll seek to the beginning again before playing
273  continue;
274  }
275 
276  if (packet.packetno <= 2) {
277  if (vorbis_synthesis_headerin(&vi, &vc, &packet) < 0) {
278  throw MSXException("invalid vorbis header");
279  }
280  --audioHeaders;
281  }
282 
283  if (packet.packetno == 2) {
284  vorbis_synthesis_init(&vd, &vi) ;
285  vorbis_block_init(&vd, &vb);
286  }
287  }
288 }
289 
290 void OggReader::theoraHeaderPage(ogg_page* page, th_info& ti, th_comment& tc,
291  th_setup_info*& tsi)
292 {
293  ogg_stream_pagein(&theoraStream, page);
294 
295  while (true) {
296  ogg_packet packet;
297 
298  int res = ogg_stream_packetout(&theoraStream, &packet);
299  if (res < 0) {
300  throw MSXException("error in vorbis stream");
301  }
302  if (res == 0) break;
303 
304  if (theora) {
305  // ignore, we'll seek to the beginning again before playing
306  continue;
307  }
308 
309  if (packet.packetno <= 2) {
310  res = th_decode_headerin(&ti, &tc, &tsi, &packet);
311  if (res <= 0) {
312  throw MSXException("invalid theora header");
313  }
314  }
315 
316  if (packet.packetno == 2) {
317  theora = th_decode_alloc(&ti, tsi);
318  readMetadata(tc);
319  granuleShift = ti.keyframe_granule_shift;
320  }
321  }
322 }
323 
324 void OggReader::readVorbis(ogg_packet* packet)
325 {
326  // deal with header packets
327  if (unlikely(packet->packetno <= 2)) {
328  return;
329  }
330 
331  if (state == FIND_LAST) {
332  if (packet->granulepos != -1) {
333  if ((currentSample == AudioFragment::UNKNOWN_POS) ||
334  (size_t(packet->granulepos) > currentSample)) {
335  currentSample = packet->granulepos;
336  }
337  }
338  return;
339 
340  } else if (state == FIND_FIRST) {
341  if (packet->granulepos != -1 &&
342  currentSample == AudioFragment::UNKNOWN_POS) {
343  currentSample = packet->granulepos;
344  }
345  return;
346 
347  } else if (state == FIND_KEYFRAME) {
348  // Not relevant for vorbis
349  return;
350  }
351 
352  // generate pcm
353  if (vorbis_synthesis(&vb, packet) != 0) {
354  return;
355  }
356 
357  vorbis_synthesis_blockin(&vd, &vb);
358 
359  float** pcm;
360  long decoded = vorbis_synthesis_pcmout(&vd, &pcm);
361  long pos = 0;
362 
363  while (pos < decoded) {
364  // Find memory to copy PCM into
365  if (recycleAudioList.empty()) {
366  auto audio = std::make_unique<AudioFragment>();
367  audio->length = 0;
368  recycleAudioList.push_back(std::move(audio));
369  }
370  auto& audio = recycleAudioList.front();
371  if (audio->length == 0) {
372  audio->position = vorbisPos;
373  } else {
374  // first element was already partially filled
375  }
376 
377  // Copy PCM
378  unsigned len = std::min<long>(decoded - pos,
379  AudioFragment::MAX_SAMPLES - audio->length);
380 
381  memcpy(audio->pcm[0] + audio->length, pcm[0] + pos,
382  len * sizeof(float));
383  memcpy(audio->pcm[1] + audio->length, pcm[1] + pos,
384  len * sizeof(float));
385 
386  audio->length += len;
387  pos += len;
388 
389  // Last packet or found position after seeking?
390  bool last = (decoded == pos && (packet->e_o_s ||
391  (vorbisPos == AudioFragment::UNKNOWN_POS &&
392  packet->granulepos != -1)));
393 
394  if (vorbisPos != AudioFragment::UNKNOWN_POS) {
395  vorbisPos += len;
396  currentSample += len;
397  }
398 
399  if (audio->length == AudioFragment::MAX_SAMPLES || last) {
400  audioList.push_back(recycleAudioList.pop_front());
401  }
402  }
403 
404  // The granulepos is the no. of samples since the begining of the
405  // stream. Only once per ogg page is this populated.
406  if (packet->granulepos != -1) {
407  if (vorbisPos == AudioFragment::UNKNOWN_POS) {
408  vorbisPos = packet->granulepos;
409  vorbisFoundPosition();
410  } else {
411  if (vorbisPos != size_t(packet->granulepos)) {
412  cli.printWarning(
413  "vorbis audio out of sync, expected ",
414  vorbisPos, ", got ", packet->granulepos);
415  vorbisPos = packet->granulepos;
416  }
417  }
418  }
419 
420  // done with PCM data
421  vorbis_synthesis_read(&vd, decoded);
422 }
423 
424 size_t OggReader::frameNo(ogg_packet* packet) const
425 {
426  if (packet->granulepos == -1) {
427  return size_t(-1);
428  }
429 
430  size_t intra = packet->granulepos & ((size_t(1) << granuleShift) - 1);
431  size_t key = packet->granulepos >> granuleShift;
432  return key + intra;
433 }
434 
435 void OggReader::readMetadata(th_comment& tc)
436 {
437  char* metadata = nullptr;
438  for (int i = 0; i < tc.comments; ++i) {
439  if (!strncasecmp(tc.user_comments[i], "location=",
440  strlen("location="))) {
441  metadata = tc.user_comments[i] + strlen("location=");
442  break;
443  }
444  }
445  if (!metadata) {
446  return;
447  }
448 
449  // Maybe there is a better way of doing this parsing in C++
450  char* p = metadata;
451  while (p) {
452  while (isspace(*p)) {
453  p++;
454  }
455  if (strncasecmp(p, "chapter: ", 9) == 0) {
456  int chapter = atoi(p + 9);
457  p = strchr(p, ',');
458  if (!p) break;
459  ++p;
460  size_t frame = atol(p);
461  if (frame) {
462  chapters.emplace_back(chapter, frame);
463  }
464  } else if (strncasecmp(p, "stop: ", 6) == 0) {
465  size_t stopframe = atol(p + 6);
466  if (stopframe) {
467  stopFrames.push_back(stopframe);
468  }
469  }
470  p = strchr(p, '\n');
471  if (p) ++p;
472  }
473  ranges::sort(stopFrames);
474  ranges::sort(chapters, LessTupleElement<0>());
475 }
476 
477 void OggReader::readTheora(ogg_packet* packet)
478 {
479  if (th_packet_isheader(packet)) {
480  return;
481  }
482 
483  size_t frameno = frameNo(packet);
484 
485  // If we're seeking, we're only interested in packets with
486  // frame numbers
487  if ((state != PLAYING) && (frameno == size_t(-1))) {
488  return;
489  }
490 
491  if (state == FIND_LAST) {
492  if ((currentFrame == size_t(-1)) || (currentFrame < frameno)) {
493  currentFrame = frameno;
494  }
495  return;
496 
497  } else if (state == FIND_FIRST) {
498  if ((currentFrame == size_t(-1)) || (currentFrame > frameno)) {
499  currentFrame = frameno;
500  }
501  return;
502 
503  } else if (state == FIND_KEYFRAME) {
504  if (frameno < currentFrame) {
505  keyFrame = packet->granulepos >> granuleShift;
506  } else if (currentFrame == frameno) {
507  keyFrame = packet->granulepos >> granuleShift;
508  currentSample = 1;
509  } else if (frameno > currentFrame) {
510  currentSample = 1;
511  }
512  return;
513  }
514 
515  if ((keyFrame != size_t(-1)) && (frameno != size_t(-1)) &&
516  (frameno < keyFrame)) {
517  // We're reading before the keyframe, discard
518  return;
519  }
520 
521  if (packet->bytes == 0 && frameList.empty()) {
522  // No use passing empty packets (which represent dup frame)
523  // before we've read any frame.
524  return;
525  }
526 
527  keyFrame = size_t(-1);
528 
529  int rc = th_decode_packetin(theora, packet, nullptr);
530  switch (rc) {
531  case TH_DUPFRAME:
532  if (frameList.empty()) {
533  cli.printWarning("Theora error: dup frame encountered "
534  "without preceding frame");
535  } else {
536  frameList.back()->length++;
537  }
538  break;
539  case TH_EIMPL:
540  cli.printWarning("Theora error: not capable of reading this");
541  break;
542  case TH_EFAULT:
543  cli.printWarning("Theora error: API not used correctly");
544  break;
545  case TH_EBADPACKET:
546  cli.printWarning("Theora error: bad packet");
547  break;
548  case 0:
549  break;
550  default:
551  cli.printWarning("Theora error: unknown error ", rc);
552  break;
553  }
554 
555  if (rc) {
556  return;
557  }
558 
559  th_ycbcr_buffer yuv;
560 
561  if (th_decode_ycbcr_out(theora, yuv) != 0) {
562  return;
563  }
564 
565  if ((frameno != size_t(-1)) && (frameno < currentFrame)) {
566  return;
567  }
568 
569  currentFrame = frameno + 1;
570 
571  std::unique_ptr<Frame> frame;
572  if (recycleFrameList.empty()) {
573  frame = std::make_unique<Frame>(yuv);
574  } else {
575  frame = std::move(recycleFrameList.back());
576  recycleFrameList.pop_back();
577  }
578 
579  int y_size = yuv[0].height * yuv[0].stride;
580  int uv_size = yuv[1].height * yuv[1].stride;
581  memcpy(frame->buffer[0].data, yuv[0].data, y_size);
582  memcpy(frame->buffer[1].data, yuv[1].data, uv_size);
583  memcpy(frame->buffer[2].data, yuv[2].data, uv_size);
584 
585  // At lot of frames have framenumber -1, only some have the correct
586  // frame number. We continue counting from the previous known
587  // postion
588  Frame* last = frameList.empty() ? nullptr : frameList.back().get();
589  if (last && (last->no != size_t(-1))) {
590  if (frameno != one_of(size_t(-1), last->no + last->length)) {
591  cli.printWarning("Theora frame sequence wrong");
592  } else {
593  frameno = last->no + last->length;
594  }
595  }
596 
597  frame->no = frameno;
598  frame->length = 1;
599 
600  // We may read some frames before we encounter one with a proper
601  // frame number. When we do, go back and populate the frame
602  // numbers correctly
603  if (!frameList.empty() && (frameno != size_t(-1)) &&
604  (frameList[0]->no == size_t(-1))) {
605  for (auto& frm : view::reverse(frameList)) {
606  frameno -= frm->length;
607  frm->no = frameno;
608  }
609  }
610 
611  frameList.push_back(std::move(frame));
612 }
613 
614 void OggReader::getFrameNo(RawFrame& rawFrame, size_t frameno)
615 {
616  Frame* frame;
617  while (true) {
618  // If there are no frames or the frames we have read
619  // does not include a proper frame number, just read
620  // more data
621  if (frameList.empty() || (frameList[0]->no == size_t(-1))) {
622  if (!nextPacket()) {
623  return;
624  }
625  continue;
626  }
627 
628  // Remove unneeded frames. Note that at 60Hz the odd and
629  // and even frame are displayed during still, so we can
630  // only throw away the one two frames ago
631  while (frameList.size() >= 3 && frameList[2]->no <= frameno) {
632  recycleFrameList.push_back(frameList.pop_front());
633  }
634 
635  if (!frameList.empty() && frameList[0]->no > frameno) {
636  // we're missing frames!
637  frame = frameList[0].get();
638  cli.printWarning(
639  "Cannot find frame ", frameno, " using ",
640  frame->no, " instead");
641  break;
642  }
643 
644  if ((frameList.size() >= 2) &&
645  ((frameno >= frameList[0]->no) &&
646  (frameno < frameList[1]->no))) {
647  frame = frameList[0].get();
648  break;
649  }
650 
651  if ((frameList.size() >= 3) &&
652  ((frameno >= frameList[1]->no) &&
653  (frameno < frameList[2]->no))) {
654  frame = frameList[1].get();
655  break;
656  }
657 
658  // Sanity check, should not happen
659  if (frameList.size() > (size_t(2) << granuleShift)) {
660  // We've got more than twice as many frames
661  // as the maximum distance between key frames.
662  cli.printWarning("Cannot find frame ", frameno);
663  return;
664  }
665 
666  // ..add read some new ones
667  if (!nextPacket()) {
668  return;
669  }
670  }
671 
672  yuv2rgb::convert(frame->buffer, rawFrame);
673 }
674 
675 void OggReader::recycleAudio(std::unique_ptr<AudioFragment> audio)
676 {
677  audio->length = 0;
678  recycleAudioList.push_back(std::move(audio));
679 }
680 
681 const AudioFragment* OggReader::getAudio(size_t sample)
682 {
683  // Read while position is unknown
684  while (audioList.empty() ||
685  audioList.front()->position == AudioFragment::UNKNOWN_POS) {
686  if (!nextPacket()) {
687  return nullptr;
688  }
689  }
690 
691  auto it = begin(audioList);
692  while (true) {
693  auto& audio = *it;
694  if (audio->position + audio->length + getSampleRate() <= sample) {
695  // Dispose if this, more than 1 second old
696  recycleAudio(std::move(*it));
697  it = audioList.erase(it);
698  } else if (audio->position + audio->length <= sample) {
699  ++it;
700  } else {
701  if (audio->position <= sample) {
702  return audio.get();
703  } else {
704  // gone too far?
705  return nullptr;
706  }
707  }
708 
709  // read more if we're at the end of the list
710  if (it == end(audioList)) {
711  size_t size = audioList.size();
712  while (size == audioList.size()) {
713  if (!nextPacket()) {
714  return nullptr;
715  }
716  }
717 
718  // reset the iterator to not point to the end
719  it = begin(audioList);
720  }
721  }
722 }
723 
724 bool OggReader::nextPacket()
725 {
726  ogg_packet packet;
727  ogg_page page;
728 
729  while (true) {
730  int ret = ogg_stream_packetout(&vorbisStream, &packet);
731  if (ret == 1) {
732  readVorbis(&packet);
733  return true;
734  } else if (ret == -1) {
735  // recoverable error
736  continue;
737  }
738 
739  ret = ogg_stream_packetout(&theoraStream, &packet);
740  if (ret == 1) {
741  readTheora(&packet);
742  return true;
743  } else if (ret == -1) {
744  // recoverable error
745  continue;
746  }
747 
748  if (!nextPage(&page)) {
749  return false;
750  }
751 
752  int serial = ogg_page_serialno(&page);
753  if (serial == audioSerial) {
754  if (ogg_stream_pagein(&vorbisStream, &page)) {
755  cli.printWarning("Failed to submit vorbis page");
756  }
757  } else if (serial == videoSerial) {
758  if (ogg_stream_pagein(&theoraStream, &page)) {
759  cli.printWarning("Failed to submit theora page");
760  }
761  } else if (serial != skeletonSerial) {
762  cli.printWarning("Unexpected stream with serial ",
763  serial, " in ogg file");
764  }
765  }
766 }
767 
768 
769 bool OggReader::nextPage(ogg_page* page)
770 {
771  constexpr size_t CHUNK = 4096;
772 
773  int ret;
774  while ((ret = ogg_sync_pageseek(&sync, page)) <= 0) {
775  if (ret < 0) {
776  //throw MSXException("Invalid Ogg file");
777  }
778 
779  size_t chunk;
780  if (fileSize - fileOffset >= CHUNK) {
781  chunk = CHUNK;
782  } else if (fileOffset < fileSize) {
783  chunk = fileSize - fileOffset;
784  } else {
785  return false;
786  }
787 
788  char* buffer = ogg_sync_buffer(&sync, long(chunk));
789  file.read(buffer, chunk);
790  fileOffset += chunk;
791 
792  if (ogg_sync_wrote(&sync, long(chunk)) == -1) {
793  cli.printWarning("Internal error: ogg_sync_wrote failed");
794  }
795  }
796 
797  return true;
798 }
799 
800 size_t OggReader::bisection(
801  size_t frame, size_t sample,
802  size_t maxOffset, size_t maxSamples, size_t maxFrames)
803 {
804  // Defined to be a power-of-two such that the arthmetic can be done faster.
805  // Note that the sample-number is in the range of: 1..(44100*60*60)
806  constexpr uint64_t SHIFT = 0x20000000ull;
807 
808  uint64_t offsetA = 0, offsetB = maxOffset;
809  uint64_t sampleA = 0, sampleB = maxSamples;
810  uint64_t frameA = 1, frameB = maxFrames;
811 
812  while (true) {
813  uint64_t ratio = (frame - frameA) * SHIFT / (frameB - frameA);
814  if (ratio < 5) {
815  return offsetA;
816  }
817 
818  uint64_t frameOffset = ratio * (offsetB - offsetA) / SHIFT + offsetA;
819  ratio = (sample - sampleA) * SHIFT / (sampleB - sampleA);
820  if (ratio < 5) {
821  return offsetA;
822  }
823  uint64_t sampleOffset = ratio * (offsetB - offsetA) / SHIFT + offsetA;
824  auto offset = std::min(sampleOffset, frameOffset);
825 
826  file.seek(offset);
827  fileOffset = offset;
828  ogg_sync_reset(&sync);
829  currentFrame = size_t(-1);
830  currentSample = AudioFragment::UNKNOWN_POS;
831  state = FIND_FIRST;
832 
833  while (((currentFrame == size_t(-1)) ||
834  (currentSample == AudioFragment::UNKNOWN_POS)) &&
835  nextPacket()) {
836  // continue reading
837  }
838 
839  state = PLAYING;
840 
841  if (currentSample > sample || currentFrame > frame) {
842  offsetB = offset;
843  sampleB = currentSample;
844  frameB = currentFrame;
845  } else if (currentSample + getSampleRate() < sample &&
846  currentFrame + 64 < frame) {
847  offsetA = offset;
848  sampleA = currentSample;
849  frameA = currentFrame;
850  } else {
851  return offset;
852  }
853  }
854 }
855 
856 size_t OggReader::findOffset(size_t frame, size_t sample)
857 {
858  constexpr size_t STEP = 32 * 1024;
859 
860  // first calculate total length in bytes, samples and frames
861 
862  // The file might have changed since we last requested its size,
863  // we assume that only data will be added to it and the ogg streams
864  // are exactly as before
865  fileSize = file.getSize();
866  auto offset = fileSize - 1;
867 
868  while (offset > 0) {
869  if (offset > STEP) {
870  offset -= STEP;
871  } else {
872  offset = 0;
873  }
874 
875  file.seek(offset);
876  fileOffset = offset;
877  ogg_sync_reset(&sync);
878  currentFrame = size_t(-1);
879  currentSample = AudioFragment::UNKNOWN_POS;
880  state = FIND_LAST;
881 
882  while (nextPacket()) {
883  // continue reading
884  }
885 
886  state = PLAYING;
887 
888  if ((currentFrame != size_t(-1)) &&
889  (currentSample != AudioFragment::UNKNOWN_POS)) {
890  break;
891  }
892  }
893 
894  totalFrames = currentFrame;
895 
896  // If we're close to beginning, don't bother searching for it,
897  // just start at the beginning (arbitrary boundary of 1 second).
898  if (sample < getSampleRate() || frame <= 30) {
899  keyFrame = 1;
900  return 0;
901  }
902 
903  auto maxOffset = offset;
904  auto maxSamples = currentSample;
905  auto maxFrames = currentFrame;
906 
907  if ((sample > maxSamples) || (frame > maxFrames)) {
908  sample = maxSamples;
909  frame = maxFrames;
910  }
911 
912  offset = bisection(frame, sample, maxOffset, maxSamples, maxFrames);
913 
914  // Find key frame
915  file.seek(offset);
916  fileOffset = offset;
917  ogg_sync_reset(&sync);
918  currentFrame = frame;
919  currentSample = 0;
920  keyFrame = size_t(-1);
921  state = FIND_KEYFRAME;
922 
923  while (currentSample == 0 && nextPacket()) {
924  // continue reading
925  }
926 
927  state = PLAYING;
928 
929  if (keyFrame == one_of(size_t(-1), frame)) {
930  return offset;
931  }
932 
933  return bisection(keyFrame, sample, maxOffset, maxSamples, maxFrames);
934 }
935 
936 bool OggReader::seek(size_t frame, size_t samples)
937 {
938  // Remove all queued frames
939  recycleFrameList.insert(end(recycleFrameList),
940  std::move_iterator(begin(frameList)),
941  std::move_iterator(end (frameList)));
942  frameList.clear();
943 
944  // Remove all queued audio
945  if (!recycleAudioList.empty()) {
946  recycleAudioList.front()->length = 0;
947  }
948  for (auto& a : audioList) {
949  recycleAudio(std::move(a));
950  }
951  audioList.clear();
952 
953  fileOffset = findOffset(frame, samples);
954  file.seek(fileOffset);
955 
956  ogg_sync_reset(&sync);
957 
958  vorbisPos = AudioFragment::UNKNOWN_POS;
959  currentFrame = frame;
960  currentSample = samples;
961 
962  vorbis_synthesis_restart(&vd);
963 
964  return true;
965 }
966 
967 bool OggReader::stopFrame(size_t frame) const
968 {
969  return ranges::binary_search(stopFrames, frame);
970 }
971 
972 size_t OggReader::getChapter(int chapterNo) const
973 {
974  auto it = ranges::lower_bound(chapters, chapterNo, LessTupleElement<0>());
975  return ((it != end(chapters)) && (it->first == chapterNo))
976  ? it->second : 0;
977 }
978 
979 } // namespace openmsx
one_of.hh
OggReader.hh
openmsx::OggReader::getFrameNo
void getFrameNo(RawFrame &frame, size_t frameno)
Definition: OggReader.cc:614
MemoryOps.hh
gl::min
vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:274
unlikely
#define unlikely(x)
Definition: likely.hh:15
openmsx::MemoryOps::freeAligned
void freeAligned(void *)
Definition: MemoryOps.cc:281
openmsx::File::getSize
size_t getSize()
Returns the size of this file.
Definition: File.cc:103
ranges::sort
void sort(RandomAccessRange &&range)
Definition: ranges.hh:35
openmsx::OggReader::~OggReader
~OggReader()
Definition: OggReader.cc:229
openmsx::AudioFragment
Definition: OggReader.hh:20
utf8::unchecked::size
size_t size(std::string_view utf8)
Definition: utf8_unchecked.hh:227
ranges::lower_bound
auto lower_bound(ForwardRange &&range, const T &value)
Definition: ranges.hh:71
ranges.hh
openmsx::OggReader::stopFrame
bool stopFrame(size_t frame) const
Definition: OggReader.cc:967
MSXException.hh
openmsx::MSXException
Definition: MSXException.hh:9
openmsx::Frame::no
size_t no
Definition: OggReader.hh:35
likely.hh
openmsx::CliComm::printWarning
void printWarning(std::string_view message)
Definition: CliComm.cc:10
openmsx::OggReader::OggReader
OggReader(const OggReader &)=delete
openmsx::Frame::buffer
th_ycbcr_buffer buffer
Definition: OggReader.hh:34
openmsx::MemoryOps::mallocAligned
void * mallocAligned(size_t alignment, size_t size)
Definition: MemoryOps.cc:251
one_of
Definition: one_of.hh:7
yuv2rgb.hh
openmsx::filename
constexpr const char *const filename
Definition: FirmwareSwitch.cc:10
openmsx::OggReader::getChapter
size_t getChapter(int chapterNo) const
Definition: OggReader.cc:972
openmsx::OggReader::getAudio
const AudioFragment * getAudio(size_t sample)
Definition: OggReader.cc:681
ranges::binary_search
bool binary_search(ForwardRange &&range, const T &value)
Definition: ranges.hh:59
openmsx::yuv2rgb::convert
void convert(const th_ycbcr_buffer &input, RawFrame &output)
Definition: yuv2rgb.cc:341
view.hh
openmsx::OggReader::getSampleRate
unsigned getSampleRate() const
Definition: OggReader.hh:49
openmsx::RawFrame
A video frame as output by the VDP scanline conversion unit, before any postprocessing filters are ap...
Definition: RawFrame.hh:25
openmsx::OggReader::seek
bool seek(size_t frame, size_t sample)
Definition: OggReader.cc:936
openmsx::AudioFragment::MAX_SAMPLES
static constexpr unsigned MAX_SAMPLES
Definition: OggReader.hh:23
CmpTupleElement
Definition: stl.hh:28
openmsx::File::seek
void seek(size_t pos)
Move read/write pointer to the specified position.
Definition: File.cc:108
openmsx::Frame::~Frame
~Frame()
Definition: OggReader.cc:39
openmsx::CliComm
Definition: CliComm.hh:10
stringsp.hh
openmsx::File::read
void read(void *buffer, size_t num)
Read from file.
Definition: File.cc:83
stl.hh
openmsx::Frame::Frame
Frame(const th_ycbcr_buffer &yuv)
Definition: OggReader.cc:23
openmsx::AudioFragment::UNKNOWN_POS
static constexpr size_t UNKNOWN_POS
Definition: OggReader.hh:22
view::reverse
auto reverse(Range &&range)
Definition: view.hh:300
CliComm.hh
openmsx::Filename
This class represents a filename.
Definition: Filename.hh:17
openmsx
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
openmsx::Frame
Definition: OggReader.hh:29