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 ::append(wave, {127, 127, -127, -127});
75}
76static void write1(std::vector<int8_t>& wave)
77{
78 ::append(wave, {127, -127, 127, -127});
79}
80
81static void writeHeader(std::vector<int8_t>& wave, unsigned s)
82{
83 repeat(s, [&] { write1(wave); });
84}
85
86static void writeByte(std::vector<int8_t>& wave, uint8_t b)
87{
88 // one start bit
89 write0(wave);
90 // eight data bits
91 for (auto i : xrange(8)) {
92 if (b & (1 << i)) {
93 write1(wave);
94 } else {
95 write0(wave);
96 }
97 }
98 // two stop bits
99 write1(wave);
100 write1(wave);
101}
102
103// write data until a header is detected
104static bool writeData(std::vector<int8_t>& wave, std::span<const uint8_t> cas, size_t& pos)
105{
106 bool eof = false;
107 while ((pos + CAS_HEADER.size()) <= cas.size()) {
108 if (compare(&cas[pos], CAS_HEADER)) {
109 return eof;
110 }
111 writeByte(wave, cas[pos]);
112 if (cas[pos] == 0x1A) {
113 eof = true;
114 }
115 pos++;
116 }
117 while (pos < cas.size()) {
118 writeByte(wave, cas[pos++]);
119 }
120 return false;
121}
122
123static CasImage::Data convert(std::span<const uint8_t> cas, const std::string& filename, CliComm& cliComm,
124 CassetteImage::FileType& firstFileType)
125{
126 CasImage::Data data;
127 data.frequency = OUTPUT_FREQUENCY;
128 auto& wave = data.wave;
129
130 // search for a header in the .cas file
131 bool issueWarning = false;
132 bool headerFound = false;
133 bool firstFile = true;
134 size_t pos = 0;
135 while ((pos + CAS_HEADER.size()) <= cas.size()) {
136 if (compare(&cas[pos], CAS_HEADER)) {
137 // it probably works fine if a long header is used for every
138 // header but since the msx bios makes a distinction between
139 // them, we do also (hence a lot of code).
140 headerFound = true;
141 pos += CAS_HEADER.size();
142 writeSilence(wave, LONG_SILENCE);
143 writeHeader(wave, LONG_HEADER);
144 if ((pos + ASCII_HEADER.size()) <= cas.size()) {
145 // determine file type
146 auto type = [&] {
147 if (compare(&cas[pos], ASCII_HEADER)) {
149 } else if (compare(&cas[pos], BINARY_HEADER)) {
151 } else if (compare(&cas[pos], BASIC_HEADER)) {
153 } else {
155 }
156 }();
157 if (firstFile) firstFileType = type;
158 switch (type) {
160 writeData(wave, cas, pos);
161 do {
162 pos += CAS_HEADER.size();
163 writeSilence(wave, SHORT_SILENCE);
164 writeHeader(wave, SHORT_HEADER);
165 bool eof = writeData(wave, cas, pos);
166 if (eof) break;
167 } while ((pos + CAS_HEADER.size()) <= cas.size());
168 break;
171 writeData(wave, cas, pos);
172 writeSilence(wave, SHORT_SILENCE);
173 writeHeader(wave, SHORT_HEADER);
174 pos += CAS_HEADER.size();
175 writeData(wave, cas, pos);
176 break;
177 default:
178 // unknown file type: using long header
179 writeData(wave, cas, pos);
180 break;
181 }
182 } else {
183 // unknown file type: using long header
184 writeData(wave, cas, pos);
185 }
186 firstFile = false;
187 } else {
188 // skipping unhandled data, shouldn't occur in normal cas file
189 pos++;
190 issueWarning = true;
191 }
192 }
193 if (!headerFound) {
194 throw MSXException(filename, ": not a valid CAS image");
195 }
196 if (issueWarning) {
197 cliComm.printWarning("Skipped unhandled data in ", filename);
198 }
199
200 return data;
201}
202
203} // namespace MSX_CAS
204
205namespace SVI_CAS {
206
207static constexpr std::array<uint8_t, 17> header = {
208 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
209 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
210 0x7f,
211};
212
213static void writeBit(std::vector<int8_t>& wave, bool bit)
214{
215 size_t count = bit ? 1 : 2;
216 append(wave, count, 127);
217 append(wave, count, -127);
218}
219
220static void writeByte(std::vector<int8_t>& wave, uint8_t byte)
221{
222 for (int i = 7; i >= 0; --i) {
223 writeBit(wave, (byte >> i) & 1);
224 }
225}
226
227static void processBlock(std::span<const uint8_t> subBuf, std::vector<int8_t>& wave)
228{
229 writeSilence(wave, 1200);
230 writeBit(wave, true);
231 repeat(199, [&] { writeByte(wave, 0x55); });
232 writeByte(wave, 0x7f);
233 for (uint8_t val : subBuf) {
234 writeBit(wave, false);
235 writeByte(wave, val);
236 }
237}
238
239static CasImage::Data convert(std::span<const uint8_t> cas, CassetteImage::FileType& firstFileType)
240{
241 CasImage::Data data;
242 data.frequency = 4800;
243
244 if (cas.size() >= (header.size() + ASCII_HEADER.size())) {
245 if (compare(&cas[header.size()], ASCII_HEADER)) {
246 firstFileType = CassetteImage::ASCII;
247 } else if (compare(&cas[header.size()], BINARY_HEADER)) {
248 firstFileType = CassetteImage::BINARY;
249 } else if (compare(&cas[header.size()], BASIC_HEADER)) {
250 firstFileType = CassetteImage::BASIC;
251 }
252 }
253
254 auto prevHeader = cas.begin() + header.size();
255 while (true) {
256 auto nextHeader = std::search(prevHeader, cas.end(),
257 header.begin(), header.end());
258 // Workaround clang-13/libc++ bug
259 //processBlock(std::span(prevHeader, nextHeader), data.wave);
260 processBlock(std::span(&*prevHeader, nextHeader - prevHeader), data.wave);
261 if (nextHeader == cas.end()) break;
262 prevHeader = nextHeader + header.size();
263 }
264 return data;
265}
266
267} // namespace SVI_CAS
268
269CasImage::Data CasImage::init(const Filename& filename, FilePool& filePool, CliComm& cliComm)
270{
271 File file(filename);
272 auto cas = file.mmap();
273
274 auto fileType = CassetteImage::UNKNOWN;
275 auto result = [&] {
276 if ((cas.size() >= SVI_CAS::header.size()) &&
277 (compare(cas.data(), SVI_CAS::header))) {
278 return SVI_CAS::convert(cas, fileType);
279 } else {
280 return MSX_CAS::convert(cas, filename.getOriginal(), cliComm, fileType);
281 }
282 }();
283 setFirstFileType(fileType);
284
285 // conversion successful, now calc sha1sum
286 setSha1Sum(filePool.getSha1Sum(file));
287
288 return result;
289}
290
291CasImage::CasImage(const Filename& filename, FilePool& filePool, CliComm& cliComm)
292 : data(init(filename, filePool, cliComm))
293{
294}
295
296int16_t CasImage::getSampleAt(EmuTime::param time) const
297{
298 EmuDuration d = time - EmuTime::zero();
299 unsigned pos = d.getTicksAt(data.frequency);
300 return narrow<int16_t>((pos < data.wave.size()) ? (data.wave[pos] * 256) : 0);
301}
302
303EmuTime CasImage::getEndTime() const
304{
305 EmuDuration d = EmuDuration::hz(data.frequency) * data.wave.size();
306 return EmuTime::zero() + d;
307}
308
310{
311 return data.frequency * AUDIO_OVERSAMPLE;
312}
313
314void CasImage::fillBuffer(unsigned pos, std::span<float*, 1> bufs, unsigned num) const
315{
316 size_t nbSamples = data.wave.size();
317 if ((pos / AUDIO_OVERSAMPLE) < nbSamples) {
318 for (auto i : xrange(num)) {
319 bufs[0][i] = ((pos / AUDIO_OVERSAMPLE) < nbSamples)
320 ? narrow_cast<float>(data.wave[pos / AUDIO_OVERSAMPLE])
321 : 0.0f;
322 ++pos;
323 }
324 } else {
325 bufs[0] = nullptr;
326 }
327}
328
330{
331 return 1.0f / 128.0f;
332}
333
334} // namespace openmsx
int16_t getSampleAt(EmuTime::param time) const override
Definition: CasImage.cc:296
unsigned getFrequency() const override
Definition: CasImage.cc:309
CasImage(const Filename &fileName, FilePool &filePool, CliComm &cliComm)
Definition: CasImage.cc:291
void fillBuffer(unsigned pos, std::span< float *, 1 > bufs, unsigned num) const override
Definition: CasImage.cc:314
EmuTime getEndTime() const override
Definition: CasImage.cc:303
float getAmplificationFactorImpl() const override
Definition: CasImage.cc:329
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:47
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
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:147
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:343
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:148
constexpr auto xrange(T e)
Definition: xrange.hh:133