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