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