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