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 RECIP_RECORD_FREQ = 1.0 / RECORD_FREQ;
68static constexpr double OUTPUT_AMP = 60.0;
70static std::string_view getCassettePlayerName()
72 return "cassetteplayer";
76 :
ResampledSoundDevice(hwConf.getMotherBoard(), getCassettePlayerName(), DESCRIPTION, 1, DUMMY_INPUT_RATE, false)
77 , syncEndOfTape(hwConf.getMotherBoard().getScheduler())
78 , syncAudioEmu (hwConf.getMotherBoard().getScheduler())
79 , motherBoard(hwConf.getMotherBoard())
80 , cassettePlayerCommand(
82 motherBoard.getCommandController(),
83 motherBoard.getStateChangeDistributor(),
84 motherBoard.getScheduler())
86 motherBoard.getReactor().getGlobalSettings().getThrottleManager())
88 motherBoard.getCommandController(),
89 "autoruncassettes",
"automatically try to run cassettes", true)
93 XMLElement* result = doc.allocateElement(
"cassetteplayer");
103 removeTape(EmuTime::zero());
110 c->unplug(getCurrentTime());
119 "state", getStateString(),
122 "motorcontrol", motorControl);
125void CassettePlayer::autoRun()
127 if (!playImage)
return;
141 string H_READ = is_SVI ?
"0xFE8E" :
"0xFF07";
142 string H_MAIN = is_SVI ?
"0xFE94" :
"0xFF0C";
143 string instr1, instr2;
146 instr1 = R
"({RUN\"CAS:\"\r})";
149 instr1 = R
"({BLOAD\"CAS:\",R\r})";
154 instr1 =
"{CLOAD\\r}";
161 "namespace eval ::openmsx {\n"
162 " variable auto_run_bp\n"
164 " proc auto_run_cb {args} {\n"
165 " variable auto_run_bp\n"
166 " debug remove_bp $auto_run_bp\n"
167 " unset auto_run_bp\n"
174 " after time 0.2 \"type [lindex $args 0]\"\n"
176 " set next [lrange $args 1 end]\n"
177 " if {[llength $next] == 0} return\n"
181 " set cmd \"openmsx::auto_run_cb $next\"\n"
182 " set openmsx::auto_run_bp [debug set_bp ", H_MAIN,
" 1 \"$cmd\"]\n"
185 " if {[info exists auto_run_bp]} {debug remove_bp $auto_run_bp\n}\n"
186 " set auto_run_bp [debug set_bp ", H_READ,
" 1 {\n"
187 " openmsx::auto_run_cb {{}} ", instr1,
' ', instr2,
"\n"
191 " type_via_keyboard \'\\r\n"
195 }
catch (CommandException& e) {
197 "Error executing loading instruction using command \"",
198 command,
"\" for AutoRun: ",
199 e.getMessage(),
"\n Please report a bug.");
203string CassettePlayer::getStateString()
const
207 case PLAY:
return "play";
208 case RECORD:
return "record";
209 case STOP:
return "stop";
214bool CassettePlayer::isRolling()
const
229 return (
double(recordImage->getBytes()) + partialInterval) * RECIP_RECORD_FREQ;
231 return (tapePos - EmuTime::zero()).toDouble();
235void CassettePlayer::setTapePos(EmuTime::param time,
double newPos)
247 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
255void CassettePlayer::checkInvariants()
const
259 assert(!recordImage);
269 assert(!recordImage);
282void CassettePlayer::setState(State newState,
const Filename& newImage,
289 if (oldState == newState)
return;
300 bool empty = recordImage->isEmpty();
311 setImageName(newImage);
316 partialInterval = 0.0;
317 lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
323 updateLoadingState(time);
328void CassettePlayer::updateLoadingState(EmuTime::param time)
330 assert(prevSyncTime == time);
335 syncEndOfTape.removeSyncPoint();
337 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
341void CassettePlayer::setImageName(
const Filename& newImage)
348void CassettePlayer::insertTape(
const Filename& filename, EmuTime::param time)
350 if (!filename.empty()) {
352 string msgWav, msgCas, msgTsx;
353 std::unique_ptr<CassetteImage> newImage;
357 newImage = std::make_unique<WavImage>(filename, filePool);
358 }
catch (MSXException& e) {
359 msgWav =
e.getMessage();
365 newImage = std::make_unique<CasImage>(filename, filePool,
367 }
catch (MSXException& e) {
368 msgCas =
e.getMessage();
374 newImage = std::make_unique<TsxImage>(
377 }
catch (MSXException& e) {
378 msgTsx =
e.getMessage();
383 "Failed to insert image: "
384 "tried WAV: \"", msgWav +
"\""
385 ", CAS: \"", msgCas +
"\""
386 " and TSX: \"", msgTsx,
"\".");
388 playImage = std::move(newImage);
400 if (
unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
409 setImageName(filename);
412void CassettePlayer::playTape(
const Filename& filename, EmuTime::param time)
420 insertTape(filename, time);
424void CassettePlayer::rewind(EmuTime::param time)
428 tapePos = EmuTime::zero();
434void CassettePlayer::wind(EmuTime::param time)
443 updateLoadingState(time);
446void CassettePlayer::recordTape(
const Filename& filename, EmuTime::param time)
449 recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
450 tapePos = EmuTime::zero();
454void CassettePlayer::removeTape(EmuTime::param time)
460 tapePos = EmuTime::zero();
466 if (status != motor) {
469 updateLoadingState(time);
473void CassettePlayer::setMotorControl(
bool status, EmuTime::param time)
475 if (status != motorControl) {
477 motorControl = status;
478 updateLoadingState(time);
487 return isRolling() ? playImage->getSampleAt(tapePos) : int16_t(0);
500void CassettePlayer::sync(EmuTime::param time)
505 updateTapePosition(duration, time);
506 generateRecordOutput(duration);
509void CassettePlayer::updateTapePosition(
515 assert(tapePos <= playImage->getEndTime());
518 if (!syncScheduled) {
520 syncScheduled =
true;
527 if (!recordImage || !isRolling())
return;
529 double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
530 double samples = duration.toDouble() * RECORD_FREQ;
531 if (
auto rest = 1.0 - partialInterval; rest <= samples) {
533 partialOut += out * rest;
534 fillBuf(1, partialOut);
538 auto count = int(samples);
543 assert(samples < 1.0);
546 partialOut = samples * out;
547 partialInterval = samples;
549 assert(samples < 1.0);
550 partialOut += samples * out;
551 partialInterval += samples;
553 assert(partialInterval < 1.0);
556void CassettePlayer::fillBuf(
size_t length,
double x)
559 static constexpr double A = 252.0 / 256.0;
561 double y = lastY + (x - lastX);
564 size_t len = std::min(length, buf.size() - sampCnt);
566 buf[sampCnt++] = narrow<uint8_t>(
int(y) + 128);
570 assert(sampCnt <= buf.size());
571 if (sampCnt == buf.size()) {
579void CassettePlayer::flushOutput()
582 recordImage->write(
subspan(buf, 0, sampCnt));
584 recordImage->flush();
585 }
catch (MSXException& e) {
587 "Failed to write to tape: ",
e.getMessage());
594 return getCassettePlayerName();
605 lastOutput = checked_cast<CassettePort&>(conn).lastOut();
618 assert(buffers.size() == 1);
620 buffers[0] =
nullptr;
623 assert(buffers.size() == 1);
624 playImage->fillBuffer(audioPos, buffers.first<1>(), num);
630 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
633void CassettePlayer::execEndOfTape(EmuTime::param time)
637 assert(tapePos == playImage->getEndTime());
639 "Tape end reached... stopping. "
640 "You may need to insert another tape image "
641 "that contains side B. (Or you used the wrong "
642 "loading command.)");
646void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
651 DynamicClock clk(EmuTime::zero());
652 clk.setFreq(playImage->getFrequency());
653 audioPos = clk.getTicksTill(tapePos);
655 syncScheduled =
false;
658static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
667template<
typename Archive>
675 ar.serialize(
"casImage", casImage);
678 if constexpr (!Archive::IS_LOADER) {
680 oldChecksum = playImage->getSha1Sum();
683 if (ar.versionAtLeast(version, 2)) {
684 string oldChecksumStr = oldChecksum.
empty()
687 ar.serialize(
"checksum", oldChecksumStr);
688 oldChecksum = oldChecksumStr.
empty()
693 if constexpr (Archive::IS_LOADER) {
695 auto time = getCurrentTime();
697 if (!oldChecksum.
empty() &&
700 if (file.is_open()) {
705 insertTape(casImage, time);
707 if (oldChecksum.
empty()) {
722 if (playImage && !oldChecksum.
empty()) {
723 Sha1Sum newChecksum = playImage->getSha1Sum();
724 if (oldChecksum != newChecksum) {
726 "The content of the tape ",
728 " has changed since the time this "
729 "savestate was created. This might "
730 "result in emulation problems.");
742 ar.serialize(
"tapePos", tapePos,
743 "prevSyncTime", prevSyncTime,
744 "audioPos", audioPos,
746 "lastOutput", lastOutput,
748 "motorControl", motorControl);
750 if constexpr (Archive::IS_LOADER) {
751 auto time = getCurrentTime();
752 if (playImage && (tapePos > playImage->getEndTime())) {
753 tapePos = playImage->getEndTime();
755 "beyond tape end! Setting tape position to end. "
756 "This can happen if you load a replay from an "
757 "older openMSX version with a different CAS-to-WAV "
758 "baud rate or when the tape image has been changed "
759 "compared to when the replay was created.");
764 "Restoring a state where the MSX was saving to "
765 "tape is not yet supported. Emulation will "
766 "continue without actually saving.");
775 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.