openMSX
CassettePlayer.cc
Go to the documentation of this file.
1// TODO:
2// - improve consistency when a reset occurs: tape is removed when you were
3// recording, but it is not removed when you were playing
4// - specify prefix for auto file name generation when recording (setting?)
5// - append to existing wav files when recording (record command), but this is
6// basically a special case (pointer at the end) of:
7// - (partly) overwrite an existing wav file from any given time index
8// - seek in cassette images for the next and previous file (using empty space?)
9// - (partly) overwrite existing wav files with new tape data (not very hi prio)
10// - handle read-only cassette images (e.g.: CAS images or WAV files with a RO
11// flag): refuse to go to record mode when those are selected
12// - smartly auto-set the position of tapes: if you insert an existing WAV
13// file, it will have the position at the start, assuming PLAY mode by
14// default. When specifying record mode at insert (somehow), it should be
15// at the back.
16// Alternatively, we could remember the index in tape images by storing the
17// index in some persistent data file with its SHA1 sum as it was as we last
18// saw it. When there are write actions to the tape, the hash has to be
19// recalculated and replaced in the data file. An optimization would be to
20// first simply check on the length of the file and fall back to SHA1 if that
21// results in multiple matches.
22
23#include "CassettePlayer.hh"
24#include "Connector.hh"
25#include "CassettePort.hh"
26#include "CommandController.hh"
27#include "DeviceConfig.hh"
28#include "HardwareConfig.hh"
29#include "XMLElement.hh"
30#include "FileContext.hh"
31#include "FilePool.hh"
32#include "File.hh"
33#include "ReverseManager.hh"
34#include "WavImage.hh"
35#include "CasImage.hh"
36#include "TsxImage.hh"
37#include "MSXCliComm.hh"
38#include "MSXMotherBoard.hh"
39#include "Reactor.hh"
40#include "GlobalSettings.hh"
41#include "CommandException.hh"
42#include "FileOperations.hh"
43#include "WavWriter.hh"
44#include "TclObject.hh"
45#include "DynamicClock.hh"
46#include "EmuDuration.hh"
47#include "checked_cast.hh"
48#include "narrow.hh"
49#include "serialize.hh"
50#include "unreachable.hh"
51#include "xrange.hh"
52#include <algorithm>
53#include <cassert>
54#include <memory>
55
56using std::string;
57
58namespace openmsx {
59
60// TODO: this description is not entirely accurate, but it is used
61// as an identifier for this audio device in e.g. Catapult. We should
62// use another way to identify audio devices A.S.A.P.!
63static constexpr static_string_view DESCRIPTION = "Cassetteplayer, use to read .cas or .wav files.";
64
65static constexpr unsigned DUMMY_INPUT_RATE = 44100; // actual rate depends on .cas/.wav file
66static constexpr unsigned RECORD_FREQ = 44100;
67static constexpr double RECIP_RECORD_FREQ = 1.0 / RECORD_FREQ;
68static constexpr double OUTPUT_AMP = 60.0;
69
70static std::string_view getCassettePlayerName()
71{
72 return "cassetteplayer";
73}
74
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(
81 this,
82 motherBoard.getCommandController(),
83 motherBoard.getStateChangeDistributor(),
84 motherBoard.getScheduler())
85 , loadingIndicator(
86 motherBoard.getReactor().getGlobalSettings().getThrottleManager())
87 , autoRunSetting(
88 motherBoard.getCommandController(),
89 "autoruncassettes", "automatically try to run cassettes", true)
90{
91 static const XMLElement* xml = [] {
93 XMLElement* result = doc.allocateElement("cassetteplayer");
94 result->setFirstChild(doc.allocateElement("sound"))
95 ->setFirstChild(doc.allocateElement("volume", "5000"));
96 return result;
97 }();
98 registerSound(DeviceConfig(hwConf, *xml));
99
100 motherBoard.registerMediaInfo(getCassettePlayerName(), *this);
101 motherBoard.getMSXCliComm().update(CliComm::UpdateType::HARDWARE, getCassettePlayerName(), "add");
102
103 removeTape(EmuTime::zero());
104}
105
107{
109 if (auto* c = getConnector()) {
110 c->unplug(getCurrentTime());
111 }
112 motherBoard.unregisterMediaInfo(*this);
113 motherBoard.getMSXCliComm().update(CliComm::UpdateType::HARDWARE, getCassettePlayerName(), "remove");
114}
115
117{
118 result.addDictKeyValues("target", getImageName().getResolved(),
119 "state", getStateString(),
120 "position", getTapePos(getCurrentTime()),
121 "length", getTapeLength(getCurrentTime()),
122 "motorcontrol", motorControl);
123}
124
125void CassettePlayer::autoRun()
126{
127 if (!playImage) return;
128 if (motherBoard.getReverseManager().isReplaying()) {
129 // Don't execute the loading commands (keyboard type commands)
130 // when we're replaying a recording. Because the recording
131 // already contains those commands.
132 return;
133 }
134
135 // try to automatically run the tape, if that's set
136 CassetteImage::FileType type = playImage->getFirstFileType();
137 if (!autoRunSetting.getBoolean() || type == CassetteImage::UNKNOWN) {
138 return;
139 }
140 bool is_SVI = motherBoard.getMachineType() == "SVI"; // assume all other are 'MSX*' (might not be correct for 'Coleco')
141 string H_READ = is_SVI ? "0xFE8E" : "0xFF07"; // Hook for Ready
142 string H_MAIN = is_SVI ? "0xFE94" : "0xFF0C"; // Hook for Main Loop
143 string instr1, instr2;
144 switch (type) {
146 instr1 = R"({RUN\"CAS:\"\r})";
147 break;
149 instr1 = R"({BLOAD\"CAS:\",R\r})";
150 break;
152 // Note that CLOAD:RUN won't work: BASIC ignores stuff
153 // after the CLOAD command. That's why it's split in two.
154 instr1 = "{CLOAD\\r}";
155 instr2 = "{RUN\\r}";
156 break;
157 default:
158 UNREACHABLE; // Shouldn't be possible
159 }
160 string command = strCat(
161 "namespace eval ::openmsx {\n"
162 " variable auto_run_bp\n"
163
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"
168
169 // Without the 0.2s delay here, the type command gets messed up
170 // on MSX1 machines for some reason (starting to type too early?)
171 // When using 0.1s delay only, the typing works, but still some
172 // things go wrong on some machines with some games (see #1509
173 // for instance)
174 " after time 0.2 \"type [lindex $args 0]\"\n"
175
176 " set next [lrange $args 1 end]\n"
177 " if {[llength $next] == 0} return\n"
178
179 // H_READ is used by some firmwares; we need to hook the
180 // H_MAIN that happens immediately after H_READ.
181 " set cmd \"openmsx::auto_run_cb $next\"\n"
182 " set openmsx::auto_run_bp [debug set_bp ", H_MAIN, " 1 \"$cmd\"]\n"
183 " }\n"
184
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"
188 " }]\n"
189
190 // re-trigger hook(s), needed when already booted in BASIC
191 " type_via_keyboard \'\\r\n"
192 "}");
193 try {
194 motherBoard.getCommandController().executeCommand(command);
195 } catch (CommandException& e) {
196 motherBoard.getMSXCliComm().printWarning(
197 "Error executing loading instruction using command \"",
198 command, "\" for AutoRun: ",
199 e.getMessage(), "\n Please report a bug.");
200 }
201}
202
203string CassettePlayer::getStateString() const
204{
205 switch (getState()) {
206 using enum State;
207 case PLAY: return "play";
208 case RECORD: return "record";
209 case STOP: return "stop";
210 }
212}
213
214bool CassettePlayer::isRolling() const
215{
216 // Is the tape 'rolling'?
217 // is true when:
218 // not in stop mode (there is a tape inserted and not at end-of-tape)
219 // AND [ user forced playing (motorControl=off) OR motor enabled by
220 // software (motor=on) ]
221 return (getState() != State::STOP) && (motor || !motorControl);
222}
223
224double CassettePlayer::getTapePos(EmuTime::param time)
225{
226 sync(time);
227 if (getState() == State::RECORD) {
228 // we record 8-bit mono, so bytes == samples
229 return (double(recordImage->getBytes()) + partialInterval) * RECIP_RECORD_FREQ;
230 } else {
231 return (tapePos - EmuTime::zero()).toDouble();
232 }
233}
234
235void CassettePlayer::setTapePos(EmuTime::param time, double newPos)
236{
237 assert(getState() != State::RECORD);
238 sync(time);
239 auto pos = std::clamp(newPos, 0.0, getTapeLength(time));
240 tapePos = EmuTime::zero() + EmuDuration(pos);
241 wind(time);
242}
243
244double CassettePlayer::getTapeLength(EmuTime::param time)
245{
246 if (playImage) {
247 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
248 } else if (getState() == State::RECORD) {
249 return getTapePos(time);
250 } else {
251 return 0.0;
252 }
253}
254
255void CassettePlayer::checkInvariants() const
256{
257 switch (getState()) {
258 case State::STOP:
259 assert(!recordImage);
260 if (playImage) {
261 // we're at end-of tape
262 assert(!getImageName().empty());
263 } else {
264 // no tape inserted, imageName may or may not be empty
265 }
266 break;
267 case State::PLAY:
268 assert(!getImageName().empty());
269 assert(!recordImage);
270 assert(playImage);
271 break;
272 case State::RECORD:
273 assert(!getImageName().empty());
274 assert(recordImage);
275 assert(!playImage);
276 break;
277 default:
279 }
280}
281
282void CassettePlayer::setState(State newState, const Filename& newImage,
283 EmuTime::param time)
284{
285 sync(time);
286
287 // set new state if different from old state
288 State oldState = getState();
289 if (oldState == newState) return;
290
291 // cannot directly switch from PLAY to RECORD or vice-versa,
292 // (should always go via STOP)
293 assert(!((oldState == State::PLAY) && (newState == State::RECORD)));
294 assert(!((oldState == State::RECORD) && (newState == State::PLAY)));
295
296 // stuff for leaving the old state
297 // 'recordImage==nullptr' can happen in case of loadstate.
298 if ((oldState == State::RECORD) && recordImage) {
299 flushOutput();
300 bool empty = recordImage->isEmpty();
301 recordImage.reset();
302 if (empty) {
303 // delete the created WAV file, as it is useless
304 FileOperations::unlink(getImageName().getResolved()); // ignore errors
305 setImageName(Filename());
306 }
307 }
308
309 // actually switch state
310 state = newState;
311 setImageName(newImage);
312
313 // stuff for entering the new state
314 if (newState == State::RECORD) {
315 partialOut = 0.0;
316 partialInterval = 0.0;
317 lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
318 lastY = 0.0;
319 }
320 motherBoard.getMSXCliComm().update(
321 CliComm::UpdateType::STATUS, "cassetteplayer", getStateString());
322
323 updateLoadingState(time); // sets SP for tape-end detection
324
325 checkInvariants();
326}
327
328void CassettePlayer::updateLoadingState(EmuTime::param time)
329{
330 assert(prevSyncTime == time); // sync() must be called
331 // TODO also set loadingIndicator for RECORD?
332 // note: we don't use isRolling()
333 loadingIndicator.update(motor && (getState() == State::PLAY));
334
335 syncEndOfTape.removeSyncPoint();
336 if (isRolling() && (getState() == State::PLAY)) {
337 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
338 }
339}
340
341void CassettePlayer::setImageName(const Filename& newImage)
342{
343 casImage = newImage;
344 motherBoard.getMSXCliComm().update(
345 CliComm::UpdateType::MEDIA, "cassetteplayer", casImage.getResolved());
346}
347
348void CassettePlayer::insertTape(const Filename& filename, EmuTime::param time)
349{
350 if (!filename.empty()) {
351 FilePool& filePool = motherBoard.getReactor().getFilePool();
352 string msgWav, msgCas, msgTsx;
353 std::unique_ptr<CassetteImage> newImage;
354 if (!newImage) {
355 try {
356 // first try WAV
357 newImage = std::make_unique<WavImage>(filename, filePool);
358 } catch (MSXException& e) {
359 msgWav = e.getMessage();
360 }
361 }
362 if (!newImage) {
363 try {
364 // if that fails use CAS
365 newImage = std::make_unique<CasImage>(filename, filePool,
366 motherBoard.getMSXCliComm());
367 } catch (MSXException& e) {
368 msgCas = e.getMessage();
369 }
370 }
371 if (!newImage) {
372 try {
373 // if that fails use TSX
374 newImage = std::make_unique<TsxImage>(
375 filename, filePool,
376 motherBoard.getMSXCliComm());
377 } catch (MSXException& e) {
378 msgTsx = e.getMessage();
379 }
380 }
381 if (!newImage) {
382 throw MSXException(
383 "Failed to insert image: "
384 "tried WAV: \"", msgWav + "\""
385 ", CAS: \"", msgCas + "\""
386 " and TSX: \"", msgTsx, "\".");
387 }
388 playImage = std::move(newImage);
389 } else {
390 // This is a bit tricky, consider this scenario: we switch from
391 // RECORD->PLAY, but we didn't actually record anything: The
392 // removeTape() call above (indirectly) deletes the empty
393 // recorded wav image and also clears imageName. Now because
394 // the 'filename' parameter is passed by reference, and because
395 // getImageName() returns a reference, this 'filename'
396 // parameter now also is an empty string.
397 }
398
399 // possibly recreate resampler
400 if (unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
401 inputRate != getInputRate()) {
402 setInputRate(inputRate);
404 }
405
406 // trigger (re-)query of getAmplificationFactorImpl()
407 setSoftwareVolume(1.0f, time);
408
409 setImageName(filename);
410}
411
412void CassettePlayer::playTape(const Filename& filename, EmuTime::param time)
413{
414 // Temporally go to STOP state:
415 // RECORD: First close the recorded image. Otherwise it goes wrong
416 // if you switch from RECORD->PLAY on the same image.
417 // PLAY: Go to stop because we temporally violate some invariants
418 // (tapePos can be beyond end-of-tape).
419 setState(State::STOP, getImageName(), time); // keep current image
420 insertTape(filename, time);
421 rewind(time); // sets PLAY mode
422}
423
424void CassettePlayer::rewind(EmuTime::param time)
425{
426 sync(time); // before tapePos changes
427 assert(getState() != State::RECORD);
428 tapePos = EmuTime::zero();
429 audioPos = 0;
430 wind(time);
431 autoRun();
432}
433
434void CassettePlayer::wind(EmuTime::param time)
435{
436 if (getImageName().empty()) {
437 // no image inserted, do nothing
438 assert(getState() == State::STOP);
439 } else {
440 // keep current image
441 setState(State::PLAY, getImageName(), time);
442 }
443 updateLoadingState(time);
444}
445
446void CassettePlayer::recordTape(const Filename& filename, EmuTime::param time)
447{
448 removeTape(time); // flush (possible) previous recording
449 recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
450 tapePos = EmuTime::zero();
451 setState(State::RECORD, filename, time);
452}
453
454void CassettePlayer::removeTape(EmuTime::param time)
455{
456 // first stop with tape still inserted
457 setState(State::STOP, getImageName(), time);
458 // then remove the tape
459 playImage.reset();
460 tapePos = EmuTime::zero();
461 setImageName({});
462}
463
464void CassettePlayer::setMotor(bool status, EmuTime::param time)
465{
466 if (status != motor) {
467 sync(time);
468 motor = status;
469 updateLoadingState(time);
470 }
471}
472
473void CassettePlayer::setMotorControl(bool status, EmuTime::param time)
474{
475 if (status != motorControl) {
476 sync(time);
477 motorControl = status;
478 updateLoadingState(time);
479 }
480}
481
482int16_t CassettePlayer::readSample(EmuTime::param time)
483{
484 if (getState() == State::PLAY) {
485 // playing
486 sync(time);
487 return isRolling() ? playImage->getSampleAt(tapePos) : int16_t(0);
488 } else {
489 // record or stop
490 return 0;
491 }
492}
493
494void CassettePlayer::setSignal(bool output, EmuTime::param time)
495{
496 sync(time);
497 lastOutput = output;
498}
499
500void CassettePlayer::sync(EmuTime::param time)
501{
502 EmuDuration duration = time - prevSyncTime;
503 prevSyncTime = time;
504
505 updateTapePosition(duration, time);
506 generateRecordOutput(duration);
507}
508
509void CassettePlayer::updateTapePosition(
510 EmuDuration::param duration, EmuTime::param time)
511{
512 if (!isRolling() || (getState() != State::PLAY)) return;
513
514 tapePos += duration;
515 assert(tapePos <= playImage->getEndTime());
516
517 // synchronize audio with actual tape position
518 if (!syncScheduled) {
519 // don't sync too often, this improves sound quality
520 syncScheduled = true;
521 syncAudioEmu.setSyncPoint(time + EmuDuration::sec(1));
522 }
523}
524
525void CassettePlayer::generateRecordOutput(EmuDuration::param duration)
526{
527 if (!recordImage || !isRolling()) return;
528
529 double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
530 double samples = duration.toDouble() * RECORD_FREQ;
531 if (auto rest = 1.0 - partialInterval; rest <= samples) {
532 // enough to fill next interval
533 partialOut += out * rest;
534 fillBuf(1, partialOut);
535 samples -= rest;
536
537 // fill complete intervals
538 auto count = int(samples);
539 if (count > 0) {
540 fillBuf(count, out);
541 }
542 samples -= count;
543 assert(samples < 1.0);
544
545 // partial last interval
546 partialOut = samples * out;
547 partialInterval = samples;
548 } else {
549 assert(samples < 1.0);
550 partialOut += samples * out;
551 partialInterval += samples;
552 }
553 assert(partialInterval < 1.0);
554}
555
556void CassettePlayer::fillBuf(size_t length, double x)
557{
558 assert(recordImage);
559 static constexpr double A = 252.0 / 256.0;
560
561 double y = lastY + (x - lastX);
562
563 while (length) {
564 size_t len = std::min(length, buf.size() - sampCnt);
565 repeat(len, [&] {
566 buf[sampCnt++] = narrow<uint8_t>(int(y) + 128);
567 y *= A;
568 });
569 length -= len;
570 assert(sampCnt <= buf.size());
571 if (sampCnt == buf.size()) {
572 flushOutput();
573 }
574 }
575 lastY = y;
576 lastX = x;
577}
578
579void CassettePlayer::flushOutput()
580{
581 try {
582 recordImage->write(subspan(buf, 0, sampCnt));
583 sampCnt = 0;
584 recordImage->flush(); // update wav header
585 } catch (MSXException& e) {
586 motherBoard.getMSXCliComm().printWarning(
587 "Failed to write to tape: ", e.getMessage());
588 }
589}
590
591
592std::string_view CassettePlayer::getName() const
593{
594 return getCassettePlayerName();
595}
596
597std::string_view CassettePlayer::getDescription() const
598{
599 return DESCRIPTION;
600}
601
602void CassettePlayer::plugHelper(Connector& conn, EmuTime::param time)
603{
604 sync(time);
605 lastOutput = checked_cast<CassettePort&>(conn).lastOut();
606}
607
608void CassettePlayer::unplugHelper(EmuTime::param time)
609{
610 // note: may not throw exceptions
611 setState(State::STOP, getImageName(), time); // keep current image
612}
613
614
615void CassettePlayer::generateChannels(std::span<float*> buffers, unsigned num)
616{
617 // Single channel device: replace content of buffers[0] (not add to it).
618 assert(buffers.size() == 1);
619 if ((getState() != State::PLAY) || !isRolling()) {
620 buffers[0] = nullptr;
621 return;
622 }
623 assert(buffers.size() == 1);
624 playImage->fillBuffer(audioPos, buffers.first<1>(), num);
625 audioPos += num;
626}
627
629{
630 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
631}
632
633void CassettePlayer::execEndOfTape(EmuTime::param time)
634{
635 // tape ended
636 sync(time);
637 assert(tapePos == playImage->getEndTime());
638 motherBoard.getMSXCliComm().printWarning(
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.)");
643 setState(State::STOP, getImageName(), time); // keep current image
644}
645
646void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
647{
648 if (getState() == State::PLAY) {
649 updateStream(time);
650 sync(time);
651 DynamicClock clk(EmuTime::zero());
652 clk.setFreq(playImage->getFrequency());
653 audioPos = clk.getTicksTill(tapePos);
654 }
655 syncScheduled = false;
656}
657
658static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
659 { "PLAY", CassettePlayer::State::PLAY },
660 { "RECORD", CassettePlayer::State::RECORD },
662};
664
665// version 1: initial version
666// version 2: added checksum
667template<typename Archive>
668void CassettePlayer::serialize(Archive& ar, unsigned version)
669{
670 if (recordImage) {
671 // buf, sampCnt
672 flushOutput();
673 }
674
675 ar.serialize("casImage", casImage);
676
677 Sha1Sum oldChecksum;
678 if constexpr (!Archive::IS_LOADER) {
679 if (playImage) {
680 oldChecksum = playImage->getSha1Sum();
681 }
682 }
683 if (ar.versionAtLeast(version, 2)) {
684 string oldChecksumStr = oldChecksum.empty()
685 ? string{}
686 : oldChecksum.toString();
687 ar.serialize("checksum", oldChecksumStr);
688 oldChecksum = oldChecksumStr.empty()
689 ? Sha1Sum()
690 : Sha1Sum(oldChecksumStr);
691 }
692
693 if constexpr (Archive::IS_LOADER) {
694 FilePool& filePool = motherBoard.getReactor().getFilePool();
695 auto time = getCurrentTime();
696 casImage.updateAfterLoadState();
697 if (!oldChecksum.empty() &&
698 !FileOperations::exists(casImage.getResolved())) {
699 auto file = filePool.getFile(FileType::TAPE, oldChecksum);
700 if (file.is_open()) {
701 casImage.setResolved(file.getURL());
702 }
703 }
704 try {
705 insertTape(casImage, time);
706 } catch (MSXException&) {
707 if (oldChecksum.empty()) {
708 // It's OK if we cannot reinsert an empty
709 // image. One likely scenario for this case is
710 // the following:
711 // - cassetteplayer new myfile.wav
712 // - don't actually start saving to tape yet
713 // - create a savestate and load that state
714 // Because myfile.wav contains no data yet, it
715 // is deleted from the filesystem. So on a
716 // loadstate it won't be found.
717 } else {
718 throw;
719 }
720 }
721
722 if (playImage && !oldChecksum.empty()) {
723 Sha1Sum newChecksum = playImage->getSha1Sum();
724 if (oldChecksum != newChecksum) {
725 motherBoard.getMSXCliComm().printWarning(
726 "The content of the tape ",
727 casImage.getResolved(),
728 " has changed since the time this "
729 "savestate was created. This might "
730 "result in emulation problems.");
731 }
732 }
733 }
734
735 // only for RECORD
736 //double lastX;
737 //double lastY;
738 //double partialOut;
739 //double partialInterval;
740 //std::unique_ptr<WavWriter> recordImage;
741
742 ar.serialize("tapePos", tapePos,
743 "prevSyncTime", prevSyncTime,
744 "audioPos", audioPos,
745 "state", state,
746 "lastOutput", lastOutput,
747 "motor", motor,
748 "motorControl", motorControl);
749
750 if constexpr (Archive::IS_LOADER) {
751 auto time = getCurrentTime();
752 if (playImage && (tapePos > playImage->getEndTime())) {
753 tapePos = playImage->getEndTime();
754 motherBoard.getMSXCliComm().printWarning("Tape position "
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.");
760 }
761 if (state == State::RECORD) {
762 // TODO we don't support savestates in RECORD mode yet
763 motherBoard.getMSXCliComm().printWarning(
764 "Restoring a state where the MSX was saving to "
765 "tape is not yet supported. Emulation will "
766 "continue without actually saving.");
767 setState(State::STOP, getImageName(), time);
768 }
769 if (!playImage && (state == State::PLAY)) {
770 // This should only happen for manually edited
771 // savestates, though we shouldn't crash on it.
772 setState(State::STOP, getImageName(), time);
773 }
774 sync(time);
775 updateLoadingState(time);
776 }
777}
780
781} // namespace openmsx
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.
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)
Definition CliComm.cc:12
virtual TclObject executeCommand(zstring_view command, CliConnection *connection=nullptr)=0
Execute the given command.
Represents something you can plug devices into.
Definition Connector.hh:21
static constexpr EmuDuration sec(unsigned x)
const EmuDuration & param
File getFile(FileType fileType, const Sha1Sum &sha1sum)
Search file with the given sha1sum.
Definition FilePool.cc:53
void setResolved(std::string resolved)
Change the resolved part of this filename E.g.
Definition Filename.hh:58
const std::string & getResolved() const &
Definition Filename.hh:38
void updateAfterLoadState()
After a loadstate we prefer to use the exact same file as before savestate.
Definition Filename.cc:8
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
Definition MSXCliComm.cc:21
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)
ReverseManager & getReverseManager()
std::string_view getMachineType() const
Connector * getConnector() const
Get the connector this Pluggable is plugged into.
Definition Pluggable.hh:43
FilePool & getFilePool()
Definition Reactor.hh:98
This class represents the result of a sha1 calculation (a 160-bit value).
Definition sha1.hh:24
bool empty() const
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)
Definition TclObject.hh:150
static XMLDocument & getStaticDocument()
XMLElement * setFirstChild(XMLElement *child)
static_string_view
constexpr double e
Definition Math.hh:21
T length(const vecN< N, T > &x)
Definition gl_vec.hh:505
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:
Definition Autofire.cc:11
std::array< const EDStorage, 4 > A
auto count(InputRange &&range, const T &value)
Definition ranges.hh:349
constexpr auto subspan(Range &&range, size_t offset, size_t count=std::dynamic_extent)
Definition ranges.hh:481
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
#define SERIALIZE_ENUM(TYPE, INFO)
#define REGISTER_POLYMORPHIC_INITIALIZER(BASE, CLASS, NAME)
std::string strCat()
Definition strCat.hh:703
#define UNREACHABLE
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition xrange.hh:147