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