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