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