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 "ranges.hh"
8 #include "stl.hh"
9 #include "stringsp.hh" // for strncasecmp
10 #include "view.hh"
11 #include <cstring> // for memcpy, memcmp
12 #include <cstdlib> // for atoi
13 #include <cctype> // for isspace
14 #include <memory>
15 
16 // TODO
17 // - Improve error handling
18 // - When an non-ogg file is passed, the entire file is scanned
19 // - Clean up this mess!
20 namespace openmsx {
21 
22 Frame::Frame(const th_ycbcr_buffer& yuv)
23 {
24  unsigned y_size = yuv[0].height * yuv[0].stride;
25  unsigned uv_size = yuv[1].height * yuv[1].stride;
26 
27  buffer[0] = yuv[0];
28  buffer[0].data = static_cast<unsigned char*>(
29  MemoryOps::mallocAligned(16, y_size));
30  buffer[1] = yuv[1];
31  buffer[1].data = static_cast<unsigned char*>(
32  MemoryOps::mallocAligned(16, uv_size));
33  buffer[2] = yuv[2];
34  buffer[2].data = static_cast<unsigned char*>(
35  MemoryOps::mallocAligned(16, uv_size));
36 }
37 
39 {
43 }
44 
45 
47  : cli(cli_)
48  , file(filename)
49 {
50  audioSerial = -1;
51  videoSerial = -1;
52  skeletonSerial = -1;
53  audioHeaders = 3;
54  keyFrame = size_t(-1);
55  currentSample = 0;
56  currentFrame = 1;
57  vorbisPos = 0;
58 
59  th_info ti;
60  th_comment tc;
61  th_setup_info* tsi = nullptr;
62 
63  th_info_init(&ti);
64  th_comment_init(&tc);
65  theora = nullptr;
66 
67  vorbis_info_init(&vi);
68  vorbis_comment_init(&vc);
69 
70  ogg_sync_init(&sync);
71 
72  state = PLAYING;
73  fileOffset = 0;
74  fileSize = file.getSize();
75 
76  ogg_page page;
77 
78  try {
79  while ((audioHeaders || !theora) && nextPage(&page)) {
80  int serial = ogg_page_serialno(&page);
81 
82  if (serial == audioSerial) {
83  vorbisHeaderPage(&page);
84  continue;
85  } else if (serial == videoSerial) {
86  theoraHeaderPage(&page, ti, tc, tsi);
87  continue;
88  } else if (serial == skeletonSerial) {
89  continue;
90  }
91 
92  if (!ogg_page_bos(&page)) {
93  if (videoSerial == -1) {
94  throw MSXException("No video track found");
95  }
96 
97  if (audioSerial == -1) {
98  throw MSXException("No audio track found");
99  }
100 
101  // This should be unreachable, right?
102  continue;
103  }
104 
105  ogg_stream_state stream;
106  ogg_packet packet;
107 
108  ogg_stream_init(&stream, serial);
109  ogg_stream_pagein(&stream, &page);
110  if (ogg_stream_packetout(&stream, &packet) <= 0) {
111  ogg_stream_clear(&stream);
112  throw MSXException("Invalid header");
113  }
114 
115  if (packet.bytes < 8) {
116  ogg_stream_clear(&stream);
117  throw MSXException("Header to small");
118  }
119 
120  if (memcmp(packet.packet, "\x01vorbis", 7) == 0) {
121  if (audioSerial != -1) {
122  ogg_stream_clear(&stream);
123  throw MSXException("Duplicate audio stream");
124  }
125 
126  audioSerial = serial;
127  ogg_stream_init(&vorbisStream, serial);
128 
129  vorbisHeaderPage(&page);
130 
131  } else if (memcmp(packet.packet, "\x80theora", 7) == 0) {
132  if (videoSerial != -1) {
133  ogg_stream_clear(&stream);
134  throw MSXException("Duplicate video stream");
135  }
136 
137  videoSerial = serial;
138  ogg_stream_init(&theoraStream, serial);
139 
140  theoraHeaderPage(&page, ti, tc, tsi);
141 
142  } else if (memcmp(packet.packet, "fishead", 8) == 0) {
143  skeletonSerial = serial;
144 
145  } else if (memcmp(packet.packet, "BBCD", 4) == 0) {
146  ogg_stream_clear(&stream);
147  throw MSXException("DIRAC not supported");
148 
149  } else if (memcmp(packet.packet, "\177FLAC", 5) == 0) {
150  ogg_stream_clear(&stream);
151  throw MSXException("FLAC not supported");
152 
153  } else {
154  ogg_stream_clear(&stream);
155  throw MSXException("Unknown stream in ogg file");
156  }
157 
158  ogg_stream_clear(&stream);
159  }
160 
161  if (videoSerial == -1) {
162  throw MSXException("No video track found");
163  }
164 
165  if (audioSerial == -1) {
166  throw MSXException("No audio track found");
167  }
168 
169  if (vi.channels != 2) {
170  throw MSXException("Audio must be stereo");
171  }
172 
173  if (ti.frame_width != 640 || ti.frame_height != 480) {
174  throw MSXException("Video must be size 640x480");
175  }
176 
177  if (ti.fps_numerator == 30000 && ti.fps_denominator == 1001) {
178  frameRate = 30;
179  } else if (ti.fps_numerator == 60000 && ti.fps_denominator == 1001) {
180  frameRate = 60;
181  } else {
182  throw MSXException("Video frame rate must be 59.94Hz or 29.97Hz");
183  }
184 
185  // FIXME: Support YUV444 before release
186  // It would be much better to use YUV444, however the existing
187  // captures are in YUV420 format. yuv2rgb will have to be
188  // updated too.
189  if (ti.pixel_fmt != TH_PF_420) {
190  throw MSXException("Video must be YUV420");
191  }
192  }
193  catch (MSXException&) {
194  th_setup_free(tsi);
195  th_info_clear(&ti);
196  th_comment_clear(&tc);
197  cleanup();
198  throw;
199  }
200 
201  th_setup_free(tsi);
202  th_info_clear(&ti);
203  th_comment_clear(&tc);
204 }
205 
206 void OggReader::cleanup()
207 {
208  if (audioHeaders == 0) {
209  vorbis_dsp_clear(&vd);
210  vorbis_block_clear(&vb);
211  }
212 
213  th_decode_free(theora);
214 
215  vorbis_info_clear(&vi);
216  vorbis_comment_clear(&vc);
217  if (audioSerial != -1) {
218  ogg_stream_clear(&vorbisStream);
219  }
220 
221  if (videoSerial != -1) {
222  ogg_stream_clear(&theoraStream);
223  }
224 
225  ogg_sync_clear(&sync);
226 }
227 
229 {
230  cleanup();
231 }
232 
238 void OggReader::vorbisFoundPosition()
239 {
240  auto last = vorbisPos;
241  for (auto& audioFrag : view::reverse(audioList)) {
242  last -= audioFrag->length;
243  audioFrag->position = last;
244  }
245 
246  // last is now the first vorbis audio decoded
247  if (last > currentSample) {
248  cli.printWarning("missing part of audio stream");
249  }
250 
251  if (vorbisPos > currentSample) {
252  currentSample = vorbisPos;
253  }
254 }
255 
256 
257 void OggReader::vorbisHeaderPage(ogg_page* page)
258 {
259  ogg_stream_pagein(&vorbisStream, page);
260 
261  while (true) {
262  ogg_packet packet;
263 
264  int res = ogg_stream_packetout(&vorbisStream, &packet);
265  if (res < 0) {
266  throw MSXException("error in vorbis stream");
267  }
268  if (res == 0) break;
269 
270  if (audioHeaders == 0) {
271  // ignore, we'll seek to the beginning again before playing
272  continue;
273  }
274 
275  if (packet.packetno <= 2) {
276  if (vorbis_synthesis_headerin(&vi, &vc, &packet) < 0) {
277  throw MSXException("invalid vorbis header");
278  }
279  --audioHeaders;
280  }
281 
282  if (packet.packetno == 2) {
283  vorbis_synthesis_init(&vd, &vi) ;
284  vorbis_block_init(&vd, &vb);
285  }
286  }
287 }
288 
289 void OggReader::theoraHeaderPage(ogg_page* page, th_info& ti, th_comment& tc,
290  th_setup_info*& tsi)
291 {
292  ogg_stream_pagein(&theoraStream, page);
293 
294  while (true) {
295  ogg_packet packet;
296 
297  int res = ogg_stream_packetout(&theoraStream, &packet);
298  if (res < 0) {
299  throw MSXException("error in vorbis stream");
300  }
301  if (res == 0) break;
302 
303  if (theora) {
304  // ignore, we'll seek to the beginning again before playing
305  continue;
306  }
307 
308  if (packet.packetno <= 2) {
309  res = th_decode_headerin(&ti, &tc, &tsi, &packet);
310  if (res <= 0) {
311  throw MSXException("invalid theora header");
312  }
313  }
314 
315  if (packet.packetno == 2) {
316  theora = th_decode_alloc(&ti, tsi);
317  readMetadata(tc);
318  granuleShift = ti.keyframe_granule_shift;
319  }
320  }
321 }
322 
323 void OggReader::readVorbis(ogg_packet* packet)
324 {
325  // deal with header packets
326  if (unlikely(packet->packetno <= 2)) {
327  return;
328  }
329 
330  if (state == FIND_LAST) {
331  if (packet->granulepos != -1) {
332  if ((currentSample == AudioFragment::UNKNOWN_POS) ||
333  (size_t(packet->granulepos) > currentSample)) {
334  currentSample = packet->granulepos;
335  }
336  }
337  return;
338 
339  } else if (state == FIND_FIRST) {
340  if (packet->granulepos != -1 &&
341  currentSample == AudioFragment::UNKNOWN_POS) {
342  currentSample = packet->granulepos;
343  }
344  return;
345 
346  } else if (state == FIND_KEYFRAME) {
347  // Not relevant for vorbis
348  return;
349  }
350 
351  // generate pcm
352  if (vorbis_synthesis(&vb, packet) != 0) {
353  return;
354  }
355 
356  vorbis_synthesis_blockin(&vd, &vb);
357 
358  float** pcm;
359  long decoded = vorbis_synthesis_pcmout(&vd, &pcm);
360  long pos = 0;
361 
362  while (pos < decoded) {
363  // Find memory to copy PCM into
364  if (recycleAudioList.empty()) {
365  auto audio = std::make_unique<AudioFragment>();
366  audio->length = 0;
367  recycleAudioList.push_back(std::move(audio));
368  }
369  auto& audio = recycleAudioList.front();
370  if (audio->length == 0) {
371  audio->position = vorbisPos;
372  } else {
373  // first element was already partially filled
374  }
375 
376  // Copy PCM
377  unsigned len = std::min<long>(decoded - pos,
378  AudioFragment::MAX_SAMPLES - audio->length);
379 
380  memcpy(audio->pcm[0] + audio->length, pcm[0] + pos,
381  len * sizeof(float));
382  memcpy(audio->pcm[1] + audio->length, pcm[1] + pos,
383  len * sizeof(float));
384 
385  audio->length += len;
386  pos += len;
387 
388  // Last packet or found position after seeking?
389  bool last = (decoded == pos && (packet->e_o_s ||
390  (vorbisPos == AudioFragment::UNKNOWN_POS &&
391  packet->granulepos != -1)));
392 
393  if (vorbisPos != AudioFragment::UNKNOWN_POS) {
394  vorbisPos += len;
395  currentSample += len;
396  }
397 
398  if (audio->length == AudioFragment::MAX_SAMPLES || last) {
399  audioList.push_back(recycleAudioList.pop_front());
400  }
401  }
402 
403  // The granulepos is the no. of samples since the begining of the
404  // stream. Only once per ogg page is this populated.
405  if (packet->granulepos != -1) {
406  if (vorbisPos == AudioFragment::UNKNOWN_POS) {
407  vorbisPos = packet->granulepos;
408  vorbisFoundPosition();
409  } else {
410  if (vorbisPos != size_t(packet->granulepos)) {
411  cli.printWarning(
412  "vorbis audio out of sync, expected ",
413  vorbisPos, ", got ", packet->granulepos);
414  vorbisPos = packet->granulepos;
415  }
416  }
417  }
418 
419  // done with PCM data
420  vorbis_synthesis_read(&vd, decoded);
421 }
422 
423 size_t OggReader::frameNo(ogg_packet* packet)
424 {
425  if (packet->granulepos == -1) {
426  return size_t(-1);
427  }
428 
429  size_t intra = packet->granulepos & ((size_t(1) << granuleShift) - 1);
430  size_t key = packet->granulepos >> granuleShift;
431  return key + intra;
432 }
433 
434 void OggReader::readMetadata(th_comment& tc)
435 {
436  char* metadata = nullptr;
437  for (int i = 0; i < tc.comments; ++i) {
438  if (!strncasecmp(tc.user_comments[i], "location=",
439  strlen("location="))) {
440  metadata = tc.user_comments[i] + strlen("location=");
441  break;
442  }
443  }
444  if (!metadata) {
445  return;
446  }
447 
448  // Maybe there is a better way of doing this parsing in C++
449  char* p = metadata;
450  while (p) {
451  while (isspace(*p)) {
452  p++;
453  }
454  if (strncasecmp(p, "chapter: ", 9) == 0) {
455  int chapter = atoi(p + 9);
456  p = strchr(p, ',');
457  if (!p) break;
458  ++p;
459  size_t frame = atol(p);
460  if (frame) {
461  chapters.emplace_back(chapter, frame);
462  }
463  } else if (strncasecmp(p, "stop: ", 6) == 0) {
464  size_t stopframe = atol(p + 6);
465  if (stopframe) {
466  stopFrames.push_back(stopframe);
467  }
468  }
469  p = strchr(p, '\n');
470  if (p) ++p;
471  }
472  ranges::sort(stopFrames);
473  ranges::sort(chapters, LessTupleElement<0>());
474 }
475 
476 void OggReader::readTheora(ogg_packet* packet)
477 {
478  if (th_packet_isheader(packet)) {
479  return;
480  }
481 
482  size_t frameno = frameNo(packet);
483 
484  // If we're seeking, we're only interested in packets with
485  // frame numbers
486  if ((state != PLAYING) && (frameno == size_t(-1))) {
487  return;
488  }
489 
490  if (state == FIND_LAST) {
491  if ((currentFrame == size_t(-1)) || (currentFrame < frameno)) {
492  currentFrame = frameno;
493  }
494  return;
495 
496  } else if (state == FIND_FIRST) {
497  if ((currentFrame == size_t(-1)) || (currentFrame > frameno)) {
498  currentFrame = frameno;
499  }
500  return;
501 
502  } else if (state == FIND_KEYFRAME) {
503  if (frameno < currentFrame) {
504  keyFrame = packet->granulepos >> granuleShift;
505  } else if (currentFrame == frameno) {
506  keyFrame = packet->granulepos >> granuleShift;
507  currentSample = 1;
508  } else if (frameno > currentFrame) {
509  currentSample = 1;
510  }
511  return;
512  }
513 
514  if ((keyFrame != size_t(-1)) && (frameno != size_t(-1)) &&
515  (frameno < keyFrame)) {
516  // We're reading before the keyframe, discard
517  return;
518  }
519 
520  if (packet->bytes == 0 && frameList.empty()) {
521  // No use passing empty packets (which represent dup frame)
522  // before we've read any frame.
523  return;
524  }
525 
526  keyFrame = size_t(-1);
527 
528  int rc = th_decode_packetin(theora, packet, nullptr);
529  switch (rc) {
530  case TH_DUPFRAME:
531  if (frameList.empty()) {
532  cli.printWarning("Theora error: dup frame encountered "
533  "without preceding frame");
534  } else {
535  frameList.back()->length++;
536  }
537  break;
538  case TH_EIMPL:
539  cli.printWarning("Theora error: not capable of reading this");
540  break;
541  case TH_EFAULT:
542  cli.printWarning("Theora error: API not used correctly");
543  break;
544  case TH_EBADPACKET:
545  cli.printWarning("Theora error: bad packet");
546  break;
547  case 0:
548  break;
549  default:
550  cli.printWarning("Theora error: unknown error ", rc);
551  break;
552  }
553 
554  if (rc) {
555  return;
556  }
557 
558  th_ycbcr_buffer yuv;
559 
560  if (th_decode_ycbcr_out(theora, yuv) != 0) {
561  return;
562  }
563 
564  if ((frameno != size_t(-1)) && (frameno < currentFrame)) {
565  return;
566  }
567 
568  currentFrame = frameno + 1;
569 
570  std::unique_ptr<Frame> frame;
571  if (recycleFrameList.empty()) {
572  frame = std::make_unique<Frame>(yuv);
573  } else {
574  frame = std::move(recycleFrameList.back());
575  recycleFrameList.pop_back();
576  }
577 
578  int y_size = yuv[0].height * yuv[0].stride;
579  int uv_size = yuv[1].height * yuv[1].stride;
580  memcpy(frame->buffer[0].data, yuv[0].data, y_size);
581  memcpy(frame->buffer[1].data, yuv[1].data, uv_size);
582  memcpy(frame->buffer[2].data, yuv[2].data, uv_size);
583 
584  // At lot of frames have framenumber -1, only some have the correct
585  // frame number. We continue counting from the previous known
586  // postion
587  Frame* last = frameList.empty() ? nullptr : frameList.back().get();
588  if (last && (last->no != size_t(-1))) {
589  if ((frameno != size_t(-1)) &&
590  (frameno != 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 == size_t(-1)) || (frame == keyFrame)) {
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
bool binary_search(ForwardRange &&range, const T &value)
Definition: ranges.hh:59
size_t getSize()
Returns the size of this file.
Definition: File.cc:103
static constexpr unsigned MAX_SAMPLES
Definition: OggReader.hh:23
#define unlikely(x)
Definition: likely.hh:15
vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:269
void freeAligned(void *)
Definition: MemoryOps.cc:281
bool seek(size_t frame, size_t sample)
Definition: OggReader.cc:936
size_t size(std::string_view utf8)
OggReader(const OggReader &)=delete
void getFrameNo(RawFrame &frame, size_t frameno)
Definition: OggReader.cc:614
static constexpr size_t UNKNOWN_POS
Definition: OggReader.hh:22
A video frame as output by the VDP scanline conversion unit, before any postprocessing filters are ap...
Definition: RawFrame.hh:25
void convert(const th_ycbcr_buffer &input, RawFrame &output)
Definition: yuv2rgb.cc:341
auto reverse(Range &&range)
Definition: view.hh:300
th_ycbcr_buffer buffer
Definition: OggReader.hh:34
constexpr const char *const filename
void * mallocAligned(size_t alignment, size_t size)
Definition: MemoryOps.cc:251
This class represents a filename.
Definition: Filename.hh:17
void sort(RandomAccessRange &&range)
Definition: ranges.hh:35
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
const AudioFragment * getAudio(size_t sample)
Definition: OggReader.cc:681
size_t getChapter(int chapterNo) const
Definition: OggReader.cc:972
void printWarning(std::string_view message)
Definition: CliComm.cc:10
bool stopFrame(size_t frame) const
Definition: OggReader.cc:967
unsigned getSampleRate() const
Definition: OggReader.hh:49
Frame(const th_ycbcr_buffer &yuv)
Definition: OggReader.cc:22
auto lower_bound(ForwardRange &&range, const T &value)
Definition: ranges.hh:71
void seek(size_t pos)
Move read/write pointer to the specified position.
Definition: File.cc:108
void read(void *buffer, size_t num)
Read from file.
Definition: File.cc:83