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 if (getState() ==
RECORD) {
222 return (
double(recordImage->getBytes()) + partialInterval) /
RECORD_FREQ;
224 return (tapePos - EmuTime::zero()).toDouble();
228 double CassettePlayer::getTapeLength(EmuTime::param time)
231 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
232 }
else if (getState() ==
RECORD) {
233 return getTapePos(time);
239 void CassettePlayer::checkInvariants()
const
241 switch (getState()) {
243 assert(!recordImage);
246 assert(!getImageName().empty());
252 assert(!getImageName().empty());
253 assert(!recordImage);
257 assert(!getImageName().empty());
266 void CassettePlayer::setState(State newState,
const Filename& newImage,
272 State oldState = getState();
273 if (oldState == newState)
return;
277 assert(!((oldState ==
PLAY) && (newState ==
RECORD)));
278 assert(!((oldState ==
RECORD) && (newState ==
PLAY)));
282 if ((oldState ==
RECORD) && recordImage) {
284 bool empty = recordImage->isEmpty();
295 setImageName(newImage);
300 partialInterval = 0.0;
307 updateLoadingState(time);
312 void CassettePlayer::updateLoadingState(EmuTime::param time)
314 assert(prevSyncTime == time);
317 loadingIndicator.
update(motor && (getState() ==
PLAY));
319 syncEndOfTape.removeSyncPoint();
320 if (isRolling() && (getState() ==
PLAY)) {
321 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
325 void CassettePlayer::setImageName(
const Filename& newImage)
332 void CassettePlayer::insertTape(
const Filename&
filename, EmuTime::param time)
338 playImage = std::make_unique<WavImage>(
filename, filePool);
339 }
catch (MSXException&
e) {
342 playImage = std::make_unique<CasImage>(
345 }
catch (MSXException& e2) {
347 "Failed to insert WAV image: \"",
349 "\" and also failed to insert CAS image: \"",
350 e2.getMessage(),
'\"');
364 unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
376 void CassettePlayer::playTape(
const Filename&
filename, EmuTime::param time)
383 setState(
STOP, getImageName(), time);
389 void CassettePlayer::rewind(EmuTime::param time)
392 assert(getState() !=
RECORD);
393 tapePos = EmuTime::zero();
396 if (getImageName().empty()) {
398 assert(getState() ==
STOP);
401 setState(
PLAY, getImageName(), time);
403 updateLoadingState(time);
406 void CassettePlayer::recordTape(
const Filename&
filename, EmuTime::param time)
410 tapePos = EmuTime::zero();
414 void CassettePlayer::removeTape(EmuTime::param time)
417 setState(
STOP, getImageName(), time);
420 tapePos = EmuTime::zero();
426 if (status != motor) {
429 updateLoadingState(time);
433 void CassettePlayer::setMotorControl(
bool status, EmuTime::param time)
435 if (status != motorControl) {
437 motorControl = status;
438 updateLoadingState(time);
444 if (getState() ==
PLAY) {
447 return isRolling() ? playImage->getSampleAt(tapePos) : 0;
460 void CassettePlayer::sync(EmuTime::param time)
465 updateTapePosition(duration, time);
466 generateRecordOutput(duration);
469 void CassettePlayer::updateTapePosition(
472 if (!isRolling() || (getState() !=
PLAY))
return;
475 assert(tapePos <= playImage->getEndTime());
478 if (!syncScheduled) {
480 syncScheduled =
true;
487 if (!recordImage || !isRolling())
return;
490 double samples = duration.toDouble() *
RECORD_FREQ;
491 double rest = 1.0 - partialInterval;
492 if (rest <= samples) {
494 partialOut += out * rest;
495 fillBuf(1,
int(partialOut));
499 int count = int(samples);
501 fillBuf(
count,
int(out));
504 assert(samples < 1.0);
507 partialOut = samples * out;
508 partialInterval = samples;
510 assert(samples < 1.0);
511 partialOut += samples * out;
512 partialInterval += samples;
514 assert(partialInterval < 1.0);
517 void CassettePlayer::fillBuf(
size_t length,
double x)
520 constexpr
double A = 252.0 / 256.0;
522 double y = lastY + (
x - lastX);
527 buf[sampCnt++] = int(y) + 128;
531 assert(sampCnt <= BUF_SIZE);
532 if (BUF_SIZE == sampCnt) {
540 void CassettePlayer::flushOutput()
543 recordImage->write(buf, 1,
unsigned(sampCnt));
545 recordImage->flush();
546 }
catch (MSXException&
e) {
548 "Failed to write to tape: ",
e.getMessage());
555 return getCassettePlayerName();
566 lastOutput =
static_cast<CassettePort&
>(conn).lastOut();
572 setState(
STOP, getImageName(), time);
579 if ((getState() !=
PLAY) || !isRolling()) {
580 buffers[0] =
nullptr;
583 playImage->fillBuffer(audioPos, buffers, num);
589 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
592 int CassettePlayer::signalEvent(
const Event& event) noexcept
595 if (!getImageName().empty()) {
598 playTape(getImageName(), getCurrentTime());
599 }
catch (MSXException&
e) {
600 motherBoard.getMSXCliComm().printWarning(
601 "Failed to insert tape: ",
e.getMessage());
608 void CassettePlayer::execEndOfTape(EmuTime::param time)
612 assert(tapePos == playImage->getEndTime());
614 "Tape end reached... stopping. "
615 "You may need to insert another tape image "
616 "that contains side B. (Or you used the wrong "
617 "loading command.)");
618 setState(
STOP, getImageName(), time);
621 void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
623 if (getState() ==
PLAY) {
627 clk.setFreq(playImage->getFrequency());
628 audioPos = clk.getTicksTill(tapePos);
630 syncScheduled =
false;
636 CassettePlayer::TapeCommand::TapeCommand(
637 CommandController& commandController_,
638 StateChangeDistributor& stateChangeDistributor_,
640 : RecordedCommand(commandController_, stateChangeDistributor_,
641 scheduler_,
"cassetteplayer")
645 void CassettePlayer::TapeCommand::execute(
646 std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
649 if (tokens.size() == 1) {
652 TclObject options =
makeTclList(cassettePlayer.getStateString());
654 cassettePlayer.getImageName().getResolved(),
657 }
else if (tokens[1] ==
"new") {
658 std::string_view directory =
"taperecordings";
659 std::string_view prefix =
"openmsx";
660 std::string_view extension =
".wav";
662 (tokens.size() == 3) ? tokens[2].getString() :
string{},
663 directory, prefix, extension);
666 "Created new cassette image file: ",
filename,
667 ", inserted it and set recording mode.");
669 }
else if (tokens[1] ==
"insert" && tokens.size() == 3) {
671 result =
"Changing tape";
673 cassettePlayer.playTape(
filename, time);
674 }
catch (MSXException&
e) {
675 throw CommandException(std::move(
e).getMessage());
678 }
else if (tokens[1] ==
"motorcontrol" && tokens.size() == 3) {
679 if (tokens[2] ==
"on") {
680 cassettePlayer.setMotorControl(
true, time);
681 result =
"Motor control enabled.";
682 }
else if (tokens[2] ==
"off") {
683 cassettePlayer.setMotorControl(
false, time);
684 result =
"Motor control disabled.";
689 }
else if (tokens.size() != 2) {
692 }
else if (tokens[1] ==
"motorcontrol") {
694 (cassettePlayer.motorControl ?
"on" :
"off"));
696 }
else if (tokens[1] ==
"record") {
697 result =
"TODO: implement this... (sorry)";
699 }
else if (tokens[1] ==
"play") {
702 result =
"Play mode set, rewinding tape.";
703 cassettePlayer.playTape(
704 cassettePlayer.getImageName(), time);
705 }
catch (MSXException&
e) {
706 throw CommandException(std::move(
e).getMessage());
709 throw CommandException(
"No tape inserted or tape at end!");
712 result =
"Already in play mode.";
715 }
else if (tokens[1] ==
"eject") {
716 result =
"Tape ejected";
717 cassettePlayer.removeTape(time);
719 }
else if (tokens[1] ==
"rewind") {
723 r =
"First stopping recording... ";
724 cassettePlayer.playTape(
725 cassettePlayer.getImageName(), time);
726 }
catch (MSXException&
e) {
727 throw CommandException(std::move(
e).getMessage());
730 cassettePlayer.rewind(time);
734 }
else if (tokens[1] ==
"getpos") {
735 result = cassettePlayer.getTapePos(time);
737 }
else if (tokens[1] ==
"getlength") {
738 result = cassettePlayer.getTapeLength(time);
742 result =
"Changing tape";
744 cassettePlayer.playTape(
filename, time);
745 }
catch (MSXException&
e) {
746 throw CommandException(std::move(
e).getMessage());
754 string CassettePlayer::TapeCommand::help(std::span<const TclObject> tokens)
const
757 if (tokens.size() >= 2) {
758 if (tokens[1] ==
"eject") {
760 "Well, just eject the cassette from the cassette "
762 }
else if (tokens[1] ==
"rewind") {
764 "Indeed, rewind the tape that is currently in the "
765 "cassette player/recorder...";
766 }
else if (tokens[1] ==
"motorcontrol") {
768 "Setting this to 'off' is equivalent to "
769 "disconnecting the black remote plug from the "
770 "cassette player: it makes the cassette player "
771 "run (if in play mode); the motor signal from the "
772 "MSX will be ignored. Normally this is set to "
773 "'on': the cassetteplayer obeys the motor control "
774 "signal from the MSX.";
775 }
else if (tokens[1] ==
"play") {
777 "Go to play mode. Only useful if you were in "
778 "record mode (which is currently the only other "
780 }
else if (tokens[1] ==
"new") {
782 "Create a new cassette image. If the file name is "
783 "omitted, one will be generated in the default "
784 "directory for tape recordings. Implies going to "
785 "record mode (why else do you want a new cassette "
787 }
else if (tokens[1] ==
"insert") {
789 "Inserts the specified cassette image into the "
790 "cassette player, rewinds it and switches to play "
792 }
else if (tokens[1] ==
"record") {
794 "Go to record mode. NOT IMPLEMENTED YET. Will be "
795 "used to be able to resume recording to an "
796 "existing cassette image, previously inserted with "
797 "the insert command.";
798 }
else if (tokens[1] ==
"getpos") {
800 "Return the position of the tape, in seconds from "
801 "the beginning of the tape.";
802 }
else if (tokens[1] ==
"getlength") {
804 "Return the length of the tape in seconds.";
808 "cassetteplayer eject "
809 ": remove tape from virtual player\n"
810 "cassetteplayer rewind "
811 ": rewind tape in virtual player\n"
812 "cassetteplayer motorcontrol "
813 ": enables or disables motor control (remote)\n"
814 "cassetteplayer play "
815 ": change to play mode (default)\n"
816 "cassetteplayer record "
817 ": change to record mode (NOT IMPLEMENTED YET)\n"
818 "cassetteplayer new [<filename>] "
819 ": create and insert new tape image file and go to record mode\n"
820 "cassetteplayer insert <filename> "
821 ": insert (a different) tape file\n"
822 "cassetteplayer getpos "
823 ": query the position of the tape\n"
824 "cassetteplayer getlength "
825 ": query the total length of the tape\n"
826 "cassetteplayer <filename> "
827 ": insert (a different) tape file\n";
832 void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens)
const
834 using namespace std::literals;
835 if (tokens.size() == 2) {
836 static constexpr std::array cmds = {
837 "eject"sv,
"rewind"sv,
"motorcontrol"sv,
"insert"sv,
"new"sv,
838 "play"sv,
"getpos"sv,
"getlength"sv,
842 }
else if ((tokens.size() == 3) && (tokens[1] ==
"insert")) {
844 }
else if ((tokens.size() == 3) && (tokens[1] ==
"motorcontrol")) {
845 static constexpr std::array extra = {
"on"sv,
"off"sv};
846 completeString(tokens, extra);
850 bool CassettePlayer::TapeCommand::needRecord(std::span<const TclObject> tokens)
const
852 return tokens.size() > 1;
856 static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
865 template<
typename Archive>
873 ar.serialize(
"casImage", casImage);
876 if constexpr (!Archive::IS_LOADER) {
878 oldChecksum = playImage->getSha1Sum();
881 if (ar.versionAtLeast(version, 2)) {
882 string oldChecksumStr = oldChecksum.
empty()
885 ar.serialize(
"checksum", oldChecksumStr);
886 oldChecksum = oldChecksumStr.
empty()
891 if constexpr (Archive::IS_LOADER) {
893 auto time = getCurrentTime();
895 if (!oldChecksum.
empty() &&
898 if (file.is_open()) {
903 insertTape(casImage, time);
905 if (oldChecksum.
empty()) {
920 if (playImage && !oldChecksum.
empty()) {
921 Sha1Sum newChecksum = playImage->getSha1Sum();
922 if (oldChecksum != newChecksum) {
924 "The content of the tape ",
926 " has changed since the time this "
927 "savestate was created. This might "
928 "result in emulation problems.");
940 ar.serialize(
"tapePos", tapePos,
941 "prevSyncTime", prevSyncTime,
942 "audioPos", audioPos,
944 "lastOutput", lastOutput,
946 "motorControl", motorControl);
948 if constexpr (Archive::IS_LOADER) {
949 auto time = getCurrentTime();
950 if (playImage && (tapePos > playImage->getEndTime())) {
951 tapePos = playImage->getEndTime();
953 "beyond tape end! Setting tape position to end. "
954 "This can happen if you load a replay from an "
955 "older openMSX version with a different CAS-to-WAV "
956 "baud rate or when the tape image has been changed "
957 "compared to when the replay was created.");
962 "Restoring a state where the MSX was saving to "
963 "tape is not yet supported. Emulation will "
964 "continue without actually saving.");
965 setState(
STOP, getImageName(), time);
967 if (!playImage && (state ==
PLAY)) {
970 setState(
STOP, getImageName(), time);
973 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.