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 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");
103 removeTape(EmuTime::zero());
110 c->unplug(getCurrentTime());
121 "state", getStateString(),
122 "position", getTapePos(getCurrentTime()),
123 "length", getTapeLength(getCurrentTime()),
124 "motorcontrol", motorControl);
127void CassettePlayer::autoRun()
129 if (!playImage)
return;
143 string H_READ = is_SVI ?
"0xFE8E" :
"0xFF07";
144 string H_MAIN = is_SVI ?
"0xFE94" :
"0xFF0C";
145 string instr1, instr2;
148 instr1 = R
"({RUN\"CAS:\"\r})";
151 instr1 = R
"({BLOAD\"CAS:\",R\r})";
156 instr1 =
"{CLOAD\\r}";
163 "namespace eval ::openmsx {\n"
164 " variable auto_run_bp\n"
166 " proc auto_run_cb {args} {\n"
167 " variable auto_run_bp\n"
168 " debug remove_bp $auto_run_bp\n"
169 " unset auto_run_bp\n"
176 " after time 0.2 \"type [lindex $args 0]\"\n"
178 " set next [lrange $args 1 end]\n"
179 " if {[llength $next] == 0} return\n"
183 " set cmd \"openmsx::auto_run_cb $next\"\n"
184 " set openmsx::auto_run_bp [debug set_bp ", H_MAIN,
" 1 \"$cmd\"]\n"
187 " if {[info exists auto_run_bp]} {debug remove_bp $auto_run_bp\n}\n"
188 " set auto_run_bp [debug set_bp ", H_READ,
" 1 {\n"
189 " openmsx::auto_run_cb {{}} ", instr1,
' ', instr2,
"\n"
193 " type_via_keyboard \'\\r\n"
197 }
catch (CommandException&
e) {
199 "Error executing loading instruction using command \"",
200 command,
"\" for AutoRun: ",
201 e.getMessage(),
"\n Please report a bug.");
205string CassettePlayer::getStateString()
const
207 switch (getState()) {
208 case PLAY:
return "play";
209 case RECORD:
return "record";
210 case STOP:
return "stop";
215bool CassettePlayer::isRolling()
const
222 return (getState() !=
STOP) && (motor || !motorControl);
225double CassettePlayer::getTapePos(EmuTime::param time)
228 if (getState() ==
RECORD) {
230 return (
double(recordImage->getBytes()) + partialInterval) / RECORD_FREQ;
232 return (tapePos - EmuTime::zero()).toDouble();
236double CassettePlayer::getTapeLength(EmuTime::param time)
239 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
240 }
else if (getState() ==
RECORD) {
241 return getTapePos(time);
247void CassettePlayer::checkInvariants()
const
249 switch (getState()) {
251 assert(!recordImage);
254 assert(!getImageName().empty());
260 assert(!getImageName().empty());
261 assert(!recordImage);
265 assert(!getImageName().empty());
274void CassettePlayer::setState(State newState,
const Filename& newImage,
280 State oldState = getState();
281 if (oldState == newState)
return;
285 assert(!((oldState ==
PLAY) && (newState ==
RECORD)));
286 assert(!((oldState ==
RECORD) && (newState ==
PLAY)));
290 if ((oldState ==
RECORD) && recordImage) {
292 bool empty = recordImage->isEmpty();
303 setImageName(newImage);
308 partialInterval = 0.0;
309 lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
315 updateLoadingState(time);
320void CassettePlayer::updateLoadingState(EmuTime::param time)
322 assert(prevSyncTime == time);
325 loadingIndicator.
update(motor && (getState() ==
PLAY));
327 syncEndOfTape.removeSyncPoint();
328 if (isRolling() && (getState() ==
PLAY)) {
329 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
333void CassettePlayer::setImageName(
const Filename& newImage)
340void CassettePlayer::insertTape(
const Filename& filename, EmuTime::param time)
342 if (!filename.empty()) {
346 playImage = std::make_unique<WavImage>(filename, filePool);
347 }
catch (MSXException&
e) {
350 playImage = std::make_unique<CasImage>(
353 }
catch (MSXException& e2) {
355 "Failed to insert WAV image: \"",
357 "\" and also failed to insert CAS image: \"",
358 e2.getMessage(),
'\"');
372 unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
381 setImageName(filename);
384void CassettePlayer::playTape(
const Filename& filename, EmuTime::param time)
391 setState(
STOP, getImageName(), time);
392 insertTape(filename, time);
397void CassettePlayer::rewind(EmuTime::param time)
400 assert(getState() !=
RECORD);
401 tapePos = EmuTime::zero();
404 if (getImageName().empty()) {
406 assert(getState() ==
STOP);
409 setState(
PLAY, getImageName(), time);
411 updateLoadingState(time);
414void CassettePlayer::recordTape(
const Filename& filename, EmuTime::param time)
417 recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
418 tapePos = EmuTime::zero();
419 setState(
RECORD, filename, time);
422void CassettePlayer::removeTape(EmuTime::param time)
425 setState(
STOP, getImageName(), time);
428 tapePos = EmuTime::zero();
434 if (status != motor) {
437 updateLoadingState(time);
441void CassettePlayer::setMotorControl(
bool status, EmuTime::param time)
443 if (status != motorControl) {
445 motorControl = status;
446 updateLoadingState(time);
452 if (getState() ==
PLAY) {
455 return isRolling() ? playImage->getSampleAt(tapePos) : int16_t(0);
468void CassettePlayer::sync(EmuTime::param time)
473 updateTapePosition(duration, time);
474 generateRecordOutput(duration);
477void CassettePlayer::updateTapePosition(
480 if (!isRolling() || (getState() !=
PLAY))
return;
483 assert(tapePos <= playImage->getEndTime());
486 if (!syncScheduled) {
488 syncScheduled =
true;
495 if (!recordImage || !isRolling())
return;
497 double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
498 double samples = duration.toDouble() * RECORD_FREQ;
499 double rest = 1.0 - partialInterval;
500 if (rest <= samples) {
502 partialOut += out * rest;
503 fillBuf(1, partialOut);
507 int 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 constexpr double A = 252.0 / 256.0;
530 double y = lastY + (x - lastX);
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;
602int CassettePlayer::signalEvent(
const Event& event)
605 if (!getImageName().empty()) {
608 playTape(getImageName(), getCurrentTime());
609 }
catch (MSXException&
e) {
611 "Failed to insert tape: ",
e.getMessage());
618void CassettePlayer::execEndOfTape(EmuTime::param time)
622 assert(tapePos == playImage->getEndTime());
624 "Tape end reached... stopping. "
625 "You may need to insert another tape image "
626 "that contains side B. (Or you used the wrong "
627 "loading command.)");
628 setState(
STOP, getImageName(), time);
631void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
633 if (getState() ==
PLAY) {
637 clk.setFreq(playImage->getFrequency());
638 audioPos = clk.getTicksTill(tapePos);
640 syncScheduled =
false;
646CassettePlayer::TapeCommand::TapeCommand(
647 CommandController& commandController_,
648 StateChangeDistributor& stateChangeDistributor_,
650 : RecordedCommand(commandController_, stateChangeDistributor_,
651 scheduler_,
"cassetteplayer")
655void CassettePlayer::TapeCommand::execute(
656 std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
659 if (tokens.size() == 1) {
662 TclObject options =
makeTclList(cassettePlayer.getStateString());
664 cassettePlayer.getImageName().getResolved(),
667 }
else if (tokens[1] ==
"new") {
668 std::string_view directory =
"taperecordings";
669 std::string_view prefix =
"openmsx";
670 std::string_view extension =
".wav";
672 (tokens.size() == 3) ? tokens[2].getString() :
string{},
673 directory, prefix, extension);
674 cassettePlayer.recordTape(
Filename(filename), time);
676 "Created new cassette image file: ", filename,
677 ", inserted it and set recording mode.");
679 }
else if (tokens[1] ==
"insert" && tokens.size() == 3) {
681 result =
"Changing tape";
683 cassettePlayer.playTape(filename, time);
684 }
catch (MSXException&
e) {
685 throw CommandException(std::move(
e).getMessage());
688 }
else if (tokens[1] ==
"motorcontrol" && tokens.size() == 3) {
689 if (tokens[2] ==
"on") {
690 cassettePlayer.setMotorControl(
true, time);
691 result =
"Motor control enabled.";
692 }
else if (tokens[2] ==
"off") {
693 cassettePlayer.setMotorControl(
false, time);
694 result =
"Motor control disabled.";
699 }
else if (tokens.size() != 2) {
702 }
else if (tokens[1] ==
"motorcontrol") {
704 (cassettePlayer.motorControl ?
"on" :
"off"));
706 }
else if (tokens[1] ==
"record") {
707 result =
"TODO: implement this... (sorry)";
709 }
else if (tokens[1] ==
"play") {
712 result =
"Play mode set, rewinding tape.";
713 cassettePlayer.playTape(
714 cassettePlayer.getImageName(), time);
715 }
catch (MSXException&
e) {
716 throw CommandException(std::move(
e).getMessage());
719 throw CommandException(
"No tape inserted or tape at end!");
722 result =
"Already in play mode.";
725 }
else if (tokens[1] ==
"eject") {
726 result =
"Tape ejected";
727 cassettePlayer.removeTape(time);
729 }
else if (tokens[1] ==
"rewind") {
733 r =
"First stopping recording... ";
734 cassettePlayer.playTape(
735 cassettePlayer.getImageName(), time);
736 }
catch (MSXException&
e) {
737 throw CommandException(std::move(
e).getMessage());
740 cassettePlayer.rewind(time);
744 }
else if (tokens[1] ==
"getpos") {
745 result = cassettePlayer.getTapePos(time);
747 }
else if (tokens[1] ==
"getlength") {
748 result = cassettePlayer.getTapeLength(time);
752 result =
"Changing tape";
754 cassettePlayer.playTape(filename, time);
755 }
catch (MSXException&
e) {
756 throw CommandException(std::move(
e).getMessage());
764string CassettePlayer::TapeCommand::help(std::span<const TclObject> tokens)
const
767 if (tokens.size() >= 2) {
768 if (tokens[1] ==
"eject") {
770 "Well, just eject the cassette from the cassette "
772 }
else if (tokens[1] ==
"rewind") {
774 "Indeed, rewind the tape that is currently in the "
775 "cassette player/recorder...";
776 }
else if (tokens[1] ==
"motorcontrol") {
778 "Setting this to 'off' is equivalent to "
779 "disconnecting the black remote plug from the "
780 "cassette player: it makes the cassette player "
781 "run (if in play mode); the motor signal from the "
782 "MSX will be ignored. Normally this is set to "
783 "'on': the cassetteplayer obeys the motor control "
784 "signal from the MSX.";
785 }
else if (tokens[1] ==
"play") {
787 "Go to play mode. Only useful if you were in "
788 "record mode (which is currently the only other "
790 }
else if (tokens[1] ==
"new") {
792 "Create a new cassette image. If the file name is "
793 "omitted, one will be generated in the default "
794 "directory for tape recordings. Implies going to "
795 "record mode (why else do you want a new cassette "
797 }
else if (tokens[1] ==
"insert") {
799 "Inserts the specified cassette image into the "
800 "cassette player, rewinds it and switches to play "
802 }
else if (tokens[1] ==
"record") {
804 "Go to record mode. NOT IMPLEMENTED YET. Will be "
805 "used to be able to resume recording to an "
806 "existing cassette image, previously inserted with "
807 "the insert command.";
808 }
else if (tokens[1] ==
"getpos") {
810 "Return the position of the tape, in seconds from "
811 "the beginning of the tape.";
812 }
else if (tokens[1] ==
"getlength") {
814 "Return the length of the tape in seconds.";
818 "cassetteplayer eject "
819 ": remove tape from virtual player\n"
820 "cassetteplayer rewind "
821 ": rewind tape in virtual player\n"
822 "cassetteplayer motorcontrol "
823 ": enables or disables motor control (remote)\n"
824 "cassetteplayer play "
825 ": change to play mode (default)\n"
826 "cassetteplayer record "
827 ": change to record mode (NOT IMPLEMENTED YET)\n"
828 "cassetteplayer new [<filename>] "
829 ": create and insert new tape image file and go to record mode\n"
830 "cassetteplayer insert <filename> "
831 ": insert (a different) tape file\n"
832 "cassetteplayer getpos "
833 ": query the position of the tape\n"
834 "cassetteplayer getlength "
835 ": query the total length of the tape\n"
836 "cassetteplayer <filename> "
837 ": insert (a different) tape file\n";
842void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens)
const
844 using namespace std::literals;
845 if (tokens.size() == 2) {
846 static constexpr std::array cmds = {
847 "eject"sv,
"rewind"sv,
"motorcontrol"sv,
"insert"sv,
"new"sv,
848 "play"sv,
"getpos"sv,
"getlength"sv,
852 }
else if ((tokens.size() == 3) && (tokens[1] ==
"insert")) {
854 }
else if ((tokens.size() == 3) && (tokens[1] ==
"motorcontrol")) {
855 static constexpr std::array extra = {
"on"sv,
"off"sv};
856 completeString(tokens, extra);
860bool CassettePlayer::TapeCommand::needRecord(std::span<const TclObject> tokens)
const
862 return tokens.size() > 1;
866static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
875template<
typename Archive>
883 ar.serialize(
"casImage", casImage);
886 if constexpr (!Archive::IS_LOADER) {
888 oldChecksum = playImage->getSha1Sum();
891 if (ar.versionAtLeast(version, 2)) {
892 string oldChecksumStr = oldChecksum.
empty()
895 ar.serialize(
"checksum", oldChecksumStr);
896 oldChecksum = oldChecksumStr.
empty()
901 if constexpr (Archive::IS_LOADER) {
903 auto time = getCurrentTime();
905 if (!oldChecksum.
empty() &&
908 if (file.is_open()) {
913 insertTape(casImage, time);
915 if (oldChecksum.
empty()) {
930 if (playImage && !oldChecksum.
empty()) {
931 Sha1Sum newChecksum = playImage->getSha1Sum();
932 if (oldChecksum != newChecksum) {
934 "The content of the tape ",
936 " has changed since the time this "
937 "savestate was created. This might "
938 "result in emulation problems.");
950 ar.serialize(
"tapePos", tapePos,
951 "prevSyncTime", prevSyncTime,
952 "audioPos", audioPos,
954 "lastOutput", lastOutput,
956 "motorControl", motorControl);
958 if constexpr (Archive::IS_LOADER) {
959 auto time = getCurrentTime();
960 if (playImage && (tapePos > playImage->getEndTime())) {
961 tapePos = playImage->getEndTime();
963 "beyond tape end! Setting tape position to end. "
964 "This can happen if you load a replay from an "
965 "older openMSX version with a different CAS-to-WAV "
966 "baud rate or when the tape image has been changed "
967 "compared to when the replay was created.");
972 "Restoring a state where the MSX was saving to "
973 "tape is not yet supported. Emulation will "
974 "continue without actually saving.");
975 setState(
STOP, getImageName(), time);
977 if (!playImage && (state ==
PLAY)) {
980 setState(
STOP, getImageName(), time);
983 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...
virtual void update(UpdateType type, std::string_view name, std::string_view value)=0
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.
CliComm & getMSXCliComm()
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)
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.
std::string getName(KeyCode keyCode)
Translate key code to key name.
This file implemented 3 utility functions:
SERIALIZE_ENUM(CassettePlayer::State, stateInfo)
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
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.