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