OXYGENHUB
◆ main openmsx / src / sound / Y8950Adpcm.cc
15.0 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
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
26
namespace openmsx {
27
28
// Bitmask for register 0x07
29
static constexpr int R07_RESET       = 0x01;
30
static constexpr int R07_SP_OFF      = 0x08;
31
static constexpr int R07_REPEAT      = 0x10;
32
static constexpr int R07_MEMORY_DATA = 0x20;
33
static constexpr int R07_REC         = 0x40;
34
static constexpr int R07_START       = 0x80;
35
static constexpr int R07_MODE        = 0xE0;
36
37
// Bitmask for register 0x08
38
static constexpr int R08_ROM         = 0x01;
39
static constexpr int R08_64K         = 0x02;
40
static constexpr int R08_DA_AD       = 0x04;
41
static constexpr int R08_SAMPL       = 0x08;
42
static constexpr int R08_NOTE_SET    = 0x40;
43
static constexpr int R08_CSM         = 0x80;
44
45
static constexpr int DIFF_MAX     = 0x6000;
46
static constexpr int DIFF_MIN     = 0x7F;
47
static constexpr int DIFF_DEFAULT = 0x7F;
48
49
static constexpr int STEP_BITS = 16;
50
static constexpr int STEP_MASK = (1 << STEP_BITS) -1;
51
52
53
Y8950Adpcm::Y8950Adpcm(Y8950& y8950_, const DeviceConfig& config,
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
63
void Y8950Adpcm::clearRam()
64
{
65
	ram.clear(0xFF);
66
}
67
68
void Y8950Adpcm::reset(EmuTime time)
69
{
70
	removeSyncPoint();
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
87
	y8950.setStatus(Y8950::STATUS_BUF_RDY);
88
}
89
90
bool Y8950Adpcm::isPlaying() const
91
{
92
	return (reg7 & 0xC0) == 0x80;
93
}
94
bool Y8950Adpcm::isMuted() const
95
{
96
	return !isPlaying() || (reg7 & R07_SP_OFF);
97
}
98
99
void 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
111
void Y8950Adpcm::sync(EmuTime 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
122
void 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
131
			Clock<Y8950::CLOCK_FREQ, Y8950::CLOCK_FREQ_DIV> stop(clock);
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
146
void Y8950Adpcm::executeUntil(EmuTime 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
156
void Y8950Adpcm::writeReg(uint8_t rg, uint8_t data, EmuTime 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) {
163
			y8950.setStatus(Y8950::STATUS_PCM_BSY);
164
		} else {
165
			y8950.resetStatus(Y8950::STATUS_PCM_BSY);
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
182
				y8950.setStatus(Y8950::STATUS_BUF_RDY);
183
			}
184
		} else {
185
			// access via CPU
186
			emu.memPtr = 0;
187
			aud.memPtr = 0;
188
		}
189
		removeSyncPoint();
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()) {
210
			removeSyncPoint();
211
			schedule();
212
		}
213
		break;
214
	case 0x0C: // STOP ADDRESS (H)
215
		stopAddr = (stopAddr & 0x007FF) | (data << 11);
216
		if (isPlaying()) {
217
			removeSyncPoint();
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()) {
230
			removeSyncPoint();
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()) {
238
			removeSyncPoint();
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
259
void Y8950Adpcm::writeData(uint8_t 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
275
			y8950.resetStatus(Y8950::STATUS_BUF_RDY);
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.
284
			y8950.setStatus(Y8950::STATUS_BUF_RDY);
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
290
				y8950.setStatus(Y8950::STATUS_EOS);
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
302
		y8950.resetStatus(Y8950::STATUS_BUF_RDY);
303
	}
304
}
305
306
uint8_t Y8950Adpcm::readReg(uint8_t rg, EmuTime time)
307
{
308
	sync(time); // TODO only when needed
309
	uint8_t result = (rg == 0x0F)
310
	            ? readData()   // ADPCM-DATA
311
	            : peekReg(rg); // other
312
	return result;
313
}
314
315
uint8_t Y8950Adpcm::peekReg(uint8_t rg, EmuTime time) const
316
{
317
	const_cast<Y8950Adpcm*>(this)->sync(time); // TODO only when needed
318
	return peekReg(rg);
319
}
320
321
uint8_t Y8950Adpcm::peekReg(uint8_t 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<uint8_t>((emu.output >>  8) & 0xFF);
331
	case 0x14:
332
		return narrow_cast<uint8_t>((emu.output >> 16) & 0xFF);
333
	default:
334
		return 255;
335
	}
336
}
337
338
void Y8950Adpcm::resetStatus()
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
359
		y8950.setStatus(Y8950::STATUS_BUF_RDY);
360
	}
361
}
362
363
uint8_t 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
	uint8_t 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;
378
			y8950.setStatus(Y8950::STATUS_BUF_RDY);
379
		} else if (emu.memPtr > stopAddr) {
380
			// set EOS bit in status register
381
			y8950.setStatus(Y8950::STATUS_EOS);
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
387
			y8950.resetStatus(Y8950::STATUS_BUF_RDY);
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
396
			y8950.setStatus(Y8950::STATUS_BUF_RDY);
397
		}
398
	}
399
	return result;
400
}
401
402
uint8_t 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
419
void Y8950Adpcm::writeMemory(unsigned memPtr, uint8_t value)
420
{
421
	unsigned addr = (memPtr / 2) & addrMask;
422
	if ((addr < ram.size()) && !romBank) {
423
		ram.write(addr, value);
424
	}
425
}
426
uint8_t 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
436
int Y8950Adpcm::calcSample()
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
444
int 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
		uint8_t 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) {
471
						y8950.setStatus(Y8950::STATUS_BUF_RDY);
472
					}
473
				}
474
				return uint8_t(pd.adpcm_data >> 4);
475
			} else {
476
				// odd nibble
477
				return uint8_t(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) {
501
				y8950.setStatus(Y8950::STATUS_EOS);
502
			}
503
			if (reg7 & R07_REPEAT) {
504
				restart(pd);
505
			} else {
506
				if (doEmu) {
507
					removeSyncPoint();
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.
525
template<typename Archive>
526
void 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()
560
		removeSyncPoint();
561
		if (isPlaying()) {
562
			schedule();
563
		}
564
	} else {
565
		ar.serialize("clock", clock);
566
	}
567
}
568
INSTANTIATE_SERIALIZE_METHODS(Y8950Adpcm);
569
570
} // namespace openmsx