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  if (getState() == RECORD) {
221  // we record 8-bit mono, so bytes == samples
222  return (double(recordImage->getBytes()) + partialInterval) / RECORD_FREQ;
223  } else {
224  return (tapePos - EmuTime::zero()).toDouble();
225  }
226 }
227 
228 double CassettePlayer::getTapeLength(EmuTime::param time)
229 {
230  if (playImage) {
231  return (playImage->getEndTime() - EmuTime::zero()).toDouble();
232  } else if (getState() == RECORD) {
233  return getTapePos(time);
234  } else {
235  return 0.0;
236  }
237 }
238 
239 void CassettePlayer::checkInvariants() const
240 {
241  switch (getState()) {
242  case STOP:
243  assert(!recordImage);
244  if (playImage) {
245  // we're at end-of tape
246  assert(!getImageName().empty());
247  } else {
248  // no tape inserted, imageName may or may not be empty
249  }
250  break;
251  case PLAY:
252  assert(!getImageName().empty());
253  assert(!recordImage);
254  assert(playImage);
255  break;
256  case RECORD:
257  assert(!getImageName().empty());
258  assert(recordImage);
259  assert(!playImage);
260  break;
261  default:
262  UNREACHABLE;
263  }
264 }
265 
266 void CassettePlayer::setState(State newState, const Filename& newImage,
267  EmuTime::param time)
268 {
269  sync(time);
270 
271  // set new state if different from old state
272  State oldState = getState();
273  if (oldState == newState) return;
274 
275  // cannot directly switch from PLAY to RECORD or vice-versa,
276  // (should always go via STOP)
277  assert(!((oldState == PLAY) && (newState == RECORD)));
278  assert(!((oldState == RECORD) && (newState == PLAY)));
279 
280  // stuff for leaving the old state
281  // 'recordImage==nullptr' can happen in case of loadstate.
282  if ((oldState == RECORD) && recordImage) {
283  flushOutput();
284  bool empty = recordImage->isEmpty();
285  recordImage.reset();
286  if (empty) {
287  // delete the created WAV file, as it is useless
288  FileOperations::unlink(getImageName().getResolved()); // ignore errors
289  setImageName(Filename());
290  }
291  }
292 
293  // actually switch state
294  state = newState;
295  setImageName(newImage);
296 
297  // stuff for entering the new state
298  if (newState == RECORD) {
299  partialOut = 0.0;
300  partialInterval = 0.0;
301  lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
302  lastY = 0.0;
303  }
304  motherBoard.getMSXCliComm().update(
305  CliComm::STATUS, "cassetteplayer", getStateString());
306 
307  updateLoadingState(time); // sets SP for tape-end detection
308 
309  checkInvariants();
310 }
311 
312 void CassettePlayer::updateLoadingState(EmuTime::param time)
313 {
314  assert(prevSyncTime == time); // sync() must be called
315  // TODO also set loadingIndicator for RECORD?
316  // note: we don't use isRolling()
317  loadingIndicator.update(motor && (getState() == PLAY));
318 
319  syncEndOfTape.removeSyncPoint();
320  if (isRolling() && (getState() == PLAY)) {
321  syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
322  }
323 }
324 
325 void CassettePlayer::setImageName(const Filename& newImage)
326 {
327  casImage = newImage;
328  motherBoard.getMSXCliComm().update(
329  CliComm::MEDIA, "cassetteplayer", casImage.getResolved());
330 }
331 
332 void CassettePlayer::insertTape(const Filename& filename, EmuTime::param time)
333 {
334  if (!filename.empty()) {
335  FilePool& filePool = motherBoard.getReactor().getFilePool();
336  try {
337  // first try WAV
338  playImage = std::make_unique<WavImage>(filename, filePool);
339  } catch (MSXException& e) {
340  try {
341  // if that fails use CAS
342  playImage = std::make_unique<CasImage>(
343  filename, filePool,
344  motherBoard.getMSXCliComm());
345  } catch (MSXException& e2) {
346  throw MSXException(
347  "Failed to insert WAV image: \"",
348  e.getMessage(),
349  "\" and also failed to insert CAS image: \"",
350  e2.getMessage(), '\"');
351  }
352  }
353  } else {
354  // This is a bit tricky, consider this scenario: we switch from
355  // RECORD->PLAY, but we didn't actually record anything: The
356  // removeTape() call above (indirectly) deletes the empty
357  // recorded wav image and also clears imageName. Now because
358  // the 'filename' parameter is passed by reference, and because
359  // getImageName() returns a reference, this 'filename'
360  // parameter now also is an empty string.
361  }
362 
363  // possibly recreate resampler
364  unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
365  if (inputRate != getInputRate()) {
366  setInputRate(inputRate);
367  createResampler();
368  }
369 
370  // trigger (re-)query of getAmplificationFactorImpl()
371  setSoftwareVolume(1.0f, time);
372 
373  setImageName(filename);
374 }
375 
376 void CassettePlayer::playTape(const Filename& filename, EmuTime::param time)
377 {
378  // Temporally go to STOP state:
379  // RECORD: First close the recorded image. Otherwise it goes wrong
380  // if you switch from RECORD->PLAY on the same image.
381  // PLAY: Go to stop because we temporally violate some invariants
382  // (tapePos can be beyond end-of-tape).
383  setState(STOP, getImageName(), time); // keep current image
384  insertTape(filename, time);
385  rewind(time); // sets PLAY mode
386  autoRun();
387 }
388 
389 void CassettePlayer::rewind(EmuTime::param time)
390 {
391  sync(time); // before tapePos changes
392  assert(getState() != RECORD);
393  tapePos = EmuTime::zero();
394  audioPos = 0;
395 
396  if (getImageName().empty()) {
397  // no image inserted, do nothing
398  assert(getState() == STOP);
399  } else {
400  // keep current image
401  setState(PLAY, getImageName(), time);
402  }
403  updateLoadingState(time);
404 }
405 
406 void CassettePlayer::recordTape(const Filename& filename, EmuTime::param time)
407 {
408  removeTape(time); // flush (possible) previous recording
409  recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
410  tapePos = EmuTime::zero();
411  setState(RECORD, filename, time);
412 }
413 
414 void CassettePlayer::removeTape(EmuTime::param time)
415 {
416  // first stop with tape still inserted
417  setState(STOP, getImageName(), time);
418  // then remove the tape
419  playImage.reset();
420  tapePos = EmuTime::zero();
421  setImageName({});
422 }
423 
424 void CassettePlayer::setMotor(bool status, EmuTime::param time)
425 {
426  if (status != motor) {
427  sync(time);
428  motor = status;
429  updateLoadingState(time);
430  }
431 }
432 
433 void CassettePlayer::setMotorControl(bool status, EmuTime::param time)
434 {
435  if (status != motorControl) {
436  sync(time);
437  motorControl = status;
438  updateLoadingState(time);
439  }
440 }
441 
442 int16_t CassettePlayer::readSample(EmuTime::param time)
443 {
444  if (getState() == PLAY) {
445  // playing
446  sync(time);
447  return isRolling() ? playImage->getSampleAt(tapePos) : 0;
448  } else {
449  // record or stop
450  return 0;
451  }
452 }
453 
454 void CassettePlayer::setSignal(bool output, EmuTime::param time)
455 {
456  sync(time);
457  lastOutput = output;
458 }
459 
460 void CassettePlayer::sync(EmuTime::param time)
461 {
462  EmuDuration duration = time - prevSyncTime;
463  prevSyncTime = time;
464 
465  updateTapePosition(duration, time);
466  generateRecordOutput(duration);
467 }
468 
469 void CassettePlayer::updateTapePosition(
470  EmuDuration::param duration, EmuTime::param time)
471 {
472  if (!isRolling() || (getState() != PLAY)) return;
473 
474  tapePos += duration;
475  assert(tapePos <= playImage->getEndTime());
476 
477  // synchronize audio with actual tape position
478  if (!syncScheduled) {
479  // don't sync too often, this improves sound quality
480  syncScheduled = true;
481  syncAudioEmu.setSyncPoint(time + EmuDuration::sec(1));
482  }
483 }
484 
485 void CassettePlayer::generateRecordOutput(EmuDuration::param duration)
486 {
487  if (!recordImage || !isRolling()) return;
488 
489  double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
490  double samples = duration.toDouble() * RECORD_FREQ;
491  double rest = 1.0 - partialInterval;
492  if (rest <= samples) {
493  // enough to fill next interval
494  partialOut += out * rest;
495  fillBuf(1, int(partialOut));
496  samples -= rest;
497 
498  // fill complete intervals
499  int count = int(samples);
500  if (count > 0) {
501  fillBuf(count, int(out));
502  }
503  samples -= count;
504  assert(samples < 1.0);
505 
506  // partial last interval
507  partialOut = samples * out;
508  partialInterval = samples;
509  } else {
510  assert(samples < 1.0);
511  partialOut += samples * out;
512  partialInterval += samples;
513  }
514  assert(partialInterval < 1.0);
515 }
516 
517 void CassettePlayer::fillBuf(size_t length, double x)
518 {
519  assert(recordImage);
520  constexpr double A = 252.0 / 256.0;
521 
522  double y = lastY + (x - lastX);
523 
524  while (length) {
525  size_t len = std::min(length, BUF_SIZE - sampCnt);
526  repeat(len, [&] {
527  buf[sampCnt++] = int(y) + 128;
528  y *= A;
529  });
530  length -= len;
531  assert(sampCnt <= BUF_SIZE);
532  if (BUF_SIZE == sampCnt) {
533  flushOutput();
534  }
535  }
536  lastY = y;
537  lastX = x;
538 }
539 
540 void CassettePlayer::flushOutput()
541 {
542  try {
543  recordImage->write(buf, 1, unsigned(sampCnt));
544  sampCnt = 0;
545  recordImage->flush(); // update wav header
546  } catch (MSXException& e) {
547  motherBoard.getMSXCliComm().printWarning(
548  "Failed to write to tape: ", e.getMessage());
549  }
550 }
551 
552 
553 std::string_view CassettePlayer::getName() const
554 {
555  return getCassettePlayerName();
556 }
557 
558 std::string_view CassettePlayer::getDescription() const
559 {
560  return DESCRIPTION;
561 }
562 
563 void CassettePlayer::plugHelper(Connector& conn, EmuTime::param time)
564 {
565  sync(time);
566  lastOutput = static_cast<CassettePort&>(conn).lastOut();
567 }
568 
569 void CassettePlayer::unplugHelper(EmuTime::param time)
570 {
571  // note: may not throw exceptions
572  setState(STOP, getImageName(), time); // keep current image
573 }
574 
575 
576 void CassettePlayer::generateChannels(float** buffers, unsigned num)
577 {
578  // Single channel device: replace content of buffers[0] (not add to it).
579  if ((getState() != PLAY) || !isRolling()) {
580  buffers[0] = nullptr;
581  return;
582  }
583  playImage->fillBuffer(audioPos, buffers, num);
584  audioPos += num;
585 }
586 
588 {
589  return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
590 }
591 
592 int CassettePlayer::signalEvent(const Event& event) noexcept
593 {
594  if (getType(event) == EventType::BOOT) {
595  if (!getImageName().empty()) {
596  // Reinsert tape to make sure everything is reset.
597  try {
598  playTape(getImageName(), getCurrentTime());
599  } catch (MSXException& e) {
600  motherBoard.getMSXCliComm().printWarning(
601  "Failed to insert tape: ", e.getMessage());
602  }
603  }
604  }
605  return 0;
606 }
607 
608 void CassettePlayer::execEndOfTape(EmuTime::param time)
609 {
610  // tape ended
611  sync(time);
612  assert(tapePos == playImage->getEndTime());
613  motherBoard.getMSXCliComm().printWarning(
614  "Tape end reached... stopping. "
615  "You may need to insert another tape image "
616  "that contains side B. (Or you used the wrong "
617  "loading command.)");
618  setState(STOP, getImageName(), time); // keep current image
619 }
620 
621 void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
622 {
623  if (getState() == PLAY) {
624  updateStream(time);
625  sync(time);
626  DynamicClock clk(EmuTime::zero());
627  clk.setFreq(playImage->getFrequency());
628  audioPos = clk.getTicksTill(tapePos);
629  }
630  syncScheduled = false;
631 }
632 
633 
634 // class TapeCommand
635 
636 CassettePlayer::TapeCommand::TapeCommand(
637  CommandController& commandController_,
638  StateChangeDistributor& stateChangeDistributor_,
639  Scheduler& scheduler_)
640  : RecordedCommand(commandController_, stateChangeDistributor_,
641  scheduler_, "cassetteplayer")
642 {
643 }
644 
645 void CassettePlayer::TapeCommand::execute(
646  std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
647 {
648  auto& cassettePlayer = OUTER(CassettePlayer, tapeCommand);
649  if (tokens.size() == 1) {
650  // Returning Tcl lists here, similar to the disk commands in
651  // DiskChanger
652  TclObject options = makeTclList(cassettePlayer.getStateString());
653  result.addListElement(tmpStrCat(getName(), ':'),
654  cassettePlayer.getImageName().getResolved(),
655  options);
656 
657  } else if (tokens[1] == "new") {
658  std::string_view directory = "taperecordings";
659  std::string_view prefix = "openmsx";
660  std::string_view extension = ".wav";
662  (tokens.size() == 3) ? tokens[2].getString() : string{},
663  directory, prefix, extension);
664  cassettePlayer.recordTape(Filename(filename), time);
665  result = tmpStrCat(
666  "Created new cassette image file: ", filename,
667  ", inserted it and set recording mode.");
668 
669  } else if (tokens[1] == "insert" && tokens.size() == 3) {
670  try {
671  result = "Changing tape";
672  Filename filename(tokens[2].getString(), userFileContext());
673  cassettePlayer.playTape(filename, time);
674  } catch (MSXException& e) {
675  throw CommandException(std::move(e).getMessage());
676  }
677 
678  } else if (tokens[1] == "motorcontrol" && tokens.size() == 3) {
679  if (tokens[2] == "on") {
680  cassettePlayer.setMotorControl(true, time);
681  result = "Motor control enabled.";
682  } else if (tokens[2] == "off") {
683  cassettePlayer.setMotorControl(false, time);
684  result = "Motor control disabled.";
685  } else {
686  throw SyntaxError();
687  }
688 
689  } else if (tokens.size() != 2) {
690  throw SyntaxError();
691 
692  } else if (tokens[1] == "motorcontrol") {
693  result = tmpStrCat("Motor control is ",
694  (cassettePlayer.motorControl ? "on" : "off"));
695 
696  } else if (tokens[1] == "record") {
697  result = "TODO: implement this... (sorry)";
698 
699  } else if (tokens[1] == "play") {
700  if (cassettePlayer.getState() == CassettePlayer::RECORD) {
701  try {
702  result = "Play mode set, rewinding tape.";
703  cassettePlayer.playTape(
704  cassettePlayer.getImageName(), time);
705  } catch (MSXException& e) {
706  throw CommandException(std::move(e).getMessage());
707  }
708  } else if (cassettePlayer.getState() == CassettePlayer::STOP) {
709  throw CommandException("No tape inserted or tape at end!");
710  } else {
711  // PLAY mode
712  result = "Already in play mode.";
713  }
714 
715  } else if (tokens[1] == "eject") {
716  result = "Tape ejected";
717  cassettePlayer.removeTape(time);
718 
719  } else if (tokens[1] == "rewind") {
720  string r;
721  if (cassettePlayer.getState() == CassettePlayer::RECORD) {
722  try {
723  r = "First stopping recording... ";
724  cassettePlayer.playTape(
725  cassettePlayer.getImageName(), time);
726  } catch (MSXException& e) {
727  throw CommandException(std::move(e).getMessage());
728  }
729  }
730  cassettePlayer.rewind(time);
731  r += "Tape rewound";
732  result = r;
733 
734  } else if (tokens[1] == "getpos") {
735  result = cassettePlayer.getTapePos(time);
736 
737  } else if (tokens[1] == "getlength") {
738  result = cassettePlayer.getTapeLength(time);
739 
740  } else {
741  try {
742  result = "Changing tape";
743  Filename filename(tokens[1].getString(), userFileContext());
744  cassettePlayer.playTape(filename, time);
745  } catch (MSXException& e) {
746  throw CommandException(std::move(e).getMessage());
747  }
748  }
749  //if (!cassettePlayer.getConnector()) {
750  // cassettePlayer.cliComm.printWarning("Cassetteplayer not plugged in.");
751  //}
752 }
753 
754 string CassettePlayer::TapeCommand::help(std::span<const TclObject> tokens) const
755 {
756  string helptext;
757  if (tokens.size() >= 2) {
758  if (tokens[1] == "eject") {
759  helptext =
760  "Well, just eject the cassette from the cassette "
761  "player/recorder!";
762  } else if (tokens[1] == "rewind") {
763  helptext =
764  "Indeed, rewind the tape that is currently in the "
765  "cassette player/recorder...";
766  } else if (tokens[1] == "motorcontrol") {
767  helptext =
768  "Setting this to 'off' is equivalent to "
769  "disconnecting the black remote plug from the "
770  "cassette player: it makes the cassette player "
771  "run (if in play mode); the motor signal from the "
772  "MSX will be ignored. Normally this is set to "
773  "'on': the cassetteplayer obeys the motor control "
774  "signal from the MSX.";
775  } else if (tokens[1] == "play") {
776  helptext =
777  "Go to play mode. Only useful if you were in "
778  "record mode (which is currently the only other "
779  "mode available).";
780  } else if (tokens[1] == "new") {
781  helptext =
782  "Create a new cassette image. If the file name is "
783  "omitted, one will be generated in the default "
784  "directory for tape recordings. Implies going to "
785  "record mode (why else do you want a new cassette "
786  "image?).";
787  } else if (tokens[1] == "insert") {
788  helptext =
789  "Inserts the specified cassette image into the "
790  "cassette player, rewinds it and switches to play "
791  "mode.";
792  } else if (tokens[1] == "record") {
793  helptext =
794  "Go to record mode. NOT IMPLEMENTED YET. Will be "
795  "used to be able to resume recording to an "
796  "existing cassette image, previously inserted with "
797  "the insert command.";
798  } else if (tokens[1] == "getpos") {
799  helptext =
800  "Return the position of the tape, in seconds from "
801  "the beginning of the tape.";
802  } else if (tokens[1] == "getlength") {
803  helptext =
804  "Return the length of the tape in seconds.";
805  }
806  } else {
807  helptext =
808  "cassetteplayer eject "
809  ": remove tape from virtual player\n"
810  "cassetteplayer rewind "
811  ": rewind tape in virtual player\n"
812  "cassetteplayer motorcontrol "
813  ": enables or disables motor control (remote)\n"
814  "cassetteplayer play "
815  ": change to play mode (default)\n"
816  "cassetteplayer record "
817  ": change to record mode (NOT IMPLEMENTED YET)\n"
818  "cassetteplayer new [<filename>] "
819  ": create and insert new tape image file and go to record mode\n"
820  "cassetteplayer insert <filename> "
821  ": insert (a different) tape file\n"
822  "cassetteplayer getpos "
823  ": query the position of the tape\n"
824  "cassetteplayer getlength "
825  ": query the total length of the tape\n"
826  "cassetteplayer <filename> "
827  ": insert (a different) tape file\n";
828  }
829  return helptext;
830 }
831 
832 void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens) const
833 {
834  using namespace std::literals;
835  if (tokens.size() == 2) {
836  static constexpr std::array cmds = {
837  "eject"sv, "rewind"sv, "motorcontrol"sv, "insert"sv, "new"sv,
838  "play"sv, "getpos"sv, "getlength"sv,
839  //"record"sv,
840  };
841  completeFileName(tokens, userFileContext(), cmds);
842  } else if ((tokens.size() == 3) && (tokens[1] == "insert")) {
843  completeFileName(tokens, userFileContext());
844  } else if ((tokens.size() == 3) && (tokens[1] == "motorcontrol")) {
845  static constexpr std::array extra = {"on"sv, "off"sv};
846  completeString(tokens, extra);
847  }
848 }
849 
850 bool CassettePlayer::TapeCommand::needRecord(std::span<const TclObject> tokens) const
851 {
852  return tokens.size() > 1;
853 }
854 
855 
856 static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
857  { "PLAY", CassettePlayer::PLAY },
858  { "RECORD", CassettePlayer::RECORD },
859  { "STOP", CassettePlayer::STOP }
860 };
862 
863 // version 1: initial version
864 // version 2: added checksum
865 template<typename Archive>
866 void CassettePlayer::serialize(Archive& ar, unsigned version)
867 {
868  if (recordImage) {
869  // buf, sampcnt
870  flushOutput();
871  }
872 
873  ar.serialize("casImage", casImage);
874 
875  Sha1Sum oldChecksum;
876  if constexpr (!Archive::IS_LOADER) {
877  if (playImage) {
878  oldChecksum = playImage->getSha1Sum();
879  }
880  }
881  if (ar.versionAtLeast(version, 2)) {
882  string oldChecksumStr = oldChecksum.empty()
883  ? string{}
884  : oldChecksum.toString();
885  ar.serialize("checksum", oldChecksumStr);
886  oldChecksum = oldChecksumStr.empty()
887  ? Sha1Sum()
888  : Sha1Sum(oldChecksumStr);
889  }
890 
891  if constexpr (Archive::IS_LOADER) {
892  FilePool& filePool = motherBoard.getReactor().getFilePool();
893  auto time = getCurrentTime();
894  casImage.updateAfterLoadState();
895  if (!oldChecksum.empty() &&
896  !FileOperations::exists(casImage.getResolved())) {
897  auto file = filePool.getFile(FileType::TAPE, oldChecksum);
898  if (file.is_open()) {
899  casImage.setResolved(file.getURL());
900  }
901  }
902  try {
903  insertTape(casImage, time);
904  } catch (MSXException&) {
905  if (oldChecksum.empty()) {
906  // It's OK if we cannot reinsert an empty
907  // image. One likely scenario for this case is
908  // the following:
909  // - cassetteplayer new myfile.wav
910  // - don't actually start saving to tape yet
911  // - create a savestate and load that state
912  // Because myfile.wav contains no data yet, it
913  // is deleted from the filesystem. So on a
914  // loadstate it won't be found.
915  } else {
916  throw;
917  }
918  }
919 
920  if (playImage && !oldChecksum.empty()) {
921  Sha1Sum newChecksum = playImage->getSha1Sum();
922  if (oldChecksum != newChecksum) {
923  motherBoard.getMSXCliComm().printWarning(
924  "The content of the tape ",
925  casImage.getResolved(),
926  " has changed since the time this "
927  "savestate was created. This might "
928  "result in emulation problems.");
929  }
930  }
931  }
932 
933  // only for RECORD
934  //double lastX;
935  //double lastY;
936  //double partialOut;
937  //double partialInterval;
938  //std::unique_ptr<WavWriter> recordImage;
939 
940  ar.serialize("tapePos", tapePos,
941  "prevSyncTime", prevSyncTime,
942  "audioPos", audioPos,
943  "state", state,
944  "lastOutput", lastOutput,
945  "motor", motor,
946  "motorControl", motorControl);
947 
948  if constexpr (Archive::IS_LOADER) {
949  auto time = getCurrentTime();
950  if (playImage && (tapePos > playImage->getEndTime())) {
951  tapePos = playImage->getEndTime();
952  motherBoard.getMSXCliComm().printWarning("Tape position "
953  "beyond tape end! Setting tape position to end. "
954  "This can happen if you load a replay from an "
955  "older openMSX version with a different CAS-to-WAV "
956  "baud rate or when the tape image has been changed "
957  "compared to when the replay was created.");
958  }
959  if (state == RECORD) {
960  // TODO we don't support savestates in RECORD mode yet
961  motherBoard.getMSXCliComm().printWarning(
962  "Restoring a state where the MSX was saving to "
963  "tape is not yet supported. Emulation will "
964  "continue without actually saving.");
965  setState(STOP, getImageName(), time);
966  }
967  if (!playImage && (state == PLAY)) {
968  // This should only happen for manually edited
969  // savestates, though we shouldn't crash on it.
970  setState(STOP, getImageName(), time);
971  }
972  sync(time);
973  updateLoadingState(time);
974  }
975 }
978 
979 } // 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:40
const EmuDuration & param
Definition: EmuDuration.hh:28
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:241
std::string toString() const
Definition: utils/sha1.cc:229
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 setSoftwareVolume(float volume, EmuTime::param time)
Change the 'software volume' of this sound device.
Definition: SoundDevice.cc:143
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
XMLElement * setFirstChild(XMLElement *child)
Definition: XMLElement.hh:211
static_string_view
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:146
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:265
T length(const vecN< N, T > &x)
Definition: gl_vec.hh:339
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:727
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:127
EventType getType(const Event &event)
Definition: Event.hh:644
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:171
constexpr static_string_view DESCRIPTION
constexpr unsigned RECORD_FREQ
TclObject makeTclList(Args &&... args)
Definition: TclObject.hh:281
constexpr double OUTPUT_AMP
#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
#define UNREACHABLE
Definition: unreachable.hh:38
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition: xrange.hh:148