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 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 double rest = 1.0 - partialInterval;
501 if (rest <= samples) {
503 partialOut += out * rest;
504 fillBuf(1, partialOut);
508 int count = int(samples);
513 assert(samples < 1.0);
516 partialOut = samples * out;
517 partialInterval = samples;
519 assert(samples < 1.0);
520 partialOut += samples * out;
521 partialInterval += samples;
523 assert(partialInterval < 1.0);
526void CassettePlayer::fillBuf(
size_t length,
double x)
529 static constexpr double A = 252.0 / 256.0;
531 double y = lastY + (x - lastX);
536 buf[sampCnt++] = narrow<uint8_t>(
int(y) + 128);
540 assert(sampCnt <= buf.size());
541 if (sampCnt == buf.size()) {
549void CassettePlayer::flushOutput()
552 recordImage->write(
subspan(buf, 0, sampCnt));
554 recordImage->flush();
555 }
catch (MSXException&
e) {
557 "Failed to write to tape: ",
e.getMessage());
564 return getCassettePlayerName();
575 lastOutput = checked_cast<CassettePort&>(conn).lastOut();
581 setState(
STOP, getImageName(), time);
588 assert(buffers.size() == 1);
589 if ((getState() !=
PLAY) || !isRolling()) {
590 buffers[0] =
nullptr;
593 assert(buffers.size() == 1);
594 playImage->fillBuffer(audioPos, buffers.first<1>(), num);
600 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
603int CassettePlayer::signalEvent(
const Event& event)
606 if (!getImageName().empty()) {
609 playTape(getImageName(), getCurrentTime());
610 }
catch (MSXException&
e) {
612 "Failed to insert tape: ",
e.getMessage());
619void CassettePlayer::execEndOfTape(EmuTime::param time)
623 assert(tapePos == playImage->getEndTime());
625 "Tape end reached... stopping. "
626 "You may need to insert another tape image "
627 "that contains side B. (Or you used the wrong "
628 "loading command.)");
629 setState(
STOP, getImageName(), time);
632void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
634 if (getState() ==
PLAY) {
638 clk.setFreq(playImage->getFrequency());
639 audioPos = clk.getTicksTill(tapePos);
641 syncScheduled =
false;
647CassettePlayer::TapeCommand::TapeCommand(
648 CommandController& commandController_,
649 StateChangeDistributor& stateChangeDistributor_,
651 : RecordedCommand(commandController_, stateChangeDistributor_,
652 scheduler_,
"cassetteplayer")
656void CassettePlayer::TapeCommand::execute(
657 std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
660 if (tokens.size() == 1) {
663 TclObject options =
makeTclList(cassettePlayer.getStateString());
664 result.addListElement(
tmpStrCat(getName(),
':'),
665 cassettePlayer.getImageName().getResolved(),
668 }
else if (tokens[1] ==
"new") {
669 std::string_view directory =
"taperecordings";
670 std::string_view prefix =
"openmsx";
671 std::string_view extension =
".wav";
673 (tokens.size() == 3) ? tokens[2].getString() :
string{},
674 directory, prefix, extension);
675 cassettePlayer.recordTape(
Filename(filename), time);
677 "Created new cassette image file: ", filename,
678 ", inserted it and set recording mode.");
680 }
else if (tokens[1] ==
"insert" && tokens.size() == 3) {
682 result =
"Changing tape";
684 cassettePlayer.playTape(filename, time);
685 }
catch (MSXException&
e) {
686 throw CommandException(std::move(
e).getMessage());
689 }
else if (tokens[1] ==
"motorcontrol" && tokens.size() == 3) {
690 if (tokens[2] ==
"on") {
691 cassettePlayer.setMotorControl(
true, time);
692 result =
"Motor control enabled.";
693 }
else if (tokens[2] ==
"off") {
694 cassettePlayer.setMotorControl(
false, time);
695 result =
"Motor control disabled.";
700 }
else if (tokens.size() != 2) {
703 }
else if (tokens[1] ==
"motorcontrol") {
705 (cassettePlayer.motorControl ?
"on" :
"off"));
707 }
else if (tokens[1] ==
"record") {
708 result =
"TODO: implement this... (sorry)";
710 }
else if (tokens[1] ==
"play") {
713 result =
"Play mode set, rewinding tape.";
714 cassettePlayer.playTape(
715 cassettePlayer.getImageName(), time);
716 }
catch (MSXException&
e) {
717 throw CommandException(std::move(
e).getMessage());
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") {
734 r =
"First stopping recording... ";
735 cassettePlayer.playTape(
736 cassettePlayer.getImageName(), time);
737 }
catch (MSXException&
e) {
738 throw CommandException(std::move(
e).getMessage());
741 cassettePlayer.rewind(time);
745 }
else if (tokens[1] ==
"getpos") {
746 result = cassettePlayer.getTapePos(time);
748 }
else if (tokens[1] ==
"getlength") {
749 result = cassettePlayer.getTapeLength(time);
753 result =
"Changing tape";
755 cassettePlayer.playTape(filename, time);
756 }
catch (MSXException&
e) {
757 throw CommandException(std::move(
e).getMessage());
765string CassettePlayer::TapeCommand::help(std::span<const TclObject> tokens)
const
768 if (tokens.size() >= 2) {
769 if (tokens[1] ==
"eject") {
771 "Well, just eject the cassette from the cassette "
773 }
else if (tokens[1] ==
"rewind") {
775 "Indeed, rewind the tape that is currently in the "
776 "cassette player/recorder...";
777 }
else if (tokens[1] ==
"motorcontrol") {
779 "Setting this to 'off' is equivalent to "
780 "disconnecting the black remote plug from the "
781 "cassette player: it makes the cassette player "
782 "run (if in play mode); the motor signal from the "
783 "MSX will be ignored. Normally this is set to "
784 "'on': the cassetteplayer obeys the motor control "
785 "signal from the MSX.";
786 }
else if (tokens[1] ==
"play") {
788 "Go to play mode. Only useful if you were in "
789 "record mode (which is currently the only other "
791 }
else if (tokens[1] ==
"new") {
793 "Create a new cassette image. If the file name is "
794 "omitted, one will be generated in the default "
795 "directory for tape recordings. Implies going to "
796 "record mode (why else do you want a new cassette "
798 }
else if (tokens[1] ==
"insert") {
800 "Inserts the specified cassette image into the "
801 "cassette player, rewinds it and switches to play "
803 }
else if (tokens[1] ==
"record") {
805 "Go to record mode. NOT IMPLEMENTED YET. Will be "
806 "used to be able to resume recording to an "
807 "existing cassette image, previously inserted with "
808 "the insert command.";
809 }
else if (tokens[1] ==
"getpos") {
811 "Return the position of the tape, in seconds from "
812 "the beginning of the tape.";
813 }
else if (tokens[1] ==
"getlength") {
815 "Return the length of the tape in seconds.";
819 "cassetteplayer eject "
820 ": remove tape from virtual player\n"
821 "cassetteplayer rewind "
822 ": rewind tape in virtual player\n"
823 "cassetteplayer motorcontrol "
824 ": enables or disables motor control (remote)\n"
825 "cassetteplayer play "
826 ": change to play mode (default)\n"
827 "cassetteplayer record "
828 ": change to record mode (NOT IMPLEMENTED YET)\n"
829 "cassetteplayer new [<filename>] "
830 ": create and insert new tape image file and go to record mode\n"
831 "cassetteplayer insert <filename> "
832 ": insert (a different) tape file\n"
833 "cassetteplayer getpos "
834 ": query the position of the tape\n"
835 "cassetteplayer getlength "
836 ": query the total length of the tape\n"
837 "cassetteplayer <filename> "
838 ": insert (a different) tape file\n";
843void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens)
const
845 using namespace std::literals;
846 if (tokens.size() == 2) {
847 static constexpr std::array cmds = {
848 "eject"sv,
"rewind"sv,
"motorcontrol"sv,
"insert"sv,
"new"sv,
849 "play"sv,
"getpos"sv,
"getlength"sv,
853 }
else if ((tokens.size() == 3) && (tokens[1] ==
"insert")) {
855 }
else if ((tokens.size() == 3) && (tokens[1] ==
"motorcontrol")) {
856 static constexpr std::array extra = {
"on"sv,
"off"sv};
857 completeString(tokens, extra);
861bool CassettePlayer::TapeCommand::needRecord(std::span<const TclObject> tokens)
const
863 return tokens.size() > 1;
867static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
876template<
typename Archive>
884 ar.serialize(
"casImage", casImage);
887 if constexpr (!Archive::IS_LOADER) {
889 oldChecksum = playImage->getSha1Sum();
892 if (ar.versionAtLeast(version, 2)) {
893 string oldChecksumStr = oldChecksum.
empty()
896 ar.serialize(
"checksum", oldChecksumStr);
897 oldChecksum = oldChecksumStr.
empty()
902 if constexpr (Archive::IS_LOADER) {
904 auto time = getCurrentTime();
906 if (!oldChecksum.
empty() &&
909 if (file.is_open()) {
914 insertTape(casImage, time);
916 if (oldChecksum.
empty()) {
931 if (playImage && !oldChecksum.
empty()) {
932 Sha1Sum newChecksum = playImage->getSha1Sum();
933 if (oldChecksum != newChecksum) {
935 "The content of the tape ",
937 " has changed since the time this "
938 "savestate was created. This might "
939 "result in emulation problems.");
951 ar.serialize(
"tapePos", tapePos,
952 "prevSyncTime", prevSyncTime,
953 "audioPos", audioPos,
955 "lastOutput", lastOutput,
957 "motorControl", motorControl);
959 if constexpr (Archive::IS_LOADER) {
960 auto time = getCurrentTime();
961 if (playImage && (tapePos > playImage->getEndTime())) {
962 tapePos = playImage->getEndTime();
964 "beyond tape end! Setting tape position to end. "
965 "This can happen if you load a replay from an "
966 "older openMSX version with a different CAS-to-WAV "
967 "baud rate or when the tape image has been changed "
968 "compared to when the replay was created.");
973 "Restoring a state where the MSX was saving to "
974 "tape is not yet supported. Emulation will "
975 "continue without actually saving.");
976 setState(
STOP, getImageName(), time);
978 if (!playImage && (state ==
PLAY)) {
981 setState(
STOP, getImageName(), time);
984 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=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)
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
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:
SERIALIZE_ENUM(CassettePlayer::State, stateInfo)
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
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, ImGuiDelayedActionEvent, ImGuiActiveEvent > Event
EventType getType(const Event &event)
FileContext userFileContext(string_view savePath)
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)
TemporaryString tmpStrCat(Ts &&... ts)
std::string strCat(Ts &&...ts)
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.