56 using std::unique_ptr;
62 static const unsigned RECORD_FREQ = 44100;
63 static const double OUTPUT_AMP = 60.0;
78 virtual string help(
const vector<string>& tokens)
const;
80 virtual bool needRecord(
const vector<string>& tokens)
const;
96 ,
Schedulable(hwConf.getMotherBoard().getScheduler())
100 , motherBoard(hwConf.getMotherBoard())
102 motherBoard.getCommandController(),
103 motherBoard.getStateChangeDistributor(),
104 motherBoard.getScheduler(), *this))
106 motherBoard.getReactor().getGlobalSettings().getThrottleManager()))
108 motherBoard.getCommandController(),
109 "autoruncassettes",
"automatically try to run cassettes", true))
113 , motor(false), motorControl(true)
114 , syncScheduled(false)
139 void CassettePlayer::autoRun()
141 if (!playImage.get())
return;
148 string loadingInstruction;
151 loadingInstruction =
"RUN\\\"CAS:\\\"";
154 loadingInstruction =
"BLOAD\\\"CAS:\\\",R";
157 loadingInstruction =
"CLOAD\\rRUN";
162 string var =
"::temp_bp_for_auto_run";
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";
174 }
catch (CommandException& e) {
176 "Error executing loading instruction using command \"" + command +
"\" for AutoRun: " +
177 e.getMessage() +
"\n Please report a bug.");
186 string CassettePlayer::getStateString()
const
188 switch (getState()) {
189 case PLAY:
return "play";
190 case RECORD:
return "record";
191 case STOP:
return "stop";
196 bool CassettePlayer::isRolling()
const
203 return (getState() !=
STOP) && (motor || !motorControl);
214 if (playImage.get()) {
216 }
else if (getState() ==
RECORD) {
217 return getTapePos(time);
223 void CassettePlayer::checkInvariants()
const
225 switch (getState()) {
227 assert(!recordImage.get());
228 if (playImage.get()) {
230 assert(!getImageName().empty());
236 assert(!getImageName().empty());
237 assert(!recordImage.get());
238 assert(playImage.get());
241 assert(!getImageName().empty());
242 assert(recordImage.get());
243 assert(!playImage.get());
250 void CassettePlayer::setState(State newState,
const Filename& newImage,
256 State oldState = getState();
257 if (oldState == newState)
return;
261 assert(!((oldState ==
PLAY) && (newState ==
RECORD)));
262 assert(!((oldState ==
RECORD) && (newState ==
PLAY)));
266 if ((oldState ==
RECORD) && recordImage.get()) {
268 bool empty = recordImage.get()->isEmpty();
279 setImageName(newImage);
284 partialInterval = 0.0;
285 lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
291 updateLoadingState(time);
300 loadingIndicator->update(motor && (getState() ==
PLAY));
303 if (isRolling() && (getState() ==
PLAY)) {
308 void CassettePlayer::setImageName(
const Filename& newImage)
315 const Filename& CassettePlayer::getImageName()
const
320 void CassettePlayer::insertTape(
const Filename& filename)
322 if (!filename.empty()) {
326 playImage = make_unique<WavImage>(filename, filePool);
327 }
catch (MSXException& e) {
330 playImage = make_unique<CasImage>(
333 }
catch (MSXException& e2) {
335 "Failed to insert WAV image: \"" +
337 "\" and also failed to insert CAS image: \"" +
338 e2.getMessage() +
'\"');
352 unsigned inputRate = playImage.get() ? playImage->getFrequency()
359 setImageName(filename);
364 if (getState() ==
RECORD) {
367 setState(
STOP, getImageName(), time);
369 insertTape(filename);
376 assert(getState() !=
RECORD);
380 if (getImageName().empty()) {
382 assert(getState() ==
STOP);
385 setState(
PLAY, getImageName(), time);
392 recordImage = make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
394 setState(
RECORD, filename, time);
406 if (status != motor) {
409 updateLoadingState(time);
413 void CassettePlayer::setMotorControl(
bool status,
EmuTime::param time)
415 if (status != motorControl) {
417 motorControl = status;
418 updateLoadingState(time);
424 if (getState() ==
PLAY) {
427 return isRolling() ? playImage->getSampleAt(tapePos) : 0;
445 updateTapePosition(duration, time);
446 generateRecordOutput(duration);
449 void CassettePlayer::updateTapePosition(
452 if (!isRolling())
return;
457 if ((getState() ==
PLAY) && !syncScheduled) {
459 syncScheduled =
true;
466 if (!recordImage.get() || !isRolling())
return;
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) {
473 partialOut += out * rest;
474 fillBuf(1,
int(partialOut));
478 int count = int(samples);
480 fillBuf(count,
int(out));
485 partialOut = samples * out;
486 partialInterval = 0.0;
488 partialOut += samples * out;
489 partialInterval += samples;
493 void CassettePlayer::fillBuf(
size_t length,
double x)
495 assert(recordImage.get());
496 static const double A = 252.0 / 256.0;
498 double y = lastY + (x - lastX);
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;
507 assert(sampcnt <= BUF_SIZE);
508 if (BUF_SIZE == sampcnt) {
516 void CassettePlayer::flushOutput()
519 recordImage->write(buf, 1,
unsigned(sampcnt));
521 recordImage->flush();
522 }
catch (MSXException& e) {
524 "Failed to write to tape: " + e.getMessage());
531 static const string name(
"cassetteplayer");
541 return "Cassetteplayer, use to read .cas or .wav files.";
547 lastOutput =
static_cast<CassettePort&
>(connector).lastOut();
553 setState(
STOP, getImageName(), time);
559 if ((getState() !=
PLAY) || !isRolling()) {
560 buffers[0] =
nullptr;
566 playImage->fillBuffer(audioPos, buffers, num);
571 int CassettePlayer::signalEvent(
const std::shared_ptr<const Event>& event)
574 if (!getImageName().
empty()) {
578 }
catch (MSXException& e) {
580 "Failed to insert tape: " + e.getMessage());
587 void CassettePlayer::executeUntil(
EmuTime::param time,
int userData)
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);
600 if (getState() ==
PLAY) {
604 clk.setFreq(playImage->getFrequency());
605 audioPos = clk.getTicksTill(tapePos);
607 syncScheduled =
false;
620 scheduler,
"cassetteplayer")
621 , cassettePlayer(cassettePlayer_)
628 if (tokens.size() == 1) {
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.";
652 }
else if (tokens[1] ==
"insert" && tokens.size() == 3) {
654 result <<
"Changing tape";
656 cassettePlayer.playTape(filename, time);
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.";
672 }
else if (tokens.size() != 2) {
675 }
else if (tokens[1] ==
"motorcontrol") {
676 result <<
"Motor control is "
677 << (cassettePlayer.motorControl ?
"on" :
"off");
679 }
else if (tokens[1] ==
"record") {
680 result <<
"TODO: implement this... (sorry)";
682 }
else if (tokens[1] ==
"play") {
685 result <<
"Play mode set, rewinding tape.";
686 cassettePlayer.playTape(
687 cassettePlayer.getImageName(), time);
695 result <<
"Already in play mode.";
698 }
else if (tokens[1] ==
"eject") {
699 result <<
"Tape ejected";
700 cassettePlayer.removeTape(time);
702 }
else if (tokens[1] ==
"rewind") {
705 result <<
"First stopping recording... ";
706 cassettePlayer.playTape(
707 cassettePlayer.getImageName(), time);
712 cassettePlayer.rewind(time);
713 result <<
"Tape rewound";
715 }
else if (tokens[1] ==
"getpos") {
716 result << cassettePlayer.getTapePos(time);
718 }
else if (tokens[1] ==
"getlength") {
719 result << cassettePlayer.getTapeLength(time);
723 result <<
"Changing tape";
725 cassettePlayer.playTape(filename, time);
739 if (tokens.size() >= 2) {
740 if (tokens[1] ==
"eject") {
742 "Well, just eject the cassette from the cassette "
744 }
else if (tokens[1] ==
"rewind") {
746 "Indeed, rewind the tape that is currently in the "
747 "cassette player/recorder...";
748 }
else if (tokens[1] ==
"motorcontrol") {
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") {
759 "Go to play mode. Only useful if you were in "
760 "record mode (which is currently the only other "
762 }
else if (tokens[1] ==
"new") {
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 "
769 }
else if (tokens[1] ==
"insert") {
771 "Inserts the specified cassette image into the "
772 "cassette player, rewinds it and switches to play "
774 }
else if (tokens[1] ==
"record") {
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") {
782 "Return the position of the tape, in seconds from "
783 "the beginning of the tape.";
784 }
else if (tokens[1] ==
"getlength") {
786 "Return the length of the tape in seconds.";
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";
816 if (tokens.size() == 2) {
817 static const char*
const cmds[] = {
818 "eject",
"rewind",
"motorcontrol",
"insert",
"new",
819 "play",
"getpos",
"getlength",
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" };
833 return tokens.size() > 1;
846 template<
typename Archive>
849 if (recordImage.get()) {
854 ar.serialize(
"casImage", casImage);
857 if (!ar.isLoader() && playImage.get()) {
858 oldChecksum = playImage->getSha1Sum();
860 if (ar.versionAtLeast(version, 2)) {
861 string oldChecksumStr = oldChecksum.
empty()
864 ar.serialize(
"checksum", oldChecksumStr);
865 oldChecksum = oldChecksumStr.
empty()
874 if (!oldChecksum.
empty() &&
876 std::unique_ptr<File> file = filePool.
getFile(
883 insertTape(casImage);
885 if (oldChecksum.
empty()) {
900 if (playImage.get() && !oldChecksum.
empty()) {
901 Sha1Sum newChecksum = playImage->getSha1Sum();
902 if (oldChecksum != newChecksum) {
904 "The content of the tape " +
906 " has changed since the time this "
907 "savestate was created. This might "
908 "result in emulation problems.");
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);
932 "Restoring a state where the MSX was saving to "
933 "tape is not yet supported. Emulation will "
934 "continue without actually saving.");
937 if (!playImage.get() && (state ==
PLAY)) {