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