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