OXYGENHUB
◆ main openmsx / src / cassette / CasImage.cc
9.5 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "CasImage.hh"
2
3
#include "CliComm.hh"
4
#include "File.hh"
5
#include "FilePool.hh"
6
#include "Filename.hh"
7
#include "MSXException.hh"
8
9
#include "narrow.hh"
10
#include "stl.hh"
11
#include "xrange.hh"
12
13
#include <algorithm>
14
#include <span>
15
16
static constexpr std::array<uint8_t, 10> ASCII_HEADER  = { 0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA };
17
static constexpr std::array<uint8_t, 10> BINARY_HEADER = { 0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0 };
18
static constexpr std::array<uint8_t, 10> BASIC_HEADER  = { 0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3 };
19
// parsing code assumes these all have the same size
20
static_assert(ASCII_HEADER.size() == BINARY_HEADER.size());
21
static_assert(ASCII_HEADER.size() == BASIC_HEADER.size());
22
23
namespace openmsx {
24
25
// We oversample the audio signal for better sound quality (especially in
26
// combination with the hq resampler). Without oversampling the audio output
27
// could contain portions like this:
28
//   -1, +1, -1, +1, -1, +1, ...
29
// So it contains a signal at the Nyquist frequency. The hq resampler contains
30
// a low-pass filter, and (all practically implementable) filters cut off a
31
// portion of the spectrum near the Nyquist freq. So this high freq signal was
32
// lost after the hq resampler. After oversampling, the signal looks like this:
33
//   -1, -1, -1, -1, +1, +1, +1, +1, -1, -1, -1, -1, ...
34
// So every sample repeated 4 times.
35
static constexpr unsigned AUDIO_OVERSAMPLE = 4;
36
37
static void append(std::vector<int8_t>& wave, size_t count, int8_t value)
38
{
39
	wave.insert(wave.end(), count, value);
40
}
41
42
static void writeSilence(std::vector<int8_t>& wave, unsigned s)
43
{
44
	append(wave, s, 0);
45
}
46
47
static bool compare(const uint8_t* p, std::span<const uint8_t> rhs)
48
{
49
	return std::ranges::equal(std::span{p, rhs.size()}, rhs);
50
}
51
52
namespace MSX_CAS {
53
54
// a higher baudrate doesn't work anymore, but it is unclear why, because 4600
55
// baud should work (known from Speedsave 4000 and Turbo 5000 programs).
56
// 3765 still works on a Toshiba HX-10 and Philips NMS 8250, but not on a
57
// Panasonic FS-A1WSX, on which 3763 is the max. National CF-2000 has 3762 as
58
// the max. Let's take 3760 then as a safe value.
59
// UPDATE: that seems to break RUN"CAS:" type of programs. 3744 seems to work
60
// for those as well (we don't understand why yet)
61
static constexpr unsigned BAUDRATE = 3744;
62
static constexpr unsigned OUTPUT_FREQUENCY = 4 * BAUDRATE; // 4 samples per bit
63
64
// number of output bytes for silent parts
65
static constexpr unsigned SHORT_SILENCE = OUTPUT_FREQUENCY * 1; // 1 second
66
static constexpr unsigned LONG_SILENCE  = OUTPUT_FREQUENCY * 2; // 2 seconds
67
68
// number of 1-bits for headers
69
static constexpr unsigned LONG_HEADER  = 16000 / 2;
70
static constexpr unsigned SHORT_HEADER =  4000 / 2;
71
72
// headers definitions
73
static constexpr std::array<uint8_t, 8> CAS_HEADER = { 0x1F,0xA6,0xDE,0xBA,0xCC,0x13,0x7D,0x74 };
74
75
static void write0(std::vector<int8_t>& wave)
76
{
77
	static constexpr std::array<int8_t, 4> chunk{127, 127, -127, -127};
78
	::append(wave, chunk);
79
}
80
static void write1(std::vector<int8_t>& wave)
81
{
82
	static constexpr std::array<int8_t, 4> chunk{127, -127, 127, -127};
83
	::append(wave, chunk);
84
}
85
86
static void writeHeader(std::vector<int8_t>& wave, unsigned s)
87
{
88
	repeat(s, [&] { write1(wave); });
89
}
90
91
static void writeByte(std::vector<int8_t>& wave, uint8_t b)
92
{
93
	// one start bit
94
	write0(wave);
95
	// eight data bits
96
	for (auto i : xrange(8)) {
97
		if (b & (1 << i)) {
98
			write1(wave);
99
		} else {
100
			write0(wave);
101
		}
102
	}
103
	// two stop bits
104
	write1(wave);
105
	write1(wave);
106
}
107
108
// write data until a header is detected
109
static bool writeData(std::vector<int8_t>& wave, std::span<const uint8_t> cas, size_t& pos)
110
{
111
	bool eof = false;
112
	while ((pos + CAS_HEADER.size()) <= cas.size()) {
113
		if (compare(&cas[pos], CAS_HEADER)) {
114
			return eof;
115
		}
116
		writeByte(wave, cas[pos]);
117
		if (cas[pos] == 0x1A) {
118
			eof = true;
119
		}
120
		pos++;
121
	}
122
	while (pos < cas.size()) {
123
		writeByte(wave, cas[pos++]);
124
	}
125
	return false;
126
}
127
128
static CasImage::Data convert(std::span<const uint8_t> cas, const std::string& filename, CliComm& cliComm,
129
                              CassetteImage::FileType& firstFileType)
130
{
131
	CasImage::Data data;
132
	data.frequency = OUTPUT_FREQUENCY;
133
	auto& wave = data.wave;
134
135
	// search for a header in the .cas file
136
	bool issueWarning = false;
137
	bool headerFound = false;
138
	bool firstFile = true;
139
	size_t pos = 0;
140
	while ((pos + CAS_HEADER.size()) <= cas.size()) {
141
		if (compare(&cas[pos], CAS_HEADER)) {
142
			// it probably works fine if a long header is used for every
143
			// header but since the msx bios makes a distinction between
144
			// them, we do also (hence a lot of code).
145
			headerFound = true;
146
			pos += CAS_HEADER.size();
147
			writeSilence(wave, LONG_SILENCE);
148
			writeHeader(wave, LONG_HEADER);
149
			if ((pos + ASCII_HEADER.size()) <= cas.size()) {
150
				// determine file type
151
				using enum CassetteImage::FileType;
152
				auto type = [&] {
153
					if (compare(&cas[pos], ASCII_HEADER)) {
154
						return ASCII;
155
					} else if (compare(&cas[pos], BINARY_HEADER)) {
156
						return BINARY;
157
					} else if (compare(&cas[pos], BASIC_HEADER)) {
158
						return BASIC;
159
					} else {
160
						return UNKNOWN;
161
					}
162
				}();
163
				if (firstFile) firstFileType = type;
164
				switch (type) {
165
					case ASCII:
166
						writeData(wave, cas, pos);
167
						do {
168
							pos += CAS_HEADER.size();
169
							writeSilence(wave, SHORT_SILENCE);
170
							writeHeader(wave, SHORT_HEADER);
171
							bool eof = writeData(wave, cas, pos);
172
							if (eof) break;
173
						} while ((pos + CAS_HEADER.size()) <= cas.size());
174
						break;
175
					case BINARY:
176
					case BASIC:
177
						writeData(wave, cas, pos);
178
						writeSilence(wave, SHORT_SILENCE);
179
						writeHeader(wave, SHORT_HEADER);
180
						pos += CAS_HEADER.size();
181
						writeData(wave, cas, pos);
182
						break;
183
					default:
184
						// unknown file type: using long header
185
						writeData(wave, cas, pos);
186
						break;
187
				}
188
			} else {
189
				// unknown file type: using long header
190
				writeData(wave, cas, pos);
191
			}
192
			firstFile = false;
193
		} else {
194
			// skipping unhandled data, shouldn't occur in normal cas file
195
			pos++;
196
			issueWarning = true;
197
		}
198
	}
199
	if (!headerFound) {
200
		throw MSXException(filename, ": not a valid CAS image");
201
	}
202
	if (issueWarning) {
203
		 cliComm.printWarning("Skipped unhandled data in ", filename);
204
	}
205
206
	return data;
207
}
208
209
} // namespace MSX_CAS
210
211
namespace SVI_CAS {
212
213
static constexpr std::array<uint8_t, 17> header = {
214
	0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
215
	0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
216
	0x7f,
217
};
218
219
static void writeBit(std::vector<int8_t>& wave, bool bit)
220
{
221
	size_t count = bit ? 1 : 2;
222
	append(wave, count,  127);
223
	append(wave, count, -127);
224
}
225
226
static void writeByte(std::vector<int8_t>& wave, uint8_t byte)
227
{
228
	for (int i = 7; i >= 0; --i) {
229
		writeBit(wave, (byte >> i) & 1);
230
	}
231
}
232
233
static void processBlock(std::span<const uint8_t> subBuf, std::vector<int8_t>& wave)
234
{
235
	writeSilence(wave, 1200);
236
	writeBit(wave, true);
237
	repeat(199, [&] { writeByte(wave, 0x55); });
238
	writeByte(wave, 0x7f);
239
	for (uint8_t val : subBuf) {
240
		writeBit(wave, false);
241
		writeByte(wave, val);
242
	}
243
}
244
245
static CasImage::Data convert(std::span<const uint8_t> cas, CassetteImage::FileType& firstFileType)
246
{
247
	CasImage::Data data;
248
	data.frequency = 4800;
249
250
	if (cas.size() >= (header.size() + ASCII_HEADER.size())) {
251
		using enum CassetteImage::FileType;
252
		if (compare(&cas[header.size()], ASCII_HEADER)) {
253
			firstFileType = ASCII;
254
		} else if (compare(&cas[header.size()], BINARY_HEADER)) {
255
			firstFileType = BINARY;
256
		} else if (compare(&cas[header.size()], BASIC_HEADER)) {
257
			firstFileType = BASIC;
258
		}
259
	}
260
261
	auto prevHeader = cas.begin() + header.size();
262
	while (true) {
263
		auto nextHeader = std::search(prevHeader, cas.end(),
264
		                              header.begin(), header.end());
265
		processBlock(std::span(prevHeader, nextHeader), data.wave);
266
		if (nextHeader == cas.end()) break;
267
		prevHeader = nextHeader + header.size();
268
	}
269
	return data;
270
}
271
272
} // namespace SVI_CAS
273
274
CasImage::Data CasImage::init(const Filename& filename, FilePool& filePool, CliComm& cliComm)
275
{
276
	File file(filename.getResolved());
277
	auto cas = file.mmap<const uint8_t>();
278
279
	auto fileType = CassetteImage::FileType::UNKNOWN;
280
	auto result = [&] {
281
		// TODO c++23 std::ranges::starts_with()
282
		if ((cas.size() >= SVI_CAS::header.size()) &&
283
		    (compare(cas.data(), SVI_CAS::header))) {
284
			return SVI_CAS::convert(cas, fileType);
285
		} else {
286
			return MSX_CAS::convert(cas, filename.getOriginal(), cliComm, fileType);
287
		}
288
	}();
289
	setFirstFileType(fileType, filename);
290
291
	// conversion successful, now calc sha1sum
292
	setSha1Sum(filePool.getSha1Sum(file, filename.getResolved()));
293
294
	return result;
295
}
296
297
CasImage::CasImage(const Filename& filename, FilePool& filePool, CliComm& cliComm)
298
	: data(init(filename, filePool, cliComm))
299
{
300
}
301
302
int16_t CasImage::getSampleAt(EmuTime time) const
303
{
304
	EmuDuration d = time - EmuTime::zero();
305
	unsigned pos = d.getTicksAt(data.frequency);
306
	return narrow<int16_t>((pos < data.wave.size()) ? (data.wave[pos] * 256) : 0);
307
}
308
309
EmuTime CasImage::getEndTime() const
310
{
311
	EmuDuration d = EmuDuration::hz(data.frequency) * data.wave.size();
312
	return EmuTime::zero() + d;
313
}
314
315
unsigned CasImage::getFrequency() const
316
{
317
	return data.frequency * AUDIO_OVERSAMPLE;
318
}
319
320
void CasImage::fillBuffer(unsigned pos, std::span<float*, 1> bufs, unsigned num) const
321
{
322
	size_t nbSamples = data.wave.size();
323
	if ((pos / AUDIO_OVERSAMPLE) < nbSamples) {
324
		for (auto i : xrange(num)) {
325
			bufs[0][i] = ((pos / AUDIO_OVERSAMPLE) < nbSamples)
326
			           ? narrow_cast<float>(data.wave[pos / AUDIO_OVERSAMPLE])
327
			           : 0.0f;
328
			++pos;
329
		}
330
	} else {
331
		bufs[0] = nullptr;
332
	}
333
}
334
335
float CasImage::getAmplificationFactorImpl() const
336
{
337
	return 1.0f / 128.0f;
338
}
339
340
} // namespace openmsx