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"
173 " after time 0.1 \"type [lindex $args 0]\"\n"
175 " set next [lrange $args 1 end]\n"
176 " if {[llength $next] == 0} return\n"
180 " set cmd \"openmsx::auto_run_cb $next\"\n"
181 " set openmsx::auto_run_bp [debug set_bp ", H_MAIN,
" 1 \"$cmd\"]\n"
184 " if {[info exists auto_run_bp]} {debug remove_bp $auto_run_bp\n}\n"
185 " set auto_run_bp [debug set_bp ", H_READ,
" 1 {\n"
186 " openmsx::auto_run_cb {{}} ", instr1,
' ', instr2,
"\n"
190 " type_via_keyboard \'\\r\n"
194 }
catch (CommandException&
e) {
196 "Error executing loading instruction using command \"",
197 command,
"\" for AutoRun: ",
198 e.getMessage(),
"\n Please report a bug.");
202string CassettePlayer::getStateString()
const
204 switch (getState()) {
205 case PLAY:
return "play";
206 case RECORD:
return "record";
207 case STOP:
return "stop";
212bool CassettePlayer::isRolling()
const
219 return (getState() !=
STOP) && (motor || !motorControl);
222double CassettePlayer::getTapePos(EmuTime::param time)
225 if (getState() ==
RECORD) {
227 return (
double(recordImage->getBytes()) + partialInterval) / RECORD_FREQ;
229 return (tapePos - EmuTime::zero()).toDouble();
233double CassettePlayer::getTapeLength(EmuTime::param time)
236 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
237 }
else if (getState() ==
RECORD) {
238 return getTapePos(time);
244void CassettePlayer::checkInvariants()
const
246 switch (getState()) {
248 assert(!recordImage);
251 assert(!getImageName().empty());
257 assert(!getImageName().empty());
258 assert(!recordImage);
262 assert(!getImageName().empty());
271void CassettePlayer::setState(State newState,
const Filename& newImage,
277 State oldState = getState();
278 if (oldState == newState)
return;
282 assert(!((oldState ==
PLAY) && (newState ==
RECORD)));
283 assert(!((oldState ==
RECORD) && (newState ==
PLAY)));
287 if ((oldState ==
RECORD) && recordImage) {
289 bool empty = recordImage->isEmpty();
300 setImageName(newImage);
305 partialInterval = 0.0;
306 lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
312 updateLoadingState(time);
317void CassettePlayer::updateLoadingState(EmuTime::param time)
319 assert(prevSyncTime == time);
322 loadingIndicator.
update(motor && (getState() ==
PLAY));
324 syncEndOfTape.removeSyncPoint();
325 if (isRolling() && (getState() ==
PLAY)) {
326 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
330void CassettePlayer::setImageName(
const Filename& newImage)
337void CassettePlayer::insertTape(
const Filename& filename, EmuTime::param time)
339 if (!filename.empty()) {
343 playImage = std::make_unique<WavImage>(filename, filePool);
344 }
catch (MSXException&
e) {
347 playImage = std::make_unique<CasImage>(
350 }
catch (MSXException& e2) {
352 "Failed to insert WAV image: \"",
354 "\" and also failed to insert CAS image: \"",
355 e2.getMessage(),
'\"');
369 unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
378 setImageName(filename);
381void CassettePlayer::playTape(
const Filename& filename, EmuTime::param time)
388 setState(
STOP, getImageName(), time);
389 insertTape(filename, time);
394void CassettePlayer::rewind(EmuTime::param time)
397 assert(getState() !=
RECORD);
398 tapePos = EmuTime::zero();
401 if (getImageName().empty()) {
403 assert(getState() ==
STOP);
406 setState(
PLAY, getImageName(), time);
408 updateLoadingState(time);
411void CassettePlayer::recordTape(
const Filename& filename, EmuTime::param time)
414 recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
415 tapePos = EmuTime::zero();
416 setState(
RECORD, filename, time);
419void CassettePlayer::removeTape(EmuTime::param time)
422 setState(
STOP, getImageName(), time);
425 tapePos = EmuTime::zero();
431 if (status != motor) {
434 updateLoadingState(time);
438void CassettePlayer::setMotorControl(
bool status, EmuTime::param time)
440 if (status != motorControl) {
442 motorControl = status;
443 updateLoadingState(time);
449 if (getState() ==
PLAY) {
452 return isRolling() ? playImage->getSampleAt(tapePos) : int16_t(0);
465void CassettePlayer::sync(EmuTime::param time)
470 updateTapePosition(duration, time);
471 generateRecordOutput(duration);
474void CassettePlayer::updateTapePosition(
477 if (!isRolling() || (getState() !=
PLAY))
return;
480 assert(tapePos <= playImage->getEndTime());
483 if (!syncScheduled) {
485 syncScheduled =
true;
492 if (!recordImage || !isRolling())
return;
494 double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
495 double samples = duration.toDouble() * RECORD_FREQ;
496 double rest = 1.0 - partialInterval;
497 if (rest <= samples) {
499 partialOut += out * rest;
500 fillBuf(1, partialOut);
504 int count = int(samples);
509 assert(samples < 1.0);
512 partialOut = samples * out;
513 partialInterval = samples;
515 assert(samples < 1.0);
516 partialOut += samples * out;
517 partialInterval += samples;
519 assert(partialInterval < 1.0);
522void CassettePlayer::fillBuf(
size_t length,
double x)
525 constexpr double A = 252.0 / 256.0;
527 double y = lastY + (x - lastX);
532 buf[sampCnt++] = narrow<uint8_t>(
int(y) + 128);
536 assert(sampCnt <= buf.size());
537 if (sampCnt == buf.size()) {
545void CassettePlayer::flushOutput()
548 recordImage->write(
subspan(buf, 0, sampCnt));
550 recordImage->flush();
551 }
catch (MSXException&
e) {
553 "Failed to write to tape: ",
e.getMessage());
560 return getCassettePlayerName();
571 lastOutput = checked_cast<CassettePort&>(conn).lastOut();
577 setState(
STOP, getImageName(), time);
584 assert(buffers.size() == 1);
585 if ((getState() !=
PLAY) || !isRolling()) {
586 buffers[0] =
nullptr;
589 assert(buffers.size() == 1);
590 playImage->fillBuffer(audioPos, buffers.first<1>(), num);
596 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
599int CassettePlayer::signalEvent(
const Event& event)
602 if (!getImageName().empty()) {
605 playTape(getImageName(), getCurrentTime());
606 }
catch (MSXException&
e) {
608 "Failed to insert tape: ",
e.getMessage());
615void CassettePlayer::execEndOfTape(EmuTime::param time)
619 assert(tapePos == playImage->getEndTime());
621 "Tape end reached... stopping. "
622 "You may need to insert another tape image "
623 "that contains side B. (Or you used the wrong "
624 "loading command.)");
625 setState(
STOP, getImageName(), time);
628void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
630 if (getState() ==
PLAY) {
634 clk.setFreq(playImage->getFrequency());
635 audioPos = clk.getTicksTill(tapePos);
637 syncScheduled =
false;
643CassettePlayer::TapeCommand::TapeCommand(
644 CommandController& commandController_,
645 StateChangeDistributor& stateChangeDistributor_,
647 : RecordedCommand(commandController_, stateChangeDistributor_,
648 scheduler_,
"cassetteplayer")
652void CassettePlayer::TapeCommand::execute(
653 std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
656 if (tokens.size() == 1) {
659 TclObject options =
makeTclList(cassettePlayer.getStateString());
661 cassettePlayer.getImageName().getResolved(),
664 }
else if (tokens[1] ==
"new") {
665 std::string_view directory =
"taperecordings";
666 std::string_view prefix =
"openmsx";
667 std::string_view extension =
".wav";
669 (tokens.size() == 3) ? tokens[2].getString() :
string{},
670 directory, prefix, extension);
671 cassettePlayer.recordTape(
Filename(filename), time);
673 "Created new cassette image file: ", filename,
674 ", inserted it and set recording mode.");
676 }
else if (tokens[1] ==
"insert" && tokens.size() == 3) {
678 result =
"Changing tape";
680 cassettePlayer.playTape(filename, time);
681 }
catch (MSXException&
e) {
682 throw CommandException(std::move(
e).getMessage());
685 }
else if (tokens[1] ==
"motorcontrol" && tokens.size() == 3) {
686 if (tokens[2] ==
"on") {
687 cassettePlayer.setMotorControl(
true, time);
688 result =
"Motor control enabled.";
689 }
else if (tokens[2] ==
"off") {
690 cassettePlayer.setMotorControl(
false, time);
691 result =
"Motor control disabled.";
696 }
else if (tokens.size() != 2) {
699 }
else if (tokens[1] ==
"motorcontrol") {
701 (cassettePlayer.motorControl ?
"on" :
"off"));
703 }
else if (tokens[1] ==
"record") {
704 result =
"TODO: implement this... (sorry)";
706 }
else if (tokens[1] ==
"play") {
709 result =
"Play mode set, rewinding tape.";
710 cassettePlayer.playTape(
711 cassettePlayer.getImageName(), time);
712 }
catch (MSXException&
e) {
713 throw CommandException(std::move(
e).getMessage());
716 throw CommandException(
"No tape inserted or tape at end!");
719 result =
"Already in play mode.";
722 }
else if (tokens[1] ==
"eject") {
723 result =
"Tape ejected";
724 cassettePlayer.removeTape(time);
726 }
else if (tokens[1] ==
"rewind") {
730 r =
"First stopping recording... ";
731 cassettePlayer.playTape(
732 cassettePlayer.getImageName(), time);
733 }
catch (MSXException&
e) {
734 throw CommandException(std::move(
e).getMessage());
737 cassettePlayer.rewind(time);
741 }
else if (tokens[1] ==
"getpos") {
742 result = cassettePlayer.getTapePos(time);
744 }
else if (tokens[1] ==
"getlength") {
745 result = cassettePlayer.getTapeLength(time);
749 result =
"Changing tape";
751 cassettePlayer.playTape(filename, time);
752 }
catch (MSXException&
e) {
753 throw CommandException(std::move(
e).getMessage());
761string CassettePlayer::TapeCommand::help(std::span<const TclObject> tokens)
const
764 if (tokens.size() >= 2) {
765 if (tokens[1] ==
"eject") {
767 "Well, just eject the cassette from the cassette "
769 }
else if (tokens[1] ==
"rewind") {
771 "Indeed, rewind the tape that is currently in the "
772 "cassette player/recorder...";
773 }
else if (tokens[1] ==
"motorcontrol") {
775 "Setting this to 'off' is equivalent to "
776 "disconnecting the black remote plug from the "
777 "cassette player: it makes the cassette player "
778 "run (if in play mode); the motor signal from the "
779 "MSX will be ignored. Normally this is set to "
780 "'on': the cassetteplayer obeys the motor control "
781 "signal from the MSX.";
782 }
else if (tokens[1] ==
"play") {
784 "Go to play mode. Only useful if you were in "
785 "record mode (which is currently the only other "
787 }
else if (tokens[1] ==
"new") {
789 "Create a new cassette image. If the file name is "
790 "omitted, one will be generated in the default "
791 "directory for tape recordings. Implies going to "
792 "record mode (why else do you want a new cassette "
794 }
else if (tokens[1] ==
"insert") {
796 "Inserts the specified cassette image into the "
797 "cassette player, rewinds it and switches to play "
799 }
else if (tokens[1] ==
"record") {
801 "Go to record mode. NOT IMPLEMENTED YET. Will be "
802 "used to be able to resume recording to an "
803 "existing cassette image, previously inserted with "
804 "the insert command.";
805 }
else if (tokens[1] ==
"getpos") {
807 "Return the position of the tape, in seconds from "
808 "the beginning of the tape.";
809 }
else if (tokens[1] ==
"getlength") {
811 "Return the length of the tape in seconds.";
815 "cassetteplayer eject "
816 ": remove tape from virtual player\n"
817 "cassetteplayer rewind "
818 ": rewind tape in virtual player\n"
819 "cassetteplayer motorcontrol "
820 ": enables or disables motor control (remote)\n"
821 "cassetteplayer play "
822 ": change to play mode (default)\n"
823 "cassetteplayer record "
824 ": change to record mode (NOT IMPLEMENTED YET)\n"
825 "cassetteplayer new [<filename>] "
826 ": create and insert new tape image file and go to record mode\n"
827 "cassetteplayer insert <filename> "
828 ": insert (a different) tape file\n"
829 "cassetteplayer getpos "
830 ": query the position of the tape\n"
831 "cassetteplayer getlength "
832 ": query the total length of the tape\n"
833 "cassetteplayer <filename> "
834 ": insert (a different) tape file\n";
839void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens)
const
841 using namespace std::literals;
842 if (tokens.size() == 2) {
843 static constexpr std::array cmds = {
844 "eject"sv,
"rewind"sv,
"motorcontrol"sv,
"insert"sv,
"new"sv,
845 "play"sv,
"getpos"sv,
"getlength"sv,
849 }
else if ((tokens.size() == 3) && (tokens[1] ==
"insert")) {
851 }
else if ((tokens.size() == 3) && (tokens[1] ==
"motorcontrol")) {
852 static constexpr std::array extra = {
"on"sv,
"off"sv};
853 completeString(tokens, extra);
857bool CassettePlayer::TapeCommand::needRecord(std::span<const TclObject> tokens)
const
859 return tokens.size() > 1;
863static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
872template<
typename Archive>
880 ar.serialize(
"casImage", casImage);
883 if constexpr (!Archive::IS_LOADER) {
885 oldChecksum = playImage->getSha1Sum();
888 if (ar.versionAtLeast(version, 2)) {
889 string oldChecksumStr = oldChecksum.
empty()
892 ar.serialize(
"checksum", oldChecksumStr);
893 oldChecksum = oldChecksumStr.
empty()
898 if constexpr (Archive::IS_LOADER) {
900 auto time = getCurrentTime();
902 if (!oldChecksum.
empty() &&
905 if (file.is_open()) {
910 insertTape(casImage, time);
912 if (oldChecksum.
empty()) {
927 if (playImage && !oldChecksum.
empty()) {
928 Sha1Sum newChecksum = playImage->getSha1Sum();
929 if (oldChecksum != newChecksum) {
931 "The content of the tape ",
933 " has changed since the time this "
934 "savestate was created. This might "
935 "result in emulation problems.");
947 ar.serialize(
"tapePos", tapePos,
948 "prevSyncTime", prevSyncTime,
949 "audioPos", audioPos,
951 "lastOutput", lastOutput,
953 "motorControl", motorControl);
955 if constexpr (Archive::IS_LOADER) {
956 auto time = getCurrentTime();
957 if (playImage && (tapePos > playImage->getEndTime())) {
958 tapePos = playImage->getEndTime();
960 "beyond tape end! Setting tape position to end. "
961 "This can happen if you load a replay from an "
962 "older openMSX version with a different CAS-to-WAV "
963 "baud rate or when the tape image has been changed "
964 "compared to when the replay was created.");
969 "Restoring a state where the MSX was saving to "
970 "tape is not yet supported. Emulation will "
971 "continue without actually saving.");
972 setState(
STOP, getImageName(), time);
974 if (!playImage && (state ==
PLAY)) {
977 setState(
STOP, getImageName(), time);
980 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.