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