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