75 static std::string_view getCassettePlayerName()
77 return "cassetteplayer";
82 , syncEndOfTape(hwConf.getMotherBoard().getScheduler())
83 , syncAudioEmu (hwConf.getMotherBoard().getScheduler())
84 , tapePos(EmuTime::zero())
85 , prevSyncTime(EmuTime::zero())
87 , motherBoard(hwConf.getMotherBoard())
89 motherBoard.getCommandController(),
90 motherBoard.getStateChangeDistributor(),
91 motherBoard.getScheduler())
93 motherBoard.getReactor().getGlobalSettings().getThrottleManager())
95 motherBoard.getCommandController(),
96 "autoruncassettes",
"automatically try to run cassettes", true)
100 , motor(false), motorControl(true)
101 , syncScheduled(false)
110 removeTape(EmuTime::zero());
117 c->unplug(getCurrentTime());
124 void CassettePlayer::autoRun()
126 if (!playImage)
return;
139 string instr1, instr2;
142 instr1 = R
"(RUN\"CAS:\")";
145 instr1 = R
"(BLOAD\"CAS:\",R)";
157 "namespace eval ::openmsx {\n"
158 " variable auto_run_bp\n"
160 " proc auto_run_cb {args} {\n"
161 " variable auto_run_bp\n"
162 " debug remove_bp $auto_run_bp\n"
163 " unset auto_run_bp\n"
167 " after time 0.1 \"type [lindex $args 0]\\\\r\"\n"
169 " set next [lrange $args 1 end]\n"
170 " if {[llength $next] == 0} return\n"
175 " set cmd1 \"openmsx::auto_run_cb $next\"\n"
176 " set cmd2 \"set openmsx::auto_run_bp \\[debug set_bp 0xFF0C 1 \\\"$cmd1\\\"\\]\"\n"
177 " after time 0.2 $cmd2\n"
180 " if {[info exists auto_run_bp]} {debug remove_bp $auto_run_bp\n}\n"
181 " set auto_run_bp [debug set_bp 0xFF07 1 {\n"
182 " openmsx::auto_run_cb ", instr1,
' ', instr2,
"\n"
186 " type_via_keyboard \'\\r\n"
190 }
catch (CommandException& e) {
192 "Error executing loading instruction using command \"",
193 command,
"\" for AutoRun: ",
194 e.getMessage(),
"\n Please report a bug.");
198 string CassettePlayer::getStateString()
const
200 switch (getState()) {
201 case PLAY:
return "play";
202 case RECORD:
return "record";
203 case STOP:
return "stop";
208 bool CassettePlayer::isRolling()
const
215 return (getState() !=
STOP) && (motor || !motorControl);
218 double CassettePlayer::getTapePos(EmuTime::param time)
221 return (tapePos - EmuTime::zero()).toDouble();
224 double CassettePlayer::getTapeLength(EmuTime::param time)
227 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
228 }
else if (getState() ==
RECORD) {
229 return getTapePos(time);
235 void CassettePlayer::checkInvariants()
const
237 switch (getState()) {
239 assert(!recordImage);
242 assert(!getImageName().empty());
248 assert(!getImageName().empty());
249 assert(!recordImage);
253 assert(!getImageName().empty());
262 void CassettePlayer::setState(State newState,
const Filename& newImage,
268 State oldState = getState();
269 if (oldState == newState)
return;
273 assert(!((oldState ==
PLAY) && (newState ==
RECORD)));
274 assert(!((oldState ==
RECORD) && (newState ==
PLAY)));
278 if ((oldState ==
RECORD) && recordImage) {
280 bool empty = recordImage->isEmpty();
291 setImageName(newImage);
296 partialInterval = 0.0;
303 updateLoadingState(time);
308 void CassettePlayer::updateLoadingState(EmuTime::param time)
310 assert(prevSyncTime == time);
313 loadingIndicator.
update(motor && (getState() ==
PLAY));
315 syncEndOfTape.removeSyncPoint();
316 if (isRolling() && (getState() ==
PLAY)) {
317 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
321 void CassettePlayer::setImageName(
const Filename& newImage)
328 void CassettePlayer::insertTape(
const Filename&
filename, EmuTime::param time)
334 playImage = std::make_unique<WavImage>(
filename, filePool);
335 }
catch (MSXException& e) {
338 playImage = std::make_unique<CasImage>(
341 }
catch (MSXException& e2) {
343 "Failed to insert WAV image: \"",
345 "\" and also failed to insert CAS image: \"",
346 e2.getMessage(),
'\"');
360 unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
372 void CassettePlayer::playTape(
const Filename&
filename, EmuTime::param time)
379 setState(
STOP, getImageName(), time);
385 void CassettePlayer::rewind(EmuTime::param time)
388 assert(getState() !=
RECORD);
389 tapePos = EmuTime::zero();
392 if (getImageName().empty()) {
394 assert(getState() ==
STOP);
397 setState(
PLAY, getImageName(), time);
399 updateLoadingState(time);
402 void CassettePlayer::recordTape(
const Filename&
filename, EmuTime::param time)
406 tapePos = EmuTime::zero();
410 void CassettePlayer::removeTape(EmuTime::param time)
413 setState(
STOP, getImageName(), time);
416 tapePos = EmuTime::zero();
422 if (status != motor) {
425 updateLoadingState(time);
429 void CassettePlayer::setMotorControl(
bool status, EmuTime::param time)
431 if (status != motorControl) {
433 motorControl = status;
434 updateLoadingState(time);
440 if (getState() ==
PLAY) {
443 return isRolling() ? playImage->getSampleAt(tapePos) : 0;
456 void CassettePlayer::sync(EmuTime::param time)
461 updateTapePosition(duration, time);
462 generateRecordOutput(duration);
465 void CassettePlayer::updateTapePosition(
468 if (!isRolling() || (getState() !=
PLAY))
return;
471 assert(tapePos <= playImage->getEndTime());
474 if (!syncScheduled) {
476 syncScheduled =
true;
483 if (!recordImage || !isRolling())
return;
486 double samples = duration.toDouble() *
RECORD_FREQ;
487 double rest = 1.0 - partialInterval;
488 if (rest <= samples) {
490 partialOut += out * rest;
491 fillBuf(1,
int(partialOut));
495 int count = int(samples);
497 fillBuf(
count,
int(out));
502 partialOut = samples * out;
503 partialInterval = 0.0;
505 partialOut += samples * out;
506 partialInterval += samples;
510 void CassettePlayer::fillBuf(
size_t length,
double x)
513 constexpr
double A = 252.0 / 256.0;
515 double y = lastY + (
x - lastX);
520 buf[sampcnt++] = int(y) + 128;
524 assert(sampcnt <= BUF_SIZE);
525 if (BUF_SIZE == sampcnt) {
533 void CassettePlayer::flushOutput()
536 recordImage->write(buf, 1,
unsigned(sampcnt));
538 recordImage->flush();
539 }
catch (MSXException& e) {
541 "Failed to write to tape: ", e.getMessage());
548 return getCassettePlayerName();
559 lastOutput =
static_cast<CassettePort&
>(conn).lastOut();
565 setState(
STOP, getImageName(), time);
572 if ((getState() !=
PLAY) || !isRolling()) {
573 buffers[0] =
nullptr;
576 playImage->fillBuffer(audioPos, buffers, num);
582 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
585 int CassettePlayer::signalEvent(
const std::shared_ptr<const Event>& event) noexcept
588 if (!getImageName().empty()) {
591 playTape(getImageName(), getCurrentTime());
592 }
catch (MSXException& e) {
593 motherBoard.getMSXCliComm().printWarning(
594 "Failed to insert tape: ", e.getMessage());
601 void CassettePlayer::execEndOfTape(EmuTime::param time)
605 assert(tapePos == playImage->getEndTime());
607 "Tape end reached... stopping. "
608 "You may need to insert another tape image "
609 "that contains side B. (Or you used the wrong "
610 "loading command.)");
611 setState(
STOP, getImageName(), time);
614 void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
616 if (getState() ==
PLAY) {
620 clk.setFreq(playImage->getFrequency());
621 audioPos = clk.getTicksTill(tapePos);
623 syncScheduled =
false;
629 CassettePlayer::TapeCommand::TapeCommand(
630 CommandController& commandController_,
631 StateChangeDistributor& stateChangeDistributor_,
633 : RecordedCommand(commandController_, stateChangeDistributor_,
634 scheduler_,
"cassetteplayer")
638 void CassettePlayer::TapeCommand::execute(
642 if (tokens.
size() == 1) {
645 TclObject options =
makeTclList(cassettePlayer.getStateString());
647 cassettePlayer.getImageName().getResolved(),
650 }
else if (tokens[1] ==
"new") {
651 std::string_view directory =
"taperecordings";
652 std::string_view prefix =
"openmsx";
653 std::string_view extension =
".wav";
655 (tokens.
size() == 3) ? tokens[2].getString() :
string{},
656 directory, prefix, extension);
659 "Created new cassette image file: ",
filename,
660 ", inserted it and set recording mode.");
662 }
else if (tokens[1] ==
"insert" && tokens.
size() == 3) {
664 result =
"Changing tape";
666 cassettePlayer.playTape(
filename, time);
667 }
catch (MSXException& e) {
668 throw CommandException(std::move(e).getMessage());
671 }
else if (tokens[1] ==
"motorcontrol" && tokens.
size() == 3) {
672 if (tokens[2] ==
"on") {
673 cassettePlayer.setMotorControl(
true, time);
674 result =
"Motor control enabled.";
675 }
else if (tokens[2] ==
"off") {
676 cassettePlayer.setMotorControl(
false, time);
677 result =
"Motor control disabled.";
682 }
else if (tokens.
size() != 2) {
685 }
else if (tokens[1] ==
"motorcontrol") {
687 (cassettePlayer.motorControl ?
"on" :
"off"));
689 }
else if (tokens[1] ==
"record") {
690 result =
"TODO: implement this... (sorry)";
692 }
else if (tokens[1] ==
"play") {
695 result =
"Play mode set, rewinding tape.";
696 cassettePlayer.playTape(
697 cassettePlayer.getImageName(), time);
698 }
catch (MSXException& e) {
699 throw CommandException(std::move(e).getMessage());
702 throw CommandException(
"No tape inserted or tape at end!");
705 result =
"Already in play mode.";
708 }
else if (tokens[1] ==
"eject") {
709 result =
"Tape ejected";
710 cassettePlayer.removeTape(time);
712 }
else if (tokens[1] ==
"rewind") {
716 r =
"First stopping recording... ";
717 cassettePlayer.playTape(
718 cassettePlayer.getImageName(), time);
719 }
catch (MSXException& e) {
720 throw CommandException(std::move(e).getMessage());
723 cassettePlayer.rewind(time);
727 }
else if (tokens[1] ==
"getpos") {
728 result = cassettePlayer.getTapePos(time);
730 }
else if (tokens[1] ==
"getlength") {
731 result = cassettePlayer.getTapeLength(time);
735 result =
"Changing tape";
737 cassettePlayer.playTape(
filename, time);
738 }
catch (MSXException& e) {
739 throw CommandException(std::move(e).getMessage());
747 string CassettePlayer::TapeCommand::help(
const vector<string>& tokens)
const
750 if (tokens.size() >= 2) {
751 if (tokens[1] ==
"eject") {
753 "Well, just eject the cassette from the cassette "
755 }
else if (tokens[1] ==
"rewind") {
757 "Indeed, rewind the tape that is currently in the "
758 "cassette player/recorder...";
759 }
else if (tokens[1] ==
"motorcontrol") {
761 "Setting this to 'off' is equivalent to "
762 "disconnecting the black remote plug from the "
763 "cassette player: it makes the cassette player "
764 "run (if in play mode); the motor signal from the "
765 "MSX will be ignored. Normally this is set to "
766 "'on': the cassetteplayer obeys the motor control "
767 "signal from the MSX.";
768 }
else if (tokens[1] ==
"play") {
770 "Go to play mode. Only useful if you were in "
771 "record mode (which is currently the only other "
773 }
else if (tokens[1] ==
"new") {
775 "Create a new cassette image. If the file name is "
776 "omitted, one will be generated in the default "
777 "directory for tape recordings. Implies going to "
778 "record mode (why else do you want a new cassette "
780 }
else if (tokens[1] ==
"insert") {
782 "Inserts the specified cassette image into the "
783 "cassette player, rewinds it and switches to play "
785 }
else if (tokens[1] ==
"record") {
787 "Go to record mode. NOT IMPLEMENTED YET. Will be "
788 "used to be able to resume recording to an "
789 "existing cassette image, previously inserted with "
790 "the insert command.";
791 }
else if (tokens[1] ==
"getpos") {
793 "Return the position of the tape, in seconds from "
794 "the beginning of the tape.";
795 }
else if (tokens[1] ==
"getlength") {
797 "Return the length of the tape in seconds.";
801 "cassetteplayer eject "
802 ": remove tape from virtual player\n"
803 "cassetteplayer rewind "
804 ": rewind tape in virtual player\n"
805 "cassetteplayer motorcontrol "
806 ": enables or disables motor control (remote)\n"
807 "cassetteplayer play "
808 ": change to play mode (default)\n"
809 "cassetteplayer record "
810 ": change to record mode (NOT IMPLEMENTED YET)\n"
811 "cassetteplayer new [<filename>] "
812 ": create and insert new tape image file and go to record mode\n"
813 "cassetteplayer insert <filename> "
814 ": insert (a different) tape file\n"
815 "cassetteplayer getpos "
816 ": query the position of the tape\n"
817 "cassetteplayer getlength "
818 ": query the total length of the tape\n"
819 "cassetteplayer <filename> "
820 ": insert (a different) tape file\n";
825 void CassettePlayer::TapeCommand::tabCompletion(vector<string>& tokens)
const
827 using namespace std::literals;
828 if (tokens.size() == 2) {
829 static constexpr std::array cmds = {
830 "eject"sv,
"rewind"sv,
"motorcontrol"sv,
"insert"sv,
"new"sv,
831 "play"sv,
"getpos"sv,
"getlength"sv,
835 }
else if ((tokens.size() == 3) && (tokens[1] ==
"insert")) {
837 }
else if ((tokens.size() == 3) && (tokens[1] ==
"motorcontrol")) {
838 static constexpr std::array extra = {
"on"sv,
"off"sv};
839 completeString(tokens, extra);
845 return tokens.
size() > 1;
849 static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
858 template<
typename Archive>
866 ar.serialize(
"casImage", casImage);
869 if (!ar.isLoader() && playImage) {
870 oldChecksum = playImage->getSha1Sum();
872 if (ar.versionAtLeast(version, 2)) {
873 string oldChecksumStr = oldChecksum.
empty()
876 ar.serialize(
"checksum", oldChecksumStr);
877 oldChecksum = oldChecksumStr.
empty()
884 auto time = getCurrentTime();
886 if (!oldChecksum.
empty() &&
889 if (file.is_open()) {
894 insertTape(casImage, time);
896 if (oldChecksum.
empty()) {
911 if (playImage && !oldChecksum.
empty()) {
912 Sha1Sum newChecksum = playImage->getSha1Sum();
913 if (oldChecksum != newChecksum) {
915 "The content of the tape ",
917 " has changed since the time this "
918 "savestate was created. This might "
919 "result in emulation problems.");
931 ar.serialize(
"tapePos", tapePos,
932 "prevSyncTime", prevSyncTime,
933 "audioPos", audioPos,
935 "lastOutput", lastOutput,
937 "motorControl", motorControl);
940 auto time = getCurrentTime();
941 if (playImage && (tapePos > playImage->getEndTime())) {
942 tapePos = playImage->getEndTime();
944 "beyond tape end! Setting tape position to end. "
945 "This can happen if you load a replay from an "
946 "older openMSX version with a different CAS-to-WAV "
947 "baud rate or when the tape image has been changed "
948 "compared to when the replay was created.");
953 "Restoring a state where the MSX was saving to "
954 "tape is not yet supported. Emulation will "
955 "continue without actually saving.");
956 setState(
STOP, getImageName(), time);
958 if (!playImage && (state ==
PLAY)) {
961 setState(
STOP, getImageName(), time);
964 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()
Connector * getConnector() const
Get the connector this Pluggable is plugged into.
EventDistributor & getEventDistributor()
bool isReplaying() const override
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.
XMLElement & addChild(String &&childName)
constexpr index_type size() const noexcept
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.
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.
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.