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 "CliComm.hh"
37#include "MSXMotherBoard.hh"
38#include "Reactor.hh"
39#include "GlobalSettings.hh"
40#include "CommandException.hh"
41#include "EventDistributor.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 OUTPUT_AMP = 60.0;
68
69static std::string_view getCassettePlayerName()
70{
71 return "cassetteplayer";
72}
73
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 , tapeCommand(
80 motherBoard.getCommandController(),
81 motherBoard.getStateChangeDistributor(),
82 motherBoard.getScheduler())
83 , loadingIndicator(
84 motherBoard.getReactor().getGlobalSettings().getThrottleManager())
85 , autoRunSetting(
86 motherBoard.getCommandController(),
87 "autoruncassettes", "automatically try to run cassettes", true)
88{
89 static const XMLElement* xml = [] {
91 XMLElement* result = doc.allocateElement("cassetteplayer");
92 result->setFirstChild(doc.allocateElement("sound"))
93 ->setFirstChild(doc.allocateElement("volume", "5000"));
94 return result;
95 }();
96 registerSound(DeviceConfig(hwConf, *xml));
97
99 EventType::BOOT, *this);
100 motherBoard.registerMediaInfo(getCassettePlayerName(), *this);
101 motherBoard.getMSXCliComm().update(CliComm::HARDWARE, getCassettePlayerName(), "add");
102
103 removeTape(EmuTime::zero());
104}
105
107{
109 if (auto* c = getConnector()) {
110 c->unplug(getCurrentTime());
111 }
113 EventType::BOOT, *this);
114 motherBoard.unregisterMediaInfo(*this);
115 motherBoard.getMSXCliComm().update(CliComm::HARDWARE, getCassettePlayerName(), "remove");
116}
117
119{
120 result.addDictKeyValues("target", getImageName().getResolved(),
121 "state", getStateString(),
122 "position", getTapePos(getCurrentTime()),
123 "length", getTapeLength(getCurrentTime()),
124 "motorcontrol", motorControl);
125}
126
127void CassettePlayer::autoRun()
128{
129 if (!playImage) return;
130 if (motherBoard.getReverseManager().isReplaying()) {
131 // Don't execute the loading commands (keyboard type commands)
132 // when we're replaying a recording. Because the recording
133 // already contains those commands.
134 return;
135 }
136
137 // try to automatically run the tape, if that's set
138 CassetteImage::FileType type = playImage->getFirstFileType();
139 if (!autoRunSetting.getBoolean() || type == CassetteImage::UNKNOWN) {
140 return;
141 }
142 bool is_SVI = motherBoard.getMachineType() == "SVI"; // assume all other are 'MSX*' (might not be correct for 'Coleco')
143 string H_READ = is_SVI ? "0xFE8E" : "0xFF07"; // Hook for Ready
144 string H_MAIN = is_SVI ? "0xFE94" : "0xFF0C"; // Hook for Main Loop
145 string instr1, instr2;
146 switch (type) {
148 instr1 = R"({RUN\"CAS:\"\r})";
149 break;
151 instr1 = R"({BLOAD\"CAS:\",R\r})";
152 break;
154 // Note that CLOAD:RUN won't work: BASIC ignores stuff
155 // after the CLOAD command. That's why it's split in two.
156 instr1 = "{CLOAD\\r}";
157 instr2 = "{RUN\\r}";
158 break;
159 default:
160 UNREACHABLE; // Shouldn't be possible
161 }
162 string command = strCat(
163 "namespace eval ::openmsx {\n"
164 " variable auto_run_bp\n"
165
166 " proc auto_run_cb {args} {\n"
167 " variable auto_run_bp\n"
168 " debug remove_bp $auto_run_bp\n"
169 " unset auto_run_bp\n"
170
171 // Without the 0.1s delay here, the type command gets messed up
172 // on MSX1 machines for some reason (starting to type too early?)
173 " after time 0.1 \"type [lindex $args 0]\"\n"
174
175 " set next [lrange $args 1 end]\n"
176 " if {[llength $next] == 0} return\n"
177
178 // H_READ is used by some firmwares; we need to hook the
179 // H_MAIN that happens immediately after H_READ.
180 " set cmd \"openmsx::auto_run_cb $next\"\n"
181 " set openmsx::auto_run_bp [debug set_bp ", H_MAIN, " 1 \"$cmd\"]\n"
182 " }\n"
183
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"
187 " }]\n"
188
189 // re-trigger hook(s), needed when already booted in BASIC
190 " type_via_keyboard \'\\r\n"
191 "}");
192 try {
193 motherBoard.getCommandController().executeCommand(command);
194 } catch (CommandException& e) {
195 motherBoard.getMSXCliComm().printWarning(
196 "Error executing loading instruction using command \"",
197 command, "\" for AutoRun: ",
198 e.getMessage(), "\n Please report a bug.");
199 }
200}
201
202string CassettePlayer::getStateString() const
203{
204 switch (getState()) {
205 case PLAY: return "play";
206 case RECORD: return "record";
207 case STOP: return "stop";
208 }
209 UNREACHABLE; return {};
210}
211
212bool CassettePlayer::isRolling() const
213{
214 // Is the tape 'rolling'?
215 // is true when:
216 // not in stop mode (there is a tape inserted and not at end-of-tape)
217 // AND [ user forced playing (motorControl=off) OR motor enabled by
218 // software (motor=on) ]
219 return (getState() != STOP) && (motor || !motorControl);
220}
221
222double CassettePlayer::getTapePos(EmuTime::param time)
223{
224 sync(time);
225 if (getState() == RECORD) {
226 // we record 8-bit mono, so bytes == samples
227 return (double(recordImage->getBytes()) + partialInterval) / RECORD_FREQ;
228 } else {
229 return (tapePos - EmuTime::zero()).toDouble();
230 }
231}
232
233double CassettePlayer::getTapeLength(EmuTime::param time)
234{
235 if (playImage) {
236 return (playImage->getEndTime() - EmuTime::zero()).toDouble();
237 } else if (getState() == RECORD) {
238 return getTapePos(time);
239 } else {
240 return 0.0;
241 }
242}
243
244void CassettePlayer::checkInvariants() const
245{
246 switch (getState()) {
247 case STOP:
248 assert(!recordImage);
249 if (playImage) {
250 // we're at end-of tape
251 assert(!getImageName().empty());
252 } else {
253 // no tape inserted, imageName may or may not be empty
254 }
255 break;
256 case PLAY:
257 assert(!getImageName().empty());
258 assert(!recordImage);
259 assert(playImage);
260 break;
261 case RECORD:
262 assert(!getImageName().empty());
263 assert(recordImage);
264 assert(!playImage);
265 break;
266 default:
268 }
269}
270
271void CassettePlayer::setState(State newState, const Filename& newImage,
272 EmuTime::param time)
273{
274 sync(time);
275
276 // set new state if different from old state
277 State oldState = getState();
278 if (oldState == newState) return;
279
280 // cannot directly switch from PLAY to RECORD or vice-versa,
281 // (should always go via STOP)
282 assert(!((oldState == PLAY) && (newState == RECORD)));
283 assert(!((oldState == RECORD) && (newState == PLAY)));
284
285 // stuff for leaving the old state
286 // 'recordImage==nullptr' can happen in case of loadstate.
287 if ((oldState == RECORD) && recordImage) {
288 flushOutput();
289 bool empty = recordImage->isEmpty();
290 recordImage.reset();
291 if (empty) {
292 // delete the created WAV file, as it is useless
293 FileOperations::unlink(getImageName().getResolved()); // ignore errors
294 setImageName(Filename());
295 }
296 }
297
298 // actually switch state
299 state = newState;
300 setImageName(newImage);
301
302 // stuff for entering the new state
303 if (newState == RECORD) {
304 partialOut = 0.0;
305 partialInterval = 0.0;
306 lastX = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
307 lastY = 0.0;
308 }
309 motherBoard.getMSXCliComm().update(
310 CliComm::STATUS, "cassetteplayer", getStateString());
311
312 updateLoadingState(time); // sets SP for tape-end detection
313
314 checkInvariants();
315}
316
317void CassettePlayer::updateLoadingState(EmuTime::param time)
318{
319 assert(prevSyncTime == time); // sync() must be called
320 // TODO also set loadingIndicator for RECORD?
321 // note: we don't use isRolling()
322 loadingIndicator.update(motor && (getState() == PLAY));
323
324 syncEndOfTape.removeSyncPoint();
325 if (isRolling() && (getState() == PLAY)) {
326 syncEndOfTape.setSyncPoint(time + (playImage->getEndTime() - tapePos));
327 }
328}
329
330void CassettePlayer::setImageName(const Filename& newImage)
331{
332 casImage = newImage;
333 motherBoard.getMSXCliComm().update(
334 CliComm::MEDIA, "cassetteplayer", casImage.getResolved());
335}
336
337void CassettePlayer::insertTape(const Filename& filename, EmuTime::param time)
338{
339 if (!filename.empty()) {
340 FilePool& filePool = motherBoard.getReactor().getFilePool();
341 try {
342 // first try WAV
343 playImage = std::make_unique<WavImage>(filename, filePool);
344 } catch (MSXException& e) {
345 try {
346 // if that fails use CAS
347 playImage = std::make_unique<CasImage>(
348 filename, filePool,
349 motherBoard.getMSXCliComm());
350 } catch (MSXException& e2) {
351 throw MSXException(
352 "Failed to insert WAV image: \"",
353 e.getMessage(),
354 "\" and also failed to insert CAS image: \"",
355 e2.getMessage(), '\"');
356 }
357 }
358 } else {
359 // This is a bit tricky, consider this scenario: we switch from
360 // RECORD->PLAY, but we didn't actually record anything: The
361 // removeTape() call above (indirectly) deletes the empty
362 // recorded wav image and also clears imageName. Now because
363 // the 'filename' parameter is passed by reference, and because
364 // getImageName() returns a reference, this 'filename'
365 // parameter now also is an empty string.
366 }
367
368 // possibly recreate resampler
369 unsigned inputRate = playImage ? playImage->getFrequency() : 44100;
370 if (inputRate != getInputRate()) {
371 setInputRate(inputRate);
373 }
374
375 // trigger (re-)query of getAmplificationFactorImpl()
376 setSoftwareVolume(1.0f, time);
377
378 setImageName(filename);
379}
380
381void CassettePlayer::playTape(const Filename& filename, EmuTime::param time)
382{
383 // Temporally go to STOP state:
384 // RECORD: First close the recorded image. Otherwise it goes wrong
385 // if you switch from RECORD->PLAY on the same image.
386 // PLAY: Go to stop because we temporally violate some invariants
387 // (tapePos can be beyond end-of-tape).
388 setState(STOP, getImageName(), time); // keep current image
389 insertTape(filename, time);
390 rewind(time); // sets PLAY mode
391 autoRun();
392}
393
394void CassettePlayer::rewind(EmuTime::param time)
395{
396 sync(time); // before tapePos changes
397 assert(getState() != RECORD);
398 tapePos = EmuTime::zero();
399 audioPos = 0;
400
401 if (getImageName().empty()) {
402 // no image inserted, do nothing
403 assert(getState() == STOP);
404 } else {
405 // keep current image
406 setState(PLAY, getImageName(), time);
407 }
408 updateLoadingState(time);
409}
410
411void CassettePlayer::recordTape(const Filename& filename, EmuTime::param time)
412{
413 removeTape(time); // flush (possible) previous recording
414 recordImage = std::make_unique<Wav8Writer>(filename, 1, RECORD_FREQ);
415 tapePos = EmuTime::zero();
416 setState(RECORD, filename, time);
417}
418
419void CassettePlayer::removeTape(EmuTime::param time)
420{
421 // first stop with tape still inserted
422 setState(STOP, getImageName(), time);
423 // then remove the tape
424 playImage.reset();
425 tapePos = EmuTime::zero();
426 setImageName({});
427}
428
429void CassettePlayer::setMotor(bool status, EmuTime::param time)
430{
431 if (status != motor) {
432 sync(time);
433 motor = status;
434 updateLoadingState(time);
435 }
436}
437
438void CassettePlayer::setMotorControl(bool status, EmuTime::param time)
439{
440 if (status != motorControl) {
441 sync(time);
442 motorControl = status;
443 updateLoadingState(time);
444 }
445}
446
447int16_t CassettePlayer::readSample(EmuTime::param time)
448{
449 if (getState() == PLAY) {
450 // playing
451 sync(time);
452 return isRolling() ? playImage->getSampleAt(tapePos) : int16_t(0);
453 } else {
454 // record or stop
455 return 0;
456 }
457}
458
459void CassettePlayer::setSignal(bool output, EmuTime::param time)
460{
461 sync(time);
462 lastOutput = output;
463}
464
465void CassettePlayer::sync(EmuTime::param time)
466{
467 EmuDuration duration = time - prevSyncTime;
468 prevSyncTime = time;
469
470 updateTapePosition(duration, time);
471 generateRecordOutput(duration);
472}
473
474void CassettePlayer::updateTapePosition(
475 EmuDuration::param duration, EmuTime::param time)
476{
477 if (!isRolling() || (getState() != PLAY)) return;
478
479 tapePos += duration;
480 assert(tapePos <= playImage->getEndTime());
481
482 // synchronize audio with actual tape position
483 if (!syncScheduled) {
484 // don't sync too often, this improves sound quality
485 syncScheduled = true;
486 syncAudioEmu.setSyncPoint(time + EmuDuration::sec(1));
487 }
488}
489
490void CassettePlayer::generateRecordOutput(EmuDuration::param duration)
491{
492 if (!recordImage || !isRolling()) return;
493
494 double out = lastOutput ? OUTPUT_AMP : -OUTPUT_AMP;
495 double samples = duration.toDouble() * RECORD_FREQ;
496 double rest = 1.0 - partialInterval;
497 if (rest <= samples) {
498 // enough to fill next interval
499 partialOut += out * rest;
500 fillBuf(1, partialOut);
501 samples -= rest;
502
503 // fill complete intervals
504 int count = int(samples);
505 if (count > 0) {
506 fillBuf(count, out);
507 }
508 samples -= count;
509 assert(samples < 1.0);
510
511 // partial last interval
512 partialOut = samples * out;
513 partialInterval = samples;
514 } else {
515 assert(samples < 1.0);
516 partialOut += samples * out;
517 partialInterval += samples;
518 }
519 assert(partialInterval < 1.0);
520}
521
522void CassettePlayer::fillBuf(size_t length, double x)
523{
524 assert(recordImage);
525 constexpr double A = 252.0 / 256.0;
526
527 double y = lastY + (x - lastX);
528
529 while (length) {
530 size_t len = std::min(length, buf.size() - sampCnt);
531 repeat(len, [&] {
532 buf[sampCnt++] = narrow<uint8_t>(int(y) + 128);
533 y *= A;
534 });
535 length -= len;
536 assert(sampCnt <= buf.size());
537 if (sampCnt == buf.size()) {
538 flushOutput();
539 }
540 }
541 lastY = y;
542 lastX = x;
543}
544
545void CassettePlayer::flushOutput()
546{
547 try {
548 recordImage->write(subspan(buf, 0, sampCnt));
549 sampCnt = 0;
550 recordImage->flush(); // update wav header
551 } catch (MSXException& e) {
552 motherBoard.getMSXCliComm().printWarning(
553 "Failed to write to tape: ", e.getMessage());
554 }
555}
556
557
558std::string_view CassettePlayer::getName() const
559{
560 return getCassettePlayerName();
561}
562
563std::string_view CassettePlayer::getDescription() const
564{
565 return DESCRIPTION;
566}
567
568void CassettePlayer::plugHelper(Connector& conn, EmuTime::param time)
569{
570 sync(time);
571 lastOutput = checked_cast<CassettePort&>(conn).lastOut();
572}
573
574void CassettePlayer::unplugHelper(EmuTime::param time)
575{
576 // note: may not throw exceptions
577 setState(STOP, getImageName(), time); // keep current image
578}
579
580
581void CassettePlayer::generateChannels(std::span<float*> buffers, unsigned num)
582{
583 // Single channel device: replace content of buffers[0] (not add to it).
584 assert(buffers.size() == 1);
585 if ((getState() != PLAY) || !isRolling()) {
586 buffers[0] = nullptr;
587 return;
588 }
589 assert(buffers.size() == 1);
590 playImage->fillBuffer(audioPos, buffers.first<1>(), num);
591 audioPos += num;
592}
593
595{
596 return playImage ? playImage->getAmplificationFactorImpl() : 1.0f;
597}
598
599int CassettePlayer::signalEvent(const Event& event)
600{
601 if (getType(event) == EventType::BOOT) {
602 if (!getImageName().empty()) {
603 // Reinsert tape to make sure everything is reset.
604 try {
605 playTape(getImageName(), getCurrentTime());
606 } catch (MSXException& e) {
607 motherBoard.getMSXCliComm().printWarning(
608 "Failed to insert tape: ", e.getMessage());
609 }
610 }
611 }
612 return 0;
613}
614
615void CassettePlayer::execEndOfTape(EmuTime::param time)
616{
617 // tape ended
618 sync(time);
619 assert(tapePos == playImage->getEndTime());
620 motherBoard.getMSXCliComm().printWarning(
621 "Tape end reached... stopping. "
622 "You may need to insert another tape image "
623 "that contains side B. (Or you used the wrong "
624 "loading command.)");
625 setState(STOP, getImageName(), time); // keep current image
626}
627
628void CassettePlayer::execSyncAudioEmu(EmuTime::param time)
629{
630 if (getState() == PLAY) {
631 updateStream(time);
632 sync(time);
633 DynamicClock clk(EmuTime::zero());
634 clk.setFreq(playImage->getFrequency());
635 audioPos = clk.getTicksTill(tapePos);
636 }
637 syncScheduled = false;
638}
639
640
641// class TapeCommand
642
643CassettePlayer::TapeCommand::TapeCommand(
644 CommandController& commandController_,
645 StateChangeDistributor& stateChangeDistributor_,
646 Scheduler& scheduler_)
647 : RecordedCommand(commandController_, stateChangeDistributor_,
648 scheduler_, "cassetteplayer")
649{
650}
651
652void CassettePlayer::TapeCommand::execute(
653 std::span<const TclObject> tokens, TclObject& result, EmuTime::param time)
654{
655 auto& cassettePlayer = OUTER(CassettePlayer, tapeCommand);
656 if (tokens.size() == 1) {
657 // Returning Tcl lists here, similar to the disk commands in
658 // DiskChanger
659 TclObject options = makeTclList(cassettePlayer.getStateString());
660 result.addListElement(tmpStrCat(getName(), ':'),
661 cassettePlayer.getImageName().getResolved(),
662 options);
663
664 } else if (tokens[1] == "new") {
665 std::string_view directory = "taperecordings";
666 std::string_view prefix = "openmsx";
667 std::string_view extension = ".wav";
669 (tokens.size() == 3) ? tokens[2].getString() : string{},
670 directory, prefix, extension);
671 cassettePlayer.recordTape(Filename(filename), time);
672 result = tmpStrCat(
673 "Created new cassette image file: ", filename,
674 ", inserted it and set recording mode.");
675
676 } else if (tokens[1] == "insert" && tokens.size() == 3) {
677 try {
678 result = "Changing tape";
679 Filename filename(tokens[2].getString(), userFileContext());
680 cassettePlayer.playTape(filename, time);
681 } catch (MSXException& e) {
682 throw CommandException(std::move(e).getMessage());
683 }
684
685 } else if (tokens[1] == "motorcontrol" && tokens.size() == 3) {
686 if (tokens[2] == "on") {
687 cassettePlayer.setMotorControl(true, time);
688 result = "Motor control enabled.";
689 } else if (tokens[2] == "off") {
690 cassettePlayer.setMotorControl(false, time);
691 result = "Motor control disabled.";
692 } else {
693 throw SyntaxError();
694 }
695
696 } else if (tokens.size() != 2) {
697 throw SyntaxError();
698
699 } else if (tokens[1] == "motorcontrol") {
700 result = tmpStrCat("Motor control is ",
701 (cassettePlayer.motorControl ? "on" : "off"));
702
703 } else if (tokens[1] == "record") {
704 result = "TODO: implement this... (sorry)";
705
706 } else if (tokens[1] == "play") {
707 if (cassettePlayer.getState() == CassettePlayer::RECORD) {
708 try {
709 result = "Play mode set, rewinding tape.";
710 cassettePlayer.playTape(
711 cassettePlayer.getImageName(), time);
712 } catch (MSXException& e) {
713 throw CommandException(std::move(e).getMessage());
714 }
715 } else if (cassettePlayer.getState() == CassettePlayer::STOP) {
716 throw CommandException("No tape inserted or tape at end!");
717 } else {
718 // PLAY mode
719 result = "Already in play mode.";
720 }
721
722 } else if (tokens[1] == "eject") {
723 result = "Tape ejected";
724 cassettePlayer.removeTape(time);
725
726 } else if (tokens[1] == "rewind") {
727 string r;
728 if (cassettePlayer.getState() == CassettePlayer::RECORD) {
729 try {
730 r = "First stopping recording... ";
731 cassettePlayer.playTape(
732 cassettePlayer.getImageName(), time);
733 } catch (MSXException& e) {
734 throw CommandException(std::move(e).getMessage());
735 }
736 }
737 cassettePlayer.rewind(time);
738 r += "Tape rewound";
739 result = r;
740
741 } else if (tokens[1] == "getpos") {
742 result = cassettePlayer.getTapePos(time);
743
744 } else if (tokens[1] == "getlength") {
745 result = cassettePlayer.getTapeLength(time);
746
747 } else {
748 try {
749 result = "Changing tape";
750 Filename filename(tokens[1].getString(), userFileContext());
751 cassettePlayer.playTape(filename, time);
752 } catch (MSXException& e) {
753 throw CommandException(std::move(e).getMessage());
754 }
755 }
756 //if (!cassettePlayer.getConnector()) {
757 // cassettePlayer.cliComm.printWarning("Cassette player not plugged in.");
758 //}
759}
760
761string CassettePlayer::TapeCommand::help(std::span<const TclObject> tokens) const
762{
763 string helpText;
764 if (tokens.size() >= 2) {
765 if (tokens[1] == "eject") {
766 helpText =
767 "Well, just eject the cassette from the cassette "
768 "player/recorder!";
769 } else if (tokens[1] == "rewind") {
770 helpText =
771 "Indeed, rewind the tape that is currently in the "
772 "cassette player/recorder...";
773 } else if (tokens[1] == "motorcontrol") {
774 helpText =
775 "Setting this to 'off' is equivalent to "
776 "disconnecting the black remote plug from the "
777 "cassette player: it makes the cassette player "
778 "run (if in play mode); the motor signal from the "
779 "MSX will be ignored. Normally this is set to "
780 "'on': the cassetteplayer obeys the motor control "
781 "signal from the MSX.";
782 } else if (tokens[1] == "play") {
783 helpText =
784 "Go to play mode. Only useful if you were in "
785 "record mode (which is currently the only other "
786 "mode available).";
787 } else if (tokens[1] == "new") {
788 helpText =
789 "Create a new cassette image. If the file name is "
790 "omitted, one will be generated in the default "
791 "directory for tape recordings. Implies going to "
792 "record mode (why else do you want a new cassette "
793 "image?).";
794 } else if (tokens[1] == "insert") {
795 helpText =
796 "Inserts the specified cassette image into the "
797 "cassette player, rewinds it and switches to play "
798 "mode.";
799 } else if (tokens[1] == "record") {
800 helpText =
801 "Go to record mode. NOT IMPLEMENTED YET. Will be "
802 "used to be able to resume recording to an "
803 "existing cassette image, previously inserted with "
804 "the insert command.";
805 } else if (tokens[1] == "getpos") {
806 helpText =
807 "Return the position of the tape, in seconds from "
808 "the beginning of the tape.";
809 } else if (tokens[1] == "getlength") {
810 helpText =
811 "Return the length of the tape in seconds.";
812 }
813 } else {
814 helpText =
815 "cassetteplayer eject "
816 ": remove tape from virtual player\n"
817 "cassetteplayer rewind "
818 ": rewind tape in virtual player\n"
819 "cassetteplayer motorcontrol "
820 ": enables or disables motor control (remote)\n"
821 "cassetteplayer play "
822 ": change to play mode (default)\n"
823 "cassetteplayer record "
824 ": change to record mode (NOT IMPLEMENTED YET)\n"
825 "cassetteplayer new [<filename>] "
826 ": create and insert new tape image file and go to record mode\n"
827 "cassetteplayer insert <filename> "
828 ": insert (a different) tape file\n"
829 "cassetteplayer getpos "
830 ": query the position of the tape\n"
831 "cassetteplayer getlength "
832 ": query the total length of the tape\n"
833 "cassetteplayer <filename> "
834 ": insert (a different) tape file\n";
835 }
836 return helpText;
837}
838
839void CassettePlayer::TapeCommand::tabCompletion(std::vector<string>& tokens) const
840{
841 using namespace std::literals;
842 if (tokens.size() == 2) {
843 static constexpr std::array cmds = {
844 "eject"sv, "rewind"sv, "motorcontrol"sv, "insert"sv, "new"sv,
845 "play"sv, "getpos"sv, "getlength"sv,
846 //"record"sv,
847 };
848 completeFileName(tokens, userFileContext(), cmds);
849 } else if ((tokens.size() == 3) && (tokens[1] == "insert")) {
850 completeFileName(tokens, userFileContext());
851 } else if ((tokens.size() == 3) && (tokens[1] == "motorcontrol")) {
852 static constexpr std::array extra = {"on"sv, "off"sv};
853 completeString(tokens, extra);
854 }
855}
856
857bool CassettePlayer::TapeCommand::needRecord(std::span<const TclObject> tokens) const
858{
859 return tokens.size() > 1;
860}
861
862
863static constexpr std::initializer_list<enum_string<CassettePlayer::State>> stateInfo = {
864 { "PLAY", CassettePlayer::PLAY },
865 { "RECORD", CassettePlayer::RECORD },
866 { "STOP", CassettePlayer::STOP }
867};
869
870// version 1: initial version
871// version 2: added checksum
872template<typename Archive>
873void CassettePlayer::serialize(Archive& ar, unsigned version)
874{
875 if (recordImage) {
876 // buf, sampcnt
877 flushOutput();
878 }
879
880 ar.serialize("casImage", casImage);
881
882 Sha1Sum oldChecksum;
883 if constexpr (!Archive::IS_LOADER) {
884 if (playImage) {
885 oldChecksum = playImage->getSha1Sum();
886 }
887 }
888 if (ar.versionAtLeast(version, 2)) {
889 string oldChecksumStr = oldChecksum.empty()
890 ? string{}
891 : oldChecksum.toString();
892 ar.serialize("checksum", oldChecksumStr);
893 oldChecksum = oldChecksumStr.empty()
894 ? Sha1Sum()
895 : Sha1Sum(oldChecksumStr);
896 }
897
898 if constexpr (Archive::IS_LOADER) {
899 FilePool& filePool = motherBoard.getReactor().getFilePool();
900 auto time = getCurrentTime();
901 casImage.updateAfterLoadState();
902 if (!oldChecksum.empty() &&
903 !FileOperations::exists(casImage.getResolved())) {
904 auto file = filePool.getFile(FileType::TAPE, oldChecksum);
905 if (file.is_open()) {
906 casImage.setResolved(file.getURL());
907 }
908 }
909 try {
910 insertTape(casImage, time);
911 } catch (MSXException&) {
912 if (oldChecksum.empty()) {
913 // It's OK if we cannot reinsert an empty
914 // image. One likely scenario for this case is
915 // the following:
916 // - cassetteplayer new myfile.wav
917 // - don't actually start saving to tape yet
918 // - create a savestate and load that state
919 // Because myfile.wav contains no data yet, it
920 // is deleted from the filesystem. So on a
921 // loadstate it won't be found.
922 } else {
923 throw;
924 }
925 }
926
927 if (playImage && !oldChecksum.empty()) {
928 Sha1Sum newChecksum = playImage->getSha1Sum();
929 if (oldChecksum != newChecksum) {
930 motherBoard.getMSXCliComm().printWarning(
931 "The content of the tape ",
932 casImage.getResolved(),
933 " has changed since the time this "
934 "savestate was created. This might "
935 "result in emulation problems.");
936 }
937 }
938 }
939
940 // only for RECORD
941 //double lastX;
942 //double lastY;
943 //double partialOut;
944 //double partialInterval;
945 //std::unique_ptr<WavWriter> recordImage;
946
947 ar.serialize("tapePos", tapePos,
948 "prevSyncTime", prevSyncTime,
949 "audioPos", audioPos,
950 "state", state,
951 "lastOutput", lastOutput,
952 "motor", motor,
953 "motorControl", motorControl);
954
955 if constexpr (Archive::IS_LOADER) {
956 auto time = getCurrentTime();
957 if (playImage && (tapePos > playImage->getEndTime())) {
958 tapePos = playImage->getEndTime();
959 motherBoard.getMSXCliComm().printWarning("Tape position "
960 "beyond tape end! Setting tape position to end. "
961 "This can happen if you load a replay from an "
962 "older openMSX version with a different CAS-to-WAV "
963 "baud rate or when the tape image has been changed "
964 "compared to when the replay was created.");
965 }
966 if (state == RECORD) {
967 // TODO we don't support savestates in RECORD mode yet
968 motherBoard.getMSXCliComm().printWarning(
969 "Restoring a state where the MSX was saving to "
970 "tape is not yet supported. Emulation will "
971 "continue without actually saving.");
972 setState(STOP, getImageName(), time);
973 }
974 if (!playImage && (state == PLAY)) {
975 // This should only happen for manually edited
976 // savestates, though we shouldn't crash on it.
977 setState(STOP, getImageName(), time);
978 }
979 sync(time);
980 updateLoadingState(time);
981 }
982}
985
986} // namespace openmsx
bool getBoolean() const noexcept
void plugHelper(Connector &connector, EmuTime::param time) override
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.
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.
void getMediaInfo(TclObject &result) override
This method gets called when information is required on the media inserted in the media slot of the p...
virtual void update(UpdateType type, std::string_view name, std::string_view value)=0
void printWarning(std::string_view message)
Definition: CliComm.cc:10
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)
Definition: EmuDuration.hh:41
const EmuDuration & param
Definition: EmuDuration.hh:29
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.
Definition: FilePool.cc:53
void setResolved(std::string resolved)
Change the resolved part of this filename E.g.
Definition: Filename.hh:67
const std::string & getResolved() const &
Definition: Filename.hh:47
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 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
EventDistributor & getEventDistributor()
Definition: Reactor.hh:82
FilePool & getFilePool()
Definition: Reactor.hh:91
This class represents the result of a sha1 calculation (a 160-bit value).
Definition: sha1.hh:23
bool empty() const
Definition: utils/sha1.cc:242
std::string toString() const
Definition: utils/sha1.cc:230
void updateStream(EmuTime::param time)
Definition: SoundDevice.cc:138
unsigned getInputRate() const
Definition: SoundDevice.hh:120
void setInputRate(unsigned sampleRate)
Definition: SoundDevice.hh:119
void setSoftwareVolume(float volume, EmuTime::param time)
Change the 'software volume' of this sound device.
Definition: SoundDevice.cc:143
void unregisterSound()
Unregisters this sound device with the Mixer.
Definition: SoundDevice.cc:133
void registerSound(const DeviceConfig &config)
Registers this sound device with the Mixer.
Definition: SoundDevice.cc:88
void addDictKeyValues(Args &&... args)
Definition: TclObject.hh:145
static XMLDocument & getStaticDocument()
Definition: XMLElement.hh:255
XMLElement * setFirstChild(XMLElement *child)
Definition: XMLElement.hh:211
static_string_view
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:147
constexpr double e
Definition: Math.hh:21
T length(const vecN< N, T > &x)
Definition: gl_vec.hh:341
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:267
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.
Definition: Keys.cc:730
This file implemented 3 utility functions:
Definition: Autofire.cc:9
SERIALIZE_ENUM(CassettePlayer::State, stateInfo)
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
EventType getType(const Event &event)
Definition: Event.hh:647
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:171
std::array< const EDStorage, 4 > A
TclObject makeTclList(Args &&... args)
Definition: TclObject.hh:283
#define OUTER(type, member)
Definition: outer.hh:41
constexpr auto subspan(Range &&range, size_t offset, size_t count=std::dynamic_extent)
Definition: ranges.hh:446
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:1021
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:610
std::string strCat(Ts &&...ts)
Definition: strCat.hh:542
#define UNREACHABLE
Definition: unreachable.hh:38
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition: xrange.hh:147