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