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