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