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