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