openMSX
CassettePlayer.cc
Go to the documentation of this file.
1 // TODO:
2 // - improve consistency when a reset occurs: tape is removed when you were
3 // recording, but it is not removed when you were playing
4 // - specify prefix for auto file name generation when recording (setting?)
5 // - append to existing wav files when recording (record command), but this is
6 // basically a special case (pointer at the end) of:
7 // - (partly) overwrite an existing wav file from any given time index
8 // - seek in cassette images for the next and previous file (using empty space?)
9 // - (partly) overwrite existing wav files with new tape data (not very hi prio)
10 // - handle read-only cassette images (e.g.: CAS images or WAV files with a RO
11 // flag): refuse to go to record mode when those are selected
12 // - smartly auto-set the position of tapes: if you insert an existing WAV
13 // file, it will have the position at the start, assuming PLAY mode by
14 // default. When specifying record mode at insert (somehow), it should be
15 // at the back.
16 // Alternatively, we could remember the index in tape images by storing the
17 // index in some persistent data file with its SHA1 sum as it was as we last
18 // saw it. When there are write actions to the tape, the hash has to be
19 // recalculated and replaced in the data file. An optimization would be to
20 // first simply check on the length of the file and fall back to SHA1 if that
21 // results in multiple matches.
22 
23 #include "CassettePlayer.hh"
24 #include "Connector.hh"
25 #include "CassettePort.hh"
26 #include "CommandController.hh"
27 #include "DeviceConfig.hh"
28 #include "HardwareConfig.hh"
29 #include "XMLElement.hh"
30 #include "FileContext.hh"
31 #include "FilePool.hh"
32 #include "File.hh"
33 #include "ReverseManager.hh"
34 #include "WavImage.hh"
35 #include "CasImage.hh"
36 #include "CliComm.hh"
37 #include "MSXMotherBoard.hh"
38 #include "Reactor.hh"
39 #include "GlobalSettings.hh"
40 #include "CommandException.hh"
41 #include "EventDistributor.hh"
42 #include "FileOperations.hh"
43 #include "WavWriter.hh"
44 #include "TclObject.hh"
45 #include "DynamicClock.hh"
46 #include "EmuDuration.hh"
47 #include "serialize.hh"
48 #include "unreachable.hh"
49 #include "xrange.hh"
50 #include <algorithm>
51 #include <cassert>
52 #include <memory>
53 
54 using std::string;
55 
56 namespace openmsx {
57 
58 // TODO: this description is not entirely accurate, but it is used
59 // as an identifier for this audio device in e.g. Catapult. We should
60 // use another way to identify audio devices A.S.A.P.!
61 constexpr static_string_view DESCRIPTION = "Cassetteplayer, use to read .cas or .wav files.";
62 
63 constexpr unsigned DUMMY_INPUT_RATE = 44100; // actual rate depends on .cas/.wav file
64 constexpr unsigned RECORD_FREQ = 44100;
65 constexpr double OUTPUT_AMP = 60.0;
66 
67 static std::string_view getCassettePlayerName()
68 {
69  return "cassetteplayer";
70 }
71 
73  : ResampledSoundDevice(hwConf.getMotherBoard(), getCassettePlayerName(), DESCRIPTION, 1, DUMMY_INPUT_RATE, false)
74  , syncEndOfTape(hwConf.getMotherBoard().getScheduler())
75  , syncAudioEmu (hwConf.getMotherBoard().getScheduler())
76  , tapePos(EmuTime::zero())
77  , prevSyncTime(EmuTime::zero())
78  , audioPos(0)
79  , motherBoard(hwConf.getMotherBoard())
80  , tapeCommand(
81  motherBoard.getCommandController(),
82  motherBoard.getStateChangeDistributor(),
83  motherBoard.getScheduler())
84  , loadingIndicator(
85  motherBoard.getReactor().getGlobalSettings().getThrottleManager())
86  , autoRunSetting(
87  motherBoard.getCommandController(),
88  "autoruncassettes", "automatically try to run cassettes", true)
89  , sampcnt(0)
90  , state(STOP)
91  , lastOutput(false)
92  , motor(false), motorControl(true)
93  , syncScheduled(false)
94 {
95  static XMLElement* xml = [] {
96  auto& doc = XMLDocument::getStaticDocument();
97  XMLElement* result = doc.allocateElement("cassetteplayer");
98  result->setFirstChild(doc.allocateElement("sound"))
99  ->setFirstChild(doc.allocateElement("volume", "5000"));
100  return result;
101  }();
102  registerSound(DeviceConfig(hwConf, *xml));
103 
105  EventType::BOOT, *this);
106  motherBoard.getMSXCliComm().update(CliComm::HARDWARE, getCassettePlayerName(), "add");
107 
108  removeTape(EmuTime::zero());
109 }
110 
112 {
113  unregisterSound();
114  if (auto* c = getConnector()) {
115  c->unplug(getCurrentTime());
116  }
118  EventType::BOOT, *this);
119  motherBoard.getMSXCliComm().update(CliComm::HARDWARE, getCassettePlayerName(), "remove");
120 }
121 
122 void CassettePlayer::autoRun()
123 {
124  if (!playImage) return;
125  if (motherBoard.getReverseManager().isReplaying()) {
126  // Don't execute the loading commands (keyboard type commands)
127  // when we're replaying a recording. Because the recording
128  // already contains those commands.
129  return;
130  }
131 
132  // try to automatically run the tape, if that's set
133  CassetteImage::FileType type = playImage->getFirstFileType();
134  if (!autoRunSetting.getBoolean() || type == CassetteImage::UNKNOWN) {
135  return;
136  }
137  bool is_SVI = motherBoard.getMachineType() == "SVI"; // assume all other are 'MSX*' (might not be correct for 'Coleco')
138  string H_READ = is_SVI ? "0xFE8E" : "0xFF07"; // Hook for Ready
139  string H_MAIN = is_SVI ? "0xFE94" : "0xFF0C"; // Hook for Main Loop
140  string instr1, instr2;
141  switch (type) {
143  instr1 = R"({RUN\"CAS:\"\r})";
144  break;
146  instr1 = R"({BLOAD\"CAS:\",R\r})";
147  break;
149  // Note that CLOAD:RUN won't work: BASIC ignores stuff
150  // after the CLOAD command. That's why it's split in two.
151  instr1 = "{CLOAD\\r}";
152  instr2 = "{RUN\\r}";
153  break;
154  default:
155  UNREACHABLE; // Shouldn't be possible
156  }
157  string command = strCat(
158  "namespace eval ::openmsx {\n"
159  " variable auto_run_bp\n"
160 
161  " proc auto_run_cb {args} {\n"
162  " variable auto_run_bp\n"
163  " debug remove_bp $auto_run_bp\n"
164  " unset auto_run_bp\n"
165 
166  // Without the 0.1s delay here, the type command gets messed up
167  // on MSX1 machines for some reason (starting to type too early?)
168  " after time 0.1 \"type [lindex $args 0]\"\n"
169 
170  " set next [lrange $args 1 end]\n"
171  " if {[llength $next] == 0} return\n"
172 
173  // H_READ is used by some firmwares; we need to hook the
174  // H_MAIN that happens immediately after H_READ.
175  " set cmd \"openmsx::auto_run_cb $next\"\n"
176  " set openmsx::auto_run_bp [debug set_bp ", H_MAIN, " 1 \"$cmd\"]\n"
177  " }\n"
178 
179  " if {[info exists auto_run_bp]} {debug remove_bp $auto_run_bp\n}\n"
180  " set auto_run_bp [debug set_bp ", H_READ, " 1 {\n"
181  " openmsx::auto_run_cb {{}} ", instr1, ' ', instr2, "\n"
182  " }]\n"
183 
184  // re-trigger hook(s), needed when already booted in BASIC
185  " type_via_keyboard \'\\r\n"
186  "}");
187  try {
188  motherBoard.getCommandController().executeCommand(command);
189  } catch (CommandException& e) {
190  motherBoard.getMSXCliComm().printWarning(
191  "Error executing loading instruction using command \"",
192  command, "\" for AutoRun: ",
193  e.getMessage(), "\n Please report a bug.");
194  }
195 }
196 
197 string CassettePlayer::getStateString() const
198 {
199  switch (getState()) {
200  case PLAY: return "play";
201  case RECORD: return "record";
202  case STOP: return "stop";
203  }
204  UNREACHABLE; return {};
205 }
206 
207 bool CassettePlayer::isRolling() const
208 {
209  // Is the tape 'rolling'?
210  // is true when:
211  // not in stop mode (there is a tape inserted and not at end-of-tape)
212  // AND [ user forced playing (motorcontrol=off) OR motor enabled by
213  // software (motor=on) ]
214  return (getState() != STOP) && (motor || !motorControl);
215 }
216 
217 double CassettePlayer::getTapePos(EmuTime::param time)
218 {
219  sync(time);
220  return (tapePos - EmuTime::zero()).toDouble();
221 }
222 
223 double CassettePlayer::getTapeLength(EmuTime::param time)
224 {
225  if (playImage) {
226  return (playImage->getEndTime() - EmuTime::zero()).toDouble();
227  } else if (getState() == RECORD) {
228  return getTapePos(time);
229  } else {
230  return 0.0;
231  }
232 }
233 
234 void CassettePlayer::checkInvariants() const
235 {
236  switch (getState()) {
237  case STOP:
238  assert(!recordImage);
239  if (playImage) {
240  // we're at end-of tape
241  assert(!getImageName().empty());
242  } else {
243  // no tape inserted, imageName may or may not be empty
244  }
245  break;
246  case PLAY:
247  assert(!getImageName().empty());
248  assert(!recordImage);
249  assert(playImage);
250  break;
251  case RECORD:
252  assert(!getImageName().empty());
253  assert(recordImage);
254  assert(!playImage);
255  break;
256  default:
257  UNREACHABLE;
258  }
259 }
260 
261 void CassettePlayer::setState(State newState, const Filename& newImage,
262  EmuTime::param time)
263 {
264  sync(time);
265 
266  // set new state if different from old state
267  State oldState = getState();
268  if (oldState == newState) return;
269 
270  // cannot directly switch from PLAY to RECORD or vice-versa,
271  // (should always go via STOP)
272  assert(!((oldState == PLAY) && (newState == RECORD)));
273  assert(!((oldState == RECORD) && (newState == PLAY)));
274 
275  // stuff for leaving the old state
276  // 'recordImage==nullptr' can happen in case of loadstate.
277  if ((oldState == RECORD) && recordImage) {
278  flushOutput();
279  bool empty = recordImage->isEmpty();
280  recordImage.reset();
281  if (empty) {
282  // delete the created WAV file, as it is useless
283  FileOperations::unlink(getImageName().getResolved()); // ignore errors
284  setImageName(Filename());
285  }
286  }
287 
288  // actually switch state
289  state = newState;
290  setImageName(newImage);
291 
292  // stuff for entering the new state
293  if (newState == RECORD) {
294  partialOut = 0.0;
295  partialInterval = 0.0;
296  lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
297  lastY = 0.0;
298  }
299  motherBoard.getMSXCliComm().update(
300  CliComm::STATUS, "cassetteplayer", getStateString());
301 
302  updateLoadingState(time); // sets SP for tape-end detection
303 
304  checkInvariants();
305 }
306 
307 void CassettePlayer::updateLoadingState(EmuTime::param time)
308 {
309  assert(prevSyncTime == time); // sync() must be called
310  // TODO also set loadingIndicator for RECORD?
311  // note: we don't use isRolling()
312  loadingIndicator.update(motor && (getState() == PLAY));
313 
314  syncEndOfTape.removeSyncPoint();
315  if (isRolling() && (getState() == PLAY)) {
316  syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
317  }
318 }
319 
320 void CassettePlayer::setImageName(const Filename& newImage)
321 {
322  casImage = newImage;
323  motherBoard.getMSXCliComm().update(
324  CliComm::MEDIA, "cassetteplayer", casImage.getResolved());
325 }
326 
327 void CassettePlayer::insertTape(const Filename& filename, EmuTime::param time)
328 {
329  if (!filename.empty()) {
330  FilePool& filePool = motherBoard.getReactor().getFilePool();
331  try {
332  // first try WAV
333  playImage = std::make_unique<WavImage>(filename, filePool);
334  } catch (MSXException& e) {
335  try {
336  // if that fails use CAS
337  playImage = std::make_unique<CasImage>(
338  filename, filePool,
339  motherBoard.getMSXCliComm());
340  } catch (MSXException& e2) {
341  throw MSXException(
342  "Failed to insert WAV image: \"",
343  e.getMessage(),
344  "\" and also failed to insert CAS image: \"",
345  e2.getMessage(), '\"');
346  }
347  }
348  } else {
349  // This is a bit tricky, consider this scenario: we switch from
350  // RECORD->PLAY, but we didn't actually record anything: The
351  // removeTape() call above (indirectly) deletes the empty
352  // recorded wav image and also clears imageName. Now because
353  // the 'filename' parameter is passed by reference, and because
354  // getImageName() returns a reference, this 'filename'
355  // parameter now also is an empty string.
356  }
357 
358  // possibly recreate resampler
359  unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
360  if (inputRate != getInputRate()) {
361  setInputRate(inputRate);
362  createResampler();
363  }
364 
365  // trigger (re-)query of getAmplificationFactorImpl()
366  setSoftwareVolume(1.0f, time);
367 
368  setImageName(filename);
369 }
370 
371 void CassettePlayer::playTape(const Filename& filename, EmuTime::param time)
372 {
373  // Temporally go to STOP state:
374  // RECORD: First close the recorded image. Otherwise it goes wrong
375  // if you switch from RECORD->PLAY on the same image.
376  // PLAY: Go to stop because we temporally violate some invariants
377  // (tapePos can be beyond end-of-tape).
378  setState(STOP, getImageName(), time); // keep current image
379  insertTape(filename, time);
380  rewind(time); // sets PLAY mode
381  autoRun();
382 }
383 
384 void CassettePlayer::rewind(EmuTime::param time)
385 {
386  sync(time); // before tapePos changes
387  assert(getState() != RECORD);
388  tapePos = EmuTime::zero();
389  audioPos = 0;
390 
391  if (getImageName().empty()) {
392  // no image inserted, do nothing
393  assert(getState() == STOP);
394  } else {
395  // keep current image
396  setState(PLAY, getImageName(), time);
397  }
398  updateLoadingState(time);
399 }
400 
401 void CassettePlayer::recordTape(const Filename& filename, EmuTime::param time)
402 {
403  removeTape(time); // flush (possible) previous recording
404  recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
405  tapePos = EmuTime::zero();
406  setState(RECORD, filename, time);
407 }
408 
409 void CassettePlayer::removeTape(EmuTime::param time)
410 {
411  // first stop with tape still inserted
412  setState(STOP, getImageName(), time);
413  // then remove the tape
414  playImage.reset();
415  tapePos = EmuTime::zero();
416  setImageName({});
417 }
418 
419 void CassettePlayer::setMotor(bool status, EmuTime::param time)
420 {
421  if (status != motor) {
422  sync(time);
423  motor = status;
424  updateLoadingState(time);
425  }
426 }
427 
428 void CassettePlayer::setMotorControl(bool status, EmuTime::param time)
429 {
430  if (status != motorControl) {
431  sync(time);
432  motorControl = status;
433  updateLoadingState(time);
434  }
435 }
436 
437 int16_t CassettePlayer::readSample(EmuTime::param time)
438 {
439  if (getState() == PLAY) {
440  // playing
441  sync(time);
442  return isRolling() ? playImage->getSampleAt(tapePos) : 0;
443  } else {
444  // record or stop
445  return 0;
446  }
447 }
448 
449 void CassettePlayer::setSignal(bool output, EmuTime::param time)
450 {
451  sync(time);
452  lastOutput = output;
453 }
454 
455 void CassettePlayer::sync(EmuTime::param time)
456 {
457  EmuDuration duration = time - prevSyncTime;
458  prevSyncTime = time;
459 
460  updateTapePosition(duration, time);
461  generateRecordOutput(duration);
462 }
463 
464 void CassettePlayer::updateTapePosition(
465  EmuDuration::param duration, EmuTime::param time)
466 {
467  if (!isRolling() || (getState() != PLAY)) return;
468 
469  tapePos += duration;
470  assert(tapePos <= playImage->getEndTime());
471 
472  // synchronize audio with actual tape position
473  if (!syncScheduled) {
474  // don't sync too often, this improves sound quality
475  syncScheduled = true;
476  syncAudioEmu.setSyncPoint(time + EmuDuration::sec(1));
477  }
478 }
479 
480 void CassettePlayer::generateRecordOutput(EmuDuration::param duration)
481 {
482  if (!recordImage || !isRolling()) return;
483 
484  double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
485  double samples = duration.toDouble() * RECORD_FREQ;
486  double rest = 1.0 - partialInterval;
487  if (rest <= samples) {
488  // enough to fill next interval
489  partialOut += out * rest;
490  fillBuf(1, int(partialOut));
491  samples -= rest;
492 
493  // fill complete intervals
494  int count = int(samples);
495  if (count > 0) {
496  fillBuf(count, int(out));
497  }
498  samples -= count;
499 
500  // partial last interval
501  partialOut = samples * out;
502  partialInterval = 0.0;
503  } else {
504  partialOut += samples * out;
505  partialInterval += samples;
506  }
507 }
508 
509 void CassettePlayer::fillBuf(size_t length, double x)
510 {
511  assert(recordImage);
512  constexpr double A = 252.0 / 256.0;
513 
514  double y = lastY + (x - lastX);
515 
516  while (length) {
517  size_t len = std::min(length, BUF_SIZE - sampcnt);
518  repeat(len, [&] {
519  buf[sampcnt++] = int(y) + 128;
520  y *= A;
521  });
522  length -= len;
523  assert(sampcnt <= BUF_SIZE);
524  if (BUF_SIZE == sampcnt) {
525  flushOutput();
526  }
527  }
528  lastY = y;
529  lastX = x;
530 }
531 
532 void CassettePlayer::flushOutput()
533 {
534  try {
535  recordImage->write(buf, 1, unsigned(sampcnt));
536  sampcnt = 0;
537  recordImage->flush(); // update wav header
538  } catch (MSXException& e) {
539  motherBoard.getMSXCliComm().printWarning(
540  "Failed to write to tape: ", e.getMessage());
541  }
542 }
543 
544 
545 std::string_view CassettePlayer::getName() const
546 {
547  return getCassettePlayerName();
548 }
549 
550 std::string_view CassettePlayer::getDescription() const
551 {
552  return DESCRIPTION;
553 }
554 
555 void CassettePlayer::plugHelper(Connector& conn, EmuTime::param time)
556 {
557  sync(time);
558  lastOutput = static_cast<CassettePort&>(conn).lastOut();
559 }
560 
561 void CassettePlayer::unplugHelper(EmuTime::param time)
562 {
563  // note: may not throw exceptions
564  setState(STOP, getImageName(), time); // keep current image
565 }
566 
567 
568 void CassettePlayer::generateChannels(float** buffers, unsigned num)
569 {
570  // Single channel device: replace content of buffers[0] (not add to it).
571  if ((getState() != PLAY) || !isRolling()) {
572  buffers[0] = nullptr;
573  return;
574  }
575  playImage->fillBuffer(audioPos, buffers, num);
576  audioPos += num;
577 }
578 
580 {
581  return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
582 }
583 
584 int CassettePlayer::signalEvent(const Event& event) noexcept
585 {
586  if (getType(event) == EventType::BOOT) {
587  if (!getImageName().empty()) {
588  // Reinsert tape to make sure everything is reset.
589  try {
590  playTape(getImageName(), getCurrentTime());
591  } catch (MSXException& e) {
592  motherBoard.getMSXCliComm().printWarning(
593  "Failed to insert tape: ", e.getMessage());
594  }
595  }
596  }
597  return 0;
598 }
599 
600 void CassettePlayer::execEndOfTape(EmuTime::param time)
601 {
602  // tape ended
603  sync(time);
604  assert(tapePos == playImage->getEndTime());
605  motherBoard.getMSXCliComm().printWarning(
606  "Tape end reached... stopping. "
607  "You may need to insert another tape image "
608  "that contains side B. (Or you used the wrong "
609  "loading command.)");
610  setState(STOP, getImageName(), time); // keep current image
611 }
612 
613 void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
614 {
615  if (getState() == PLAY) {
616  updateStream(time);
617  sync(time);
618  DynamicClock clk(EmuTime::zero());
619  clk.setFreq(playImage->getFrequency());
620  audioPos = clk.getTicksTill(tapePos);
621  }
622  syncScheduled = false;
623 }
624 
625 
626 // class TapeCommand
627 
628 CassettePlayer::TapeCommand::TapeCommand(
629  CommandController& commandController_,
630  StateChangeDistributor& stateChangeDistributor_,
631  Scheduler& scheduler_)
632  : RecordedCommand(commandController_, stateChangeDistributor_,
633  scheduler_, "cassetteplayer")
634 {
635 }
636 
637 void CassettePlayer::TapeCommand::execute(
638  span<const TclObject> tokens, TclObject& result, EmuTime::param time)
639 {
640  auto& cassettePlayer = OUTER(CassettePlayer, tapeCommand);
641  if (tokens.size() == 1) {
642  // Returning Tcl lists here, similar to the disk commands in
643  // DiskChanger
644  TclObject options = makeTclList(cassettePlayer.getStateString());
645  result.addListElement(tmpStrCat(getName(), ':'),
646  cassettePlayer.getImageName().getResolved(),
647  options);
648 
649  } else if (tokens[1] == "new") {
650  std::string_view directory = "taperecordings";
651  std::string_view prefix = "openmsx";
652  std::string_view extension = ".wav";
654  (tokens.size() == 3) ? tokens[2].getString() : string{},
655  directory, prefix, extension);
656  cassettePlayer.recordTape(Filename(filename), time);
657  result = tmpStrCat(
658  "Created new cassette image file: ", filename,
659  ", inserted it and set recording mode.");
660 
661  } else if (tokens[1] == "insert" && tokens.size() == 3) {
662  try {
663  result = "Changing tape";
664  Filename filename(tokens[2].getString(), userFileContext());
665  cassettePlayer.playTape(filename, time);
666  } catch (MSXException& e) {
667  throw CommandException(std::move(e).getMessage());
668  }
669 
670  } else if (tokens[1] == "motorcontrol" && tokens.size() == 3) {
671  if (tokens[2] == "on") {
672  cassettePlayer.setMotorControl(true, time);
673  result = "Motor control enabled.";
674  } else if (tokens[2] == "off") {
675  cassettePlayer.setMotorControl(false, time);
676  result = "Motor control disabled.";
677  } else {
678  throw SyntaxError();
679  }
680 
681  } else if (tokens.size() != 2) {
682  throw SyntaxError();
683 
684  } else if (tokens[1] == "motorcontrol") {
685  result = tmpStrCat("Motor control is ",
686  (cassettePlayer.motorControl ? "on" : "off"));
687 
688  } else if (tokens[1] == "record") {
689  result = "TODO: implement this... (sorry)";
690 
691  } else if (tokens[1] == "play") {
692  if (cassettePlayer.getState() == CassettePlayer::RECORD) {
693  try {
694  result = "Play mode set, rewinding tape.";
695  cassettePlayer.playTape(
696  cassettePlayer.getImageName(), time);
697  } catch (MSXException& e) {
698  throw CommandException(std::move(e).getMessage());
699  }
700  } else if (cassettePlayer.getState() == CassettePlayer::STOP) {
701  throw CommandException("No tape inserted or tape at end!");
702  } else {
703  // PLAY mode
704  result = "Already in play mode.";
705  }
706 
707  } else if (tokens[1] == "eject") {
708  result = "Tape ejected";
709  cassettePlayer.removeTape(time);
710 
711  } else if (tokens[1] == "rewind") {
712  string r;
713  if (cassettePlayer.getState() == CassettePlayer::RECORD) {
714  try {
715  r = "First stopping recording... ";
716  cassettePlayer.playTape(
717  cassettePlayer.getImageName(), time);
718  } catch (MSXException& e) {
719  throw CommandException(std::move(e).getMessage());
720  }
721  }
722  cassettePlayer.rewind(time);
723  r += "Tape rewound";
724  result = r;
725 
726  } else if (tokens[1] == "getpos") {
727  result = cassettePlayer.getTapePos(time);
728 
729  } else if (tokens[1] == "getlength") {
730  result = cassettePlayer.getTapeLength(time);
731 
732  } else {
733  try {
734  result = "Changing tape";
735  Filename filename(tokens[1].getString(), userFileContext());
736  cassettePlayer.playTape(filename, time);
737  } catch (MSXException& e) {
738  throw CommandException(std::move(e).getMessage());
739  }
740  }
741  //if (!cassettePlayer.getConnector()) {
742  // cassettePlayer.cliComm.printWarning("Cassetteplayer not plugged in.");
743  //}
744 }
745 
746 string CassettePlayer::TapeCommand::help(span<const TclObject> tokens) const
747 {
748  string helptext;
749  if (tokens.size() >= 2) {
750  if (tokens[1] == "eject") {
751  helptext =
752  "Well, just eject the cassette from the cassette "
753  "player/recorder!";
754  } else if (tokens[1] == "rewind") {
755  helptext =
756  "Indeed, rewind the tape that is currently in the "
757  "cassette player/recorder...";
758  } else if (tokens[1] == "motorcontrol") {
759  helptext =
760  "Setting this to 'off' is equivalent to "
761  "disconnecting the black remote plug from the "
762  "cassette player: it makes the cassette player "
763  "run (if in play mode); the motor signal from the "
764  "MSX will be ignored. Normally this is set to "
765  "'on': the cassetteplayer obeys the motor control "
766  "signal from the MSX.";
767  } else if (tokens[1] == "play") {
768  helptext =
769  "Go to play mode. Only useful if you were in "
770  "record mode (which is currently the only other "
771  "mode available).";
772  } else if (tokens[1] == "new") {
773  helptext =
774  "Create a new cassette image. If the file name is "
775  "omitted, one will be generated in the default "
776  "directory for tape recordings. Implies going to "
777  "record mode (why else do you want a new cassette "
778  "image?).";
779  } else if (tokens[1] == "insert") {
780  helptext =
781  "Inserts the specified cassette image into the "
782  "cassette player, rewinds it and switches to play "
783  "mode.";
784  } else if (tokens[1] == "record") {
785  helptext =
786  "Go to record mode. NOT IMPLEMENTED YET. Will be "
787  "used to be able to resume recording to an "
788  "existing cassette image, previously inserted with "
789  "the insert command.";
790  } else if (tokens[1] == "getpos") {
791  helptext =
792  "Return the position of the tape, in seconds from "
793  "the beginning of the tape.";
794  } else if (tokens[1] == "getlength") {
795  helptext =
796  "Return the length of the tape in seconds.";
797  }
798  } else {
799  helptext =
800  "cassetteplayer eject "
801  ": remove tape from virtual player\n"
802  "cassetteplayer rewind "
803  ": rewind tape in virtual player\n"
804  "cassetteplayer motorcontrol "
805  ": enables or disables motor control (remote)\n"
806  "cassetteplayer play "
807  ": change to play mode (default)\n"
808  "cassetteplayer record "
809  ": change to record mode (NOT IMPLEMENTED YET)\n"
810  "cassetteplayer new [<filename>] "
811  ": create and insert new tape image file and go to record mode\n"
812  "cassetteplayer insert <filename> "
813  ": insert (a different) tape file\n"
814  "cassetteplayer getpos "
815  ": query the position of the tape\n"
816  "cassetteplayer getlength "
817  ": query the total length of the tape\n"
818  "cassetteplayer <filename> "
819  ": insert (a different) tape file\n";
820  }
821  return helptext;
822 }
823 
824 void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens) const
825 {
826  using namespace std::literals;
827  if (tokens.size() == 2) {
828  static constexpr std::array cmds = {
829  "eject"sv, "rewind"sv, "motorcontrol"sv, "insert"sv, "new"sv,
830  "play"sv, "getpos"sv, "getlength"sv,
831  //"record"sv,
832  };
833  completeFileName(tokens, userFileContext(), cmds);
834  } else if ((tokens.size() == 3) && (tokens[1] == "insert")) {
835  completeFileName(tokens, userFileContext());
836  } else if ((tokens.size() == 3) && (tokens[1] == "motorcontrol")) {
837  static constexpr std::array extra = {"on"sv, "off"sv};
838  completeString(tokens, extra);
839  }
840 }
841 
842 bool CassettePlayer::TapeCommand::needRecord(span<const TclObject> tokens) const
843 {
844  return tokens.size() > 1;
845 }
846 
847 
848 static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
849  { "PLAY", CassettePlayer::PLAY },
850  { "RECORD", CassettePlayer::RECORD },
851  { "STOP", CassettePlayer::STOP }
852 };
854 
855 // version 1: initial version
856 // version 2: added checksum
857 template<typename Archive>
858 void CassettePlayer::serialize(Archive& ar, unsigned version)
859 {
860  if (recordImage) {
861  // buf, sampcnt
862  flushOutput();
863  }
864 
865  ar.serialize("casImage", casImage);
866 
867  Sha1Sum oldChecksum;
868  if constexpr (!Archive::IS_LOADER) {
869  if (playImage) {
870  oldChecksum = playImage->getSha1Sum();
871  }
872  }
873  if (ar.versionAtLeast(version, 2)) {
874  string oldChecksumStr = oldChecksum.empty()
875  ? string{}
876  : oldChecksum.toString();
877  ar.serialize("checksum", oldChecksumStr);
878  oldChecksum = oldChecksumStr.empty()
879  ? Sha1Sum()
880  : Sha1Sum(oldChecksumStr);
881  }
882 
883  if constexpr (Archive::IS_LOADER) {
884  FilePool& filePool = motherBoard.getReactor().getFilePool();
885  auto time = getCurrentTime();
886  casImage.updateAfterLoadState();
887  if (!oldChecksum.empty() &&
888  !FileOperations::exists(casImage.getResolved())) {
889  auto file = filePool.getFile(FileType::TAPE, oldChecksum);
890  if (file.is_open()) {
891  casImage.setResolved(file.getURL());
892  }
893  }
894  try {
895  insertTape(casImage, time);
896  } catch (MSXException&) {
897  if (oldChecksum.empty()) {
898  // It's OK if we cannot reinsert an empty
899  // image. One likely scenario for this case is
900  // the following:
901  // - cassetteplayer new myfile.wav
902  // - don't actually start saving to tape yet
903  // - create a savestate and load that state
904  // Because myfile.wav contains no data yet, it
905  // is deleted from the filesystem. So on a
906  // loadstate it won't be found.
907  } else {
908  throw;
909  }
910  }
911 
912  if (playImage && !oldChecksum.empty()) {
913  Sha1Sum newChecksum = playImage->getSha1Sum();
914  if (oldChecksum != newChecksum) {
915  motherBoard.getMSXCliComm().printWarning(
916  "The content of the tape ",
917  casImage.getResolved(),
918  " has changed since the time this "
919  "savestate was created. This might "
920  "result in emulation problems.");
921  }
922  }
923  }
924 
925  // only for RECORD
926  //double lastX;
927  //double lastY;
928  //double partialOut;
929  //double partialInterval;
930  //std::unique_ptr<WavWriter> recordImage;
931 
932  ar.serialize("tapePos", tapePos,
933  "prevSyncTime", prevSyncTime,
934  "audioPos", audioPos,
935  "state", state,
936  "lastOutput", lastOutput,
937  "motor", motor,
938  "motorControl", motorControl);
939 
940  if constexpr (Archive::IS_LOADER) {
941  auto time = getCurrentTime();
942  if (playImage && (tapePos > playImage->getEndTime())) {
943  tapePos = playImage->getEndTime();
944  motherBoard.getMSXCliComm().printWarning("Tape position "
945  "beyond tape end! Setting tape position to end. "
946  "This can happen if you load a replay from an "
947  "older openMSX version with a different CAS-to-WAV "
948  "baud rate or when the tape image has been changed "
949  "compared to when the replay was created.");
950  }
951  if (state == RECORD) {
952  // TODO we don't support savestates in RECORD mode yet
953  motherBoard.getMSXCliComm().printWarning(
954  "Restoring a state where the MSX was saving to "
955  "tape is not yet supported. Emulation will "
956  "continue without actually saving.");
957  setState(STOP, getImageName(), time);
958  }
959  if (!playImage && (state == PLAY)) {
960  // This should only happen for manually edited
961  // savestates, though we shouldn't crash on it.
962  setState(STOP, getImageName(), time);
963  }
964  sync(time);
965  updateLoadingState(time);
966  }
967 }
970 
971 } // namespace openmsx
bool getBoolean() const noexcept
void plugHelper(Connector &connector, EmuTime::param time) override
void generateChannels(float **buffers, unsigned num) override
Abstract method to generate the actual sound data.
float getAmplificationFactorImpl() const override
Get amplification/attenuation factor for this device.
std::string_view getName() const override
Name used to identify this pluggable.
std::string_view getDescription() const override
Description for this pluggable.
void setSignal(bool output, EmuTime::param time) override
Sets the cassette output signal false = low true = high.
void unplugHelper(EmuTime::param time) override
void setMotor(bool status, EmuTime::param time) override
Sets the cassette motor relay false = off true = on.
CassettePlayer(const HardwareConfig &hwConf)
void serialize(Archive &ar, unsigned version)
int16_t readSample(EmuTime::param time) override
Read wave data from cassette device.
virtual void update(UpdateType type, std::string_view name, std::string_view value)=0
void printWarning(std::string_view message)
Definition: CliComm.cc:10
virtual TclObject executeCommand(zstring_view command, CliConnection *connection=nullptr)=0
Execute the given command.
Represents something you can plug devices into.
Definition: Connector.hh:21
static constexpr EmuDuration sec(unsigned x)
Definition: EmuDuration.hh:39
const EmuDuration & param
Definition: EmuDuration.hh:27
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.
File getFile(FileType fileType, const Sha1Sum &sha1sum)
Search file with the given sha1sum.
Definition: FilePool.cc:53
void setResolved(std::string resolved)
Change the resolved part of this filename E.g.
Definition: Filename.hh:67
const std::string & getResolved() const &
Definition: Filename.hh:47
void updateAfterLoadState()
After a loadstate we prefer to use the exact same file as before savestate.
Definition: Filename.cc:8
void update(bool newState)
Called by the device to indicate its loading state may have changed.
ReverseManager & getReverseManager()
CommandController & getCommandController()
std::string_view getMachineType() const
Connector * getConnector() const
Get the connector this Pluggable is plugged into.
Definition: Pluggable.hh:43
EventDistributor & getEventDistributor()
Definition: Reactor.hh:82
FilePool & getFilePool()
Definition: Reactor.hh:91
This class represents the result of a sha1 calculation (a 160-bit value).
Definition: sha1.hh:22
bool empty() const
Definition: utils/sha1.cc:242
std::string toString() const
Definition: utils/sha1.cc:230
void updateStream(EmuTime::param time)
Definition: SoundDevice.cc:139
unsigned getInputRate() const
Definition: SoundDevice.hh:119
void setInputRate(unsigned sampleRate)
Definition: SoundDevice.hh:118
void setSoftwareVolume(float volume, EmuTime::param time)
Change the 'software volume' of this sound device.
Definition: SoundDevice.cc:144
void unregisterSound()
Unregisters this sound device with the Mixer.
Definition: SoundDevice.cc:134
void registerSound(const DeviceConfig &config)
Registers this sound device with the Mixer.
Definition: SoundDevice.cc:89
static XMLDocument & getStaticDocument()
Definition: XMLElement.hh:258
XMLElement * setFirstChild(XMLElement *child)
Definition: XMLElement.hh:214
Definition: span.hh:126
constexpr index_type size() const noexcept
Definition: span.hh:296
static_string_view
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:207
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:269
T length(const vecN< N, T > &x)
Definition: gl_vec.hh:343
string parseCommandFileArgument(string_view argument, string_view directory, string_view prefix, string_view extension)
Helper function for parsing filename arguments in Tcl commands.
bool exists(zstring_view filename)
Does this file (directory) exists?
int unlink(zstring_view path)
Call unlink() in a platform-independent manner.
std::string getName(KeyCode keyCode)
Translate key code to key name.
Definition: Keys.cc:741
This file implemented 3 utility functions:
Definition: Autofire.cc:9
constexpr unsigned DUMMY_INPUT_RATE
SERIALIZE_ENUM(CassettePlayer::State, stateInfo)
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
constexpr const char *const filename
constexpr KeyMatrixPosition x
Keyboard bindings.
Definition: Keyboard.cc:118
EventType getType(const Event &event)
Definition: Event.hh:645
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:173
constexpr static_string_view DESCRIPTION
constexpr unsigned RECORD_FREQ
TclObject makeTclList(Args &&... args)
Definition: TclObject.hh:290
constexpr double OUTPUT_AMP
#define OUTER(type, member)
Definition: outer.hh:41
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:998
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:659
std::string strCat(Ts &&...ts)
Definition: strCat.hh:591
#define UNREACHABLE
Definition: unreachable.hh:38
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition: xrange.hh:170