openMSX
LaserdiscPlayer.cc
Go to the documentation of this file.
1 #include "LaserdiscPlayer.hh"
2 #include "CommandException.hh"
3 #include "CommandController.hh"
4 #include "EventDistributor.hh"
5 #include "FileContext.hh"
6 #include "DeviceConfig.hh"
7 #include "HardwareConfig.hh"
8 #include "XMLElement.hh"
9 #include "CassettePort.hh"
10 #include "CliComm.hh"
11 #include "Display.hh"
12 #include "GlobalSettings.hh"
13 #include "Reactor.hh"
14 #include "ReverseManager.hh"
15 #include "MSXMotherBoard.hh"
16 #include "PioneerLDControl.hh"
17 #include "LDRenderer.hh"
18 #include "RendererFactory.hh"
19 #include "Math.hh"
20 #include "one_of.hh"
21 #include <cstdint>
22 #include <cstdlib>
23 #include <iostream>
24 #include <memory>
25 
26 using std::string;
27 
28 namespace openmsx {
29 
30 // Command
31 
32 LaserdiscPlayer::Command::Command(
33  CommandController& commandController_,
34  StateChangeDistributor& stateChangeDistributor_,
35  Scheduler& scheduler_)
36  : RecordedCommand(commandController_, stateChangeDistributor_,
37  scheduler_, "laserdiscplayer")
38 {
39 }
40 
41 void LaserdiscPlayer::Command::execute(
42  std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
43 {
44  auto& laserdiscPlayer = OUTER(LaserdiscPlayer, laserdiscCommand);
45  if (tokens.size() == 1) {
46  // Returning Tcl lists here, similar to the disk commands in
47  // DiskChanger
48  result.addListElement(tmpStrCat(getName(), ':'),
49  laserdiscPlayer.getImageName().getResolved());
50  } else if (tokens[1] == "eject") {
51  checkNumArgs(tokens, 2, Prefix{2}, nullptr);
52  result = "Ejecting laserdisc.";
53  laserdiscPlayer.eject(time);
54  } else if (tokens[1] == "insert") {
55  checkNumArgs(tokens, 3, "filename");
56  try {
57  result = "Changing laserdisc.";
58  laserdiscPlayer.setImageName(string(tokens[2].getString()), time);
59  } catch (MSXException& e) {
60  throw CommandException(std::move(e).getMessage());
61  }
62  } else {
63  throw SyntaxError();
64  }
65 }
66 
67 string LaserdiscPlayer::Command::help(std::span<const TclObject> tokens) const
68 {
69  if (tokens.size() >= 2) {
70  if (tokens[1] == "insert") {
71  return "Inserts the specified laserdisc image into "
72  "the laserdisc player.";
73  } else if (tokens[1] == "eject") {
74  return "Eject the laserdisc.";
75  }
76  }
77  return "laserdiscplayer insert <filename> "
78  ": insert a (different) laserdisc image\n"
79  "laserdiscplayer eject "
80  ": eject the laserdisc\n";
81 }
82 
83 void LaserdiscPlayer::Command::tabCompletion(std::vector<string>& tokens) const
84 {
85  if (tokens.size() == 2) {
86  using namespace std::literals;
87  static constexpr std::array extra = {"eject"sv, "insert"sv};
88  completeString(tokens, extra);
89  } else if (tokens.size() == 3 && tokens[1] == "insert") {
90  completeFileName(tokens, userFileContext());
91  }
92 }
93 
94 // LaserdiscPlayer
95 
96 constexpr unsigned DUMMY_INPUT_RATE = 44100; // actual rate depends on .ogg file
97 
99  const HardwareConfig& hwConf, PioneerLDControl& ldControl_)
100  : ResampledSoundDevice(hwConf.getMotherBoard(), "laserdiscplayer",
101  "Laserdisc Player", 1, DUMMY_INPUT_RATE, true)
102  , syncAck (hwConf.getMotherBoard().getScheduler())
103  , syncOdd (hwConf.getMotherBoard().getScheduler())
104  , syncEven(hwConf.getMotherBoard().getScheduler())
105  , motherBoard(hwConf.getMotherBoard())
106  , ldControl(ldControl_)
107  , laserdiscCommand(motherBoard.getCommandController(),
108  motherBoard.getStateChangeDistributor(),
109  motherBoard.getScheduler())
110  , sampleClock(EmuTime::zero())
111  , start(EmuTime::zero())
112  , muteLeft(false)
113  , muteRight(false)
114  , remoteState(REMOTE_IDLE)
115  , remoteLastEdge(EmuTime::zero())
116  , remoteLastBit(false)
117  , remoteProtocol(IR_NONE)
118  , ack(false)
119  , seeking(false)
120  , playerState(PLAYER_STOPPED)
121  , autoRunSetting(
122  motherBoard.getCommandController(), "autorunlaserdisc",
123  "automatically try to run Laserdisc", true)
124  , loadingIndicator(
125  motherBoard.getReactor().getGlobalSettings().getThrottleManager())
126  , sampleReads(0)
127 {
128  motherBoard.getCassettePort().setLaserdiscPlayer(this);
129 
130  Reactor& reactor = motherBoard.getReactor();
131  reactor.getDisplay().attach(*this);
132 
133  createRenderer();
135  scheduleDisplayStart(getCurrentTime());
136 
137  static XMLElement* xml = [] {
138  auto& doc = XMLDocument::getStaticDocument();
139  auto* result = doc.allocateElement("laserdiscplayer");
140  result->setFirstChild(doc.allocateElement("sound"))
141  ->setFirstChild(doc.allocateElement("volume", "30000"));
142  return result;
143  }();
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.
175 void LaserdiscPlayer::extControl(bool bit, EmuTime::param time)
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 == one_of(0x42u, 0x17u)))) {
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 << '\n';
316  } else {
317  std::cerr << "LaserdiscPlayer::remote unknown " << std::hex << code << '\n';
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 == one_of(0x49u, 0x4au, 0x4bu)) {
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->getChapter(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->getChapter(
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", hex_string<2>(code));
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 == one_of(PLAYER_PLAYING, 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.emplace(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 Event& event) noexcept
643 {
644  if (getType(event) == EventType::BOOT && video) {
645  autoRun();
646  }
647  return 0;
648 }
649 
650 void LaserdiscPlayer::autoRun()
651 {
652  if (!autoRunSetting.getBoolean()) return;
653  if (motherBoard.getReverseManager().isReplaying()) {
654  // See comments in CassettePlayer::autoRun()
655  return;
656  }
657 
658  string var = "::auto_run_ld_counter";
659  string command = strCat(
660  "if ![info exists ", var, "] { set ", var, " 0 }\n"
661  "incr ", var, "\n"
662  "after time 2 \"if $", var, "==\\$", var, " { "
663  "type_via_keyboard 1CALLLD\\\\r }\"");
664  try {
665  motherBoard.getCommandController().executeCommand(command);
666  } catch (CommandException& e) {
667  motherBoard.getMSXCliComm().printWarning(
668  "Error executing loading instruction for AutoRun: ",
669  e.getMessage(), "\n Please report a bug.");
670  }
671 }
672 
673 void LaserdiscPlayer::generateChannels(float** buffers, unsigned num)
674 {
675  // Single channel device: replace content of buffers[0] (not add to it).
676  if (playerState != PLAYER_PLAYING || seeking || (muteLeft && muteRight)) {
677  buffers[0] = nullptr;
678  return;
679  }
680 
681  unsigned pos = 0;
682  size_t currentSample;
683 
684  if (!sampleClock.before(start)) [[unlikely]] {
685  // Before playing of sounds begins
686  EmuDuration duration = sampleClock.getTime() - start;
687  unsigned len = duration.getTicksAt(video->getSampleRate());
688  if (len >= num) {
689  buffers[0] = nullptr;
690  return;
691  }
692 
693  for (; pos < len; ++pos) {
694  buffers[0][pos * 2 + 0] = 0.0f;
695  buffers[0][pos * 2 + 1] = 0.0f;
696  }
697 
698  currentSample = playingFromSample;
699  } else {
700  currentSample = getCurrentSample(start);
701  }
702 
703  unsigned drift = video->getSampleRate() / 30;
704 
705  if (currentSample > (lastPlayedSample + drift) ||
706  (currentSample + drift) < lastPlayedSample) {
707  // audio drift
708  lastPlayedSample = currentSample;
709  }
710 
711  int left = stereoMode == RIGHT ? 1 : 0;
712  int right = stereoMode == LEFT ? 0 : 1;
713 
714  while (pos < num) {
715  const AudioFragment* audio = video->getAudio(lastPlayedSample);
716 
717  if (!audio) {
718  if (pos == 0) {
719  buffers[0] = nullptr;
720  break;
721  } else {
722  for (; pos < num; ++pos) {
723  buffers[0][pos * 2 + 0] = 0.0f;
724  buffers[0][pos * 2 + 1] = 0.0f;
725  }
726  }
727  } else {
728  auto offset = unsigned(lastPlayedSample - audio->position);
729  unsigned len = std::min(audio->length - offset, num - pos);
730 
731  // maybe muting should be moved out of the loop?
732  for (unsigned i = 0; i < len; ++i, ++pos) {
733  buffers[0][pos * 2 + 0] = muteLeft ? 0.0f :
734  audio->pcm[left][offset + i];
735  buffers[0][pos * 2 + 1] = muteRight ? 0.0f :
736  audio->pcm[right][offset + i];
737  }
738 
739  lastPlayedSample += len;
740  }
741  }
742 }
743 
744 float LaserdiscPlayer::getAmplificationFactorImpl() const
745 {
746  return 2.0f;
747 }
748 
749 bool LaserdiscPlayer::updateBuffer(unsigned length, float* buffer,
750  EmuTime::param time)
751 {
752  bool result = ResampledSoundDevice::updateBuffer(length, buffer, time);
753  start = time; // current end-time is next start-time
754  return result;
755 }
756 
757 void LaserdiscPlayer::setMuting(bool left, bool right, EmuTime::param time)
758 {
759  updateStream(time);
760  muteLeft = left;
761  muteRight = right;
762 }
763 
764 void LaserdiscPlayer::play(EmuTime::param time)
765 {
766  if (video) {
767  updateStream(time);
768 
769  if (seeking) {
770  // Do not ACK, play while seeking
771  } else if (playerState == PLAYER_STOPPED) {
772  // Disk needs to spin up, which takes 9.6s on
773  // my Pioneer LD-92000. Also always seek to
774  // beginning (confirmed on real MSX and LD)
775  video->seek(1, 0);
776  lastPlayedSample = 0;
777  playingFromSample = 0;
778  currentFrame = 1;
779  // Note that with "fullspeedwhenloading" this
780  // should be reduced to.
781  setAck(time, 9600);
782  seekState = SEEK_NONE;
783  seeking = true;
784  waitFrame = 0;
785  stereoMode = STEREO;
786  playingSpeed = SPEED_1IN4;
787  } else if (playerState == PLAYER_PLAYING) {
788  // If Play command is issued while the player
789  // is already playing, then if no ACK is sent then
790  // Astron Belt will send LD1100 commands
791  setAck(time, 46);
792  } else if (playerState == PLAYER_MULTISPEED) {
793  // Should be hearing stuff again
794  playingFromSample = (currentFrame - 1ll) * 1001ll *
795  video->getSampleRate() / 30000ll;
796  sampleClock.advance(time);
797  setAck(time, 46);
798  } else {
799  // STILL or PAUSED
800  sampleClock.advance(time);
801  setAck(time, 46);
802  }
803  playerState = PLAYER_PLAYING;
804  }
805 }
806 
807 size_t LaserdiscPlayer::getCurrentSample(EmuTime::param time)
808 {
809  switch(playerState) {
810  case PLAYER_PAUSED:
811  case PLAYER_STILL:
812  return playingFromSample;
813  default:
814  return playingFromSample + sampleClock.getTicksTill(time);
815  }
816 }
817 
818 void LaserdiscPlayer::pause(EmuTime::param time)
819 {
820  if (playerState != PLAYER_STOPPED) {
821  updateStream(time);
822 
823  if (playerState == PLAYER_PLAYING) {
824  playingFromSample = getCurrentSample(time);
825  } else if (playerState == PLAYER_MULTISPEED) {
826  playingFromSample = (currentFrame - 1ll) * 1001ll *
827  video->getSampleRate() / 30000ll;
828  sampleClock.advance(time);
829  }
830 
831  playerState = PLAYER_PAUSED;
832  setAck(time, 46);
833  }
834 }
835 
836 void LaserdiscPlayer::stop(EmuTime::param time)
837 {
838  if (playerState != PLAYER_STOPPED) {
839  updateStream(time);
840  playerState = PLAYER_STOPPED;
841  }
842 }
843 
844 void LaserdiscPlayer::eject(EmuTime::param time)
845 {
846  stop(time);
847  oggImage = {};
848  video.reset();
849 }
850 
851 // Step one frame forwards or backwards. The frame will be visible and
852 // we won't be playing afterwards
853 void LaserdiscPlayer::stepFrame(bool forwards)
854 {
855  bool needseek = false;
856 
857  // Note that on real hardware, the screen goes dark momentarily
858  // if you try to step before the first frame or after the last one
859  if (playerState == PLAYER_STILL) {
860  if (forwards) {
861  if (currentFrame < video->getFrames()) {
862  currentFrame++;
863  }
864  } else {
865  if (currentFrame > 1) {
866  currentFrame--;
867  needseek = true;
868  }
869  }
870  }
871 
872  playerState = PLAYER_STILL;
873  int64_t samplePos = (currentFrame - 1ll) * 1001ll *
874  video->getSampleRate() / 30000ll;
875  playingFromSample = samplePos;
876 
877  if (needseek) {
878  if (video->getFrameRate() == 60)
879  video->seek(currentFrame * 2, samplePos);
880  else
881  video->seek(currentFrame, samplePos);
882  }
883 }
884 
885 void LaserdiscPlayer::seekFrame(size_t toFrame, EmuTime::param time)
886 {
887  if ((playerState != PLAYER_STOPPED) && video) {
888  updateStream(time);
889 
890  if (toFrame <= 0) toFrame = 1;
891  if (toFrame > video->getFrames()) toFrame = video->getFrames();
892 
893  // Seek time needs to be emulated correctly since
894  // e.g. Astron Belt does not wait for the seek
895  // to complete, it simply assumes a certain
896  // delay.
897  //
898  // This calculation is based on measurements on
899  // a Pioneer LD-92000.
900  auto dist = std::abs(int64_t(toFrame) - int64_t(currentFrame));
901  int seektime = (dist < 1000) // time in ms
902  ? (dist + 300)
903  : (1800 + dist / 12);
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  return !seeking;
962  default:
963  return false;
964  }
965  }();
966  ldControl.videoIn(videoOut);
967 
968  return videoOut;
969 }
970 
971 void LaserdiscPlayer::preVideoSystemChange() noexcept
972 {
973  renderer.reset();
974 }
975 
976 void LaserdiscPlayer::postVideoSystemChange() noexcept
977 {
978  createRenderer();
979 }
980 
981 void LaserdiscPlayer::createRenderer()
982 {
983  Display& display = getMotherBoard().getReactor().getDisplay();
984  renderer = RendererFactory::createLDRenderer(*this, display);
985 }
986 
987 static constexpr std::initializer_list<enum_string<LaserdiscPlayer::RemoteState>> RemoteStateInfo = {
988  { "IDLE", LaserdiscPlayer::REMOTE_IDLE },
989  { "HEADER_PULSE", LaserdiscPlayer::REMOTE_HEADER_PULSE },
990  { "NEC_HEADER_SPACE", LaserdiscPlayer::NEC_HEADER_SPACE },
991  { "NEC_BITS_PULSE", LaserdiscPlayer::NEC_BITS_PULSE },
992  { "NEC_BITS_SPACE", LaserdiscPlayer::NEC_BITS_SPACE },
993 };
995 
996 static constexpr std::initializer_list<enum_string<LaserdiscPlayer::PlayerState>> PlayerStateInfo = {
997  { "STOPPED", LaserdiscPlayer::PLAYER_STOPPED },
998  { "PLAYING", LaserdiscPlayer::PLAYER_PLAYING },
999  { "MULTISPEED", LaserdiscPlayer::PLAYER_MULTISPEED },
1000  { "PAUSED", LaserdiscPlayer::PLAYER_PAUSED },
1001  { "STILL", LaserdiscPlayer::PLAYER_STILL }
1002 };
1004 
1005 static constexpr std::initializer_list<enum_string<LaserdiscPlayer::SeekState>> SeekStateInfo = {
1006  { "NONE", LaserdiscPlayer::SEEK_NONE },
1007  { "CHAPTER", LaserdiscPlayer::SEEK_CHAPTER },
1008  { "FRAME", LaserdiscPlayer::SEEK_FRAME },
1009  { "WAIT", LaserdiscPlayer::SEEK_WAIT }
1010 };
1012 
1013 static constexpr std::initializer_list<enum_string<LaserdiscPlayer::StereoMode>> StereoModeInfo = {
1014  { "LEFT", LaserdiscPlayer::LEFT },
1015  { "RIGHT", LaserdiscPlayer::RIGHT },
1016  { "STEREO", LaserdiscPlayer::STEREO }
1017 };
1019 
1020 static constexpr std::initializer_list<enum_string<LaserdiscPlayer::RemoteProtocol>> RemoteProtocolInfo = {
1021  { "NONE", LaserdiscPlayer::IR_NONE },
1022  { "NEC", LaserdiscPlayer::IR_NEC },
1023 };
1025 
1026 // version 1: initial version
1027 // version 2: added 'stillOnWaitFrame'
1028 // version 3: reversed bit order of 'remoteBits' and 'remoteCode'
1029 // version 4: removed 'userData' from Schedulable
1030 template<typename Archive>
1031 void LaserdiscPlayer::serialize(Archive& ar, unsigned version)
1032 {
1033  // Serialize remote control
1034  ar.serialize("RemoteState", remoteState);
1035  if (remoteState != REMOTE_IDLE) {
1036  ar.serialize("RemoteBitNr", remoteBitNr,
1037  "RemoteBits", remoteBits);
1038  if (ar.versionBelow(version, 3)) {
1039  assert(Archive::IS_LOADER);
1040  remoteBits = Math::reverseNBits(remoteBits, remoteBitNr);
1041  }
1042  }
1043  ar.serialize("RemoteLastBit", remoteLastBit,
1044  "RemoteLastEdge", remoteLastEdge,
1045  "RemoteProtocol", remoteProtocol);
1046  if (remoteProtocol != IR_NONE) {
1047  ar.serialize("RemoteCode", remoteCode);
1048  if (ar.versionBelow(version, 3)) {
1049  assert(Archive::IS_LOADER);
1050  remoteCode = Math::reverseByte(remoteCode);
1051  }
1052  ar.serialize("RemoteExecuteDelayed", remoteExecuteDelayed,
1053  "RemoteVblanksBack", remoteVblanksBack);
1054  }
1055 
1056  // Serialize filename
1057  ar.serialize("OggImage", oggImage);
1058  if constexpr (Archive::IS_LOADER) {
1059  sampleReads = 0;
1060  if (!oggImage.empty()) {
1061  setImageName(oggImage.getResolved(), getCurrentTime());
1062  } else {
1063  video.reset();
1064  }
1065  }
1066  ar.serialize("PlayerState", playerState);
1067 
1068  if (playerState != PLAYER_STOPPED) {
1069  // Serialize seek state
1070  ar.serialize("SeekState", seekState);
1071  if (seekState != SEEK_NONE) {
1072  ar.serialize("SeekNum", seekNum);
1073  }
1074  ar.serialize("seeking", seeking);
1075 
1076  // Playing state
1077  ar.serialize("WaitFrame", waitFrame);
1078 
1079  // This was not yet implemented in openmsx 0.8.1 and earlier
1080  if (ar.versionAtLeast(version, 2)) {
1081  ar.serialize("StillOnWaitFrame", stillOnWaitFrame);
1082  }
1083 
1084  ar.serialize("ACK", ack,
1085  "PlayingSpeed", playingSpeed);
1086 
1087  // Frame position
1088  ar.serialize("CurrentFrame", currentFrame);
1089  if (playerState == PLAYER_MULTISPEED) {
1090  ar.serialize("FrameStep", frameStep);
1091  }
1092 
1093  // Audio position
1094  ar.serialize("StereoMode", stereoMode,
1095  "FromSample", playingFromSample,
1096  "SampleClock", sampleClock);
1097 
1098  if constexpr (Archive::IS_LOADER) {
1099  // If the samplerate differs, adjust accordingly
1100  if (video->getSampleRate() != sampleClock.getFreq()) {
1101  uint64_t pos = playingFromSample;
1102 
1103  pos *= video->getSampleRate();
1104  pos /= sampleClock.getFreq();
1105 
1106  playingFromSample = pos;
1107  sampleClock.setFreq(video->getSampleRate());
1108  }
1109 
1110  auto sample = getCurrentSample(getCurrentTime());
1111  if (video->getFrameRate() == 60)
1112  video->seek(currentFrame * 2, sample);
1113  else
1114  video->seek(currentFrame, sample);
1115  lastPlayedSample = sample;
1116  }
1117  }
1118 
1119  if (ar.versionAtLeast(version, 4)) {
1120  ar.serialize("syncEven", syncEven,
1121  "syncOdd", syncOdd,
1122  "syncAck", syncAck);
1123  } else {
1124  Schedulable::restoreOld(ar, {&syncEven, &syncOdd, &syncAck});
1125  }
1126 
1127  if constexpr (Archive::IS_LOADER) {
1128  (void)isVideoOutputAvailable(getCurrentTime());
1129  }
1130 }
1131 
1133 
1134 } // namespace openmsx
Definition: one_of.hh:7
bool getBoolean() const noexcept
virtual void setLaserdiscPlayer(LaserdiscPlayer *laserdisc)=0
Set the Laserdisc Player; when the motor control is off, sound is read from the laserdisc.
void printWarning(std::string_view message)
Definition: CliComm.cc:10
Represents a clock with a fixed frequency.
Definition: Clock.hh:19
virtual TclObject executeCommand(zstring_view command, CliConnection *connection=nullptr)=0
Execute the given command.
void detach(VideoSystemChangeListener &listener)
Definition: Display.cc:138
void attach(VideoSystemChangeListener &listener)
Definition: Display.cc:132
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
unsigned getTicksTill(EmuTime::param e) const
Calculate the number of ticks for this clock until the given time.
Definition: DynamicClock.hh:51
unsigned getFreq() const
Returns the frequency (in Hz) at which this clock ticks.
void advance(EmuTime::param e)
Advance this clock in time until the last tick which is not past the given time.
void setFreq(unsigned freq)
Change the frequency at which this clock ticks.
EmuTime::param getTime() const
Gets the time at which the last clock tick occurred.
Definition: DynamicClock.hh:37
static constexpr EmuDuration msec(unsigned x)
Definition: EmuDuration.hh:42
constexpr unsigned getTicksAt(unsigned freq) const
Definition: EmuDuration.hh:96
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void registerEventListener(EventType type, EventListener &listener, Priority priority=OTHER)
Registers a given object to receive certain events.
bool empty() const
Convenience method to test for empty filename.
Definition: Filename.cc:21
const std::string & getResolved() const &
Definition: Filename.hh:47
void setMuting(bool left, bool right, EmuTime::param time)
int16_t readSample(EmuTime::param time)
void extControl(bool bit, EmuTime::param time)
MSXMotherBoard & getMotherBoard()
const RawFrame * getRawFrame() const
void serialize(Archive &ar, unsigned version)
void update(bool newState)
Called by the device to indicate its loading state may have changed.
ReverseManager & getReverseManager()
CommandController & getCommandController()
CassettePortInterface & getCassettePort()
void videoIn(bool enabled)
A video frame as output by the VDP scanline conversion unit, before any postprocessing filters are ap...
Definition: RawFrame.hh:15
Contains the main loop of openMSX.
Definition: Reactor.hh:68
EventDistributor & getEventDistributor()
Definition: Reactor.hh:82
Display & getDisplay()
Definition: Reactor.hh:86
bool updateBuffer(unsigned length, float *buffer, EmuTime::param time) override
Generate sample data.
static void restoreOld(Archive &ar, std::vector< Schedulable * > schedulables)
Definition: Schedulable.hh:77
void updateStream(EmuTime::param time)
Definition: SoundDevice.cc:138
unsigned getInputRate() const
Definition: SoundDevice.hh:119
void setInputRate(unsigned sampleRate)
Definition: SoundDevice.hh:118
void unregisterSound()
Unregisters this sound device with the Mixer.
Definition: SoundDevice.cc:133
void registerSound(const DeviceConfig &config)
Registers this sound device with the Mixer.
Definition: SoundDevice.cc:88
static XMLDocument & getStaticDocument()
Definition: XMLElement.hh:255
constexpr unsigned reverseNBits(unsigned x, unsigned bits)
Reverse the lower N bits of a given value.
Definition: Math.hh:70
constexpr uint8_t reverseByte(uint8_t a)
Reverse the bits in a byte.
Definition: Math.hh:120
constexpr double e
Definition: Math.hh:18
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:258
T length(const vecN< N, T > &x)
Definition: gl_vec.hh:332
std::string getName(KeyCode keyCode)
Translate key code to key name.
Definition: Keys.cc:727
std::unique_ptr< LDRenderer > createLDRenderer(LaserdiscPlayer &ld, Display &display)
Create the Laserdisc Renderer.
This file implemented 3 utility functions:
Definition: Autofire.cc:9
constexpr unsigned DUMMY_INPUT_RATE
SERIALIZE_ENUM(CassettePlayer::State, stateInfo)
EventType getType(const Event &event)
Definition: Event.hh:644
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:171
#define OUTER(type, member)
Definition: outer.hh:41
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:1009
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:617
std::string strCat(Ts &&...ts)
Definition: strCat.hh:549