1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
|
/*
* PSn00bSDK SPU CD-ROM streaming example
* (C) 2022 spicyjpeg - MPL licensed
*
* This is an extended version of the sound/spustream example demonstrating
* playback of a large multi-channel audio file from the CD using the SPU,
* without having to rely on the CD drive's own ability to play CD-DA or XA
* tracks.
*
* The main difference from spustream is that the SPU IRQ handler does not
* upload a chunk from main RAM to SPU RAM immediately, it only sets a flag.
* The main loop checks if the flag has been set and starts reading the next
* chunk from the CD into a buffer in RAM asynchronously; the chunk is then
* uploaded to the SPU and the IRQ is re-enabled.
*
* Chunks are read once again from an interleaved .VAG file, laid out on the
* disc as follows:
*
* +--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+----
* | | +--------------------+---------------------+ |
* | .VAG | | Left channel data | Right channel data | Padding | ...
* | header | +--------------------+---------------------+ |
* +----------+----------+----------+----------+----------+----------+----
* \__________________Chunk___________________/
*
* Note that chunks have to be large enough to give the drive enough time to
* seek from one chunk to another. The included .VAG file has been encoded with
* a chunk size of 0x7000 bytes, however you might want to try smaller sizes to
* reduce SPU RAM usage. Chunk size can be set by passing the -b option to the
* .VAG interleaving script included in the spustream directory.
*
* Implementing SPU streaming might seem pointless, but it actually has a
* number of advantages over CD-DA or XA:
*
* - Any sample rate up to 44.1 kHz can be used. The sample rate can also be
* changed on-the-fly to play the stream at different speeds and pitches (as
* long as the CD drive can keep up), or even interpolated for effects like
* tape stops.
* - Manual streaming is not limited to mono or stereo but can be expanded to
* as many channels as needed, only limited by the amount of SPU RAM required
* for chunks and CD bandwidth. Having more than 2 channels can be useful for
* e.g. smoothly crossfading between tracks (not possible with XA) or
* controlling volume and panning of each instrument separately.
* - XA playback tends to skip on consoles with a worn out drive, as XA sectors
* cannot have any error correction data. SPU streaming is not subject to
* this limitation since sectors are read and processed in software.
* - Depending on how streaming/interleaving is implemented it is possible to
* have 500-1000ms idle periods during which the CD drive isn't buffering the
* stream, that can be used to read small amounts of other data without ever
* interrupting playback. This is different from XA-style interleaving as the
* drive is free to seek to *any* region of the disc during these periods (it
* must seek back to the stream's next chunk afterwards though).
* - It is also possible to seek back to the beginning of the stream and load
* the first chunk before the end is reached, allowing for seamless looping
* without having to resort to tricks like separate filler samples.
* - Finally, SPU streaming can be used on some PS1-based arcade boards that
* use IDE/SCSI drives or flash memory for storage and thus lack support for
* XA or CD-DA playback.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <psxetc.h>
#include <psxapi.h>
#include <psxgpu.h>
#include <psxpad.h>
#include <psxspu.h>
#include <psxcd.h>
#include <hwregs_c.h>
extern const uint8_t stream_data[];
#define NUM_CHANNELS 2
/* Display/GPU context utilities */
#define SCREEN_XRES 320
#define SCREEN_YRES 240
#define BGCOLOR_R 48
#define BGCOLOR_G 24
#define BGCOLOR_B 0
typedef struct {
DISPENV disp;
DRAWENV draw;
} Framebuffer;
typedef struct {
Framebuffer db[2];
int db_active;
} RenderContext;
void init_context(RenderContext *ctx) {
Framebuffer *db;
ResetGraph(0);
ctx->db_active = 0;
db = &(ctx->db[0]);
SetDefDispEnv(&(db->disp), 0, 0, SCREEN_XRES, SCREEN_YRES);
SetDefDrawEnv(&(db->draw), SCREEN_XRES, 0, SCREEN_XRES, SCREEN_YRES);
setRGB0(&(db->draw), BGCOLOR_R, BGCOLOR_G, BGCOLOR_B);
db->draw.isbg = 1;
db->draw.dtd = 1;
db = &(ctx->db[1]);
SetDefDispEnv(&(db->disp), SCREEN_XRES, 0, SCREEN_XRES, SCREEN_YRES);
SetDefDrawEnv(&(db->draw), 0, 0, SCREEN_XRES, SCREEN_YRES);
setRGB0(&(db->draw), BGCOLOR_R, BGCOLOR_G, BGCOLOR_B);
db->draw.isbg = 1;
db->draw.dtd = 1;
PutDrawEnv(&(db->draw));
//PutDispEnv(&(db->disp));
// Create a text stream at the top of the screen.
FntLoad(960, 0);
FntOpen(8, 16, 304, 208, 2, 512);
}
void display(RenderContext *ctx) {
Framebuffer *db;
DrawSync(0);
VSync(0);
ctx->db_active ^= 1;
db = &(ctx->db[ctx->db_active]);
PutDrawEnv(&(db->draw));
PutDispEnv(&(db->disp));
SetDispMask(1);
}
/* .VAG header structure */
typedef struct {
uint32_t magic; // 0x69474156 ("VAGi") for interleaved files
uint32_t version;
uint32_t interleave; // Little-endian, size of each channel buffer
uint32_t size; // Big-endian, in bytes
uint32_t sample_rate; // Big-endian, in Hertz
uint32_t _reserved[3];
char name[16];
} VAG_Header;
#define SWAP_ENDIAN(x) ( \
(((uint32_t) (x) & 0x000000ff) << 24) | \
(((uint32_t) (x) & 0x0000ff00) << 8) | \
(((uint32_t) (x) & 0x00ff0000) >> 8) | \
(((uint32_t) (x) & 0xff000000) >> 24) \
)
/* Interrupt callbacks */
// The first 4 KB of SPU RAM are reserved for capture buffers and psxspu
// additionally uploads a dummy sample (16 bytes) at 0x1000 by default, so the
// chunks must be placed after those. The dummy sample is going to be used to
// keep unused SPU channels busy, preventing them from accidentally triggering
// the SPU IRQ and throwing off the timing (all channels are always reading
// from SPU RAM, even when "stopped").
// https://problemkaputt.de/psx-spx.htm#spuinterrupt
#define DUMMY_BLOCK_ADDR 0x1000
#define BUFFER_START_ADDR 0x1010
typedef enum {
STATE_IDLE,
STATE_DATA_NEEDED,
STATE_READING,
STATE_BUFFERING
} StreamState;
typedef struct {
uint32_t *read_buffer;
int lba, chunk_secs;
int buffer_size, num_chunks, sample_rate;
volatile int next_chunk, spu_addr;
volatile int8_t db_active, state;
} StreamContext;
static StreamContext str_ctx;
void spu_irq_handler(void) {
// Acknowledge the interrupt to ensure it can be triggered again. The only
// way to do this is actually to disable the interrupt entirely; we'll
// enable it again once the chunk is ready.
SPU_CTRL &= 0xffbf;
int chunk_size = str_ctx.buffer_size * NUM_CHANNELS;
int chunk = (str_ctx.next_chunk + 1) % (uint32_t) str_ctx.num_chunks;
str_ctx.db_active ^= 1;
str_ctx.state = STATE_DATA_NEEDED;
str_ctx.next_chunk = chunk;
// Configure to SPU to trigger an IRQ once the chunk that is going to be
// filled now starts playing (so the next buffer can be loaded) and
// override both channels' loop addresses to make them "jump" to the new
// buffers, rather than actually looping when they encounter the loop flag
// at the end of the currently playing buffers.
int addr = BUFFER_START_ADDR + (str_ctx.db_active ? chunk_size : 0);
str_ctx.spu_addr = addr;
SPU_IRQ_ADDR = getSPUAddr(addr);
for (int i = 0; i < NUM_CHANNELS; i++)
SPU_CH_LOOP_ADDR(i) = getSPUAddr(addr + str_ctx.buffer_size * i);
// Note that we can't call CdRead() here as it requires interrupts to be
// enabled. Instead, feed_stream() (called from the main loop) will check
// if str_ctx.state is set to STATE_DATA_NEEDED and fetch the next chunk.
}
void cd_read_handler(CdlIntrResult event, uint8_t *payload) {
// Attempt to read the chunk again if an error has occurred, otherwise
// start uploading it to SPU RAM.
if (event == CdlDiskError) {
str_ctx.state = STATE_DATA_NEEDED;
return;
}
SpuSetTransferStartAddr(str_ctx.spu_addr);
SpuWrite(str_ctx.read_buffer, str_ctx.buffer_size * NUM_CHANNELS);
str_ctx.state = STATE_BUFFERING;
}
void spu_dma_handler(void) {
// Re-enable the SPU IRQ once the new chunk has been fully uploaded.
SPU_CTRL |= 0x0040;
str_ctx.state = STATE_IDLE;
}
/* Helper functions */
// This isn't actually required for this example, however it is necessary if
// you want to allocate the stream buffers into a region of SPU RAM that was
// previously used (to make sure the IRQ isn't going to be triggered by any
// inactive channels).
void reset_spu_channels(void) {
SpuSetKey(0, 0x00ffffff);
for (int i = 0; i < 24; i++) {
SPU_CH_ADDR(i) = getSPUAddr(DUMMY_BLOCK_ADDR);
SPU_CH_FREQ(i) = 0x1000;
}
SpuSetKey(1, 0x00ffffff);
}
void feed_stream(void) {
if (str_ctx.state != STATE_DATA_NEEDED)
return;
// Start reading the next chunk from the CD.
int lba = str_ctx.lba + str_ctx.next_chunk * str_ctx.chunk_secs;
CdlLOC pos;
CdIntToPos(lba, &pos);
CdControl(CdlSetloc, &pos, 0);
CdReadCallback(&cd_read_handler);
CdRead(str_ctx.chunk_secs, str_ctx.read_buffer, CdlModeSpeed);
str_ctx.state = STATE_READING;
}
void init_stream(const CdlLOC *pos) {
EnterCriticalSection();
InterruptCallback(IRQ_SPU, &spu_irq_handler);
DMACallback(DMA_SPU, &spu_dma_handler);
ExitCriticalSection();
// Read the header. Note that in interleaved .VAG files the first sector.
uint32_t header[512];
CdControl(CdlSetloc, pos, 0);
CdReadCallback(0);
CdRead(1, header, CdlModeSpeed);
CdReadSync(0, 0);
VAG_Header *vag = (VAG_Header *) header;
int buf_size = vag->interleave;
int chunk_secs = ((buf_size * NUM_CHANNELS) + 2047) / 2048;
str_ctx.read_buffer = malloc(chunk_secs * 2048);
str_ctx.lba = CdPosToInt(pos) + 1;
str_ctx.chunk_secs = chunk_secs;
str_ctx.buffer_size = buf_size;
str_ctx.num_chunks = (SWAP_ENDIAN(vag->size) + buf_size - 1) / buf_size;
str_ctx.sample_rate = SWAP_ENDIAN(vag->sample_rate);
str_ctx.db_active = 1;
str_ctx.next_chunk = -1;
// Ensure at least one chunk is in SPU RAM by invoking the IRQ handler
// manually and blocking until the chunk has loaded.
spu_irq_handler();
while (str_ctx.state != STATE_IDLE)
feed_stream();
}
void start_stream(void) {
int bits = 0x00ffffff >> (24 - NUM_CHANNELS);
for (int i = 0; i < NUM_CHANNELS; i++) {
SPU_CH_ADDR(i) = getSPUAddr(str_ctx.spu_addr + str_ctx.buffer_size * i);
SPU_CH_FREQ(i) = getSPUSampleRate(str_ctx.sample_rate);
SPU_CH_ADSR1(i) = 0x00ff;
SPU_CH_ADSR2(i) = 0x0000;
}
// Unmute the channels and route them for stereo output. You'll want to
// edit this if you are using more than 2 channels, and/or if you want to
// provide an option to output mono audio instead of stereo.
SPU_CH_VOL_L(0) = 0x3fff;
SPU_CH_VOL_R(0) = 0x0000;
SPU_CH_VOL_L(1) = 0x0000;
SPU_CH_VOL_R(1) = 0x3fff;
spu_irq_handler();
SpuSetKey(1, bits);
}
// This is basically a variant of reset_spu_channels() that only resets the
// channels used to play the stream, to (again) prevent them from triggering
// the SPU IRQ while the stream is paused.
void stop_stream(void) {
int bits = 0x00ffffff >> (24 - NUM_CHANNELS);
SpuSetKey(0, bits);
for (int i = 0; i < NUM_CHANNELS; i++)
SPU_CH_ADDR(i) = getSPUAddr(DUMMY_BLOCK_ADDR);
SpuSetKey(1, bits);
}
/* Main */
static RenderContext ctx;
#define SHOW_STATUS(...) { FntPrint(-1, __VA_ARGS__); FntFlush(-1); display(&ctx); }
#define SHOW_ERROR(...) { SHOW_STATUS(__VA_ARGS__); while (1) __asm__("nop"); }
static const char *state_strings[] = { "IDLE", "DATA NEEDED", "READING", "BUFFERING" };
int main(int argc, const char* argv[]) {
init_context(&ctx);
SpuInit();
CdInit();
reset_spu_channels();
SHOW_STATUS("");
// Set up controller polling.
uint8_t pad_buff[2][34];
InitPAD(pad_buff[0], 34, pad_buff[1], 34);
StartPAD();
ChangeClearPAD(0);
CdlFILE file;
SHOW_STATUS("OPENING STREAM FILE\n");
if (!CdSearchFile(&file, "\\STREAM.VAG"))
SHOW_ERROR("FAILED TO FIND STREAM.VAG\n");
SHOW_STATUS("BUFFERING STREAM\n");
init_stream(&file.pos);
start_stream();
int paused = 0, sample_rate = getSPUSampleRate(str_ctx.sample_rate);
uint16_t last_buttons = 0xffff;
while (1) {
feed_stream();
FntPrint(-1, "PLAYING SPU STREAM\n\n");
FntPrint(-1, "BUFFER: %d\n", str_ctx.db_active);
FntPrint(-1, "STATUS: %s\n\n", state_strings[str_ctx.state]);
FntPrint(-1, "POSITION: %d/%d\n", str_ctx.next_chunk, str_ctx.num_chunks);
FntPrint(-1, "SMP RATE: %5d HZ\n\n", (sample_rate * 44100) >> 12);
FntPrint(-1, "[START] %s\n", paused ? "RESUME" : "PAUSE");
FntPrint(-1, "[LEFT/RIGHT] SEEK\n");
FntPrint(-1, "[O] RESET POSITION\n");
FntPrint(-1, "[UP/DOWN] CHANGE SAMPLE RATE\n");
FntPrint(-1, "[X] RESET SAMPLE RATE\n");
FntFlush(-1);
display(&ctx);
// Check if a compatible controller is connected and handle button
// presses.
PADTYPE *pad = (PADTYPE *) pad_buff[0];
if (pad->stat)
continue;
if (
(pad->type != PAD_ID_DIGITAL) &&
(pad->type != PAD_ID_ANALOG_STICK) &&
(pad->type != PAD_ID_ANALOG)
)
continue;
if ((last_buttons & PAD_START) && !(pad->btn & PAD_START)) {
paused ^= 1;
if (paused)
stop_stream();
else
start_stream();
}
if (!(pad->btn & PAD_LEFT))
str_ctx.next_chunk--;
if (!(pad->btn & PAD_RIGHT))
str_ctx.next_chunk++;
if ((last_buttons & PAD_CIRCLE) && !(pad->btn & PAD_CIRCLE))
str_ctx.next_chunk = -1;
if (!(pad->btn & PAD_DOWN) && (sample_rate > 0x400))
sample_rate -= 0x40;
if (!(pad->btn & PAD_UP) && (sample_rate < 0x2000))
sample_rate += 0x40;
if ((last_buttons & PAD_CROSS) && !(pad->btn & PAD_CROSS))
sample_rate = getSPUSampleRate(str_ctx.sample_rate);
// Only set the sample rate registers if necessary.
if (pad->btn != 0xffff) {
for (int i = 0; i < NUM_CHANNELS; i++)
SPU_CH_FREQ(i) = sample_rate;
}
last_buttons = pad->btn;
}
return 0;
}
|