62static constexpr static_string_view DESCRIPTION =
"Cassetteplayer, use to read .cas or .wav files.";
64static constexpr unsigned DUMMY_INPUT_RATE = 44100;
65static constexpr unsigned RECORD_FREQ = 44100;
66static constexpr double RECIP_RECORD_FREQ = 1.0 / RECORD_FREQ;
67static constexpr double OUTPUT_AMP = 60.0;
69static std::string_view getCassettePlayerName()
71 return "cassetteplayer";
75 :
ResampledSoundDevice(hwConf.getMotherBoard(), getCassettePlayerName(), DESCRIPTION, 1, DUMMY_INPUT_RATE, false)
76 , syncEndOfTape(hwConf.getMotherBoard().getScheduler())
77 , syncAudioEmu (hwConf.getMotherBoard().getScheduler())
78 , motherBoard(hwConf.getMotherBoard())
80 motherBoard.getCommandController(),
81 motherBoard.getStateChangeDistributor(),
82 motherBoard.getScheduler())
84 motherBoard.getReactor().getGlobalSettings().getThrottleManager())
86 motherBoard.getCommandController(),
87 "autoruncassettes",
"automatically try to run cassettes", true)
91 XMLElement* result = doc.allocateElement(
"cassetteplayer");
101 removeTape(EmuTime::zero());
108 c->unplug(getCurrentTime());
117 "state", getStateString(),
118 "position", getTapePos(getCurrentTime()),
119 "length", getTapeLength(getCurrentTime()),
120 "motorcontrol", motorControl);
123void CassettePlayer::autoRun()
125 if (!playImage)
return;
139 string H_READ = is_SVI ?
"0xFE8E" :
"0xFF07";
140 string H_MAIN = is_SVI ?
"0xFE94" :
"0xFF0C";
141 string instr1, instr2;
144 instr1 = R
"({RUN\"CAS:\"\r})";
147 instr1 = R
"({BLOAD\"CAS:\",R\r})";
152 instr1 =
"{CLOAD\\r}";
159 "namespace eval ::openmsx {\n"
160 " variable auto_run_bp\n"
162 " proc auto_run_cb {args} {\n"
163 " variable auto_run_bp\n"
164 " debug remove_bp $auto_run_bp\n"
165 " unset auto_run_bp\n"
172 " after time 0.2 \"type [lindex $args 0]\"\n"
174 " set next [lrange $args 1 end]\n"
175 " if {[llength $next] == 0} return\n"
179 " set cmd \"openmsx::auto_run_cb $next\"\n"
180 " set openmsx::auto_run_bp [debug set_bp ", H_MAIN,
" 1 \"$cmd\"]\n"
183 " if {[info exists auto_run_bp]} {debug remove_bp $auto_run_bp\n}\n"
184 " set auto_run_bp [debug set_bp ", H_READ,
" 1 {\n"
185 " openmsx::auto_run_cb {{}} ", instr1,
' ', instr2,
"\n"
189 " type_via_keyboard \'\\r\n"
193 }
catch (CommandException& e) {
195 "Error executing loading instruction using command \"",
196 command,
"\" for AutoRun: ",
197 e.getMessage(),
"\n Please report a bug.");
201string CassettePlayer::getStateString()
const
203 switch (getState()) {
204 case PLAY:
return "play";
205 case RECORD:
return "record";
206 case STOP:
return "stop";
211bool CassettePlayer::isRolling()
const
218 return (getState() !=
STOP) && (motor || !motorControl);
221double CassettePlayer::getTapePos(EmuTime::param time)
224 if (getState() ==
RECORD) {
226 return (
double(recordImage->getBytes()) + partialInterval) * RECIP_RECORD_FREQ;
228 return (tapePos - EmuTime::zero()).toDouble();
232void CassettePlayer::setTapePos(EmuTime::param time,
double newPos)
234 assert(getState() !=
RECORD);
236 auto pos = std::clamp(newPos, 0.0, getTapeLength(time));
237 tapePos = EmuTime::zero() + EmuDuration(pos);
241double CassettePlayer::getTapeLength(EmuTime::param time)
244 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
245 }
else if (getState() ==
RECORD) {
246 return getTapePos(time);
252void CassettePlayer::checkInvariants()
const
254 switch (getState()) {
256 assert(!recordImage);
259 assert(!getImageName().empty());
265 assert(!getImageName().empty());
266 assert(!recordImage);
270 assert(!getImageName().empty());
279void CassettePlayer::setState(State newState,
const Filename& newImage,
285 State oldState = getState();
286 if (oldState == newState)
return;
290 assert(!((oldState ==
PLAY) && (newState ==
RECORD)));
291 assert(!((oldState ==
RECORD) && (newState ==
PLAY)));
295 if ((oldState ==
RECORD) && recordImage) {
297 bool empty = recordImage->isEmpty();
308 setImageName(newImage);
313 partialInterval = 0.0;
314 lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
320 updateLoadingState(time);
325void CassettePlayer::updateLoadingState(EmuTime::param time)
327 assert(prevSyncTime == time);
330 loadingIndicator.
update(motor && (getState() ==
PLAY));
332 syncEndOfTape.removeSyncPoint();
333 if (isRolling() && (getState() ==
PLAY)) {
334 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
338void CassettePlayer::setImageName(
const Filename& newImage)
345void CassettePlayer::insertTape(
const Filename& filename, EmuTime::param time)
347 if (!filename.empty()) {
351 playImage = std::make_unique<WavImage>(filename, filePool);
352 }
catch (MSXException& e) {
355 playImage = std::make_unique<CasImage>(
358 }
catch (MSXException& e2) {
360 "Failed to insert WAV image: \"",
362 "\" and also failed to insert CAS image: \"",
363 e2.getMessage(),
'\"');
377 if (
unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
386 setImageName(filename);
389void CassettePlayer::playTape(
const Filename& filename, EmuTime::param time)
396 setState(
STOP, getImageName(), time);
397 insertTape(filename, time);
401void CassettePlayer::rewind(EmuTime::param time)
404 assert(getState() !=
RECORD);
405 tapePos = EmuTime::zero();
411void CassettePlayer::wind(EmuTime::param time)
413 if (getImageName().empty()) {
415 assert(getState() ==
STOP);
418 setState(
PLAY, getImageName(), time);
420 updateLoadingState(time);
423void CassettePlayer::recordTape(
const Filename& filename, EmuTime::param time)
426 recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
427 tapePos = EmuTime::zero();
428 setState(
RECORD, filename, time);
431void CassettePlayer::removeTape(EmuTime::param time)
434 setState(
STOP, getImageName(), time);
437 tapePos = EmuTime::zero();
443 if (status != motor) {
446 updateLoadingState(time);
450void CassettePlayer::setMotorControl(
bool status, EmuTime::param time)
452 if (status != motorControl) {
454 motorControl = status;
455 updateLoadingState(time);
461 if (getState() ==
PLAY) {
464 return isRolling() ? playImage->getSampleAt(tapePos) : int16_t(0);
477void CassettePlayer::sync(EmuTime::param time)
482 updateTapePosition(duration, time);
483 generateRecordOutput(duration);
486void CassettePlayer::updateTapePosition(
489 if (!isRolling() || (getState() !=
PLAY))
return;
492 assert(tapePos <= playImage->getEndTime());
495 if (!syncScheduled) {
497 syncScheduled =
true;
504 if (!recordImage || !isRolling())
return;
506 double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
507 double samples = duration.toDouble() * RECORD_FREQ;
508 if (
auto rest = 1.0 - partialInterval; rest <= samples) {
510 partialOut += out * rest;
511 fillBuf(1, partialOut);
515 auto count = int(samples);
520 assert(samples < 1.0);
523 partialOut = samples * out;
524 partialInterval = samples;
526 assert(samples < 1.0);
527 partialOut += samples * out;
528 partialInterval += samples;
530 assert(partialInterval < 1.0);
533void CassettePlayer::fillBuf(
size_t length,
double x)
536 static constexpr double A = 252.0 / 256.0;
538 double y = lastY + (x - lastX);
541 size_t len = std::min(length, buf.size() - sampCnt);
543 buf[sampCnt++] = narrow<uint8_t>(
int(y) + 128);
547 assert(sampCnt <= buf.size());
548 if (sampCnt == buf.size()) {
556void CassettePlayer::flushOutput()
559 recordImage->write(
subspan(buf, 0, sampCnt));
561 recordImage->flush();
562 }
catch (MSXException& e) {
564 "Failed to write to tape: ",
e.getMessage());
571 return getCassettePlayerName();
582 lastOutput = checked_cast<CassettePort&>(conn).lastOut();
588 setState(
STOP, getImageName(), time);
595 assert(buffers.size() == 1);
596 if ((getState() !=
PLAY) || !isRolling()) {
597 buffers[0] =
nullptr;
600 assert(buffers.size() == 1);
601 playImage->fillBuffer(audioPos, buffers.first<1>(), num);
607 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
610void CassettePlayer::execEndOfTape(EmuTime::param time)
614 assert(tapePos == playImage->getEndTime());
616 "Tape end reached... stopping. "
617 "You may need to insert another tape image "
618 "that contains side B. (Or you used the wrong "
619 "loading command.)");
620 setState(
STOP, getImageName(), time);
623void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
625 if (getState() ==
PLAY) {
628 DynamicClock clk(EmuTime::zero());
629 clk.setFreq(playImage->getFrequency());
630 audioPos = clk.getTicksTill(tapePos);
632 syncScheduled =
false;
638CassettePlayer::TapeCommand::TapeCommand(
639 CommandController& commandController_,
640 StateChangeDistributor& stateChangeDistributor_,
641 Scheduler& scheduler_)
642 : RecordedCommand(commandController_, stateChangeDistributor_,
643 scheduler_,
"cassetteplayer")
647void CassettePlayer::TapeCommand::execute(
648 std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
650 auto& cassettePlayer =
OUTER(CassettePlayer, tapeCommand);
652 auto stopRecording = [&] {
655 cassettePlayer.playTape(cassettePlayer.getImageName(), time);
657 }
catch (MSXException& e) {
658 throw CommandException(std::move(e).getMessage());
664 if (tokens.size() == 1) {
667 TclObject options =
makeTclList(cassettePlayer.getStateString());
668 result.addListElement(
tmpStrCat(getName(),
':'),
669 cassettePlayer.getImageName().getResolved(),
672 }
else if (tokens[1] ==
"new") {
673 std::string_view prefix =
"openmsx";
675 (tokens.size() == 3) ? tokens[2].getString() : string{},
676 TAPE_RECORDING_DIR, prefix, TAPE_RECORDING_EXTENSION);
677 cassettePlayer.recordTape(
Filename(filename), time);
679 "Created new cassette image file: ", filename,
680 ", inserted it and set recording mode.");
682 }
else if (tokens[1] ==
"insert" && tokens.size() == 3) {
684 result =
"Changing tape";
686 cassettePlayer.playTape(filename, time);
687 }
catch (MSXException& e) {
688 throw CommandException(std::move(e).getMessage());
691 }
else if (tokens[1] ==
"motorcontrol" && tokens.size() == 3) {
692 if (tokens[2] ==
"on") {
693 cassettePlayer.setMotorControl(
true, time);
694 result =
"Motor control enabled.";
695 }
else if (tokens[2] ==
"off") {
696 cassettePlayer.setMotorControl(
false, time);
697 result =
"Motor control disabled.";
702 }
else if (tokens[1] ==
"setpos" && tokens.size() == 3) {
704 cassettePlayer.setTapePos(time, tokens[2].getDouble(getInterpreter()));
706 }
else if (tokens.size() != 2) {
709 }
else if (tokens[1] ==
"motorcontrol") {
711 (cassettePlayer.motorControl ?
"on" :
"off"));
713 }
else if (tokens[1] ==
"record") {
714 result =
"TODO: implement this... (sorry)";
716 }
else if (tokens[1] ==
"play") {
717 if (stopRecording()) {
718 result =
"Play mode set, rewinding tape.";
720 throw CommandException(
"No tape inserted or tape at end!");
723 result =
"Already in play mode.";
726 }
else if (tokens[1] ==
"eject") {
727 result =
"Tape ejected";
728 cassettePlayer.removeTape(time);
730 }
else if (tokens[1] ==
"rewind") {
731 string r = stopRecording() ?
"First stopping recording... " :
"";
732 cassettePlayer.rewind(time);
736 }
else if (tokens[1] ==
"getpos") {
737 result = cassettePlayer.getTapePos(time);
739 }
else if (tokens[1] ==
"getlength") {
740 result = cassettePlayer.getTapeLength(time);
744 result =
"Changing tape";
746 cassettePlayer.playTape(filename, time);
747 }
catch (MSXException& e) {
748 throw CommandException(std::move(e).getMessage());
756string CassettePlayer::TapeCommand::help(std::span<const TclObject> tokens)
const
759 if (tokens.size() >= 2) {
760 if (tokens[1] ==
"eject") {
762 "Well, just eject the cassette from the cassette "
764 }
else if (tokens[1] ==
"rewind") {
766 "Indeed, rewind the tape that is currently in the "
767 "cassette player/recorder...";
768 }
else if (tokens[1] ==
"motorcontrol") {
770 "Setting this to 'off' is equivalent to "
771 "disconnecting the black remote plug from the "
772 "cassette player: it makes the cassette player "
773 "run (if in play mode); the motor signal from the "
774 "MSX will be ignored. Normally this is set to "
775 "'on': the cassetteplayer obeys the motor control "
776 "signal from the MSX.";
777 }
else if (tokens[1] ==
"play") {
779 "Go to play mode. Only useful if you were in "
780 "record mode (which is currently the only other "
782 }
else if (tokens[1] ==
"new") {
784 "Create a new cassette image. If the file name is "
785 "omitted, one will be generated in the default "
786 "directory for tape recordings. Implies going to "
787 "record mode (why else do you want a new cassette "
789 }
else if (tokens[1] ==
"insert") {
791 "Inserts the specified cassette image into the "
792 "cassette player, rewinds it and switches to play "
794 }
else if (tokens[1] ==
"record") {
796 "Go to record mode. NOT IMPLEMENTED YET. Will be "
797 "used to be able to resume recording to an "
798 "existing cassette image, previously inserted with "
799 "the insert command.";
800 }
else if (tokens[1] ==
"getpos") {
802 "Return the position of the tape, in seconds from "
803 "the beginning of the tape.";
804 }
else if (tokens[1] ==
"setpos") {
806 "Wind the tape to the given position, in seconds from "
807 "the beginning of the tape.";
808 }
else if (tokens[1] ==
"getlength") {
810 "Return the length of the tape in seconds.";
814 "cassetteplayer eject "
815 ": remove tape from virtual player\n"
816 "cassetteplayer rewind "
817 ": rewind tape in virtual player\n"
818 "cassetteplayer motorcontrol "
819 ": enables or disables motor control (remote)\n"
820 "cassetteplayer play "
821 ": change to play mode (default)\n"
822 "cassetteplayer record "
823 ": change to record mode (NOT IMPLEMENTED YET)\n"
824 "cassetteplayer new [<filename>] "
825 ": create and insert new tape image file and go to record mode\n"
826 "cassetteplayer insert <filename> "
827 ": insert (a different) tape file\n"
828 "cassetteplayer getpos "
829 ": query the position of the tape\n"
830 "cassetteplayer setpos <new-pos> "
831 ": wind the tape to the given position\n"
832 "cassetteplayer getlength "
833 ": query the total length of the tape\n"
834 "cassetteplayer <filename> "
835 ": insert (a different) tape file\n";
840void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens)
const
842 using namespace std::literals;
843 if (tokens.size() == 2) {
844 static constexpr std::array cmds = {
845 "eject"sv,
"rewind"sv,
"motorcontrol"sv,
"insert"sv,
"new"sv,
846 "play"sv,
"getpos"sv,
"setpos"sv,
"getlength"sv,
850 }
else if ((tokens.size() == 3) && (tokens[1] ==
"insert")) {
852 }
else if ((tokens.size() == 3) && (tokens[1] ==
"motorcontrol")) {
853 static constexpr std::array extra = {
"on"sv,
"off"sv};
854 completeString(tokens, extra);
858bool CassettePlayer::TapeCommand::needRecord(std::span<const TclObject> tokens)
const
860 return tokens.size() > 1;
864static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
873template<
typename Archive>
881 ar.serialize(
"casImage", casImage);
884 if constexpr (!Archive::IS_LOADER) {
886 oldChecksum = playImage->getSha1Sum();
889 if (ar.versionAtLeast(version, 2)) {
890 string oldChecksumStr = oldChecksum.
empty()
893 ar.serialize(
"checksum", oldChecksumStr);
894 oldChecksum = oldChecksumStr.
empty()
899 if constexpr (Archive::IS_LOADER) {
901 auto time = getCurrentTime();
903 if (!oldChecksum.
empty() &&
906 if (file.is_open()) {
911 insertTape(casImage, time);
913 if (oldChecksum.
empty()) {
928 if (playImage && !oldChecksum.
empty()) {
929 Sha1Sum newChecksum = playImage->getSha1Sum();
930 if (oldChecksum != newChecksum) {
932 "The content of the tape ",
934 " has changed since the time this "
935 "savestate was created. This might "
936 "result in emulation problems.");
948 ar.serialize(
"tapePos", tapePos,
949 "prevSyncTime", prevSyncTime,
950 "audioPos", audioPos,
952 "lastOutput", lastOutput,
954 "motorControl", motorControl);
956 if constexpr (Archive::IS_LOADER) {
957 auto time = getCurrentTime();
958 if (playImage && (tapePos > playImage->getEndTime())) {
959 tapePos = playImage->getEndTime();
961 "beyond tape end! Setting tape position to end. "
962 "This can happen if you load a replay from an "
963 "older openMSX version with a different CAS-to-WAV "
964 "baud rate or when the tape image has been changed "
965 "compared to when the replay was created.");
970 "Restoring a state where the MSX was saving to "
971 "tape is not yet supported. Emulation will "
972 "continue without actually saving.");
973 setState(
STOP, getImageName(), time);
975 if (!playImage && (state ==
PLAY)) {
978 setState(
STOP, getImageName(), time);
981 updateLoadingState(time);
bool getBoolean() const noexcept
void plugHelper(Connector &connector, EmuTime::param time) override
float getAmplificationFactorImpl() const override
Get amplification/attenuation factor for this device.
std::string_view getName() const override
Name used to identify this pluggable.
std::string_view getDescription() const override
Description for this pluggable.
~CassettePlayer() override
void setSignal(bool output, EmuTime::param time) override
Sets the cassette output signal false = low true = high.
void unplugHelper(EmuTime::param time) override
void generateChannels(std::span< float * > buffers, unsigned num) override
Abstract method to generate the actual sound data.
void setMotor(bool status, EmuTime::param time) override
Sets the cassette motor relay false = off true = on.
CassettePlayer(const HardwareConfig &hwConf)
void serialize(Archive &ar, unsigned version)
int16_t readSample(EmuTime::param time) override
Read wave data from cassette device.
void getMediaInfo(TclObject &result) override
This method gets called when information is required on the media inserted in the media slot of the p...
void printWarning(std::string_view message)
virtual TclObject executeCommand(zstring_view command, CliConnection *connection=nullptr)=0
Execute the given command.
Represents something you can plug devices into.
static constexpr EmuDuration sec(unsigned x)
const EmuDuration & param
File getFile(FileType fileType, const Sha1Sum &sha1sum)
Search file with the given sha1sum.
void setResolved(std::string resolved)
Change the resolved part of this filename E.g.
const std::string & getResolved() const &
void updateAfterLoadState()
After a loadstate we prefer to use the exact same file as before savestate.
void update(bool newState)
Called by the device to indicate its loading state may have changed.
void update(UpdateType type, std::string_view name, std::string_view value) override
void registerMediaInfo(std::string_view name, MediaInfoProvider &provider)
Register and unregister providers of media info, for the media info topic.
CommandController & getCommandController()
void unregisterMediaInfo(MediaInfoProvider &provider)
MSXCliComm & getMSXCliComm()
ReverseManager & getReverseManager()
std::string_view getMachineType() const
Connector * getConnector() const
Get the connector this Pluggable is plugged into.
This class represents the result of a sha1 calculation (a 160-bit value).
std::string toString() const
void updateStream(EmuTime::param time)
unsigned getInputRate() const
void setInputRate(unsigned sampleRate)
void setSoftwareVolume(float volume, EmuTime::param time)
Change the 'software volume' of this sound device.
void unregisterSound()
Unregisters this sound device with the Mixer.
void registerSound(const DeviceConfig &config)
Registers this sound device with the Mixer.
void addDictKeyValues(Args &&... args)
static XMLDocument & getStaticDocument()
XMLElement * setFirstChild(XMLElement *child)
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
T length(const vecN< N, T > &x)
string parseCommandFileArgument(string_view argument, string_view directory, string_view prefix, string_view extension)
Helper function for parsing filename arguments in Tcl commands.
bool exists(zstring_view filename)
Does this file (directory) exists?
int unlink(zstring_view path)
Call unlink() in a platform-independent manner.
This file implemented 3 utility functions:
const FileContext & userFileContext()
std::array< const EDStorage, 4 > A
TclObject makeTclList(Args &&... args)
#define OUTER(type, member)
constexpr auto subspan(Range &&range, size_t offset, size_t count=std::dynamic_extent)
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
#define SERIALIZE_ENUM(TYPE, INFO)
TemporaryString tmpStrCat(Ts &&... ts)
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.