openMSX
CasImage.cc
Go to the documentation of this file.
1#include "CasImage.hh"
2
3#include "File.hh"
4#include "FilePool.hh"
5#include "Filename.hh"
6#include "CliComm.hh"
7#include "MSXException.hh"
8
9#include "narrow.hh"
10#include "ranges.hh"
11#include "stl.hh"
12#include "xrange.hh"
13
14#include <memory>
15#include <span>
16
17static constexpr std::array<uint8_t, 10> ASCII_HEADER = { 0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA };
18static constexpr std::array<uint8_t, 10> BINARY_HEADER = { 0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0 };
19static constexpr std::array<uint8_t, 10> BASIC_HEADER = { 0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3 };
20// parsing code assumes these all have the same size
21static_assert(ASCII_HEADER.size() == BINARY_HEADER.size());
22static_assert(ASCII_HEADER.size() == BASIC_HEADER.size());
23
24namespace openmsx {
25
26// We oversample the audio signal for better sound quality (especially in
27// combination with the hq resampler). Without oversampling the audio output
28// could contain portions like this:
29// -1, +1, -1, +1, -1, +1, ...
30// So it contains a signal at the Nyquist frequency. The hq resampler contains
31// a low-pass filter, and (all practically implementable) filters cut off a
32// portion of the spectrum near the Nyquist freq. So this high freq signal was
33// lost after the hq resampler. After oversampling, the signal looks like this:
34// -1, -1, -1, -1, +1, +1, +1, +1, -1, -1, -1, -1, ...
35// So every sample repeated 4 times.
36static constexpr unsigned AUDIO_OVERSAMPLE = 4;
37
38static void append(std::vector<int8_t>& wave, size_t count, int8_t value)
39{
40 wave.insert(wave.end(), count, value);
41}
42
43static void writeSilence(std::vector<int8_t>& wave, unsigned s)
44{
45 append(wave, s, 0);
46}
47
48static bool compare(const uint8_t* p, std::span<const uint8_t> rhs)
49{
50 return ranges::equal(std::span{p, rhs.size()}, rhs);
51}
52
53namespace MSX_CAS {
54
55// a higher baudrate doesn't work anymore, but it is unclear why, because 4600
56// baud should work (known from Speedsave 4000 and Turbo 5000 programs).
57// 3765 still works on a Toshiba HX-10 and Philips NMS 8250, but not on a
58// Panasonic FS-A1WSX, on which 3763 is the max. National CF-2000 has 3762 as
59// the max. Let's take 3760 then as a safe value.
60// UPDATE: that seems to break RUN"CAS:" type of programs. 3744 seems to work
61// for those as well (we don't understand why yet)
62static constexpr unsigned BAUDRATE = 3744;
63static constexpr unsigned OUTPUT_FREQUENCY = 4 * BAUDRATE; // 4 samples per bit
64
65// number of output bytes for silent parts
66static constexpr unsigned SHORT_SILENCE = OUTPUT_FREQUENCY * 1; // 1 second
67static constexpr unsigned LONG_SILENCE = OUTPUT_FREQUENCY * 2; // 2 seconds
68
69// number of 1-bits for headers
70static constexpr unsigned LONG_HEADER = 16000 / 2;
71static constexpr unsigned SHORT_HEADER = 4000 / 2;
72
73// headers definitions
74static constexpr std::array<uint8_t, 8> CAS_HEADER = { 0x1F,0xA6,0xDE,0xBA,0xCC,0x13,0x7D,0x74 };
75
76static void write0(std::vector<int8_t>& wave)
77{
78 static constexpr std::array<int8_t, 4> chunk{127, 127, -127, -127};
79 ::append(wave, chunk);
80}
81static void write1(std::vector<int8_t>& wave)
82{
83 static constexpr std::array<int8_t, 4> chunk{127, -127, 127, -127};
84 ::append(wave, chunk);
85}
86
87static void writeHeader(std::vector<int8_t>& wave, unsigned s)
88{
89 repeat(s, [&] { write1(wave); });
90}
91
92static void writeByte(std::vector<int8_t>& wave, uint8_t b)
93{
94 // one start bit
95 write0(wave);
96 // eight data bits
97 for (auto i : xrange(8)) {
98 if (b & (1 << i)) {
99 write1(wave);
100 } else {
101 write0(wave);
102 }
103 }
104 // two stop bits
105 write1(wave);
106 write1(wave);
107}
108
109// write data until a header is detected
110static bool writeData(std::vector<int8_t>& wave, std::span<const uint8_t> cas, size_t& pos)
111{
112 bool eof = false;
113 while ((pos + CAS_HEADER.size()) <= cas.size()) {
114 if (compare(&cas[pos], CAS_HEADER)) {
115 return eof;
116 }
117 writeByte(wave, cas[pos]);
118 if (cas[pos] == 0x1A) {
119 eof = true;
120 }
121 pos++;
122 }
123 while (pos < cas.size()) {
124 writeByte(wave, cas[pos++]);
125 }
126 return false;
127}
128
129static CasImage::Data convert(std::span<const uint8_t> cas, const std::string& filename, CliComm& cliComm,
130 CassetteImage::FileType& firstFileType)
131{
132 CasImage::Data data;
133 data.frequency = OUTPUT_FREQUENCY;
134 auto& wave = data.wave;
135
136 // search for a header in the .cas file
137 bool issueWarning = false;
138 bool headerFound = false;
139 bool firstFile = true;
140 size_t pos = 0;
141 while ((pos + CAS_HEADER.size()) <= cas.size()) {
142 if (compare(&cas[pos], CAS_HEADER)) {
143 // it probably works fine if a long header is used for every
144 // header but since the msx bios makes a distinction between
145 // them, we do also (hence a lot of code).
146 headerFound = true;
147 pos += CAS_HEADER.size();
148 writeSilence(wave, LONG_SILENCE);
149 writeHeader(wave, LONG_HEADER);
150 if ((pos + ASCII_HEADER.size()) <= cas.size()) {
151 // determine file type
152 auto type = [&] {
153 if (compare(&cas[pos], ASCII_HEADER)) {
155 } else if (compare(&cas[pos], BINARY_HEADER)) {
157 } else if (compare(&cas[pos], BASIC_HEADER)) {
159 } else {
161 }
162 }();
163 if (firstFile) firstFileType = type;
164 switch (type) {
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;
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
211namespace SVI_CAS {
212
213static 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
219static 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
226static 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
233static 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
245static 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 if (compare(&cas[header.size()], ASCII_HEADER)) {
252 firstFileType = CassetteImage::ASCII;
253 } else if (compare(&cas[header.size()], BINARY_HEADER)) {
254 firstFileType = CassetteImage::BINARY;
255 } else if (compare(&cas[header.size()], BASIC_HEADER)) {
256 firstFileType = CassetteImage::BASIC;
257 }
258 }
259
260 auto prevHeader = cas.begin() + header.size();
261 while (true) {
262 auto nextHeader = std::search(prevHeader, cas.end(),
263 header.begin(), header.end());
264 // Workaround clang-13/libc++ bug
265 //processBlock(std::span(prevHeader, nextHeader), data.wave);
266 processBlock(std::span(std::to_address(prevHeader), nextHeader - prevHeader), data.wave);
267 if (nextHeader == cas.end()) break;
268 prevHeader = nextHeader + header.size();
269 }
270 return data;
271}
272
273} // namespace SVI_CAS
274
275CasImage::Data CasImage::init(const Filename& filename, FilePool& filePool, CliComm& cliComm)
276{
277 File file(filename);
278 auto cas = file.mmap();
279
280 auto fileType = CassetteImage::UNKNOWN;
281 auto result = [&] {
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);
290
291 // conversion successful, now calc sha1sum
292 setSha1Sum(filePool.getSha1Sum(file));
293
294 return result;
295}
296
297CasImage::CasImage(const Filename& filename, FilePool& filePool, CliComm& cliComm)
298 : data(init(filename, filePool, cliComm))
299{
300}
301
302int16_t CasImage::getSampleAt(EmuTime::param 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
309EmuTime CasImage::getEndTime() const
310{
311 EmuDuration d = EmuDuration::hz(data.frequency) * data.wave.size();
312 return EmuTime::zero() + d;
313}
314
316{
317 return data.frequency * AUDIO_OVERSAMPLE;
318}
319
320void 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
336{
337 return 1.0f / 128.0f;
338}
339
340} // namespace openmsx
int16_t getSampleAt(EmuTime::param time) const override
Definition CasImage.cc:302
unsigned getFrequency() const override
Definition CasImage.cc:315
CasImage(const Filename &fileName, FilePool &filePool, CliComm &cliComm)
Definition CasImage.cc:297
void fillBuffer(unsigned pos, std::span< float *, 1 > bufs, unsigned num) const override
Definition CasImage.cc:320
EmuTime getEndTime() const override
Definition CasImage.cc:309
float getAmplificationFactorImpl() const override
Definition CasImage.cc:335
void setSha1Sum(const Sha1Sum &sha1sum)
void setFirstFileType(FileType type)
void printWarning(std::string_view message)
Definition CliComm.cc:10
static constexpr EmuDuration hz(unsigned x)
constexpr unsigned getTicksAt(unsigned freq) const
Sha1Sum getSha1Sum(File &file)
Calculate sha1sum for the given File object.
Definition FilePool.cc:58
This class represents a filename.
Definition Filename.hh:20
const std::string & getOriginal() const
Definition Filename.hh:46
This file implemented 3 utility functions:
Definition Autofire.cc:11
bool equal(InputRange1 &&range1, InputRange2 &&range2, Pred pred={}, Proj1 proj1={}, Proj2 proj2={})
Definition ranges.hh:368
std::vector< int8_t > wave
Definition CasImage.hh:30
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition xrange.hh:147
constexpr auto xrange(T e)
Definition xrange.hh:132