63static constexpr static_string_view DESCRIPTION =
"Cassetteplayer, use to read .cas or .wav files.";
65static constexpr unsigned DUMMY_INPUT_RATE = 44100;
66static constexpr unsigned RECORD_FREQ = 44100;
67static constexpr double RECIP_RECORD_FREQ = 1.0 / RECORD_FREQ;
68static constexpr double OUTPUT_AMP = 60.0;
70static std::string_view getCassettePlayerName()
72 return "cassetteplayer";
76 :
ResampledSoundDevice(hwConf.getMotherBoard(), getCassettePlayerName(), DESCRIPTION, 1, DUMMY_INPUT_RATE, false)
77 , syncEndOfTape(hwConf.getMotherBoard().getScheduler())
78 , syncAudioEmu (hwConf.getMotherBoard().getScheduler())
79 , motherBoard(hwConf.getMotherBoard())
81 motherBoard.getCommandController(),
82 motherBoard.getStateChangeDistributor(),
83 motherBoard.getScheduler())
85 motherBoard.getReactor().getGlobalSettings().getThrottleManager())
87 motherBoard.getCommandController(),
88 "autoruncassettes",
"automatically try to run cassettes", true)
92 XMLElement* result = doc.allocateElement(
"cassetteplayer");
104 removeTape(EmuTime::zero());
111 c->unplug(getCurrentTime());
122 "state", getStateString(),
123 "position", getTapePos(getCurrentTime()),
124 "length", getTapeLength(getCurrentTime()),
125 "motorcontrol", motorControl);
128void CassettePlayer::autoRun()
130 if (!playImage)
return;
144 string H_READ = is_SVI ?
"0xFE8E" :
"0xFF07";
145 string H_MAIN = is_SVI ?
"0xFE94" :
"0xFF0C";
146 string instr1, instr2;
149 instr1 = R
"({RUN\"CAS:\"\r})";
152 instr1 = R
"({BLOAD\"CAS:\",R\r})";
157 instr1 =
"{CLOAD\\r}";
164 "namespace eval ::openmsx {\n"
165 " variable auto_run_bp\n"
167 " proc auto_run_cb {args} {\n"
168 " variable auto_run_bp\n"
169 " debug remove_bp $auto_run_bp\n"
170 " unset auto_run_bp\n"
177 " after time 0.2 \"type [lindex $args 0]\"\n"
179 " set next [lrange $args 1 end]\n"
180 " if {[llength $next] == 0} return\n"
184 " set cmd \"openmsx::auto_run_cb $next\"\n"
185 " set openmsx::auto_run_bp [debug set_bp ", H_MAIN,
" 1 \"$cmd\"]\n"
188 " if {[info exists auto_run_bp]} {debug remove_bp $auto_run_bp\n}\n"
189 " set auto_run_bp [debug set_bp ", H_READ,
" 1 {\n"
190 " openmsx::auto_run_cb {{}} ", instr1,
' ', instr2,
"\n"
194 " type_via_keyboard \'\\r\n"
198 }
catch (CommandException& e) {
200 "Error executing loading instruction using command \"",
201 command,
"\" for AutoRun: ",
202 e.getMessage(),
"\n Please report a bug.");
206string CassettePlayer::getStateString()
const
208 switch (getState()) {
209 case PLAY:
return "play";
210 case RECORD:
return "record";
211 case STOP:
return "stop";
216bool CassettePlayer::isRolling()
const
223 return (getState() !=
STOP) && (motor || !motorControl);
226double CassettePlayer::getTapePos(EmuTime::param time)
229 if (getState() ==
RECORD) {
231 return (
double(recordImage->getBytes()) + partialInterval) * RECIP_RECORD_FREQ;
233 return (tapePos - EmuTime::zero()).toDouble();
237double CassettePlayer::getTapeLength(EmuTime::param time)
240 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
241 }
else if (getState() ==
RECORD) {
242 return getTapePos(time);
248void CassettePlayer::checkInvariants()
const
250 switch (getState()) {
252 assert(!recordImage);
255 assert(!getImageName().empty());
261 assert(!getImageName().empty());
262 assert(!recordImage);
266 assert(!getImageName().empty());
275void CassettePlayer::setState(State newState,
const Filename& newImage,
281 State oldState = getState();
282 if (oldState == newState)
return;
286 assert(!((oldState ==
PLAY) && (newState ==
RECORD)));
287 assert(!((oldState ==
RECORD) && (newState ==
PLAY)));
291 if ((oldState ==
RECORD) && recordImage) {
293 bool empty = recordImage->isEmpty();
304 setImageName(newImage);
309 partialInterval = 0.0;
310 lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
316 updateLoadingState(time);
321void CassettePlayer::updateLoadingState(EmuTime::param time)
323 assert(prevSyncTime == time);
326 loadingIndicator.
update(motor && (getState() ==
PLAY));
328 syncEndOfTape.removeSyncPoint();
329 if (isRolling() && (getState() ==
PLAY)) {
330 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
334void CassettePlayer::setImageName(
const Filename& newImage)
341void CassettePlayer::insertTape(
const Filename& filename, EmuTime::param time)
343 if (!filename.empty()) {
347 playImage = std::make_unique<WavImage>(filename, filePool);
348 }
catch (MSXException& e) {
351 playImage = std::make_unique<CasImage>(
354 }
catch (MSXException& e2) {
356 "Failed to insert WAV image: \"",
358 "\" and also failed to insert CAS image: \"",
359 e2.getMessage(),
'\"');
373 if (
unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
382 setImageName(filename);
385void CassettePlayer::playTape(
const Filename& filename, EmuTime::param time)
392 setState(
STOP, getImageName(), time);
393 insertTape(filename, time);
398void CassettePlayer::rewind(EmuTime::param time)
401 assert(getState() !=
RECORD);
402 tapePos = EmuTime::zero();
405 if (getImageName().empty()) {
407 assert(getState() ==
STOP);
410 setState(
PLAY, getImageName(), time);
412 updateLoadingState(time);
415void CassettePlayer::recordTape(
const Filename& filename, EmuTime::param time)
418 recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
419 tapePos = EmuTime::zero();
420 setState(
RECORD, filename, time);
423void CassettePlayer::removeTape(EmuTime::param time)
426 setState(
STOP, getImageName(), time);
429 tapePos = EmuTime::zero();
435 if (status != motor) {
438 updateLoadingState(time);
442void CassettePlayer::setMotorControl(
bool status, EmuTime::param time)
444 if (status != motorControl) {
446 motorControl = status;
447 updateLoadingState(time);
453 if (getState() ==
PLAY) {
456 return isRolling() ? playImage->getSampleAt(tapePos) : int16_t(0);
469void CassettePlayer::sync(EmuTime::param time)
474 updateTapePosition(duration, time);
475 generateRecordOutput(duration);
478void CassettePlayer::updateTapePosition(
481 if (!isRolling() || (getState() !=
PLAY))
return;
484 assert(tapePos <= playImage->getEndTime());
487 if (!syncScheduled) {
489 syncScheduled =
true;
496 if (!recordImage || !isRolling())
return;
498 double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
499 double samples = duration.toDouble() * RECORD_FREQ;
500 if (
auto rest = 1.0 - partialInterval; rest <= samples) {
502 partialOut += out * rest;
503 fillBuf(1, partialOut);
507 auto count = int(samples);
512 assert(samples < 1.0);
515 partialOut = samples * out;
516 partialInterval = samples;
518 assert(samples < 1.0);
519 partialOut += samples * out;
520 partialInterval += samples;
522 assert(partialInterval < 1.0);
525void CassettePlayer::fillBuf(
size_t length,
double x)
528 static constexpr double A = 252.0 / 256.0;
530 double y = lastY + (x - lastX);
533 size_t len = std::min(length, buf.size() - sampCnt);
535 buf[sampCnt++] = narrow<uint8_t>(
int(y) + 128);
539 assert(sampCnt <= buf.size());
540 if (sampCnt == buf.size()) {
548void CassettePlayer::flushOutput()
551 recordImage->write(
subspan(buf, 0, sampCnt));
553 recordImage->flush();
554 }
catch (MSXException& e) {
556 "Failed to write to tape: ",
e.getMessage());
563 return getCassettePlayerName();
574 lastOutput = checked_cast<CassettePort&>(conn).lastOut();
580 setState(
STOP, getImageName(), time);
587 assert(buffers.size() == 1);
588 if ((getState() !=
PLAY) || !isRolling()) {
589 buffers[0] =
nullptr;
592 assert(buffers.size() == 1);
593 playImage->fillBuffer(audioPos, buffers.first<1>(), num);
599 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
602bool CassettePlayer::signalEvent(
const Event& event)
607 playTape(getImageName(), getCurrentTime());
608 }
catch (MSXException& e) {
610 "Failed to insert tape: ", e.getMessage());
616void CassettePlayer::execEndOfTape(EmuTime::param time)
620 assert(tapePos == playImage->getEndTime());
622 "Tape end reached... stopping. "
623 "You may need to insert another tape image "
624 "that contains side B. (Or you used the wrong "
625 "loading command.)");
626 setState(
STOP, getImageName(), time);
629void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
631 if (getState() ==
PLAY) {
635 clk.setFreq(playImage->getFrequency());
636 audioPos = clk.getTicksTill(tapePos);
638 syncScheduled =
false;
644CassettePlayer::TapeCommand::TapeCommand(
645 CommandController& commandController_,
646 StateChangeDistributor& stateChangeDistributor_,
647 Scheduler& scheduler_)
648 : RecordedCommand(commandController_, stateChangeDistributor_,
649 scheduler_,
"cassetteplayer")
653void CassettePlayer::TapeCommand::execute(
654 std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
656 auto& cassettePlayer =
OUTER(CassettePlayer, tapeCommand);
657 if (tokens.size() == 1) {
660 TclObject options =
makeTclList(cassettePlayer.getStateString());
661 result.addListElement(
tmpStrCat(getName(),
':'),
662 cassettePlayer.getImageName().getResolved(),
665 }
else if (tokens[1] ==
"new") {
666 std::string_view prefix =
"openmsx";
668 (tokens.size() == 3) ? tokens[2].getString() : string{},
669 TAPE_RECORDING_DIR, prefix, TAPE_RECORDING_EXTENSION);
670 cassettePlayer.recordTape(
Filename(filename), time);
672 "Created new cassette image file: ", filename,
673 ", inserted it and set recording mode.");
675 }
else if (tokens[1] ==
"insert" && tokens.size() == 3) {
677 result =
"Changing tape";
679 cassettePlayer.playTape(filename, time);
680 }
catch (MSXException& e) {
681 throw CommandException(std::move(e).getMessage());
684 }
else if (tokens[1] ==
"motorcontrol" && tokens.size() == 3) {
685 if (tokens[2] ==
"on") {
686 cassettePlayer.setMotorControl(
true, time);
687 result =
"Motor control enabled.";
688 }
else if (tokens[2] ==
"off") {
689 cassettePlayer.setMotorControl(
false, time);
690 result =
"Motor control disabled.";
695 }
else if (tokens.size() != 2) {
698 }
else if (tokens[1] ==
"motorcontrol") {
700 (cassettePlayer.motorControl ?
"on" :
"off"));
702 }
else if (tokens[1] ==
"record") {
703 result =
"TODO: implement this... (sorry)";
705 }
else if (tokens[1] ==
"play") {
708 result =
"Play mode set, rewinding tape.";
709 cassettePlayer.playTape(
710 cassettePlayer.getImageName(), time);
711 }
catch (MSXException& e) {
712 throw CommandException(std::move(e).getMessage());
715 throw CommandException(
"No tape inserted or tape at end!");
718 result =
"Already in play mode.";
721 }
else if (tokens[1] ==
"eject") {
722 result =
"Tape ejected";
723 cassettePlayer.removeTape(time);
725 }
else if (tokens[1] ==
"rewind") {
729 r =
"First stopping recording... ";
730 cassettePlayer.playTape(
731 cassettePlayer.getImageName(), time);
732 }
catch (MSXException& e) {
733 throw CommandException(std::move(e).getMessage());
736 cassettePlayer.rewind(time);
740 }
else if (tokens[1] ==
"getpos") {
741 result = cassettePlayer.getTapePos(time);
743 }
else if (tokens[1] ==
"getlength") {
744 result = cassettePlayer.getTapeLength(time);
748 result =
"Changing tape";
750 cassettePlayer.playTape(filename, time);
751 }
catch (MSXException& e) {
752 throw CommandException(std::move(e).getMessage());
760string CassettePlayer::TapeCommand::help(std::span<const TclObject> tokens)
const
763 if (tokens.size() >= 2) {
764 if (tokens[1] ==
"eject") {
766 "Well, just eject the cassette from the cassette "
768 }
else if (tokens[1] ==
"rewind") {
770 "Indeed, rewind the tape that is currently in the "
771 "cassette player/recorder...";
772 }
else if (tokens[1] ==
"motorcontrol") {
774 "Setting this to 'off' is equivalent to "
775 "disconnecting the black remote plug from the "
776 "cassette player: it makes the cassette player "
777 "run (if in play mode); the motor signal from the "
778 "MSX will be ignored. Normally this is set to "
779 "'on': the cassetteplayer obeys the motor control "
780 "signal from the MSX.";
781 }
else if (tokens[1] ==
"play") {
783 "Go to play mode. Only useful if you were in "
784 "record mode (which is currently the only other "
786 }
else if (tokens[1] ==
"new") {
788 "Create a new cassette image. If the file name is "
789 "omitted, one will be generated in the default "
790 "directory for tape recordings. Implies going to "
791 "record mode (why else do you want a new cassette "
793 }
else if (tokens[1] ==
"insert") {
795 "Inserts the specified cassette image into the "
796 "cassette player, rewinds it and switches to play "
798 }
else if (tokens[1] ==
"record") {
800 "Go to record mode. NOT IMPLEMENTED YET. Will be "
801 "used to be able to resume recording to an "
802 "existing cassette image, previously inserted with "
803 "the insert command.";
804 }
else if (tokens[1] ==
"getpos") {
806 "Return the position of the tape, 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 getlength "
831 ": query the total length of the tape\n"
832 "cassetteplayer <filename> "
833 ": insert (a different) tape file\n";
838void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens)
const
840 using namespace std::literals;
841 if (tokens.size() == 2) {
842 static constexpr std::array cmds = {
843 "eject"sv,
"rewind"sv,
"motorcontrol"sv,
"insert"sv,
"new"sv,
844 "play"sv,
"getpos"sv,
"getlength"sv,
848 }
else if ((tokens.size() == 3) && (tokens[1] ==
"insert")) {
850 }
else if ((tokens.size() == 3) && (tokens[1] ==
"motorcontrol")) {
851 static constexpr std::array extra = {
"on"sv,
"off"sv};
852 completeString(tokens, extra);
856bool CassettePlayer::TapeCommand::needRecord(std::span<const TclObject> tokens)
const
858 return tokens.size() > 1;
862static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
871template<
typename Archive>
879 ar.serialize(
"casImage", casImage);
882 if constexpr (!Archive::IS_LOADER) {
884 oldChecksum = playImage->getSha1Sum();
887 if (ar.versionAtLeast(version, 2)) {
888 string oldChecksumStr = oldChecksum.
empty()
891 ar.serialize(
"checksum", oldChecksumStr);
892 oldChecksum = oldChecksumStr.
empty()
897 if constexpr (Archive::IS_LOADER) {
899 auto time = getCurrentTime();
901 if (!oldChecksum.
empty() &&
904 if (file.is_open()) {
909 insertTape(casImage, time);
911 if (oldChecksum.
empty()) {
926 if (playImage && !oldChecksum.
empty()) {
927 Sha1Sum newChecksum = playImage->getSha1Sum();
928 if (oldChecksum != newChecksum) {
930 "The content of the tape ",
932 " has changed since the time this "
933 "savestate was created. This might "
934 "result in emulation problems.");
946 ar.serialize(
"tapePos", tapePos,
947 "prevSyncTime", prevSyncTime,
948 "audioPos", audioPos,
950 "lastOutput", lastOutput,
952 "motorControl", motorControl);
954 if constexpr (Archive::IS_LOADER) {
955 auto time = getCurrentTime();
956 if (playImage && (tapePos > playImage->getEndTime())) {
957 tapePos = playImage->getEndTime();
959 "beyond tape end! Setting tape position to end. "
960 "This can happen if you load a replay from an "
961 "older openMSX version with a different CAS-to-WAV "
962 "baud rate or when the tape image has been changed "
963 "compared to when the replay was created.");
968 "Restoring a state where the MSX was saving to "
969 "tape is not yet supported. Emulation will "
970 "continue without actually saving.");
971 setState(
STOP, getImageName(), time);
973 if (!playImage && (state ==
PLAY)) {
976 setState(
STOP, getImageName(), time);
979 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
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void registerEventListener(EventType type, EventListener &listener, Priority priority=Priority::OTHER)
Registers a given object to receive certain events.
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.
EventDistributor & getEventDistributor()
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:
EventType getType(const Event &event)
const FileContext & userFileContext()
std::variant< KeyUpEvent, KeyDownEvent, MouseMotionEvent, MouseButtonUpEvent, MouseButtonDownEvent, MouseWheelEvent, JoystickAxisMotionEvent, JoystickHatEvent, JoystickButtonUpEvent, JoystickButtonDownEvent, OsdControlReleaseEvent, OsdControlPressEvent, WindowEvent, TextEvent, FileDropEvent, QuitEvent, FinishFrameEvent, CliCommandEvent, GroupEvent, BootEvent, FrameDrawnEvent, BreakEvent, SwitchRendererEvent, TakeReverseSnapshotEvent, AfterTimedEvent, MachineLoadedEvent, MachineActivatedEvent, MachineDeactivatedEvent, MidiInReaderEvent, MidiInWindowsEvent, MidiInCoreMidiEvent, MidiInCoreMidiVirtualEvent, MidiInALSAEvent, Rs232TesterEvent, Rs232NetEvent, ImGuiDelayedActionEvent, ImGuiActiveEvent > Event
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.