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 "span.hh"
8 #include "stl.hh"
9 #include "xrange.hh"
10 #include <cstring> // for memcmp
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 template<typename Array>
44 static bool compare(const uint8_t* p, const Array& array)
45 {
46  return memcmp(p, array.data(), array.size()) == 0;
47 }
48 
49 namespace 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)
58 constexpr unsigned BAUDRATE = 3744;
59 constexpr unsigned OUTPUT_FREQUENCY = 4 * BAUDRATE; // 4 samples per bit
60 
61 // number of output bytes for silent parts
62 constexpr unsigned SHORT_SILENCE = OUTPUT_FREQUENCY * 1; // 1 second
63 constexpr unsigned LONG_SILENCE = OUTPUT_FREQUENCY * 2; // 2 seconds
64 
65 // number of 1-bits for headers
66 constexpr unsigned LONG_HEADER = 16000 / 2;
67 constexpr unsigned SHORT_HEADER = 4000 / 2;
68 
69 // headers definitions
70 constexpr std::array<uint8_t, 8> CAS_HEADER = { 0x1F,0xA6,0xDE,0xBA,0xCC,0x13,0x7D,0x74 };
71 
72 static void write0(std::vector<int8_t>& wave)
73 {
74  ::append(wave, {127, 127, -127, -127});
75 }
76 static void write1(std::vector<int8_t>& wave)
77 {
78  ::append(wave, {127, -127, 127, -127});
79 }
80 
81 static void writeHeader(std::vector<int8_t>& wave, unsigned s)
82 {
83  repeat(s, [&] { write1(wave); });
84 }
85 
86 static 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
104 static bool writeData(std::vector<int8_t>& wave, 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 
123 static CasImage::Data convert(span<const uint8_t> cas, const std::string& filename, CliComm& cliComm,
124  CassetteImage::FileType& firstFileType)
125 {
126  CasImage::Data data;
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)) {
148  return CassetteImage::ASCII;
149  } else if (compare(&cas[pos], BINARY_HEADER)) {
150  return CassetteImage::BINARY;
151  } else if (compare(&cas[pos], BASIC_HEADER)) {
152  return CassetteImage::BASIC;
153  } else {
154  return CassetteImage::UNKNOWN;
155  }
156  }();
157  if (firstFile) firstFileType = type;
158  switch (type) {
160  writeData(wave, cas, pos);
161  bool eof;
162  do {
163  pos += CAS_HEADER.size();
164  writeSilence(wave, SHORT_SILENCE);
165  writeHeader(wave, SHORT_HEADER);
166  eof = writeData(wave, cas, pos);
167  } while (!eof && ((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 
205 namespace SVI_CAS {
206 
207 static 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 
213 static 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 
220 static 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 
227 static void processBlock(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 
239 static CasImage::Data convert(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  processBlock(span(prevHeader, nextHeader), data.wave);
259  if (nextHeader == cas.end()) break;
260  prevHeader = nextHeader + header.size();
261  }
262  return data;
263 }
264 
265 } // namespace SVI_CAS
266 
267 CasImage::Data CasImage::init(const Filename& filename, FilePool& filePool, CliComm& cliComm)
268 {
269  File file(filename);
270  auto cas = file.mmap();
271 
272  auto fileType = CassetteImage::UNKNOWN;
273  auto result = [&] {
274  if ((cas.size() >= SVI_CAS::header.size()) &&
275  (compare(cas.data(), SVI_CAS::header))) {
276  return SVI_CAS::convert(cas, fileType);
277  } else {
278  return MSX_CAS::convert(cas, filename.getOriginal(), cliComm, fileType);
279  }
280  }();
281  setFirstFileType(fileType);
282 
283  // conversion successful, now calc sha1sum
284  setSha1Sum(filePool.getSha1Sum(file));
285 
286  return result;
287 }
288 
290  : data(init(filename, filePool, cliComm))
291 {
292 }
293 
294 int16_t CasImage::getSampleAt(EmuTime::param time) const
295 {
296  EmuDuration d = time - EmuTime::zero();
297  unsigned pos = d.getTicksAt(data.frequency);
298  return pos < data.wave.size() ? data.wave[pos] * 256 : 0;
299 }
300 
301 EmuTime CasImage::getEndTime() const
302 {
303  EmuDuration d = EmuDuration::hz(data.frequency) * data.wave.size();
304  return EmuTime::zero() + d;
305 }
306 
307 unsigned CasImage::getFrequency() const
308 {
309  return data.frequency * AUDIO_OVERSAMPLE;
310 }
311 
312 void CasImage::fillBuffer(unsigned pos, float** bufs, unsigned num) const
313 {
314  size_t nbSamples = data.wave.size();
315  if ((pos / AUDIO_OVERSAMPLE) < nbSamples) {
316  for (auto i : xrange(num)) {
317  bufs[0][i] = ((pos / AUDIO_OVERSAMPLE) < nbSamples)
318  ? data.wave[pos / AUDIO_OVERSAMPLE]
319  : 0.0f;
320  ++pos;
321  }
322  } else {
323  bufs[0] = nullptr;
324  }
325 }
326 
328 {
329  return 1.0f / 128;
330 }
331 
332 } // namespace openmsx
int16_t getSampleAt(EmuTime::param time) const override
Definition: CasImage.cc:294
unsigned getFrequency() const override
Definition: CasImage.cc:307
CasImage(const Filename &fileName, FilePool &filePool, CliComm &cliComm)
Definition: CasImage.cc:289
void fillBuffer(unsigned pos, float **bufs, unsigned num) const override
Definition: CasImage.cc:312
EmuTime getEndTime() const override
Definition: CasImage.cc:301
float getAmplificationFactorImpl() const override
Definition: CasImage.cc:327
void setSha1Sum(const Sha1Sum &sha1sum)
void setFirstFileType(FileType type)
static constexpr EmuDuration hz(unsigned x)
Definition: EmuDuration.hh:45
constexpr unsigned getTicksAt(unsigned freq) const
Definition: EmuDuration.hh:105
Sha1Sum getSha1Sum(File &file)
Calculate sha1sum for the given File object.
Definition: FilePool.cc:73
This class represents a filename.
Definition: Filename.hh:18
constexpr pointer data() const noexcept
Definition: span.hh:323
constexpr iterator begin() const noexcept
Definition: span.hh:326
constexpr index_type size() const noexcept
Definition: span.hh:296
constexpr iterator end() const noexcept
Definition: span.hh:327
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:207
constexpr unsigned BAUDRATE
Definition: CasImage.cc:58
constexpr unsigned SHORT_HEADER
Definition: CasImage.cc:67
constexpr unsigned SHORT_SILENCE
Definition: CasImage.cc:62
constexpr unsigned OUTPUT_FREQUENCY
Definition: CasImage.cc:59
constexpr unsigned LONG_SILENCE
Definition: CasImage.cc:63
constexpr std::array< uint8_t, 8 > CAS_HEADER
Definition: CasImage.cc:70
constexpr unsigned LONG_HEADER
Definition: CasImage.cc:66
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:170
constexpr auto xrange(T e)
Definition: xrange.hh:155