62static constexpr static_string_view DESCRIPTION =
"Cassetteplayer, use to read .cas or .wav files.";
64static constexpr unsigned DUMMY_INPUT_RATE = 44100;
65static constexpr unsigned RECORD_FREQ = 44100;
66static constexpr double RECIP_RECORD_FREQ = 1.0 / RECORD_FREQ;
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())
79 , cassettePlayerCommand(
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 XMLElement* result = doc.allocateElement(
"cassetteplayer");
102 removeTape(EmuTime::zero());
109 c->unplug(getCurrentTime());
118 "state", getStateString(),
121 "motorcontrol", motorControl);
124void CassettePlayer::autoRun()
126 if (!playImage)
return;
140 string H_READ = is_SVI ?
"0xFE8E" :
"0xFF07";
141 string H_MAIN = is_SVI ?
"0xFE94" :
"0xFF0C";
142 string instr1, instr2;
145 instr1 = R
"({RUN\"CAS:\"\r})";
148 instr1 = R
"({BLOAD\"CAS:\",R\r})";
153 instr1 =
"{CLOAD\\r}";
160 "namespace eval ::openmsx {\n"
161 " variable auto_run_bp\n"
163 " proc auto_run_cb {args} {\n"
164 " variable auto_run_bp\n"
165 " debug remove_bp $auto_run_bp\n"
166 " unset auto_run_bp\n"
173 " after time 0.2 \"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
206 case PLAY:
return "play";
207 case RECORD:
return "record";
208 case STOP:
return "stop";
213bool CassettePlayer::isRolling()
const
228 return (
double(recordImage->getBytes()) + partialInterval) * RECIP_RECORD_FREQ;
230 return (tapePos - EmuTime::zero()).toDouble();
234void CassettePlayer::setTapePos(EmuTime::param time,
double newPos)
246 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
254void CassettePlayer::checkInvariants()
const
258 assert(!recordImage);
268 assert(!recordImage);
281void CassettePlayer::setState(State newState,
const Filename& newImage,
288 if (oldState == newState)
return;
299 bool empty = recordImage->isEmpty();
310 setImageName(newImage);
315 partialInterval = 0.0;
316 lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
322 updateLoadingState(time);
327void CassettePlayer::updateLoadingState(EmuTime::param time)
329 assert(prevSyncTime == time);
334 syncEndOfTape.removeSyncPoint();
336 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
340void CassettePlayer::setImageName(
const Filename& newImage)
347void CassettePlayer::insertTape(
const Filename& filename, EmuTime::param time)
349 if (!filename.empty()) {
353 playImage = std::make_unique<WavImage>(filename, filePool);
354 }
catch (MSXException& e) {
357 playImage = std::make_unique<CasImage>(
360 }
catch (MSXException& e2) {
362 "Failed to insert WAV image: \"",
364 "\" and also failed to insert CAS image: \"",
365 e2.getMessage(),
'\"');
379 if (
unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
388 setImageName(filename);
391void CassettePlayer::playTape(
const Filename& filename, EmuTime::param time)
399 insertTape(filename, time);
403void CassettePlayer::rewind(EmuTime::param time)
407 tapePos = EmuTime::zero();
413void CassettePlayer::wind(EmuTime::param time)
422 updateLoadingState(time);
425void CassettePlayer::recordTape(
const Filename& filename, EmuTime::param time)
428 recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
429 tapePos = EmuTime::zero();
433void CassettePlayer::removeTape(EmuTime::param time)
439 tapePos = EmuTime::zero();
445 if (status != motor) {
448 updateLoadingState(time);
452void CassettePlayer::setMotorControl(
bool status, EmuTime::param time)
454 if (status != motorControl) {
456 motorControl = status;
457 updateLoadingState(time);
466 return isRolling() ? playImage->getSampleAt(tapePos) : int16_t(0);
479void CassettePlayer::sync(EmuTime::param time)
484 updateTapePosition(duration, time);
485 generateRecordOutput(duration);
488void CassettePlayer::updateTapePosition(
494 assert(tapePos <= playImage->getEndTime());
497 if (!syncScheduled) {
499 syncScheduled =
true;
506 if (!recordImage || !isRolling())
return;
508 double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
509 double samples = duration.toDouble() * RECORD_FREQ;
510 if (
auto rest = 1.0 - partialInterval; rest <= samples) {
512 partialOut += out * rest;
513 fillBuf(1, partialOut);
517 auto count = int(samples);
522 assert(samples < 1.0);
525 partialOut = samples * out;
526 partialInterval = samples;
528 assert(samples < 1.0);
529 partialOut += samples * out;
530 partialInterval += samples;
532 assert(partialInterval < 1.0);
535void CassettePlayer::fillBuf(
size_t length,
double x)
538 static constexpr double A = 252.0 / 256.0;
540 double y = lastY + (x - lastX);
543 size_t len = std::min(length, buf.size() - sampCnt);
545 buf[sampCnt++] = narrow<uint8_t>(
int(y) + 128);
549 assert(sampCnt <= buf.size());
550 if (sampCnt == buf.size()) {
558void CassettePlayer::flushOutput()
561 recordImage->write(
subspan(buf, 0, sampCnt));
563 recordImage->flush();
564 }
catch (MSXException& e) {
566 "Failed to write to tape: ",
e.getMessage());
573 return getCassettePlayerName();
584 lastOutput = checked_cast<CassettePort&>(conn).lastOut();
597 assert(buffers.size() == 1);
599 buffers[0] =
nullptr;
602 assert(buffers.size() == 1);
603 playImage->fillBuffer(audioPos, buffers.first<1>(), num);
609 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
612void CassettePlayer::execEndOfTape(EmuTime::param time)
616 assert(tapePos == playImage->getEndTime());
618 "Tape end reached... stopping. "
619 "You may need to insert another tape image "
620 "that contains side B. (Or you used the wrong "
621 "loading command.)");
625void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
630 DynamicClock clk(EmuTime::zero());
631 clk.setFreq(playImage->getFrequency());
632 audioPos = clk.getTicksTill(tapePos);
634 syncScheduled =
false;
637static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
646template<
typename Archive>
654 ar.serialize(
"casImage", casImage);
657 if constexpr (!Archive::IS_LOADER) {
659 oldChecksum = playImage->getSha1Sum();
662 if (ar.versionAtLeast(version, 2)) {
663 string oldChecksumStr = oldChecksum.
empty()
666 ar.serialize(
"checksum", oldChecksumStr);
667 oldChecksum = oldChecksumStr.
empty()
672 if constexpr (Archive::IS_LOADER) {
674 auto time = getCurrentTime();
676 if (!oldChecksum.
empty() &&
679 if (file.is_open()) {
684 insertTape(casImage, time);
686 if (oldChecksum.
empty()) {
701 if (playImage && !oldChecksum.
empty()) {
702 Sha1Sum newChecksum = playImage->getSha1Sum();
703 if (oldChecksum != newChecksum) {
705 "The content of the tape ",
707 " has changed since the time this "
708 "savestate was created. This might "
709 "result in emulation problems.");
721 ar.serialize(
"tapePos", tapePos,
722 "prevSyncTime", prevSyncTime,
723 "audioPos", audioPos,
725 "lastOutput", lastOutput,
727 "motorControl", motorControl);
729 if constexpr (Archive::IS_LOADER) {
730 auto time = getCurrentTime();
731 if (playImage && (tapePos > playImage->getEndTime())) {
732 tapePos = playImage->getEndTime();
734 "beyond tape end! Setting tape position to end. "
735 "This can happen if you load a replay from an "
736 "older openMSX version with a different CAS-to-WAV "
737 "baud rate or when the tape image has been changed "
738 "compared to when the replay was created.");
743 "Restoring a state where the MSX was saving to "
744 "tape is not yet supported. Emulation will "
745 "continue without actually saving.");
754 updateLoadingState(time);
bool getBoolean() const noexcept
void plugHelper(Connector &connector, EmuTime::param time) override
const Filename & getImageName() const
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.
double getTapePos(EmuTime::param time)
Returns the position of the tape, in seconds from the beginning of the tape.
~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.
double getTapeLength(EmuTime::param time)
Returns the length of the tape in seconds.
void getMediaInfo(TclObject &result) override
This method gets called when information is required on the media inserted in the media slot of the p...
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
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.
void update(UpdateType type, std::string_view name, std::string_view value) override
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)
MSXCliComm & getMSXCliComm()
ReverseManager & getReverseManager()
std::string_view getMachineType() const
Connector * getConnector() const
Get the connector this Pluggable is plugged into.
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)
T length(const vecN< N, T > &x)
bool exists(zstring_view filename)
Does this file (directory) exists?
int unlink(zstring_view path)
Call unlink() in a platform-independent manner.
This file implemented 3 utility functions:
std::array< const EDStorage, 4 > A
auto count(InputRange &&range, const T &value)
constexpr auto subspan(Range &&range, size_t offset, size_t count=std::dynamic_extent)
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
#define SERIALIZE_ENUM(TYPE, INFO)
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.