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