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->chapter(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->chapter(
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 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->chapter(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 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 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 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 enum_string<LaserdiscPlayer::StereoMode> StereoModeInfo[] = {
1007  { "LEFT", LaserdiscPlayer::LEFT },
1008  { "RIGHT", LaserdiscPlayer::RIGHT },
1009  { "STEREO", LaserdiscPlayer::STEREO }
1010 };
1012 
1013 static 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
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
static EmuDuration msec(unsigned x)
Definition: EmuDuration.hh:38
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.
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
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:95
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:125
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:135
unsigned getTicksTill(EmuTime::param e) const
Calculate the number of ticks for this clock until the given time.
Definition: DynamicClock.hh:51
unsigned getTicksAt(unsigned freq) const
Definition: EmuDuration.hh:106
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)
FileContext userFileContext(string_ref savePath)
Definition: FileContext.cc:161
void attach(VideoSystemChangeListener &listener)
Definition: Display.cc:129
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:145
#define OUTER(type, member)
Definition: outer.hh:38
short readSample(EmuTime::param time)
void registerSound(const DeviceConfig &config)
Registers this sound device with the Mixer.
Definition: SoundDevice.cc:78