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