67 static std::string_view getCassettePlayerName()
69 return "cassetteplayer";
74 , syncEndOfTape(hwConf.getMotherBoard().getScheduler())
75 , syncAudioEmu (hwConf.getMotherBoard().getScheduler())
76 , tapePos(EmuTime::zero())
77 , prevSyncTime(EmuTime::zero())
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 , motor(false), motorControl(true)
93 , syncScheduled(false)
97 XMLElement* result = doc.allocateElement(
"cassetteplayer");
108 removeTape(EmuTime::zero());
115 c->unplug(getCurrentTime());
122 void CassettePlayer::autoRun()
124 if (!playImage)
return;
138 string H_READ = is_SVI ?
"0xFE8E" :
"0xFF07";
139 string H_MAIN = is_SVI ?
"0xFE94" :
"0xFF0C";
140 string instr1, instr2;
143 instr1 = R
"({RUN\"CAS:\"\r})";
146 instr1 = R
"({BLOAD\"CAS:\",R\r})";
151 instr1 =
"{CLOAD\\r}";
158 "namespace eval ::openmsx {\n"
159 " variable auto_run_bp\n"
161 " proc auto_run_cb {args} {\n"
162 " variable auto_run_bp\n"
163 " debug remove_bp $auto_run_bp\n"
164 " unset auto_run_bp\n"
168 " after time 0.1 \"type [lindex $args 0]\"\n"
170 " set next [lrange $args 1 end]\n"
171 " if {[llength $next] == 0} return\n"
175 " set cmd \"openmsx::auto_run_cb $next\"\n"
176 " set openmsx::auto_run_bp [debug set_bp ", H_MAIN,
" 1 \"$cmd\"]\n"
179 " if {[info exists auto_run_bp]} {debug remove_bp $auto_run_bp\n}\n"
180 " set auto_run_bp [debug set_bp ", H_READ,
" 1 {\n"
181 " openmsx::auto_run_cb {{}} ", instr1,
' ', instr2,
"\n"
185 " type_via_keyboard \'\\r\n"
189 }
catch (CommandException&
e) {
191 "Error executing loading instruction using command \"",
192 command,
"\" for AutoRun: ",
193 e.getMessage(),
"\n Please report a bug.");
197 string CassettePlayer::getStateString()
const
199 switch (getState()) {
200 case PLAY:
return "play";
201 case RECORD:
return "record";
202 case STOP:
return "stop";
207 bool CassettePlayer::isRolling()
const
214 return (getState() !=
STOP) && (motor || !motorControl);
217 double CassettePlayer::getTapePos(EmuTime::param time)
220 return (tapePos - EmuTime::zero()).toDouble();
223 double CassettePlayer::getTapeLength(EmuTime::param time)
226 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
227 }
else if (getState() ==
RECORD) {
228 return getTapePos(time);
234 void CassettePlayer::checkInvariants()
const
236 switch (getState()) {
238 assert(!recordImage);
241 assert(!getImageName().empty());
247 assert(!getImageName().empty());
248 assert(!recordImage);
252 assert(!getImageName().empty());
261 void CassettePlayer::setState(State newState,
const Filename& newImage,
267 State oldState = getState();
268 if (oldState == newState)
return;
272 assert(!((oldState ==
PLAY) && (newState ==
RECORD)));
273 assert(!((oldState ==
RECORD) && (newState ==
PLAY)));
277 if ((oldState ==
RECORD) && recordImage) {
279 bool empty = recordImage->isEmpty();
290 setImageName(newImage);
295 partialInterval = 0.0;
302 updateLoadingState(time);
307 void CassettePlayer::updateLoadingState(EmuTime::param time)
309 assert(prevSyncTime == time);
312 loadingIndicator.
update(motor && (getState() ==
PLAY));
314 syncEndOfTape.removeSyncPoint();
315 if (isRolling() && (getState() ==
PLAY)) {
316 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
320 void CassettePlayer::setImageName(
const Filename& newImage)
327 void CassettePlayer::insertTape(
const Filename&
filename, EmuTime::param time)
333 playImage = std::make_unique<WavImage>(
filename, filePool);
334 }
catch (MSXException&
e) {
337 playImage = std::make_unique<CasImage>(
340 }
catch (MSXException& e2) {
342 "Failed to insert WAV image: \"",
344 "\" and also failed to insert CAS image: \"",
345 e2.getMessage(),
'\"');
359 unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
371 void CassettePlayer::playTape(
const Filename&
filename, EmuTime::param time)
378 setState(
STOP, getImageName(), time);
384 void CassettePlayer::rewind(EmuTime::param time)
387 assert(getState() !=
RECORD);
388 tapePos = EmuTime::zero();
391 if (getImageName().empty()) {
393 assert(getState() ==
STOP);
396 setState(
PLAY, getImageName(), time);
398 updateLoadingState(time);
401 void CassettePlayer::recordTape(
const Filename&
filename, EmuTime::param time)
405 tapePos = EmuTime::zero();
409 void CassettePlayer::removeTape(EmuTime::param time)
412 setState(
STOP, getImageName(), time);
415 tapePos = EmuTime::zero();
421 if (status != motor) {
424 updateLoadingState(time);
428 void CassettePlayer::setMotorControl(
bool status, EmuTime::param time)
430 if (status != motorControl) {
432 motorControl = status;
433 updateLoadingState(time);
439 if (getState() ==
PLAY) {
442 return isRolling() ? playImage->getSampleAt(tapePos) : 0;
455 void CassettePlayer::sync(EmuTime::param time)
460 updateTapePosition(duration, time);
461 generateRecordOutput(duration);
464 void CassettePlayer::updateTapePosition(
467 if (!isRolling() || (getState() !=
PLAY))
return;
470 assert(tapePos <= playImage->getEndTime());
473 if (!syncScheduled) {
475 syncScheduled =
true;
482 if (!recordImage || !isRolling())
return;
485 double samples = duration.toDouble() *
RECORD_FREQ;
486 double rest = 1.0 - partialInterval;
487 if (rest <= samples) {
489 partialOut += out * rest;
490 fillBuf(1,
int(partialOut));
494 int count = int(samples);
496 fillBuf(
count,
int(out));
501 partialOut = samples * out;
502 partialInterval = 0.0;
504 partialOut += samples * out;
505 partialInterval += samples;
509 void CassettePlayer::fillBuf(
size_t length,
double x)
512 constexpr
double A = 252.0 / 256.0;
514 double y = lastY + (
x - lastX);
519 buf[sampcnt++] = int(y) + 128;
523 assert(sampcnt <= BUF_SIZE);
524 if (BUF_SIZE == sampcnt) {
532 void CassettePlayer::flushOutput()
535 recordImage->write(buf, 1,
unsigned(sampcnt));
537 recordImage->flush();
538 }
catch (MSXException&
e) {
540 "Failed to write to tape: ",
e.getMessage());
547 return getCassettePlayerName();
558 lastOutput =
static_cast<CassettePort&
>(conn).lastOut();
564 setState(
STOP, getImageName(), time);
571 if ((getState() !=
PLAY) || !isRolling()) {
572 buffers[0] =
nullptr;
575 playImage->fillBuffer(audioPos, buffers, num);
581 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
584 int CassettePlayer::signalEvent(
const Event& event) noexcept
587 if (!getImageName().empty()) {
590 playTape(getImageName(), getCurrentTime());
591 }
catch (MSXException&
e) {
592 motherBoard.getMSXCliComm().printWarning(
593 "Failed to insert tape: ",
e.getMessage());
600 void CassettePlayer::execEndOfTape(EmuTime::param time)
604 assert(tapePos == playImage->getEndTime());
606 "Tape end reached... stopping. "
607 "You may need to insert another tape image "
608 "that contains side B. (Or you used the wrong "
609 "loading command.)");
610 setState(
STOP, getImageName(), time);
613 void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
615 if (getState() ==
PLAY) {
619 clk.setFreq(playImage->getFrequency());
620 audioPos = clk.getTicksTill(tapePos);
622 syncScheduled =
false;
628 CassettePlayer::TapeCommand::TapeCommand(
629 CommandController& commandController_,
630 StateChangeDistributor& stateChangeDistributor_,
632 : RecordedCommand(commandController_, stateChangeDistributor_,
633 scheduler_,
"cassetteplayer")
637 void CassettePlayer::TapeCommand::execute(
638 std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
641 if (tokens.size() == 1) {
644 TclObject options =
makeTclList(cassettePlayer.getStateString());
646 cassettePlayer.getImageName().getResolved(),
649 }
else if (tokens[1] ==
"new") {
650 std::string_view directory =
"taperecordings";
651 std::string_view prefix =
"openmsx";
652 std::string_view extension =
".wav";
654 (tokens.size() == 3) ? tokens[2].getString() :
string{},
655 directory, prefix, extension);
658 "Created new cassette image file: ",
filename,
659 ", inserted it and set recording mode.");
661 }
else if (tokens[1] ==
"insert" && tokens.size() == 3) {
663 result =
"Changing tape";
665 cassettePlayer.playTape(
filename, time);
666 }
catch (MSXException&
e) {
667 throw CommandException(std::move(
e).getMessage());
670 }
else if (tokens[1] ==
"motorcontrol" && tokens.size() == 3) {
671 if (tokens[2] ==
"on") {
672 cassettePlayer.setMotorControl(
true, time);
673 result =
"Motor control enabled.";
674 }
else if (tokens[2] ==
"off") {
675 cassettePlayer.setMotorControl(
false, time);
676 result =
"Motor control disabled.";
681 }
else if (tokens.size() != 2) {
684 }
else if (tokens[1] ==
"motorcontrol") {
686 (cassettePlayer.motorControl ?
"on" :
"off"));
688 }
else if (tokens[1] ==
"record") {
689 result =
"TODO: implement this... (sorry)";
691 }
else if (tokens[1] ==
"play") {
694 result =
"Play mode set, rewinding tape.";
695 cassettePlayer.playTape(
696 cassettePlayer.getImageName(), time);
697 }
catch (MSXException&
e) {
698 throw CommandException(std::move(
e).getMessage());
701 throw CommandException(
"No tape inserted or tape at end!");
704 result =
"Already in play mode.";
707 }
else if (tokens[1] ==
"eject") {
708 result =
"Tape ejected";
709 cassettePlayer.removeTape(time);
711 }
else if (tokens[1] ==
"rewind") {
715 r =
"First stopping recording... ";
716 cassettePlayer.playTape(
717 cassettePlayer.getImageName(), time);
718 }
catch (MSXException&
e) {
719 throw CommandException(std::move(
e).getMessage());
722 cassettePlayer.rewind(time);
726 }
else if (tokens[1] ==
"getpos") {
727 result = cassettePlayer.getTapePos(time);
729 }
else if (tokens[1] ==
"getlength") {
730 result = cassettePlayer.getTapeLength(time);
734 result =
"Changing tape";
736 cassettePlayer.playTape(
filename, time);
737 }
catch (MSXException&
e) {
738 throw CommandException(std::move(
e).getMessage());
746 string CassettePlayer::TapeCommand::help(std::span<const TclObject> tokens)
const
749 if (tokens.size() >= 2) {
750 if (tokens[1] ==
"eject") {
752 "Well, just eject the cassette from the cassette "
754 }
else if (tokens[1] ==
"rewind") {
756 "Indeed, rewind the tape that is currently in the "
757 "cassette player/recorder...";
758 }
else if (tokens[1] ==
"motorcontrol") {
760 "Setting this to 'off' is equivalent to "
761 "disconnecting the black remote plug from the "
762 "cassette player: it makes the cassette player "
763 "run (if in play mode); the motor signal from the "
764 "MSX will be ignored. Normally this is set to "
765 "'on': the cassetteplayer obeys the motor control "
766 "signal from the MSX.";
767 }
else if (tokens[1] ==
"play") {
769 "Go to play mode. Only useful if you were in "
770 "record mode (which is currently the only other "
772 }
else if (tokens[1] ==
"new") {
774 "Create a new cassette image. If the file name is "
775 "omitted, one will be generated in the default "
776 "directory for tape recordings. Implies going to "
777 "record mode (why else do you want a new cassette "
779 }
else if (tokens[1] ==
"insert") {
781 "Inserts the specified cassette image into the "
782 "cassette player, rewinds it and switches to play "
784 }
else if (tokens[1] ==
"record") {
786 "Go to record mode. NOT IMPLEMENTED YET. Will be "
787 "used to be able to resume recording to an "
788 "existing cassette image, previously inserted with "
789 "the insert command.";
790 }
else if (tokens[1] ==
"getpos") {
792 "Return the position of the tape, in seconds from "
793 "the beginning of the tape.";
794 }
else if (tokens[1] ==
"getlength") {
796 "Return the length of the tape in seconds.";
800 "cassetteplayer eject "
801 ": remove tape from virtual player\n"
802 "cassetteplayer rewind "
803 ": rewind tape in virtual player\n"
804 "cassetteplayer motorcontrol "
805 ": enables or disables motor control (remote)\n"
806 "cassetteplayer play "
807 ": change to play mode (default)\n"
808 "cassetteplayer record "
809 ": change to record mode (NOT IMPLEMENTED YET)\n"
810 "cassetteplayer new [<filename>] "
811 ": create and insert new tape image file and go to record mode\n"
812 "cassetteplayer insert <filename> "
813 ": insert (a different) tape file\n"
814 "cassetteplayer getpos "
815 ": query the position of the tape\n"
816 "cassetteplayer getlength "
817 ": query the total length of the tape\n"
818 "cassetteplayer <filename> "
819 ": insert (a different) tape file\n";
824 void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens)
const
826 using namespace std::literals;
827 if (tokens.size() == 2) {
828 static constexpr std::array cmds = {
829 "eject"sv,
"rewind"sv,
"motorcontrol"sv,
"insert"sv,
"new"sv,
830 "play"sv,
"getpos"sv,
"getlength"sv,
834 }
else if ((tokens.size() == 3) && (tokens[1] ==
"insert")) {
836 }
else if ((tokens.size() == 3) && (tokens[1] ==
"motorcontrol")) {
837 static constexpr std::array extra = {
"on"sv,
"off"sv};
838 completeString(tokens, extra);
842 bool CassettePlayer::TapeCommand::needRecord(std::span<const TclObject> tokens)
const
844 return tokens.size() > 1;
848 static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
857 template<
typename Archive>
865 ar.serialize(
"casImage", casImage);
868 if constexpr (!Archive::IS_LOADER) {
870 oldChecksum = playImage->getSha1Sum();
873 if (ar.versionAtLeast(version, 2)) {
874 string oldChecksumStr = oldChecksum.
empty()
877 ar.serialize(
"checksum", oldChecksumStr);
878 oldChecksum = oldChecksumStr.
empty()
883 if constexpr (Archive::IS_LOADER) {
885 auto time = getCurrentTime();
887 if (!oldChecksum.
empty() &&
890 if (file.is_open()) {
895 insertTape(casImage, time);
897 if (oldChecksum.
empty()) {
912 if (playImage && !oldChecksum.
empty()) {
913 Sha1Sum newChecksum = playImage->getSha1Sum();
914 if (oldChecksum != newChecksum) {
916 "The content of the tape ",
918 " has changed since the time this "
919 "savestate was created. This might "
920 "result in emulation problems.");
932 ar.serialize(
"tapePos", tapePos,
933 "prevSyncTime", prevSyncTime,
934 "audioPos", audioPos,
936 "lastOutput", lastOutput,
938 "motorControl", motorControl);
940 if constexpr (Archive::IS_LOADER) {
941 auto time = getCurrentTime();
942 if (playImage && (tapePos > playImage->getEndTime())) {
943 tapePos = playImage->getEndTime();
945 "beyond tape end! Setting tape position to end. "
946 "This can happen if you load a replay from an "
947 "older openMSX version with a different CAS-to-WAV "
948 "baud rate or when the tape image has been changed "
949 "compared to when the replay was created.");
954 "Restoring a state where the MSX was saving to "
955 "tape is not yet supported. Emulation will "
956 "continue without actually saving.");
957 setState(
STOP, getImageName(), time);
959 if (!playImage && (state ==
PLAY)) {
962 setState(
STOP, getImageName(), time);
965 updateLoadingState(time);
bool getBoolean() const noexcept
void plugHelper(Connector &connector, EmuTime::param time) override
void generateChannels(float **buffers, unsigned num) override
Abstract method to generate the actual sound data.
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 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.
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.
ReverseManager & getReverseManager()
CliComm & getMSXCliComm()
CommandController & getCommandController()
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.
static XMLDocument & getStaticDocument()
XMLElement * setFirstChild(XMLElement *child)
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
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.
std::string getName(KeyCode keyCode)
Translate key code to key name.
This file implemented 3 utility functions:
constexpr unsigned DUMMY_INPUT_RATE
SERIALIZE_ENUM(CassettePlayer::State, stateInfo)
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
constexpr const char *const filename
constexpr KeyMatrixPosition x
Keyboard bindings.
EventType getType(const Event &event)
FileContext userFileContext(string_view savePath)
constexpr static_string_view DESCRIPTION
constexpr unsigned RECORD_FREQ
TclObject makeTclList(Args &&... args)
constexpr double OUTPUT_AMP
#define OUTER(type, member)
#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.