openMSX
CasImage.cc
Go to the documentation of this file.
1 #include "CasImage.hh"
2 #include "File.hh"
3 #include "Filename.hh"
4 #include "CliComm.hh"
5 #include "Clock.hh"
6 #include "MSXException.hh"
7 #include "xrange.hh"
8 #include <cstring> // for memcmp
9 
10 namespace openmsx {
11 
12 // output settings
13 
14 // a higher baudrate doesn't work anymore, but it is unclear why, because 4600
15 // baud should work (known from Speedsave 4000 and Turbo 5000 programs).
16 // 3765 still works on a Toshiba HX-10 and Philips NMS 8250, but not on a
17 // Panasonic FS-A1WSX, on which 3763 is the max. National CF-2000 has 3762 as
18 // the max. Let's take 3760 then as a safe value.
19 // UPDATE: that seems to break RUN"CAS:" type of programs. 3744 seems to work
20 // for those as well (we don't understand why yet)
21 static const unsigned BAUDRATE = 3744;
22 static const unsigned OUTPUT_FREQUENCY = 4 * BAUDRATE; // 4 samples per bit
23 // We oversample the audio signal for better sound quality (especially in
24 // combination with the hq resampler). Without oversampling the audio output
25 // could contain portions like this:
26 // -1, +1, -1, +1, -1, +1, ...
27 // So it contains a signal at the Nyquist frequency. The hq resampler contains
28 // a low-pass filter, and (all practically implementable) filters cut off a
29 // portion of the spectrum near the Nyquist freq. So this high freq signal was
30 // lost after the hq resampler. After oversampling, the signal looks like this:
31 // -1, -1, -1, -1, +1, +1, +1, +1, -1, -1, -1, -1, ...
32 // So every sample repeated 4 times.
33 static const unsigned AUDIO_OVERSAMPLE = 4;
34 
35 // number of output bytes for silent parts
36 static const unsigned SHORT_SILENCE = OUTPUT_FREQUENCY * 1; // 1 second
37 static const unsigned LONG_SILENCE = OUTPUT_FREQUENCY * 2; // 2 seconds
38 
39 // number of 1-bits for headers
40 static const unsigned LONG_HEADER = 16000 / 2;
41 static const unsigned SHORT_HEADER = 4000 / 2;
42 
43 // headers definitions
44 static const byte CAS_HEADER [ 8] = { 0x1F,0xA6,0xDE,0xBA,0xCC,0x13,0x7D,0x74 };
45 static const byte ASCII_HEADER [10] = { 0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA,0xEA };
46 static const byte BINARY_HEADER[10] = { 0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0 };
47 static const byte BASIC_HEADER [10] = { 0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3,0xD3 };
48 
49 
50 CasImage::CasImage(const Filename& filename, FilePool& filePool, CliComm& cliComm)
51 {
53  convert(filename, filePool, cliComm);
54 }
55 
57 {
58  static const Clock<OUTPUT_FREQUENCY> zero(EmuTime::zero);
59  unsigned pos = zero.getTicksTill(time);
60  return pos < output.size() ? output[pos] * 256 : 0;
61 }
62 
64 {
66  clk += unsigned(output.size());
67  return clk.getTime();
68 }
69 
70 unsigned CasImage::getFrequency() const
71 {
72  return OUTPUT_FREQUENCY * AUDIO_OVERSAMPLE;
73 }
74 
75 void CasImage::fillBuffer(unsigned pos, int** bufs, unsigned num) const
76 {
77  size_t nbSamples = output.size();
78  if ((pos / AUDIO_OVERSAMPLE) < nbSamples) {
79  for (auto i : xrange(num)) {
80  bufs[0][i] = ((pos / AUDIO_OVERSAMPLE) < nbSamples)
81  ? output[pos / AUDIO_OVERSAMPLE] * 256
82  : 0;
83  ++pos;
84  }
85  } else {
86  bufs[0] = nullptr;
87  }
88 }
89 
90 void CasImage::write0()
91 {
92  output.insert(end(output), { 127, 127, -127, -127 } );
93 }
94 void CasImage::write1()
95 {
96  output.insert(end(output), { 127, -127, 127, -127 } );
97 }
98 
99 // write a header signal
100 void CasImage::writeHeader(int s)
101 {
102  for (int i = 0; i < s; ++i) {
103  write1();
104  }
105 }
106 
107 // write silence
108 void CasImage::writeSilence(int s)
109 {
110  output.insert(end(output), s, 0);
111 }
112 
113 // write a byte
114 void CasImage::writeByte(byte b)
115 {
116  // one start bit
117  write0();
118  // eight data bits
119  for (auto i : xrange(8)) {
120  if (b & (1 << i)) {
121  write1();
122  } else {
123  write0();
124  }
125  }
126  // two stop bits
127  write1();
128  write1();
129 }
130 
131 // write data until a header is detected
132 bool CasImage::writeData(const byte* buf, size_t size, size_t& pos)
133 {
134  bool eof = false;
135  while ((pos + 8) <= size) {
136  if (!memcmp(&buf[pos], CAS_HEADER, 8)) {
137  return eof;
138  }
139  writeByte(buf[pos]);
140  if (buf[pos] == 0x1A) {
141  eof = true;
142  }
143  pos++;
144  }
145  while (pos < size) {
146  writeByte(buf[pos++]);
147  }
148  return false;
149 }
150 
151 void CasImage::convert(const Filename& filename, FilePool& filePool, CliComm& cliComm)
152 {
153  File file(filename);
154  size_t size;
155  const byte* buf = file.mmap(size);
156 
157  // search for a header in the .cas file
158  bool issueWarning = false;
159  bool headerFound = false;
160  bool firstFile = true;
161  size_t pos = 0;
162  while ((pos + 8) <= size) {
163  if (!memcmp(&buf[pos], CAS_HEADER, 8)) {
164  // it probably works fine if a long header is used for every
165  // header but since the msx bios makes a distinction between
166  // them, we do also (hence a lot of code).
167  headerFound = true;
168  pos += 8;
169  writeSilence(LONG_SILENCE);
170  writeHeader(LONG_HEADER);
171  if ((pos + 10) <= size) {
172  // determine file type
174  if (!memcmp(&buf[pos], ASCII_HEADER, 10)) {
175  type = CassetteImage::ASCII;
176  } else if (!memcmp(&buf[pos], BINARY_HEADER, 10)) {
177  type = CassetteImage::BINARY;
178  } else if (!memcmp(&buf[pos], BASIC_HEADER, 10)) {
179  type = CassetteImage::BASIC;
180  }
181  if (firstFile) setFirstFileType(type);
182  switch (type) {
184  writeData(buf, size, pos);
185  bool eof;
186  do {
187  pos += 8;
188  writeSilence(SHORT_SILENCE);
189  writeHeader(SHORT_HEADER);
190  eof = writeData(buf, size, pos);
191  } while (!eof && ((pos + 8) <= size));
192  break;
195  writeData(buf, size, pos);
196  writeSilence(SHORT_SILENCE);
197  writeHeader(SHORT_HEADER);
198  pos += 8;
199  writeData(buf, size, pos);
200  break;
201  default:
202  // unknown file type: using long header
203  writeData(buf, size, pos);
204  break;
205  }
206  } else {
207  // unknown file type: using long header
208  writeData(buf, size, pos);
209  }
210  firstFile = false;
211  } else {
212  // skipping unhandled data, shouldn't occur in normal cas file
213  pos++;
214  issueWarning = true;
215  }
216  }
217  if (!headerFound) {
218  throw MSXException(filename.getOriginal() +
219  ": not a valid CAS image");
220  }
221  if (issueWarning) {
222  cliComm.printWarning("Skipped unhandled data in " +
223  filename.getOriginal());
224  }
225 
226  // conversion successful, now calc sha1sum
227  file.setFilePool(filePool);
228  setSha1Sum(file.getSha1Sum());
229 }
230 
231 } // namespace openmsx
string_ref::const_iterator end(const string_ref &x)
Definition: string_ref.hh:135
short getSampleAt(EmuTime::param time) override
Definition: CasImage.cc:56
unsigned char byte
8 bit unsigned integer
Definition: openmsx.hh:27
Represents a clock with a fixed frequency.
Definition: Clock.hh:18
void setSha1Sum(const Sha1Sum &sha1sum)
This class represents a filename.
Definition: Filename.hh:17
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:7
void fillBuffer(unsigned pos, int **bufs, unsigned num) const override
Definition: CasImage.cc:75
static const EmuTime zero
Definition: EmuTime.hh:57
void setFirstFileType(FileType type)
size_t size(string_ref utf8)
EmuTime getEndTime() const override
Definition: CasImage.cc:63
unsigned getFrequency() const override
Definition: CasImage.cc:70
CasImage(const Filename &fileName, FilePool &filePool, CliComm &cliComm)
Definition: CasImage.cc:50
XRange< T > xrange(T e)
Definition: xrange.hh:98