OXYGENHUB
◆ main openmsx / src / laserdisc / LaserdiscPlayer.cc
31.0 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "LaserdiscPlayer.hh"
2
3
#include "CassettePlayer.hh"
4
#include "CassettePort.hh"
5
#include "CommandController.hh"
6
#include "CommandException.hh"
7
#include "DeviceConfig.hh"
8
#include "Display.hh"
9
#include "EventDistributor.hh"
10
#include "FileContext.hh"
11
#include "GlobalSettings.hh"
12
#include "HardwareConfig.hh"
13
#include "LDRenderer.hh"
14
#include "MSXCliComm.hh"
15
#include "MSXMotherBoard.hh"
16
#include "PioneerLDControl.hh"
17
#include "Reactor.hh"
18
#include "RendererFactory.hh"
19
#include "ReverseManager.hh"
20
#include "XMLElement.hh"
21
22
#include "Math.hh"
23
#include "narrow.hh"
24
#include "one_of.hh"
25
26
#include <cstdint>
27
#include <cstdlib>
28
#include <iostream>
29
#include <memory>
30
31
namespace openmsx {
32
33
static std::string_view getLaserDiscPlayerName()
34
{
35
	return "laserdiscplayer";
36
}
37
38
// Command
39
40
LaserdiscPlayer::Command::Command(
41
		CommandController& commandController_,
42
		StateChangeDistributor& stateChangeDistributor_,
43
		Scheduler& scheduler_)
44
	: RecordedCommand(commandController_, stateChangeDistributor_,
45
			  scheduler_, getLaserDiscPlayerName())
46
{
47
}
48
49
void LaserdiscPlayer::Command::execute(
50
	std::span<const TclObject> tokens, TclObject& result, EmuTime time)
51
{
52
	auto& laserdiscPlayer = OUTER(LaserdiscPlayer, laserdiscCommand);
53
	if (tokens.size() == 1) {
54
		// Returning Tcl lists here, similar to the disk commands in
55
		// DiskChanger
56
		result.addListElement(tmpStrCat(getName(), ':'),
57
		                      laserdiscPlayer.getImageName().getResolved());
58
	} else if (tokens[1] == "eject") {
59
		checkNumArgs(tokens, 2, Prefix{2}, nullptr);
60
		result = "Ejecting laserdisc.";
61
		laserdiscPlayer.eject(time);
62
	} else if (tokens[1] == "insert") {
63
		checkNumArgs(tokens, 3, "filename");
64
		try {
65
			result = "Changing laserdisc.";
66
			laserdiscPlayer.setImageName(std::string(tokens[2].getString()), time);
67
		} catch (MSXException& e) {
68
			throw CommandException(std::move(e).getMessage());
69
		}
70
	} else {
71
		throw SyntaxError();
72
	}
73
}
74
75
std::string LaserdiscPlayer::Command::help(std::span<const TclObject> tokens) const
76
{
77
	if (tokens.size() >= 2) {
78
		if (tokens[1] == "insert") {
79
			return "Inserts the specified laserdisc image into "
80
			       "the laserdisc player.";
81
		} else if (tokens[1] == "eject") {
82
			return "Eject the laserdisc.";
83
		}
84
	}
85
	return "laserdiscplayer insert <filename> "
86
	       ": insert a (different) laserdisc image\n"
87
	       "laserdiscplayer eject             "
88
	       ": eject the laserdisc\n";
89
}
90
91
void LaserdiscPlayer::Command::tabCompletion(std::vector<std::string>& tokens) const
92
{
93
	if (tokens.size() == 2) {
94
		using namespace std::literals;
95
		static constexpr std::array extra = {"eject"sv, "insert"sv};
96
		completeString(tokens, extra);
97
	} else if (tokens.size() == 3 && tokens[1] == "insert") {
98
		completeFileName(tokens, userFileContext());
99
	}
100
}
101
102
// LaserdiscPlayer
103
104
static constexpr unsigned DUMMY_INPUT_RATE = 44100; // actual rate depends on .ogg file
105
106
LaserdiscPlayer::LaserdiscPlayer(
107
		HardwareConfig& hwConf, PioneerLDControl& ldControl_)
108
	: ResampledSoundDevice(hwConf.getMotherBoard(), getLaserDiscPlayerName(),
109
	                       "Laserdisc Player", 1, DUMMY_INPUT_RATE, true)
110
	, syncAck (hwConf.getMotherBoard().getScheduler())
111
	, syncOdd (hwConf.getMotherBoard().getScheduler())
112
	, syncEven(hwConf.getMotherBoard().getScheduler())
113
	, motherBoard(hwConf.getMotherBoard())
114
	, ldControl(ldControl_)
115
	, laserdiscCommand(motherBoard.getCommandController(),
116
		           motherBoard.getStateChangeDistributor(),
117
		           motherBoard.getScheduler())
118
	, autoRunSetting(
119
		motherBoard.getCommandController(), "autorunlaserdisc",
120
		"automatically try to run Laserdisc", true)
121
	, loadingIndicator(
122
		motherBoard.getReactor().getGlobalSettings().getThrottleManager())
123
{
124
	motherBoard.getCassettePort().setLaserdiscPlayer(this);
125
126
	Reactor& reactor = motherBoard.getReactor();
127
	reactor.getDisplay().attach(*this);
128
129
	createRenderer();
130
	reactor.getEventDistributor().registerEventListener(EventType::BOOT, *this);
131
	scheduleDisplayStart(getCurrentTime());
132
133
	static XMLElement* xml = [] {
134
		auto& doc = XMLDocument::getStaticDocument();
135
		auto* result = doc.allocateElement(std::string(getLaserDiscPlayerName()).c_str());
136
		result->setFirstChild(doc.allocateElement("sound"))
137
		      ->setFirstChild(doc.allocateElement("volume", "30000"));
138
		return result;
139
	}();
140
	registerSound(DeviceConfig(hwConf, *xml));
141
142
	motherBoard.registerMediaProvider(getLaserDiscPlayerName(), *this);
143
	motherBoard.getMSXCliComm().update(CliComm::UpdateType::HARDWARE, getLaserDiscPlayerName(), "add");
144
}
145
146
LaserdiscPlayer::~LaserdiscPlayer()
147
{
148
	unregisterSound();
149
	Reactor& reactor = motherBoard.getReactor();
150
	reactor.getDisplay().detach(*this);
151
	reactor.getEventDistributor().unregisterEventListener(EventType::BOOT, *this);
152
	motherBoard.unregisterMediaProvider(*this);
153
	motherBoard.getMSXCliComm().update(CliComm::UpdateType::HARDWARE, getLaserDiscPlayerName(), "remove");
154
}
155
156
std::string LaserdiscPlayer::getStateString() const
157
{
158
	switch (playerState) {
159
		using enum PlayerState;
160
		case STOPPED:     return "stopped";
161
		case PLAYING:     return "playing";
162
		case MULTI_SPEED: return "multispeed";
163
		case PAUSED:      return "paused";
164
		case STILL:       return "still";
165
	}
166
	UNREACHABLE;
167
}
168
169
void LaserdiscPlayer::getMediaInfo(TclObject& result)
170
{
171
	result.addDictKeyValues("target", getImageName().getResolved(),
172
	                        "state", getStateString());
173
}
174
175
void LaserdiscPlayer::setMedia(const TclObject& info, EmuTime time)
176
{
177
	auto target = info.getOptionalDictValue(TclObject("target"));
178
	if (!target) return;
179
180
	if (auto trgt = target->getString(); trgt.empty()) {
181
		eject(time);
182
	} else {
183
		setImageName(std::string(trgt), time);
184
	}
185
}
186
187
void LaserdiscPlayer::scheduleDisplayStart(EmuTime time)
188
{
189
	Clock<60000, 1001> frameClock(time);
190
	// The video is 29.97Hz, however we need to do vblank processing
191
	// at the full 59.94Hz
192
	syncOdd .setSyncPoint(frameClock + 1);
193
	syncEven.setSyncPoint(frameClock + 2);
194
}
195
196
// The protocol used to communicate over the cable for commands to the
197
// laserdisc player is the NEC infrared protocol with minor deviations:
198
// 1) The leader pulse and space is a little shorter.
199
// 2) The remote does not send NEC repeats; full NEC codes are repeated
200
//    after 20ms. The main unit does not understand NEC repeats.
201
// 3) No carrier modulation is done over the ext protocol.
202
//
203
// My Laserdisc player is an Pioneer LD-700 which has a remote called
204
// the CU-700. This is much like the CU-CLD106 which is described
205
// here: http://lirc.sourceforge.net/remotes/pioneer/CU-CLD106
206
// The codes and protocol are exactly the same.
207
void LaserdiscPlayer::extControl(bool bit, EmuTime time)
208
{
209
	if (remoteLastBit == bit) return;
210
	remoteLastBit = bit;
211
212
	// The tolerance here is based on actual measurements of an LD-700
213
	EmuDuration duration = time - remoteLastEdge;
214
	remoteLastEdge = time;
215
	unsigned usec = duration.getTicksAt(1000000); // microseconds
216
217
	switch (remoteState) {
218
	using enum RemoteState;
219
	case IDLE:
220
		if (bit) {
221
			remoteBits = remoteBitNr = 0;
222
			remoteState = HEADER_PULSE;
223
		}
224
		break;
225
	case HEADER_PULSE:
226
		if (5800 <= usec && usec < 11200) {
227
			remoteState = NEC_HEADER_SPACE;
228
		} else {
229
			remoteState = IDLE;
230
		}
231
		break;
232
	case NEC_HEADER_SPACE:
233
		if (3400 <= usec && usec < 6200) {
234
			remoteState = NEC_BITS_PULSE;
235
		} else {
236
			remoteState = IDLE;
237
		}
238
		break;
239
	case NEC_BITS_PULSE:
240
		if (usec >= 380 && usec < 1070) {
241
			remoteState = NEC_BITS_SPACE;
242
		} else {
243
			remoteState = IDLE;
244
		}
245
		break;
246
	case NEC_BITS_SPACE:
247
		if (1260 <= usec && usec < 4720) {
248
			// bit 1
249
			remoteBits |= 1 << remoteBitNr;
250
		} else if (usec < 300 || usec >= 1065) {
251
			// error
252
			remoteState = IDLE;
253
			break;
254
		}
255
256
		// since it does not matter how long the trailing pulse
257
		// is, we process the button here. Note that real hardware
258
		// needs the trailing pulse to be at least 200µs
259
		if (++remoteBitNr == 32) {
260
			auto custom      = narrow_cast<byte>(( remoteBits >>  0) & 0xff);
261
			auto customCompl = narrow_cast<byte>((~remoteBits >>  8) & 0xff);
262
			auto code        = narrow_cast<byte>(( remoteBits >> 16) & 0xff);
263
			auto codeCompl   = narrow_cast<byte>((~remoteBits >> 24) & 0xff);
264
			if (custom == customCompl &&
265
			    custom == 0xa8 &&
266
			    code == codeCompl) {
267
				submitRemote(RemoteProtocol::NEC, code);
268
			}
269
			remoteState = IDLE;
270
		} else {
271
			remoteState = NEC_BITS_PULSE;
272
		}
273
274
		break;
275
	}
276
}
277
278
void LaserdiscPlayer::submitRemote(RemoteProtocol protocol, uint8_t code)
279
{
280
	// The END command for seeking/waiting acknowledges repeats,
281
	// Esh's Aurunmilla needs play as well.
282
	if (protocol != remoteProtocol || code != remoteCode ||
283
	    (protocol == RemoteProtocol::NEC && (code == one_of(0x42u, 0x17u)))) {
284
		remoteProtocol = protocol;
285
		remoteCode = code;
286
		remoteVblanksBack = 0;
287
		remoteExecuteDelayed = true;
288
	} else {
289
		// remote ignored
290
		remoteVblanksBack = 0;
291
		remoteExecuteDelayed = false;
292
	}
293
}
294
295
const RawFrame* LaserdiscPlayer::getRawFrame() const
296
{
297
	return renderer->getRawFrame();
298
}
299
300
void LaserdiscPlayer::setAck(EmuTime time, int wait)
301
{
302
	// activate ACK for 'wait' milliseconds
303
	syncAck.removeSyncPoint();
304
	syncAck.setSyncPoint(time + EmuDuration::msec(wait));
305
	ack = true;
306
}
307
308
void LaserdiscPlayer::remoteButtonNEC(uint8_t code, EmuTime time)
309
{
310
#ifdef DEBUG
311
	std::string f;
312
	switch (code) {
313
	case 0x47: f = "C+"; break;	// Increase playing speed
314
	case 0x46: f = "C-"; break;	// Decrease playing speed
315
	case 0x43: f = "D+"; break;	// Show Frame# & Chapter# OSD
316
	case 0x4b: f = "L+"; break;	// right
317
	case 0x49: f = "L-"; break;	// left
318
	case 0x4a: f = "L@"; break;	// stereo
319
	case 0x58: f = "M+"; break;	// multi speed forwards
320
	case 0x55: f = "M-"; break;	// multi speed backwards
321
	case 0x17: f = "P+"; break;	// play
322
	case 0x16: f = "P@"; break;	// stop
323
	case 0x18: f = "P/"; break;	// pause
324
	case 0x54: f = "S+"; break;	// frame step forward
325
	case 0x50: f = "S-"; break;	// frame step backwards
326
	case 0x45: f = "X+"; break;	// clear
327
	case 0x41: f = 'F'; break;	// seek frame
328
	case 0x40: f = 'C'; break;	// seek chapter
329
	case 0x42: f = "END"; break;	// done seek frame/chapter
330
	case 0x00: f = '0'; break;
331
	case 0x01: f = '1'; break;
332
	case 0x02: f = '2'; break;
333
	case 0x03: f = '3'; break;
334
	case 0x04: f = '4'; break;
335
	case 0x05: f = '5'; break;
336
	case 0x06: f = '6'; break;
337
	case 0x07: f = '7'; break;
338
	case 0x08: f = '8'; break;
339
	case 0x09: f = '9'; break;
340
	case 0x5f: f = "WAIT FRAME"; break;
341
342
	case 0x53: // previous chapter
343
	case 0x52: // next chapter
344
	default: break;
345
	}
346
347
	if (!f.empty()) {
348
		std::cerr << "LaserdiscPlayer::remote " << f << '\n';
349
	} else {
350
		std::cerr << "LaserdiscPlayer::remote unknown " << std::hex << code << '\n';
351
	}
352
#endif
353
	// When not playing the following buttons work
354
	// 0x17: start playing (ack sent)
355
	// 0x16: eject (no ack)
356
	// 0x49, 0x4a, 0x4b (ack sent)
357
	// if 0x49 is a repeat then no ACK is sent
358
	// if 0x49 is followed by 0x4a then ACK is sent
359
	if (code == one_of(0x49u, 0x4au, 0x4bu)) {
360
		updateStream(time);
361
362
		switch (code) {
363
		case 0x4b: // L+ (both channels play the left channel)
364
			stereoMode = StereoMode::LEFT;
365
			break;
366
		case 0x49: // L- (both channels play the right channel)
367
			stereoMode = StereoMode::RIGHT;
368
			break;
369
		case 0x4a: // L@ (normal stereo)
370
			stereoMode = StereoMode::STEREO;
371
			break;
372
		}
373
374
		setAck(time, 46);
375
	} else if (playerState == PlayerState::STOPPED) {
376
		switch (code) {
377
		case 0x16: // P@
378
			motherBoard.getMSXCliComm().printWarning(
379
				"ejecting laserdisc");
380
			eject(time);
381
			break;
382
		case 0x17: // P+
383
			play(time);
384
			break;
385
		}
386
387
		// During playing, playing will be acked if not repeated
388
		// within less than 115ms
389
	} else {
390
		// TODO: while seeking, only a small subset of buttons work
391
		bool nonSeekAck = true;
392
393
		switch (code) {
394
		using enum SeekState;
395
		case 0x5f:
396
			seekState = WAIT;
397
			seekNum = 0;
398
			stillOnWaitFrame = false;
399
			nonSeekAck = false;
400
			break;
401
		case 0x41:
402
			seekState = FRAME;
403
			seekNum = 0;
404
			break;
405
		case 0x40:
406
			seekState = CHAPTER;
407
			seekNum = 0;
408
			nonSeekAck = video->getChapter(0) != 0;
409
			break;
410
		case 0x00:
411
		case 0x01:
412
		case 0x02:
413
		case 0x03:
414
		case 0x04:
415
		case 0x05:
416
		case 0x06:
417
		case 0x07:
418
		case 0x08:
419
		case 0x09:
420
			seekNum = seekNum * 10 + code;
421
			break;
422
		case 0x42:
423
			switch (seekState) {
424
			case FRAME:
425
				seekState = NONE;
426
				seekFrame(seekNum % 100000, time);
427
				nonSeekAck = false;
428
				break;
429
			case CHAPTER:
430
				seekState = NONE;
431
				seekChapter(seekNum % 100, time);
432
				nonSeekAck = false;
433
				break;
434
			case WAIT:
435
				seekState = NONE;
436
				waitFrame = seekNum % 100000;
437
				if (waitFrame >= 101 && waitFrame < 200) {
438
					auto frame = video->getChapter(
439
						int(waitFrame - 100));
440
					if (frame) waitFrame = frame;
441
				}
442
				break;
443
			default:
444
				seekState = NONE;
445
				break;
446
			}
447
			break;
448
		case 0x45: // Clear "X+"
449
			if (seekState != NONE && seekNum != 0) {
450
				seekNum = 0;
451
			} else {
452
				seekState = NONE;
453
				seekNum = 0;
454
			}
455
			waitFrame = 0;
456
			break;
457
		case 0x18: // P/
458
			pause(time);
459
			nonSeekAck = false;
460
			break;
461
		case 0x17: // P+
462
			play(time);
463
			nonSeekAck = false;
464
			break;
465
		case 0x16: // P@ (stop/eject)
466
			stop(time);
467
			nonSeekAck = false;
468
			break;
469
		case 0xff:
470
			nonSeekAck = false;
471
			seekState = NONE;
472
			break;
473
		case 0x54: // S+ (frame step forward)
474
			if (seekState == WAIT) {
475
				stillOnWaitFrame = true;
476
			} else {
477
				stepFrame(true);
478
			}
479
			break;
480
		case 0x50: // S- (frame step backwards)
481
			stepFrame(false);
482
			break;
483
		case 0x55: // M- (multi-speed backwards)
484
			// Not supported
485
			motherBoard.getMSXCliComm().printWarning(
486
				"The Laserdisc player received a command to "
487
				"play backwards (M-). This is currently not "
488
				"supported.");
489
			nonSeekAck = false;
490
			break;
491
		case 0x58: // M+ (multi-speed forwards)
492
			playerState = PlayerState::MULTI_SPEED;
493
			setFrameStep();
494
			break;
495
		case 0x46: // C- (play slower)
496
			if (playingSpeed >= SPEED_STEP1) {
497
				playingSpeed--;
498
				frameStep = 1;	// TODO: is this correct?
499
			}
500
			break;
501
		case 0x47: // C+ (play faster)
502
			if (playingSpeed <= SPEED_X2) {
503
				playingSpeed++;
504
				frameStep = 1;	// TODO: is this correct?
505
			}
506
			break;
507
		default:
508
			motherBoard.getMSXCliComm().printWarning(
509
				"The Laserdisc player received an unknown "
510
				"command 0x", hex_string<2>(code));
511
			nonSeekAck = false;
512
			break;
513
		}
514
515
		if (nonSeekAck) {
516
			// All ACKs for operations which do not
517
			// require seeking
518
			setAck(time, 46);
519
		}
520
	}
521
}
522
523
void LaserdiscPlayer::execSyncAck(EmuTime time)
524
{
525
	updateStream(time);
526
527
	if (seeking && playerState == PlayerState::PLAYING) {
528
		sampleClock.advance(time);
529
	}
530
531
	ack = false;
532
	seeking = false;
533
}
534
535
void LaserdiscPlayer::execSyncFrame(EmuTime time, bool odd)
536
{
537
	updateStream(time);
538
539
	if (!odd || (video && video->getFrameRate() == 60)) {
540
		if ((playerState != PlayerState::STOPPED) &&
541
		    (currentFrame > video->getFrames())) {
542
			playerState = PlayerState::STOPPED;
543
		}
544
545
		if (auto* rawFrame = renderer->getRawFrame()) {
546
			renderer->frameStart(time);
547
548
			if (isVideoOutputAvailable(time)) {
549
				assert(video);
550
				auto frame = currentFrame;
551
				if (video->getFrameRate() == 60) {
552
					frame *= 2;
553
					if (odd) frame--;
554
				}
555
556
				video->getFrameNo(*rawFrame, frame);
557
558
				if (!odd) {
559
					nextFrame(time);
560
				}
561
			} else {
562
				renderer->drawBlank(0, 128, 196);
563
			}
564
			renderer->frameEnd();
565
		}
566
567
		// Update throttling
568
		loadingIndicator.update(seeking || sampleReads > 500);
569
		sampleReads = 0;
570
571
		if (!odd) {
572
			scheduleDisplayStart(time);
573
		}
574
	}
575
576
	// Processing of the remote control happens at each frame
577
	// (even and odd, so at 59.94Hz)
578
	if (remoteProtocol == RemoteProtocol::NEC) {
579
		if (remoteExecuteDelayed) {
580
			remoteButtonNEC(remoteCode, time);
581
		}
582
583
		if (++remoteVblanksBack > 6) {
584
			remoteProtocol = RemoteProtocol::NONE;
585
		}
586
	}
587
	remoteExecuteDelayed = false;
588
}
589
590
void LaserdiscPlayer::setFrameStep()
591
{
592
	switch (playingSpeed) {
593
	case SPEED_X3:
594
	case SPEED_X2:
595
	case SPEED_X1:
596
		frameStep = 1;
597
		break;
598
	case SPEED_1IN2:
599
		frameStep = 2;
600
		break;
601
	case SPEED_1IN4:
602
		frameStep = 4;
603
		break;
604
	case SPEED_1IN8:
605
		frameStep = 8;
606
		break;
607
	case SPEED_1IN16:
608
		frameStep = 16;
609
		break;
610
	case SPEED_STEP1:
611
		frameStep = 30;
612
		break;
613
	case SPEED_STEP3:
614
		frameStep = 90;
615
		break;
616
	}
617
}
618
619
void LaserdiscPlayer::nextFrame(EmuTime time)
620
{
621
	using enum PlayerState;
622
	if (waitFrame && waitFrame == currentFrame) {
623
		// Leave ACK raised until the next command
624
		ack = true;
625
		waitFrame = 0;
626
627
		if (stillOnWaitFrame) {
628
			playingFromSample = getCurrentSample(time);
629
			playerState = STILL;
630
			stillOnWaitFrame = false;
631
		}
632
	}
633
634
	if (playerState == MULTI_SPEED) {
635
		if (--frameStep)  {
636
			return;
637
		}
638
639
		switch (playingSpeed) {
640
		case SPEED_X3:
641
			currentFrame += 3;
642
			break;
643
		case SPEED_X2:
644
			currentFrame += 2;
645
			break;
646
		default:
647
			currentFrame += 1;
648
			break;
649
		}
650
		setFrameStep();
651
	} else if (playerState == PLAYING) {
652
		currentFrame++;
653
	}
654
655
	// freeze if stop frame
656
	if (playerState == one_of(PLAYING, MULTI_SPEED)) {
657
		assert(video);
658
		if (video->stopFrame(currentFrame)) {
659
			// stop frame reached
660
			playingFromSample = getCurrentSample(time);
661
			playerState = STILL;
662
		}
663
	}
664
}
665
666
void LaserdiscPlayer::setImageName(std::string newImage, EmuTime time)
667
{
668
	stop(time);
669
	oggImage = Filename(std::move(newImage), userFileContext());
670
	video.emplace(oggImage.getResolved(), motherBoard.getMSXCliComm());
671
672
	unsigned inputRate = video->getSampleRate();
673
	sampleClock.setFreq(inputRate);
674
	if (inputRate != getInputRate()) {
675
		setInputRate(inputRate);
676
		createResampler();
677
	}
678
}
679
680
bool LaserdiscPlayer::signalEvent(const Event& event)
681
{
682
	if (getType(event) == EventType::BOOT && video) {
683
		autoRun();
684
	}
685
	return false;
686
}
687
688
void LaserdiscPlayer::autoRun()
689
{
690
	if (!autoRunSetting.getBoolean()) return;
691
	if (motherBoard.getReverseManager().isReplaying()) {
692
		// See comments in CassettePlayer::autoRun()
693
		return;
694
	}
695
696
	auto *player = motherBoard.getCassettePort().getCassettePlayer();
697
698
	if (player && player->getTapeLength(EmuTime::dummy()) > 0) {
699
		// Murder Mystery laserdisc has no program encoded on the
700
		// right audio channel, but provides a seperate cassette.
701
		// So if a cassette and laserdisc is present, do not autoload
702
		// the laserdisc but autoload the tape instead.
703
		return;
704
	}
705
706
	auto machineID = motherBoard.getMachineID();
707
	std::string var = strCat(machineID, "::auto_run_ld_counter");
708
	std::string command = strCat(
709
		"if ![info exists ", var, "] { set ", var, " 0 }\n"
710
		"incr ", var, "\n"
711
		"after time 2 \"if $", var, "==\\$", var, " { ",
712
		machineID, "::type_via_keyboard 1CALLLD\\\\r }\"");
713
	try {
714
		motherBoard.getCommandController().executeCommand(command);
715
	} catch (CommandException& e) {
716
		motherBoard.getMSXCliComm().printWarning(
717
			"Error executing loading instruction for AutoRun: ",
718
			e.getMessage(), "\n Please report a bug.");
719
	}
720
}
721
722
void LaserdiscPlayer::generateChannels(std::span<float*> buffers, unsigned num)
723
{
724
	// Single channel device: replace content of buffers[0] (not add to it).
725
	assert(buffers.size() == 1);
726
	if (playerState != PlayerState::PLAYING || seeking || (muteLeft && muteRight)) {
727
		buffers[0] = nullptr;
728
		return;
729
	}
730
	assert(video);
731
732
	unsigned pos = 0;
733
	size_t currentSample;
734
735
	if (!sampleClock.before(start)) [[unlikely]] {
736
		// Before playing of sounds begins
737
		EmuDuration duration = sampleClock.getTime() - start;
738
		unsigned len = duration.getTicksAt(video->getSampleRate());
739
		if (len >= num) {
740
			buffers[0] = nullptr;
741
			return;
742
		}
743
744
		for (/**/; pos < len; ++pos) {
745
			buffers[0][pos * 2 + 0] = 0.0f;
746
			buffers[0][pos * 2 + 1] = 0.0f;
747
		}
748
749
		currentSample = playingFromSample;
750
	} else {
751
		currentSample = getCurrentSample(start);
752
	}
753
754
	unsigned drift = video->getSampleRate() / 30;
755
756
	if (currentSample > (lastPlayedSample + drift) ||
757
	    (currentSample + drift) < lastPlayedSample) {
758
		// audio drift
759
		lastPlayedSample = currentSample;
760
	}
761
762
	int left = stereoMode == StereoMode::RIGHT ? 1 : 0;
763
	int right = stereoMode == StereoMode::LEFT ? 0 : 1;
764
765
	while (pos < num) {
766
		const AudioFragment* audio = video->getAudio(lastPlayedSample);
767
768
		if (!audio) {
769
			if (pos == 0) {
770
				buffers[0] = nullptr;
771
				break;
772
			} else {
773
				for (/**/; pos < num; ++pos) {
774
					buffers[0][pos * 2 + 0] = 0.0f;
775
					buffers[0][pos * 2 + 1] = 0.0f;
776
				}
777
			}
778
		} else {
779
			auto offset = unsigned(lastPlayedSample - audio->position);
780
			unsigned len = std::min(audio->length - offset, num - pos);
781
782
			// maybe muting should be moved out of the loop?
783
			for (unsigned i = 0; i < len; ++i, ++pos) {
784
				buffers[0][pos * 2 + 0] = muteLeft ? 0.0f :
785
				   audio->pcm[left][offset + i];
786
				buffers[0][pos * 2 + 1] = muteRight ? 0.0f :
787
				   audio->pcm[right][offset + i];
788
			}
789
790
			lastPlayedSample += len;
791
		}
792
	}
793
}
794
795
float LaserdiscPlayer::getAmplificationFactorImpl() const
796
{
797
	return 2.0f;
798
}
799
800
bool LaserdiscPlayer::updateBuffer(size_t length, float* buffer,
801
                                   EmuTime time)
802
{
803
	bool result = ResampledSoundDevice::updateBuffer(length, buffer, time);
804
	start = time; // current end-time is next start-time
805
	return result;
806
}
807
808
void LaserdiscPlayer::setMuting(bool left, bool right, EmuTime time)
809
{
810
	updateStream(time);
811
	muteLeft = left;
812
	muteRight = right;
813
}
814
815
void LaserdiscPlayer::play(EmuTime time)
816
{
817
	if (!video) return;
818
819
	updateStream(time);
820
821
	using enum PlayerState;
822
	if (seeking) {
823
		// Do not ACK, play while seeking
824
	} else if (playerState == STOPPED) {
825
		// Disk needs to spin up, which takes 9.6s on
826
		// my Pioneer LD-92000. Also always seek to
827
		// beginning (confirmed on real MSX and LD)
828
		video->seek(1, 0);
829
		lastPlayedSample = 0;
830
		playingFromSample = 0;
831
		currentFrame = 1;
832
		// Note that with "fullspeedwhenloading" this
833
		// should be reduced to.
834
		setAck(time, 9600);
835
		seekState = SeekState::NONE;
836
		seeking = true;
837
		waitFrame = 0;
838
		stereoMode = StereoMode::STEREO;
839
		playingSpeed = SPEED_1IN4;
840
	} else if (playerState == PLAYING) {
841
		// If Play command is issued while the player
842
		// is already playing, then if no ACK is sent then
843
		// Astron Belt will send LD1100 commands
844
		setAck(time, 46);
845
	} else if (playerState == MULTI_SPEED) {
846
		// Should be hearing stuff again
847
		playingFromSample = (currentFrame - 1LL) * 1001LL *
848
				video->getSampleRate() / 30000LL;
849
		sampleClock.advance(time);
850
		setAck(time, 46);
851
	} else {
852
		// STILL or PAUSED
853
		sampleClock.advance(time);
854
		setAck(time, 46);
855
	}
856
	playerState = PLAYING;
857
}
858
859
size_t LaserdiscPlayer::getCurrentSample(EmuTime time)
860
{
861
	switch(playerState) {
862
	case PlayerState::PAUSED:
863
	case PlayerState::STILL:
864
		return playingFromSample;
865
	default:
866
		return playingFromSample + sampleClock.getTicksTill(time);
867
	}
868
}
869
870
void LaserdiscPlayer::pause(EmuTime time)
871
{
872
	using enum PlayerState;
873
	if (playerState == STOPPED) return;
874
875
	updateStream(time);
876
877
	if (playerState == PLAYING) {
878
		playingFromSample = getCurrentSample(time);
879
	} else if (playerState == MULTI_SPEED) {
880
		playingFromSample = (currentFrame - 1LL) * 1001LL *
881
				video->getSampleRate() / 30000LL;
882
		sampleClock.advance(time);
883
	}
884
885
	playerState = PAUSED;
886
	setAck(time, 46);
887
}
888
889
void LaserdiscPlayer::stop(EmuTime time)
890
{
891
	if (playerState == PlayerState::STOPPED) return;
892
893
	updateStream(time);
894
	playerState = PlayerState::STOPPED;
895
}
896
897
void LaserdiscPlayer::eject(EmuTime time)
898
{
899
	stop(time);
900
	oggImage = {};
901
	video.reset();
902
}
903
904
// Step one frame forwards or backwards. The frame will be visible and
905
// we won't be playing afterwards
906
void LaserdiscPlayer::stepFrame(bool forwards)
907
{
908
	// TODO can video be nullopt?
909
	bool needSeek = false;
910
911
	// Note that on real hardware, the screen goes dark momentarily
912
	// if you try to step before the first frame or after the last one
913
	if (playerState == PlayerState::STILL) {
914
		assert(video);
915
		if (forwards) {
916
			if (currentFrame < video->getFrames()) {
917
				currentFrame++;
918
			}
919
		} else {
920
			if (currentFrame > 1) {
921
				currentFrame--;
922
				needSeek = true;
923
			}
924
		}
925
	}
926
927
	playerState = PlayerState::STILL;
928
	auto samplePos = (currentFrame - 1LL) * 1001LL *
929
	                    video->getSampleRate() / 30000LL;
930
	playingFromSample = samplePos;
931
932
	if (needSeek) {
933
		if (video->getFrameRate() == 60)
934
			video->seek(currentFrame * 2, samplePos);
935
		else
936
			video->seek(currentFrame, samplePos);
937
	}
938
}
939
940
void LaserdiscPlayer::seekFrame(size_t toFrame, EmuTime time)
941
{
942
	if ((playerState == PlayerState::STOPPED) || !video) return;
943
944
	updateStream(time);
945
946
	toFrame = std::clamp(toFrame, 1uz, video->getFrames());
947
948
	// Seek time needs to be emulated correctly since
949
	// e.g. Astron Belt does not wait for the seek
950
	// to complete, it simply assumes a certain
951
	// delay.
952
	//
953
	// This calculation is based on measurements on
954
	// a Pioneer LD-92000.
955
	auto dist = std::abs(int64_t(toFrame) - int64_t(currentFrame));
956
	int seekTime = (dist < 1000) // time in ms
957
			? narrow<int>(dist + 300)
958
			: narrow<int>(1800 + dist / 12);
959
960
	auto samplePos = (toFrame - 1LL) * 1001LL *
961
				video->getSampleRate() / 30000LL;
962
963
	if (video->getFrameRate() == 60) {
964
		video->seek(toFrame * 2, samplePos);
965
	} else {
966
		video->seek(toFrame, samplePos);
967
	}
968
	playerState = PlayerState::STILL;
969
	playingFromSample = samplePos;
970
	currentFrame = toFrame;
971
972
	// Seeking clears the frame to wait for
973
	waitFrame = 0;
974
975
	seeking = true;
976
	setAck(time, seekTime);
977
}
978
979
void LaserdiscPlayer::seekChapter(int chapter, EmuTime time)
980
{
981
	if ((playerState == PlayerState::STOPPED) || !video) return;
982
983
	auto frameNo = video->getChapter(chapter);
984
	if (!frameNo) return;
985
	seekFrame(frameNo, time);
986
}
987
988
int16_t LaserdiscPlayer::readSample(EmuTime time)
989
{
990
	// Here we should return the value of the sample on the
991
	// right audio channel, ignoring muting (this is done in the MSX)
992
	// but honouring the stereo mode as this is done in the
993
	// Laserdisc player
994
	if (playerState == PlayerState::PLAYING && !seeking) {
995
		assert(video);
996
		auto sample = getCurrentSample(time);
997
		if (const AudioFragment* audio = video->getAudio(sample)) {
998
			++sampleReads;
999
			int channel = stereoMode == StereoMode::LEFT ? 0 : 1;
1000
			return narrow_cast<int16_t>
1001
				(audio->pcm[channel][sample - audio->position]
1002
							 * 32767.f);
1003
		}
1004
	}
1005
	return 0;
1006
}
1007
1008
bool LaserdiscPlayer::isVideoOutputAvailable(EmuTime time)
1009
{
1010
	updateStream(time);
1011
1012
	bool videoOut = [&] {
1013
		switch (playerState) {
1014
		case PlayerState::PLAYING:
1015
		case PlayerState::MULTI_SPEED:
1016
		case PlayerState::STILL:
1017
			return !seeking;
1018
		default:
1019
			return false;
1020
		}
1021
	}();
1022
	ldControl.videoIn(videoOut);
1023
1024
	return videoOut;
1025
}
1026
1027
void LaserdiscPlayer::preVideoSystemChange() noexcept
1028
{
1029
	renderer.reset();
1030
}
1031
1032
void LaserdiscPlayer::postVideoSystemChange() noexcept
1033
{
1034
	createRenderer();
1035
}
1036
1037
void LaserdiscPlayer::createRenderer()
1038
{
1039
	Display& display = getMotherBoard().getReactor().getDisplay();
1040
	renderer = RendererFactory::createLDRenderer(*this, display);
1041
}
1042
1043
static constexpr auto RemoteStateInfo = std::to_array<enum_string<LaserdiscPlayer::RemoteState>>({
1044
	{ "IDLE",             LaserdiscPlayer::RemoteState::IDLE             },
1045
	{ "HEADER_PULSE",     LaserdiscPlayer::RemoteState::HEADER_PULSE     },
1046
	{ "NEC_HEADER_SPACE", LaserdiscPlayer::RemoteState::NEC_HEADER_SPACE },
1047
	{ "NEC_BITS_PULSE",   LaserdiscPlayer::RemoteState::NEC_BITS_PULSE   },
1048
	{ "NEC_BITS_SPACE",   LaserdiscPlayer::RemoteState::NEC_BITS_SPACE   },
1049
});
1050
SERIALIZE_ENUM(LaserdiscPlayer::RemoteState, RemoteStateInfo);
1051
1052
static constexpr auto PlayerStateInfo = std::to_array<enum_string<LaserdiscPlayer::PlayerState>>({
1053
	{ "STOPPED",    LaserdiscPlayer::PlayerState::STOPPED     },
1054
	{ "PLAYING",    LaserdiscPlayer::PlayerState::PLAYING     },
1055
	{ "MULTISPEED", LaserdiscPlayer::PlayerState::MULTI_SPEED },
1056
	{ "PAUSED",     LaserdiscPlayer::PlayerState::PAUSED      },
1057
	{ "STILL",      LaserdiscPlayer::PlayerState::STILL       },
1058
});
1059
SERIALIZE_ENUM(LaserdiscPlayer::PlayerState, PlayerStateInfo);
1060
1061
static constexpr auto SeekStateInfo = std::to_array<enum_string<LaserdiscPlayer::SeekState>>({
1062
	{ "NONE",    LaserdiscPlayer::SeekState::NONE    },
1063
	{ "CHAPTER", LaserdiscPlayer::SeekState::CHAPTER },
1064
	{ "FRAME",   LaserdiscPlayer::SeekState::FRAME   },
1065
	{ "WAIT",    LaserdiscPlayer::SeekState::WAIT    },
1066
});
1067
SERIALIZE_ENUM(LaserdiscPlayer::SeekState, SeekStateInfo);
1068
1069
static constexpr auto StereoModeInfo = std::to_array<enum_string<LaserdiscPlayer::StereoMode>>({
1070
	{ "LEFT",   LaserdiscPlayer::StereoMode::LEFT   },
1071
	{ "RIGHT",  LaserdiscPlayer::StereoMode::RIGHT  },
1072
	{ "STEREO", LaserdiscPlayer::StereoMode::STEREO },
1073
});
1074
SERIALIZE_ENUM(LaserdiscPlayer::StereoMode, StereoModeInfo);
1075
1076
static constexpr auto RemoteProtocolInfo = std::to_array<enum_string<LaserdiscPlayer::RemoteProtocol>>({
1077
	{ "NONE", LaserdiscPlayer::RemoteProtocol::NONE },
1078
	{ "NEC",  LaserdiscPlayer::RemoteProtocol::NEC  },
1079
});
1080
SERIALIZE_ENUM(LaserdiscPlayer::RemoteProtocol, RemoteProtocolInfo);
1081
1082
// version 1: initial version
1083
// version 2: added 'stillOnWaitFrame'
1084
// version 3: reversed bit order of 'remoteBits' and 'remoteCode'
1085
// version 4: removed 'userData' from Schedulable
1086
template<typename Archive>
1087
void LaserdiscPlayer::serialize(Archive& ar, unsigned version)
1088
{
1089
	// Serialize remote control
1090
	ar.serialize("RemoteState", remoteState);
1091
	if (remoteState != RemoteState::IDLE) {
1092
		ar.serialize("RemoteBitNr", remoteBitNr,
1093
		             "RemoteBits",  remoteBits);
1094
		if (ar.versionBelow(version, 3)) {
1095
			assert(Archive::IS_LOADER);
1096
			remoteBits = Math::reverseNBits(remoteBits, remoteBitNr);
1097
		}
1098
	}
1099
	ar.serialize("RemoteLastBit",  remoteLastBit,
1100
	             "RemoteLastEdge", remoteLastEdge,
1101
	             "RemoteProtocol", remoteProtocol);
1102
	if (remoteProtocol != RemoteProtocol::NONE) {
1103
		ar.serialize("RemoteCode", remoteCode);
1104
		if (ar.versionBelow(version, 3)) {
1105
			assert(Archive::IS_LOADER);
1106
			remoteCode = Math::reverseByte(remoteCode);
1107
		}
1108
		ar.serialize("RemoteExecuteDelayed", remoteExecuteDelayed,
1109
		             "RemoteVblanksBack",    remoteVblanksBack);
1110
	}
1111
1112
	// Serialize filename
1113
	ar.serialize("OggImage", oggImage);
1114
	if constexpr (Archive::IS_LOADER) {
1115
		sampleReads = 0;
1116
		if (!oggImage.empty()) {
1117
			setImageName(oggImage.getResolved(), getCurrentTime());
1118
		} else {
1119
			video.reset();
1120
		}
1121
	}
1122
	ar.serialize("PlayerState", playerState);
1123
1124
	if (playerState != PlayerState::STOPPED) {
1125
		// Serialize seek state
1126
		ar.serialize("SeekState", seekState);
1127
		if (seekState != SeekState::NONE) {
1128
			ar.serialize("SeekNum", seekNum);
1129
		}
1130
		ar.serialize("seeking", seeking);
1131
1132
		// Playing state
1133
		ar.serialize("WaitFrame", waitFrame);
1134
1135
		// This was not yet implemented in openmsx 0.8.1 and earlier
1136
		if (ar.versionAtLeast(version, 2)) {
1137
			ar.serialize("StillOnWaitFrame", stillOnWaitFrame);
1138
		}
1139
1140
		ar.serialize("ACK",          ack,
1141
		             "PlayingSpeed", playingSpeed);
1142
1143
		// Frame position
1144
		ar.serialize("CurrentFrame", currentFrame);
1145
		if (playerState == PlayerState::MULTI_SPEED) {
1146
			ar.serialize("FrameStep", frameStep);
1147
		}
1148
1149
		// Audio position
1150
		ar.serialize("StereoMode",  stereoMode,
1151
		             "FromSample",  playingFromSample,
1152
		             "SampleClock", sampleClock);
1153
1154
		if constexpr (Archive::IS_LOADER) {
1155
			// If the sample rate differs, adjust accordingly
1156
			if (video->getSampleRate() != sampleClock.getFreq()) {
1157
				uint64_t pos = playingFromSample;
1158
1159
				pos *= video->getSampleRate();
1160
				pos /= sampleClock.getFreq();
1161
1162
				playingFromSample = pos;
1163
				sampleClock.setFreq(video->getSampleRate());
1164
			}
1165
1166
			auto sample = getCurrentSample(getCurrentTime());
1167
			if (video->getFrameRate() == 60)
1168
				video->seek(currentFrame * 2, sample);
1169
			else
1170
				video->seek(currentFrame, sample);
1171
			lastPlayedSample = sample;
1172
		}
1173
	}
1174
1175
	if (ar.versionAtLeast(version, 4)) {
1176
		ar.serialize("syncEven", syncEven,
1177
		             "syncOdd",  syncOdd,
1178
		             "syncAck",  syncAck);
1179
	} else {
1180
		Schedulable::restoreOld(ar, {&syncEven, &syncOdd, &syncAck});
1181
	}
1182
1183
	if constexpr (Archive::IS_LOADER) {
1184
		(void)isVideoOutputAvailable(getCurrentTime());
1185
	}
1186
}
1187
1188
INSTANTIATE_SERIALIZE_METHODS(LaserdiscPlayer);
1189
1190
} // namespace openmsx