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 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, EmuTime::param time)
312 {
313  if (!filename.empty()) {
314  FilePool& filePool = motherBoard.getReactor().getFilePool();
315  try {
316  // first try WAV
317  playImage = std::make_unique<WavImage>(filename, filePool);
318  } catch (MSXException& e) {
319  try {
320  // if that fails use CAS
321  playImage = std::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  // trigger (re-)query of getAmplificationFactorImpl()
350  setSoftwareVolume(1.0f, time);
351 
352  setImageName(filename);
353 }
354 
355 void CassettePlayer::playTape(const Filename& filename, EmuTime::param time)
356 {
357  // Temporally go to STOP state:
358  // RECORD: First close the recorded image. Otherwise it goes wrong
359  // if you switch from RECORD->PLAY on the same image.
360  // PLAY: Go to stop because we temporally violate some invariants
361  // (tapePos can be beyond end-of-tape).
362  setState(STOP, getImageName(), time); // keep current image
363  insertTape(filename, time);
364  rewind(time); // sets PLAY mode
365  autoRun();
366 }
367 
368 void CassettePlayer::rewind(EmuTime::param time)
369 {
370  sync(time); // before tapePos changes
371  assert(getState() != RECORD);
372  tapePos = EmuTime::zero;
373  audioPos = 0;
374 
375  if (getImageName().empty()) {
376  // no image inserted, do nothing
377  assert(getState() == STOP);
378  } else {
379  // keep current image
380  setState(PLAY, getImageName(), time);
381  }
382  updateLoadingState(time);
383 }
384 
385 void CassettePlayer::recordTape(const Filename& filename, EmuTime::param time)
386 {
387  removeTape(time); // flush (possible) previous recording
388  recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
389  tapePos = EmuTime::zero;
390  setState(RECORD, filename, time);
391 }
392 
393 void CassettePlayer::removeTape(EmuTime::param time)
394 {
395  sync(time); // before tapePos changes
396  playImage.reset();
397  tapePos = EmuTime::zero;
398  setState(STOP, Filename(), time);
399 }
400 
401 void CassettePlayer::setMotor(bool status, EmuTime::param time)
402 {
403  if (status != motor) {
404  sync(time);
405  motor = status;
406  updateLoadingState(time);
407  }
408 }
409 
410 void CassettePlayer::setMotorControl(bool status, EmuTime::param time)
411 {
412  if (status != motorControl) {
413  sync(time);
414  motorControl = status;
415  updateLoadingState(time);
416  }
417 }
418 
419 int16_t CassettePlayer::readSample(EmuTime::param time)
420 {
421  if (getState() == PLAY) {
422  // playing
423  sync(time);
424  return isRolling() ? playImage->getSampleAt(tapePos) : 0;
425  } else {
426  // record or stop
427  return 0;
428  }
429 }
430 
431 void CassettePlayer::setSignal(bool output, EmuTime::param time)
432 {
433  sync(time);
434  lastOutput = output;
435 }
436 
437 void CassettePlayer::sync(EmuTime::param time)
438 {
439  EmuDuration duration = time - prevSyncTime;
440  prevSyncTime = time;
441 
442  updateTapePosition(duration, time);
443  generateRecordOutput(duration);
444 }
445 
446 void CassettePlayer::updateTapePosition(
447  EmuDuration::param duration, EmuTime::param time)
448 {
449  if (!isRolling() || (getState() != PLAY)) return;
450 
451  tapePos += duration;
452  assert(tapePos <= playImage->getEndTime());
453 
454  // synchronize audio with actual tape position
455  if (!syncScheduled) {
456  // don't sync too often, this improves sound quality
457  syncScheduled = true;
458  syncAudioEmu.setSyncPoint(time + EmuDuration::sec(1));
459  }
460 }
461 
462 void CassettePlayer::generateRecordOutput(EmuDuration::param duration)
463 {
464  if (!recordImage || !isRolling()) return;
465 
466  double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
467  double samples = duration.toDouble() * RECORD_FREQ;
468  double rest = 1.0 - partialInterval;
469  if (rest <= samples) {
470  // enough to fill next interval
471  partialOut += out * rest;
472  fillBuf(1, int(partialOut));
473  samples -= rest;
474 
475  // fill complete intervals
476  int count = int(samples);
477  if (count > 0) {
478  fillBuf(count, int(out));
479  }
480  samples -= count;
481 
482  // partial last interval
483  partialOut = samples * out;
484  partialInterval = 0.0;
485  } else {
486  partialOut += samples * out;
487  partialInterval += samples;
488  }
489 }
490 
491 void CassettePlayer::fillBuf(size_t length, double x)
492 {
493  assert(recordImage);
494  static const double A = 252.0 / 256.0;
495 
496  double y = lastY + (x - lastX);
497 
498  while (length) {
499  size_t len = std::min(length, BUF_SIZE - sampcnt);
500  for (size_t j = 0; j < len; ++j) {
501  buf[sampcnt++] = int(y) + 128;
502  y *= A;
503  }
504  length -= len;
505  assert(sampcnt <= BUF_SIZE);
506  if (BUF_SIZE == sampcnt) {
507  flushOutput();
508  }
509  }
510  lastY = y;
511  lastX = x;
512 }
513 
514 void CassettePlayer::flushOutput()
515 {
516  try {
517  recordImage->write(buf, 1, unsigned(sampcnt));
518  sampcnt = 0;
519  recordImage->flush(); // update wav header
520  } catch (MSXException& e) {
521  motherBoard.getMSXCliComm().printWarning(
522  "Failed to write to tape: ", e.getMessage());
523  }
524 }
525 
526 
527 const string& CassettePlayer::getName() const
528 {
529  static const string pluggableName("cassetteplayer");
530  return pluggableName;
531 }
532 
534 {
535  // TODO: this description is not entirely accurate, but it is used
536  // as an identifier for this audio device in e.g. Catapult. We should
537  // use another way to identify audio devices A.S.A.P.!
538 
539  return "Cassetteplayer, use to read .cas or .wav files.";
540 }
541 
542 void CassettePlayer::plugHelper(Connector& conn, EmuTime::param time)
543 {
544  sync(time);
545  lastOutput = static_cast<CassettePort&>(conn).lastOut();
546 }
547 
548 void CassettePlayer::unplugHelper(EmuTime::param time)
549 {
550  // note: may not throw exceptions
551  setState(STOP, getImageName(), time); // keep current image
552 }
553 
554 
555 void CassettePlayer::generateChannels(float** buffers, unsigned num)
556 {
557  // Single channel device: replace content of buffers[0] (not add to it).
558  if ((getState() != PLAY) || !isRolling()) {
559  buffers[0] = nullptr;
560  return;
561  }
562  playImage->fillBuffer(audioPos, buffers, num);
563  audioPos += num;
564 }
565 
567 {
568  return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
569 }
570 
571 int CassettePlayer::signalEvent(const std::shared_ptr<const Event>& event)
572 {
573  if (event->getType() == OPENMSX_BOOT_EVENT) {
574  if (!getImageName().empty()) {
575  // Reinsert tape to make sure everything is reset.
576  try {
577  playTape(getImageName(), getCurrentTime());
578  } catch (MSXException& e) {
579  motherBoard.getMSXCliComm().printWarning(
580  "Failed to insert tape: ", e.getMessage());
581  }
582  }
583  }
584  return 0;
585 }
586 
587 void CassettePlayer::execEndOfTape(EmuTime::param time)
588 {
589  // tape ended
590  sync(time);
591  assert(tapePos == playImage->getEndTime());
592  motherBoard.getMSXCliComm().printWarning(
593  "Tape end reached... stopping. "
594  "You may need to insert another tape image "
595  "that contains side B. (Or you used the wrong "
596  "loading command.)");
597  setState(STOP, getImageName(), time); // keep current image
598 }
599 
600 void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
601 {
602  if (getState() == PLAY) {
603  updateStream(time);
604  sync(time);
605  DynamicClock clk(EmuTime::zero);
606  clk.setFreq(playImage->getFrequency());
607  audioPos = clk.getTicksTill(tapePos);
608  }
609  syncScheduled = false;
610 }
611 
612 
613 // class TapeCommand
614 
615 CassettePlayer::TapeCommand::TapeCommand(
616  CommandController& commandController_,
617  StateChangeDistributor& stateChangeDistributor_,
618  Scheduler& scheduler_)
619  : RecordedCommand(commandController_, stateChangeDistributor_,
620  scheduler_, "cassetteplayer")
621 {
622 }
623 
624 void CassettePlayer::TapeCommand::execute(
625  span<const TclObject> tokens, TclObject& result, EmuTime::param time)
626 {
627  auto& cassettePlayer = OUTER(CassettePlayer, tapeCommand);
628  if (tokens.size() == 1) {
629  // Returning Tcl lists here, similar to the disk commands in
630  // DiskChanger
631  TclObject options = makeTclList(cassettePlayer.getStateString());
632  result.addListElement(getName() + ':',
633  cassettePlayer.getImageName().getResolved(),
634  options);
635 
636  } else if (tokens[1] == "new") {
637  string directory = "taperecordings";
638  string prefix = "openmsx";
639  string extension = ".wav";
641  (tokens.size() == 3) ? tokens[2].getString() : string{},
642  directory, prefix, extension);
643  cassettePlayer.recordTape(Filename(filename), time);
644  result = strCat(
645  "Created new cassette image file: ", filename,
646  ", inserted it and set recording mode.");
647 
648  } else if (tokens[1] == "insert" && tokens.size() == 3) {
649  try {
650  result = "Changing tape";
651  Filename filename(tokens[2].getString().str(), userFileContext());
652  cassettePlayer.playTape(filename, time);
653  } catch (MSXException& e) {
654  throw CommandException(std::move(e).getMessage());
655  }
656 
657  } else if (tokens[1] == "motorcontrol" && tokens.size() == 3) {
658  if (tokens[2] == "on") {
659  cassettePlayer.setMotorControl(true, time);
660  result = "Motor control enabled.";
661  } else if (tokens[2] == "off") {
662  cassettePlayer.setMotorControl(false, time);
663  result = "Motor control disabled.";
664  } else {
665  throw SyntaxError();
666  }
667 
668  } else if (tokens.size() != 2) {
669  throw SyntaxError();
670 
671  } else if (tokens[1] == "motorcontrol") {
672  result = strCat("Motor control is ",
673  (cassettePlayer.motorControl ? "on" : "off"));
674 
675  } else if (tokens[1] == "record") {
676  result = "TODO: implement this... (sorry)";
677 
678  } else if (tokens[1] == "play") {
679  if (cassettePlayer.getState() == CassettePlayer::RECORD) {
680  try {
681  result = "Play mode set, rewinding tape.";
682  cassettePlayer.playTape(
683  cassettePlayer.getImageName(), time);
684  } catch (MSXException& e) {
685  throw CommandException(std::move(e).getMessage());
686  }
687  } else if (cassettePlayer.getState() == CassettePlayer::STOP) {
688  throw CommandException("No tape inserted or tape at end!");
689  } else {
690  // PLAY mode
691  result = "Already in play mode.";
692  }
693 
694  } else if (tokens[1] == "eject") {
695  result = "Tape ejected";
696  cassettePlayer.removeTape(time);
697 
698  } else if (tokens[1] == "rewind") {
699  string r;
700  if (cassettePlayer.getState() == CassettePlayer::RECORD) {
701  try {
702  r = "First stopping recording... ";
703  cassettePlayer.playTape(
704  cassettePlayer.getImageName(), time);
705  } catch (MSXException& e) {
706  throw CommandException(std::move(e).getMessage());
707  }
708  }
709  cassettePlayer.rewind(time);
710  r += "Tape rewound";
711  result = r;
712 
713  } else if (tokens[1] == "getpos") {
714  result = cassettePlayer.getTapePos(time);
715 
716  } else if (tokens[1] == "getlength") {
717  result = cassettePlayer.getTapeLength(time);
718 
719  } else {
720  try {
721  result = "Changing tape";
722  Filename filename(tokens[1].getString().str(), userFileContext());
723  cassettePlayer.playTape(filename, time);
724  } catch (MSXException& e) {
725  throw CommandException(std::move(e).getMessage());
726  }
727  }
728  //if (!cassettePlayer.getConnector()) {
729  // cassettePlayer.cliComm.printWarning("Cassetteplayer not plugged in.");
730  //}
731 }
732 
733 string CassettePlayer::TapeCommand::help(const vector<string>& tokens) const
734 {
735  string helptext;
736  if (tokens.size() >= 2) {
737  if (tokens[1] == "eject") {
738  helptext =
739  "Well, just eject the cassette from the cassette "
740  "player/recorder!";
741  } else if (tokens[1] == "rewind") {
742  helptext =
743  "Indeed, rewind the tape that is currently in the "
744  "cassette player/recorder...";
745  } else if (tokens[1] == "motorcontrol") {
746  helptext =
747  "Setting this to 'off' is equivalent to "
748  "disconnecting the black remote plug from the "
749  "cassette player: it makes the cassette player "
750  "run (if in play mode); the motor signal from the "
751  "MSX will be ignored. Normally this is set to "
752  "'on': the cassetteplayer obeys the motor control "
753  "signal from the MSX.";
754  } else if (tokens[1] == "play") {
755  helptext =
756  "Go to play mode. Only useful if you were in "
757  "record mode (which is currently the only other "
758  "mode available).";
759  } else if (tokens[1] == "new") {
760  helptext =
761  "Create a new cassette image. If the file name is "
762  "omitted, one will be generated in the default "
763  "directory for tape recordings. Implies going to "
764  "record mode (why else do you want a new cassette "
765  "image?).";
766  } else if (tokens[1] == "insert") {
767  helptext =
768  "Inserts the specified cassette image into the "
769  "cassette player, rewinds it and switches to play "
770  "mode.";
771  } else if (tokens[1] == "record") {
772  helptext =
773  "Go to record mode. NOT IMPLEMENTED YET. Will be "
774  "used to be able to resume recording to an "
775  "existing cassette image, previously inserted with "
776  "the insert command.";
777  } else if (tokens[1] == "getpos") {
778  helptext =
779  "Return the position of the tape, in seconds from "
780  "the beginning of the tape.";
781  } else if (tokens[1] == "getlength") {
782  helptext =
783  "Return the length of the tape in seconds.";
784  }
785  } else {
786  helptext =
787  "cassetteplayer eject "
788  ": remove tape from virtual player\n"
789  "cassetteplayer rewind "
790  ": rewind tape in virtual player\n"
791  "cassetteplayer motorcontrol "
792  ": enables or disables motor control (remote)\n"
793  "cassetteplayer play "
794  ": change to play mode (default)\n"
795  "cassetteplayer record "
796  ": change to record mode (NOT IMPLEMENTED YET)\n"
797  "cassetteplayer new [<filename>] "
798  ": create and insert new tape image file and go to record mode\n"
799  "cassetteplayer insert <filename> "
800  ": insert (a different) tape file\n"
801  "cassetteplayer getpos "
802  ": query the position of the tape\n"
803  "cassetteplayer getlength "
804  ": query the total length of the tape\n"
805  "cassetteplayer <filename> "
806  ": insert (a different) tape file\n";
807  }
808  return helptext;
809 }
810 
811 void CassettePlayer::TapeCommand::tabCompletion(vector<string>& tokens) const
812 {
813  if (tokens.size() == 2) {
814  static const char* const cmds[] = {
815  "eject", "rewind", "motorcontrol", "insert", "new",
816  "play", "getpos", "getlength",
817  //"record",
818  };
819  completeFileName(tokens, userFileContext(), cmds);
820  } else if ((tokens.size() == 3) && (tokens[1] == "insert")) {
821  completeFileName(tokens, userFileContext());
822  } else if ((tokens.size() == 3) && (tokens[1] == "motorcontrol")) {
823  static const char* const extra[] = { "on", "off" };
824  completeString(tokens, extra);
825  }
826 }
827 
828 bool CassettePlayer::TapeCommand::needRecord(span<const TclObject> tokens) const
829 {
830  return tokens.size() > 1;
831 }
832 
833 
834 static std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
835  { "PLAY", CassettePlayer::PLAY },
836  { "RECORD", CassettePlayer::RECORD },
837  { "STOP", CassettePlayer::STOP }
838 };
840 
841 // version 1: initial version
842 // version 2: added checksum
843 template<typename Archive>
844 void CassettePlayer::serialize(Archive& ar, unsigned version)
845 {
846  if (recordImage) {
847  // buf, sampcnt
848  flushOutput();
849  }
850 
851  ar.serialize("casImage", casImage);
852 
853  Sha1Sum oldChecksum;
854  if (!ar.isLoader() && playImage) {
855  oldChecksum = playImage->getSha1Sum();
856  }
857  if (ar.versionAtLeast(version, 2)) {
858  string oldChecksumStr = oldChecksum.empty()
859  ? string{}
860  : oldChecksum.toString();
861  ar.serialize("checksum", oldChecksumStr);
862  oldChecksum = oldChecksumStr.empty()
863  ? Sha1Sum()
864  : Sha1Sum(oldChecksumStr);
865  }
866 
867  if (ar.isLoader()) {
868  FilePool& filePool = motherBoard.getReactor().getFilePool();
869  auto time = getCurrentTime();
870  removeTape(time);
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  ar.serialize("prevSyncTime", prevSyncTime);
919  ar.serialize("audioPos", audioPos);
920  ar.serialize("state", state);
921  ar.serialize("lastOutput", lastOutput);
922  ar.serialize("motor", motor);
923  ar.serialize("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
SERIALIZE_ENUM(CassettePlayer::State, stateInfo)
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:343
void setSoftwareVolume(float volume, EmuTime::param time)
Change the &#39;software volume&#39; of this sound device.
Definition: SoundDevice.cc:140
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
Represents something you can plug devices into.
Definition: Connector.hh:20
const std::string & getMessage() const &
Definition: MSXException.hh:23
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:269
File getFile(FileType fileType, const Sha1Sum &sha1sum)
Search file with the given sha1sum.
Definition: FilePool.cc:302
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void unregisterSound()
Unregisters this sound device with the Mixer.
Definition: SoundDevice.cc:130
Definition: span.hh:34
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:79
void updateStream(EmuTime::param time)
Definition: SoundDevice.cc:135
void serialize(Archive &ar, unsigned version)
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:160
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:244
This class represents the result of a sha1 calculation (a 160-bit value).
Definition: sha1.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:108
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
float getAmplificationFactorImpl() const override
Get amplification/attenuation factor for this device.
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()
void generateChannels(float **buffers, unsigned num) override
Abstract method to generate the actual sound data.
bool exists(string_view filename)
Does this file (directory) exists?
auto count(InputRange &&range, const T &value)
Definition: ranges.hh:197
FilePool & getFilePool()
Definition: Reactor.hh:88
void unplugHelper(EmuTime::param time) override
void setSignal(bool output, EmuTime::param time) override
Sets the cassette output signal false = low true = high.
std::string toString() const
Definition: utils/sha1.cc:232
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:16
void addListElement(T t)
Definition: TclObject.hh:121
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:840
const std::string & getResolved() const
Definition: Filename.hh:27
string getName(KeyCode keyCode)
Translate key code to key name.
Definition: Keys.cc:589
virtual void update(UpdateType type, string_view name, string_view value)=0
std::string strCat(Ts &&...ts)
Definition: strCat.hh:577
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
TclObject makeTclList(Args &&... args)
Definition: TclObject.hh:280
void printWarning(string_view message)
Definition: CliComm.cc:20
unsigned getInputRate() const
Definition: SoundDevice.hh:109
void registerSound(const DeviceConfig &config)
Registers this sound device with the Mixer.
Definition: SoundDevice.cc:89
#define UNREACHABLE
Definition: unreachable.hh:38