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