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