openMSX
Y8950Adpcm.cc
Go to the documentation of this file.
1 // The actual sample playing part is duplicated for the 'emu' domain and the
2 // 'audio' domain. The emu part is responsible for cycle accurate sample
3 // readback (see peekReg() register 0x13 and 0x14) and for cycle accurate
4 // status register updates (the status bits related to playback, e.g.
5 // end-of-sample). The audio part is responsible for the actual sound
6 // generation. This split up allows for the two parts to be out-of-sync. So for
7 // example when emulation is running faster or slower than 100% realtime speed,
8 // we both get cycle accurate emulation behaviour and still sound generation at
9 // 100% realtime speed (which is most of the time better for sound quality).
10 
11 #include "Y8950Adpcm.hh"
12 #include "Clock.hh"
13 #include "Ram.hh"
14 #include "DeviceConfig.hh"
15 #include "MSXMotherBoard.hh"
16 #include "Math.hh"
17 #include "serialize.hh"
18 #include "memory.hh"
19 #include <cstring>
20 
21 namespace openmsx {
22 
23 // Bitmask for register 0x07
24 static const int R07_RESET = 0x01;
25 static const int R07_SP_OFF = 0x08;
26 static const int R07_REPEAT = 0x10;
27 static const int R07_MEMORY_DATA = 0x20;
28 static const int R07_REC = 0x40;
29 static const int R07_START = 0x80;
30 static const int R07_MODE = 0xE0;
31 
32 // Bitmask for register 0x08
33 static const int R08_ROM = 0x01;
34 static const int R08_64K = 0x02;
35 static const int R08_DA_AD = 0x04;
36 static const int R08_SAMPL = 0x08;
37 static const int R08_NOTE_SET = 0x40;
38 static const int R08_CSM = 0x80;
39 
40 static const int DMAX = 0x6000;
41 static const int DMIN = 0x7F;
42 static const int DDEF = 0x7F;
43 
44 static const int STEP_BITS = 16;
45 static const int STEP_MASK = (1 << STEP_BITS) -1;
46 
47 
48 Y8950Adpcm::Y8950Adpcm(Y8950& y8950_, const DeviceConfig& config,
49  const std::string& name, unsigned sampleRam)
50  : Schedulable(config.getScheduler())
51  , y8950(y8950_)
52  , ram(make_unique<Ram>(
53  config, name + " RAM", "Y8950 sample RAM", sampleRam))
54  , clock(config.getMotherBoard().getCurrentTime())
55  , volume(0)
56 {
57  clearRam();
58 }
59 
61 {
62 }
63 
65 {
66  memset(&(*ram)[0], 0xFF, ram->getSize());
67 }
68 
70 {
72 
73  clock.reset(time);
74 
75  startAddr = 0;
76  stopAddr = 7;
77  delta = 0;
78  addrMask = (1 << 18) - 1;
79  reg7 = 0;
80  reg15 = 0;
81  readDelay = 0;
82  romBank = false;
83  writeReg(0x12, 255, time); // volume
84 
85  restart(emu);
86  restart(aud);
87 
89 }
90 
91 bool Y8950Adpcm::isPlaying() const
92 {
93  return (reg7 & 0xC0) == 0x80;
94 }
95 bool Y8950Adpcm::isMuted() const
96 {
97  return !isPlaying() || (reg7 & R07_SP_OFF);
98 }
99 
100 void Y8950Adpcm::restart(PlayData& pd)
101 {
102  pd.memPntr = startAddr;
103  pd.nowStep = (1 << STEP_BITS) - delta;
104  pd.out = 0;
105  pd.output = 0;
106  pd.diff = DDEF;
107  pd.nextLeveling = 0;
108  pd.sampleStep = 0;
109  pd.adpcm_data = 0; // dummy, avoid UMR in serialize
110 }
111 
113 {
114  if (isPlaying()) { // optimization, also correct without this test
115  unsigned ticks = clock.getTicksTill(time);
116  for (unsigned i = 0; isPlaying() && (i < ticks); ++i) {
117  calcSample(true); // ignore result
118  }
119  }
120  clock.advance(time);
121 }
122 
123 void Y8950Adpcm::schedule()
124 {
125  assert(isPlaying());
126  if ((stopAddr > startAddr) && (delta != 0)) {
127  // TODO possible optimization, no need to set sync points if
128  // the corresponding bit is masked in the interupt enable
129  // register
130  if (reg7 & R07_MEMORY_DATA) {
131  // we already did a sync(time), so clock is up-to-date
133  uint64_t samples = stopAddr - emu.memPntr + 1;
134  uint64_t length = (samples << STEP_BITS) +
135  ((1 << STEP_BITS) - emu.nowStep) +
136  (delta - 1);
137  stop += unsigned(length / delta);
138  setSyncPoint(stop.getTime());
139  } else {
140  // TODO we should also set a syncpoint in this case
141  // because this mode sets the STATUS_BUF_RDY bit
142  // which also triggers an IRQ
143  }
144  }
145 }
146 
147 void Y8950Adpcm::executeUntil(EmuTime::param time, int /*userData*/)
148 {
149  assert(isPlaying());
150  sync(time); // should set STATUS_EOS
151  assert(y8950.peekRawStatus() & Y8950::STATUS_EOS);
152  if (isPlaying() && (reg7 & R07_REPEAT)) {
153  schedule();
154  }
155 }
156 
158 {
159  sync(time); // TODO only when needed
160  switch (rg) {
161  case 0x07: // START/REC/MEM DATA/REPEAT/SP-OFF/-/-/RESET
162  reg7 = data;
163  if (reg7 & R07_RESET) {
164  reg7 = 0;
165  }
166  if (reg7 & R07_START) {
167  // start ADPCM
168  restart(emu);
169  restart(aud);
170  }
171  if (reg7 & R07_MEMORY_DATA) {
172  // access external memory?
173  emu.memPntr = startAddr;
174  aud.memPntr = startAddr;
175  readDelay = 2; // two dummy reads
176  if ((reg7 & 0xA0) == 0x20) {
177  // Memory read or write
179  }
180  } else {
181  // access via CPU
182  emu.memPntr = 0;
183  aud.memPntr = 0;
184  }
185  removeSyncPoint();
186  if (isPlaying()) {
187  schedule();
188  }
189  break;
190 
191  case 0x08: // CSM/KEY BOARD SPLIT/-/-/SAMPLE/DA AD/64K/ROM
192  romBank = data & R08_ROM;
193  addrMask = data & R08_64K ? (1 << 16) - 1 : (1 << 18) - 1;
194  break;
195 
196  case 0x09: // START ADDRESS (L)
197  startAddr = (startAddr & 0x7F807) | (data << 3);
198  break;
199  case 0x0A: // START ADDRESS (H)
200  startAddr = (startAddr & 0x007FF) | (data << 11);
201  break;
202 
203  case 0x0B: // STOP ADDRESS (L)
204  stopAddr = (stopAddr & 0x7F807) | (data << 3);
205  if (isPlaying()) {
206  removeSyncPoint();
207  schedule();
208  }
209  break;
210  case 0x0C: // STOP ADDRESS (H)
211  stopAddr = (stopAddr & 0x007FF) | (data << 11);
212  if (isPlaying()) {
213  removeSyncPoint();
214  schedule();
215  }
216  break;
217 
218  case 0x0F: // ADPCM-DATA
219  writeData(data);
220  break;
221 
222  case 0x10: // DELTA-N (L)
223  delta = (delta & 0xFF00) | data;
224  volumeWStep = (volume * delta) >> STEP_BITS;
225  if (isPlaying()) {
226  removeSyncPoint();
227  schedule();
228  }
229  break;
230  case 0x11: // DELTA-N (H)
231  delta = (delta & 0x00FF) | (data << 8);
232  volumeWStep = (volume * delta) >> STEP_BITS;
233  if (isPlaying()) {
234  removeSyncPoint();
235  schedule();
236  }
237  break;
238 
239  case 0x12: { // ENVELOP CONTROL
240  volume = data;
241  volumeWStep = (volume * delta) >> STEP_BITS;
242  break;
243  }
244  case 0x0D: // PRESCALE (L)
245  case 0x0E: // PRESCALE (H)
246  case 0x15: // DAC-DATA (bit9-2)
247  case 0x16: // (bit1-0)
248  case 0x17: // (exponent)
249  case 0x1A: // PCM-DATA
250  // not implemented
251  break;
252  }
253 }
254 
255 void Y8950Adpcm::writeData(byte data)
256 {
257  reg15 = data;
258  if ((reg7 & R07_MODE) == 0x60) {
259  // external memory write
260  assert(!isPlaying()); // no need to update the 'aud' data
261  if (readDelay) {
262  emu.memPntr = startAddr;
263  readDelay = 0;
264  }
265  if (emu.memPntr <= stopAddr) {
266  writeMemory(emu.memPntr, data);
267  emu.memPntr += 2; // two nibbles at a time
268 
269  // reset BRDY bit in status register,
270  // which means we are processing the write
272 
273  // setup a timer that will callback us in 10
274  // master clock cycles for Y8950. In the
275  // callback set the BRDY flag to 1 , which
276  // means we have written the data. For now, we
277  // don't really do this; we simply reset and
278  // set the flag in zero time, so that the IRQ
279  // will work.
280 
281  // set BRDY bit in status register
283  } else {
284  // set EOS bit in status register
286  }
287 
288  } else if ((reg7 & R07_MODE) == 0x80) {
289  // ADPCM synthesis from CPU
290 
291  // Reset BRDY bit in status register, which means we
292  // are full of data
294  }
295 }
296 
298 {
299  sync(time); // TODO only when needed
300  byte result = (rg == 0x0F)
301  ? readData() // ADPCM-DATA
302  : peekReg(rg); // other
303  return result;
304 }
305 
307 {
308  sync(time); // TODO only when needed
309  return peekReg(rg);
310 }
311 
312 byte Y8950Adpcm::peekReg(byte rg) const
313 {
314  switch (rg) {
315  case 0x0F: // ADPCM-DATA
316  return peekData();
317  case 0x13:
318  // TODO check: is this before or after
319  // volume is applied
320  // filtering is performed
321  return (emu.output >> 8) & 0xFF;
322  case 0x14:
323  return emu.output >> 16;
324  default:
325  return 255;
326  }
327 }
328 
330 {
331  // If the BUF_RDY mask is cleared (e.g. by writing the value 0x80 to
332  // register R#4). Reading the status register still has the BUF_RDY
333  // bit set. Without this behavior demos like 'NOP Unknown reality'
334  // hang when testing the amount of sample ram or when uploading data
335  // to the sample ram.
336  //
337  // Before this code was added, those demos also worked but only
338  // because we had a hack that always kept bit BUF_RDY set.
339  //
340  // When the ADPCM unit is not performing any function (e.g. after a
341  // reset), the BUF_RDY bit should still be set. The AUDIO detection
342  // routine in 'MSX-Audio BIOS v1.3' depends on this. See
343  // [3533002] Y8950 not being detected by MSX-Audio v1.3
344  // https://sourceforge.net/tracker/?func=detail&aid=3533002&group_id=38274&atid=421861
345  // TODO I've implemented this as '(reg7 & R07_MODE) == 0', is this
346  // correct/complete?
347  if (((reg7 & R07_MODE & ~R07_REC) == R07_MEMORY_DATA) ||
348  ((reg7 & R07_MODE) == 0)){
349  // transfer to or from sample ram, or no function
351  }
352 }
353 
354 byte Y8950Adpcm::readData()
355 {
356  if ((reg7 & R07_MODE) == R07_MEMORY_DATA) {
357  // external memory read
358  assert(!isPlaying()); // no need to update the 'aud' data
359  if (readDelay) {
360  emu.memPntr = startAddr;
361  }
362  }
363  byte result = peekData();
364  if ((reg7 & R07_MODE) == R07_MEMORY_DATA) {
365  assert(!isPlaying()); // no need to update the 'aud' data
366  if (readDelay) {
367  // two dummy reads
368  --readDelay;
370  } else if (emu.memPntr > stopAddr) {
371  // set EOS bit in status register
373  } else {
374  emu.memPntr += 2; // two nibbles at a time
375 
376  // reset BRDY bit in status register, which means we
377  // are reading the memory now
379 
380  // setup a timer that will callback us in 10 master
381  // clock cycles for Y8950. In the callback set the BRDY
382  // flag to 1, which means we have another data ready.
383  // For now, we don't really do this; we simply reset and
384  // set the flag in zero time, so that the IRQ will work.
385 
386  // set BRDY bit in status register
388  }
389  }
390  return result;
391 }
392 
393 byte Y8950Adpcm::peekData() const
394 {
395  if ((reg7 & R07_MODE) == R07_MEMORY_DATA) {
396  // external memory read
397  assert(!isPlaying()); // no need to update the 'aud' data
398  if (readDelay) {
399  return reg15;
400  } else if (emu.memPntr > stopAddr) {
401  return 0;
402  } else {
403  return readMemory(emu.memPntr);
404  }
405  } else {
406  return 0; // TODO check
407  }
408 }
409 
410 void Y8950Adpcm::writeMemory(unsigned memPntr, byte value)
411 {
412  unsigned addr = (memPntr / 2) & addrMask;
413  if ((addr < ram->getSize()) && !romBank) {
414  (*ram)[addr] = value;
415  }
416 }
417 byte Y8950Adpcm::readMemory(unsigned memPntr) const
418 {
419  unsigned addr = (memPntr / 2) & addrMask;
420  if (romBank || (addr >= ram->getSize())) {
421  return 0; // checked on a real machine
422  } else {
423  return (*ram)[addr];
424  }
425 }
426 
428 {
429  // called by audio thread
430  if (!isPlaying()) return 0;
431  int output = calcSample(false);
432  return (reg7 & R07_SP_OFF) ? 0 : output;
433 }
434 
435 int Y8950Adpcm::calcSample(bool doEmu)
436 {
437  // values taken from ymdelta.c by Tatsuyuki Satoh.
438  static const int F1[16] = { 1, 3, 5, 7, 9, 11, 13, 15,
439  -1, -3, -5, -7, -9, -11, -13, -15 };
440  static const int F2[16] = { 57, 57, 57, 57, 77, 102, 128, 153,
441  57, 57, 57, 57, 77, 102, 128, 153 };
442 
443  assert(isPlaying());
444 
445  PlayData& pd = doEmu ? emu : aud;
446  pd.nowStep += delta;
447  if (pd.nowStep & ~STEP_MASK) {
448  pd.nowStep &= STEP_MASK;
449  byte val;
450  if (!(pd.memPntr & 1)) {
451  // even nibble
452  if (reg7 & R07_MEMORY_DATA) {
453  pd.adpcm_data = readMemory(pd.memPntr);
454  } else {
455  pd.adpcm_data = reg15;
456  // set BRDY bit, ready to accept new data
457  if (doEmu) {
459  }
460  }
461  val = pd.adpcm_data >> 4;
462  } else {
463  // odd nibble
464  val = pd.adpcm_data & 0x0F;
465  }
466  int prevOut = pd.out;
467  pd.out = Math::clipIntToShort(pd.out + (pd.diff * F1[val]) / 8);
468  pd.diff = Math::clip<DMIN, DMAX>((pd.diff * F2[val]) / 64);
469 
470  int prevLeveling = pd.nextLeveling;
471  pd.nextLeveling = (prevOut + pd.out) / 2;
472  int deltaLeveling = pd.nextLeveling - prevLeveling;
473  pd.sampleStep = deltaLeveling * volumeWStep;
474  int tmp = deltaLeveling * ((volume * pd.nowStep) >> STEP_BITS);
475  pd.output = prevLeveling * volume + tmp;
476 
477  ++pd.memPntr;
478  if ((reg7 & R07_MEMORY_DATA) &&
479  (pd.memPntr > stopAddr)) {
480  // On 2003/06/21 I commited a patch with comment:
481  // generate end-of-sample interrupt at every sample
482  // end, including loops
483  // Unfortunatly it doesn't give any reason why and now
484  // I can't remember it :-(
485  // This is different from e.g. the MAME implementation.
486  if (doEmu) {
488  }
489  if (reg7 & R07_REPEAT) {
490  restart(pd);
491  } else {
492  if (doEmu) {
493  removeSyncPoint();
494  reg7 = 0;
495  }
496  }
497  }
498  } else {
499  pd.output += pd.sampleStep;
500  }
501  return pd.output >> 12;
502 }
503 
504 
505 // version 1:
506 // Initial verson
507 // version 2:
508 // - Split PlayData in emu and audio part (though this doesn't add new state
509 // to the savestate).
510 // - Added clock object.
511 template<typename Archive>
512 void Y8950Adpcm::serialize(Archive& ar, unsigned version)
513 {
514  ar.template serializeBase<Schedulable>(*this);
515  ar.serialize("ram", *ram);
516  ar.serialize("startAddr", startAddr);
517  ar.serialize("stopAddr", stopAddr);
518  ar.serialize("addrMask", addrMask);
519  ar.serialize("volume", volume);
520  ar.serialize("volumeWStep", volumeWStep);
521  ar.serialize("readDelay", readDelay);
522  ar.serialize("delta", delta);
523  ar.serialize("reg7", reg7);
524  ar.serialize("reg15", reg15);
525  ar.serialize("romBank", romBank);
526 
527  ar.serialize("memPntr", emu.memPntr);
528  ar.serialize("nowStep", emu.nowStep);
529  ar.serialize("out", emu.out);
530  ar.serialize("output", emu.output);
531  ar.serialize("diff", emu.diff);
532  ar.serialize("nextLeveling", emu.nextLeveling);
533  ar.serialize("sampleStep", emu.sampleStep);
534  ar.serialize("adpcm_data", emu.adpcm_data);
535  if (ar.isLoader()) {
536  // ignore aud part for saving,
537  // for loading we make it the same as the emu part
538  aud = emu;
539  }
540 
541  if (ar.versionBelow(version, 2)) {
542  clock.reset(getCurrentTime());
543 
544  // reschedule, because automatically deserialized sync-point
545  // can be off, because clock.getTime() != getCurrentTime()
546  removeSyncPoint();
547  if (isPlaying()) {
548  schedule();
549  }
550  } else {
551  ar.serialize("clock", clock);
552  }
553 }
555 
556 } // namespace openmsx