OXYGENHUB
◆ rrdc-perf enhanced-x16-emulator / tests / test_vera_psg.c
12.5 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// VERA PSG: the sixteen-voice sound generator behind $9F40-$9F7F.
2
//
3
// ORACLE (strength B): the VERA Verilog, fpga/source/audio/psg.v.
4
//
5
//   repo:   https://github.com/X16Community/vera-module
6
//   tag:    v48.0.1   (commit 6e8bea68)
7
//
8
// R48 for this file, where the PCM tests use R47. psg.v is the only audio
9
// source that differs between the two tags, and the emulator implements the R48
10
// form: vera_psg.c:122-123 XOR the saw and triangle waveforms with the inverted
11
// pulse width, which entered the RTL as a8135f32 (2024-10-21) and shipped in
12
// v48.0.1. R47 has the unmodulated waveforms. The emulator backported it in
13
// #290 on 2024-08-11, two months before the RTL change landed, which is why it
14
// reports 47.0.2 while behaving like 48 here. See tests/check_vera_version.py.
15
//
16
// Every expectation quotes the RTL line it rests on, so a claim can be checked
17
// without fetching the repo. tests/fetch_vera_rtl.py retrieves the sources.
18
//
19
// vera_psg.c includes only its own header and libc, so this links it directly
20
// with no fixture and no SDL.
21
22
#include "support/harness.h"
23
24
#include "vera_psg.h"
25
26
#include <string.h>
27
28
// Registers are four bytes per voice: two of frequency, one of volume and
29
// panning, one of pulse width and waveform.
30
#define R_FREQL(ch) ((ch) * 4 + 0)
31
#define R_FREQH(ch) ((ch) * 4 + 1)
32
#define R_VOL(ch)   ((ch) * 4 + 2)
33
#define R_WAVE(ch)  ((ch) * 4 + 3)
34
35
static struct psg_debug_channel
36
chan(int ch)
37
{
38
	struct psg_debug_channel c;
39
	memset(&c, 0, sizeof c);
40
	psg_debug_get_channel(ch, &c);
41
	return c;
42
}
43
44
// Drive one voice with everything needed to make sound, so a test can vary the
45
// single field it cares about.
46
static void
47
voice(int ch, uint16_t freq, uint8_t vol, uint8_t waveform, uint8_t pw)
48
{
49
	psg_writereg(R_FREQL(ch), freq & 0xFF);
50
	psg_writereg(R_FREQH(ch), freq >> 8);
51
	psg_writereg(R_VOL(ch), 0xC0 | (vol & 0x3F));   // left and right enabled
52
	psg_writereg(R_WAVE(ch), (uint8_t)(waveform << 6) | (pw & 0x3F));
53
}
54
55
// psg.v:126   lfsr_r <= 16'd1;                         // on reset
56
static void
57
test_reset_seeds_the_noise_lfsr(void)
58
{
59
	psg_writereg(R_FREQL(0), 0x34);
60
	psg_writereg(R_VOL(0), 0x3F);
61
	psg_reset();
62
63
	check_eq(psg_debug_get_noise_state(), 1u,
64
	         "reset seeds the noise LFSR to 1");
65
	check_eq(chan(0).freq, 0u, "reset clears the frequency");
66
	check_eq(chan(0).vol_raw, 0u, "reset clears the volume");
67
	check_eq(chan(0).phase, 0u, "reset clears the phase accumulator");
68
	check_eq(psg_debug_get_reg(R_FREQL(0)), 0u, "reset clears the register shadow");
69
}
70
71
// psg.v:39-44   cur_freq       = cur_channel_attr_r[15:0];
72
//               cur_volume     = cur_channel_attr_r[21:16];
73
//               cur_left_en    = cur_channel_attr_r[22];
74
//               cur_right_en   = cur_channel_attr_r[23];
75
//               cur_pulsewidth = cur_channel_attr_r[29:24];
76
//               cur_waveform   = cur_channel_attr_r[31:30];
77
//
78
// Four attribute bytes per voice, so byte 2 carries volume in bits 5:0 with
79
// left on bit 6 and right on bit 7, and byte 3 carries pulse width in bits 5:0
80
// with the waveform in bits 7:6.
81
static void
82
test_register_decode(void)
83
{
84
	psg_reset();
85
86
	psg_writereg(R_FREQL(3), 0x34);
87
	psg_writereg(R_FREQH(3), 0x12);
88
	check_eq(chan(3).freq, 0x1234u, "the two frequency bytes form a 16-bit word");
89
90
	psg_writereg(R_VOL(3), 0x80 | 0x2A);
91
	check_eq(chan(3).vol_raw, 0x2Au, "volume is bits 5:0");
92
	check(chan(3).right, "bit 7 enables right");
93
	check(!chan(3).left, "bit 6 alone controls left");
94
95
	psg_writereg(R_VOL(3), 0x40 | 0x2A);
96
	check(chan(3).left, "bit 6 enables left");
97
	check(!chan(3).right, "and bit 7 alone controls right");
98
99
	psg_writereg(R_WAVE(3), 0x80 | 0x15);
100
	check_eq(chan(3).pw, 0x15u, "pulse width is bits 5:0");
101
	check_eq(chan(3).waveform, (uint32_t)PSG_WF_TRIANGLE, "waveform is bits 7:6");
102
}
103
104
// psg.v:26-28   dpram #(.ADDR_WIDTH(6) ...) audio_attr_ram(
105
//                   .rd_addr({cur_channel_r, cur_channel_byte_r[1:0]}),
106
//
107
// A 6-bit address: sixteen voices of four bytes, and nothing beyond. The
108
// emulator masks the register index to match, so a write past the end lands
109
// back at the start rather than running off the array.
110
static void
111
test_register_index_wraps_at_64(void)
112
{
113
	psg_reset();
114
	psg_writereg(0, 0x11);
115
	psg_writereg(64, 0x22);   // wraps onto register 0
116
	check_eq(chan(0).freq & 0xFF, 0x22u, "register 64 wraps onto register 0");
117
118
	psg_writereg(65, 0x33);
119
	check_eq(chan(0).freq >> 8, 0x33u, "register 65 wraps onto register 1");
120
121
	psg_writereg(255, 0x44);  // 255 & 0x3f == 63
122
	check_eq(chan(15).pw, 0x04u, "register 255 wraps onto register 63");
123
	check_eq(chan(15).waveform, (uint32_t)PSG_WF_SAWTOOTH, "and decodes there normally");
124
}
125
126
// psg.v:47-48   // cur_volume_log = cur_volume * 4                     ; cur_volume < 4
127
//               //                = 511 * exp2((cur_volume - 63) / 12) ; cur_volume >= 4
128
//
129
// Two regimes: linear below 4, then 0.5 dB per step. The joint at 4 is the
130
// interesting part, and the ends are fixed points.
131
static void
132
test_volume_curve(void)
133
{
134
	psg_reset();
135
136
	const struct { uint8_t raw; uint16_t want; } cases[] = {
137
		{0, 0}, {1, 4}, {2, 8}, {3, 12},   // linear regime
138
		{4, 16},                            // first logarithmic step
139
		{63, 511},                          // full scale
140
	};
141
	for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; i++) {
142
		psg_writereg(R_VOL(0), 0xC0 | cases[i].raw);
143
		check_eq(chan(0).vol_lut, cases[i].want, "volume maps through the curve");
144
	}
145
}
146
147
// psg.v:140   new_phase = (cur_left_en | cur_right_en) ? (cur_phase + cur_freq) : 17'd0;
148
//
149
// A silenced voice does not merely stop being heard, it is held at phase zero.
150
// So unmuting always restarts from the beginning of the waveform rather than
151
// wherever the voice would have been -- worth pinning because it is the sort of
152
// detail an implementation "tidies up" into a plain gate on the output.
153
static void
154
test_phase_is_held_at_zero_while_silent(void)
155
{
156
	psg_reset();
157
	int16_t buf[256];
158
159
	// 0x0700 over 64 samples does not land back on a multiple of the 17-bit
160
	// range, so "advanced" is distinguishable from "reset". 0x0800 would wrap
161
	// to exactly zero and the check would pass for the wrong reason.
162
	voice(0, 0x0700, 63, PSG_WF_SAWTOOTH, 0x20);
163
	psg_render(buf, 64);
164
	check(chan(0).phase != 0, "an audible voice advances its phase");
165
166
	// Clear both pan bits, keeping frequency and volume.
167
	psg_writereg(R_VOL(0), 0x3F);
168
	psg_render(buf, 64);
169
	check_eq(chan(0).phase, 0u, "a voice with neither side enabled resets to phase zero");
170
171
	psg_writereg(R_VOL(0), 0xC0 | 0x3F);
172
	psg_render(buf, 64);
173
	check(chan(0).phase != 0, "and runs again once a side is re-enabled");
174
}
175
176
// psg.v:124-130   always @(posedge clk ...) lfsr_r <=
177
//                     {lfsr_r[14:0], lfsr_r[1] ^ lfsr_r[2] ^ lfsr_r[4] ^ lfsr_r[15]};
178
//
179
// A 16-bit Fibonacci LFSR with taps at 1, 2, 4 and 15, shifting left, seeded to
180
// 1. The tap set is the part worth pinning: a wrong one still produces
181
// plausible noise, so only the exact sequence tells them apart.
182
//
183
// The rate is a modelling choice rather than something the RTL fixes at this
184
// granularity. In hardware the LFSR advances every system clock while the state
185
// machine walks the voices in turn (psg.v:180-188), which is why two voices
186
// latching in the same sample see different values. The emulator reproduces
187
// that by stepping it once per voice, so sixteen times per rendered sample
188
// (vera_psg.c:105-109). That is an approximation of a clock-rate process at
189
// sample resolution, and it is what this checks.
190
static void
191
test_noise_lfsr_sequence(void)
192
{
193
	psg_reset();
194
	check_eq(psg_debug_get_noise_state(), 1u, "seeded to 1");
195
196
	int16_t buf[8];
197
	uint16_t expect = psg_debug_get_noise_state();
198
	for (int sample = 0; sample < 8; sample++) {
199
		for (int voice_step = 0; voice_step < PSG_NUM_CHANNELS; voice_step++) {
200
			unsigned bit = ((expect >> 1) ^ (expect >> 2) ^ (expect >> 4) ^ (expect >> 15)) & 1;
201
			expect = (uint16_t)((expect << 1) | bit);
202
		}
203
		psg_render(buf, 1);
204
		check_eq(psg_debug_get_noise_state(), expect,
205
		         "the LFSR follows taps 1, 2, 4 and 15, once per voice");
206
	}
207
}
208
209
// psg.v:142-143   do_noise_sample = cur_phase[16] && !new_phase[16];
210
//                 new_noise = do_noise_sample ? noise_value_r : cur_noise;
211
// psg.v:123       noise_value_r = lfsr_r[6:1];
212
//
213
// A noise voice does not follow the LFSR sample by sample. It latches six bits
214
// of it only when the phase accumulator's top bit falls, so frequency controls
215
// the rate of new noise values while the LFSR runs on underneath.
216
static void
217
test_noise_is_latched_on_phase_wrap(void)
218
{
219
	psg_reset();
220
	int16_t buf[8];
221
222
	// With frequency 0x0800 the 17-bit phase advances 0x800 per sample, so bit
223
	// 16 rises at sample 32 and falls when the accumulator wraps at sample 64.
224
	// That is exactly one latch in the first 64 samples, at a known point.
225
	voice(0, 0x0800, 63, PSG_WF_NOISE, 0);
226
227
	unsigned changes = 0;
228
	int changed_at = -1;
229
	uint16_t prev = chan(0).noiseval;
230
231
	for (int sample = 1; sample <= 64; sample++) {
232
		uint16_t lfsr_before = psg_debug_get_noise_state();
233
		psg_render(buf, 1);
234
		check(psg_debug_get_noise_state() != lfsr_before,
235
		      "the LFSR advances on every sample");
236
237
		uint16_t now = chan(0).noiseval;
238
		if (now != prev) {
239
			changes++;
240
			changed_at = sample;
241
			prev = now;
242
		}
243
	}
244
245
	check_eq(changes, 1u, "the voice latches noise once per phase wrap");
246
	check_eq((uint32_t)changed_at, 64u, "and does so when the accumulator wraps");
247
	check(chan(0).noiseval <= 0x3F, "the latched value is six bits");
248
}
249
250
// psg.v:168-173   2'b00: signal = signal_pw;
251
//                 2'b01: signal = signal_saw;
252
//                 2'b10: signal = signal_triangle;
253
//                 2'b11: signal = signal_noise;
254
static void
255
test_waveform_codes(void)
256
{
257
	psg_reset();
258
	const uint8_t codes[] = {PSG_WF_PULSE, PSG_WF_SAWTOOTH, PSG_WF_TRIANGLE, PSG_WF_NOISE};
259
	for (unsigned i = 0; i < 4; i++) {
260
		psg_writereg(R_WAVE(1), (uint8_t)(codes[i] << 6));
261
		check_eq(chan(1).waveform, codes[i], "the waveform code round-trips");
262
	}
263
}
264
265
// Render a voice from reset and report whether anything came out differently.
266
static bool
267
renders_same(uint8_t waveform, uint8_t pw_a, uint8_t pw_b)
268
{
269
	int16_t a[256], b[256];
270
271
	psg_reset();
272
	voice(0, 0x0400, 63, waveform, pw_a);
273
	psg_render(a, 128);
274
275
	psg_reset();
276
	voice(0, 0x0400, 63, waveform, pw_b);
277
	psg_render(b, 128);
278
279
	return memcmp(a, b, sizeof a) == 0;
280
}
281
282
// psg.v:162-164   signal_pw       = (cur_phase[16:10] > {1'b0, cur_pulsewidth}) ? 6'd0 : 6'd63;
283
//                 signal_saw      = cur_phase[16:11] ^ ~cur_pulsewidth[5:0];
284
//                 signal_triangle = (cur_phase[16] ? ~cur_phase[15:10] : cur_phase[15:10])
285
//                                   ^ ~cur_pulsewidth[5:0];
286
//
287
// This is the R47/R48 divide, and the reason this file is pinned to R48. In R47
288
// saw and triangle read the phase directly and ignore the pulse width entirely;
289
// in R48 both are XORed with its complement, so the field shapes all three
290
// non-noise waveforms rather than only the pulse.
291
//
292
// Testing that the output *changes* with pulse width is what separates the two
293
// revisions, and it does so without depending on the exact sample values.
294
static void
295
test_pulse_width_shapes_saw_and_triangle(void)
296
{
297
	check(!renders_same(PSG_WF_PULSE, 0x10, 0x30),
298
	      "pulse width changes the pulse waveform");
299
300
	check(!renders_same(PSG_WF_SAWTOOTH, 0x00, 0x3F),
301
	      "pulse width changes the sawtooth (R48; R47 ignores it)");
302
303
	check(!renders_same(PSG_WF_TRIANGLE, 0x00, 0x3F),
304
	      "pulse width changes the triangle (R48; R47 ignores it)");
305
306
	// The complement is what is XORed in, so a full-scale pulse width
307
	// contributes nothing and leaves the plain waveform behind.
308
	check(renders_same(PSG_WF_SAWTOOTH, 0x3F, 0x3F),
309
	      "the same pulse width gives the same sawtooth");
310
}
311
312
// psg.v:175   signed_signal = signal ^ 6'h20;
313
//
314
// The waveform is generated unsigned and converted by flipping its top bit, so
315
// silence sits at the middle of the range rather than at zero. A voice at
316
// volume zero must still produce exactly nothing.
317
static void
318
test_silence_at_zero_volume(void)
319
{
320
	psg_reset();
321
	int16_t buf[128];
322
	memset(buf, 0x7F, sizeof buf);
323
324
	voice(0, 0x0400, 0, PSG_WF_SAWTOOTH, 0x20);
325
	psg_render(buf, 64);
326
327
	bool silent = true;
328
	for (unsigned i = 0; i < 128; i++) {
329
		if (buf[i] != 0) {
330
			silent = false;
331
		}
332
	}
333
	check(silent, "volume zero renders exact silence");
334
}
335
336
int
337
main(void)
338
{
339
	test_reset_seeds_the_noise_lfsr();
340
	test_register_decode();
341
	test_register_index_wraps_at_64();
342
	test_volume_curve();
343
	test_phase_is_held_at_zero_while_silent();
344
	test_noise_lfsr_sequence();
345
	test_noise_is_latched_on_phase_wrap();
346
	test_waveform_codes();
347
	test_pulse_width_shapes_saw_and_triangle();
348
	test_silence_at_zero_volume();
349
	return x16_test_summary("vera_psg");
350
}