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