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