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