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