aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorspicyjpeg <thatspicyjpeg@gmail.com>2023-06-20 11:40:13 +0200
committerspicyjpeg <thatspicyjpeg@gmail.com>2023-06-20 11:40:13 +0200
commitced21c69f7b399dce169069334c1424e5254d582 (patch)
treef8d7f3bb4eed04410881334992f664e6e0edcb64 /examples
parenteaea5649a0803cc4bfeb6d21ee9f4098d4b493fc (diff)
downloadpsn00bsdk-ced21c69f7b399dce169069334c1424e5254d582.tar.gz
Update io/pads and sound/cdstream examples
Diffstat (limited to 'examples')
-rw-r--r--examples/io/pads/spi.c23
-rw-r--r--examples/io/pads/spi.h33
-rw-r--r--examples/sound/cdstream/main.c334
-rw-r--r--examples/sound/cdstream/stream.c264
-rw-r--r--examples/sound/cdstream/stream.h212
-rw-r--r--examples/sound/cdstream/stream.vagbin4687872 -> 4646912 bytes
-rw-r--r--examples/sound/vagsample/main.c3
7 files changed, 649 insertions, 220 deletions
diff --git a/examples/io/pads/spi.c b/examples/io/pads/spi.c
index 234bdba..839f811 100644
--- a/examples/io/pads/spi.c
+++ b/examples/io/pads/spi.c
@@ -1,29 +1,6 @@
/*
* PSn00bSDK controller polling example (SPI driver)
* (C) 2021 spicyjpeg - MPL licensed
- *
- * This is a fairly complete timer driven, asynchronous high-speed SPI driver,
- * with support for sending custom commands (including memory card access), in
- * about 200 lines of code. Feel free to copypaste and adapt it.
- *
- * The way this works is by maintaining a queue of requests to send, each with
- * its own payload and callback. Timer 2 is configured to trigger an IRQ at
- * regular intervals. On each tick, the next request in the queue (or a poll
- * command if no request is pending) is prepared and the first byte is
- * sent; if the controller asks for more data by pulling /ACK low, the next
- * byte is sent and the received byte is placed into a buffer. This goes on
- * until the last byte is exchanged and the controller stops asserting /ACK.
- *
- * On the next tick, the response buffer is passed to the request's callback
- * and reset, and the next request in the queue is sent. This blindly assumes
- * it only takes one tick for a request/response to be sent, which is the case
- * for controllers' very small packets but not for memory cards. It is
- * advisable to call spi_set_poll_rate() to temporarily reduce poll rate while
- * accessing memory cards.
- *
- * Note that this driver completely takes over the SPI bus, so you won't be
- * able to use any BIOS functions that rely on SPI access (i.e. pad and memory
- * card APIs) alongside it.
*/
#include <stdint.h>
diff --git a/examples/io/pads/spi.h b/examples/io/pads/spi.h
index 8c17df3..72a2f99 100644
--- a/examples/io/pads/spi.h
+++ b/examples/io/pads/spi.h
@@ -3,8 +3,35 @@
* (C) 2021 spicyjpeg - MPL licensed
*/
-#ifndef __SPI_H
-#define __SPI_H
+/**
+ * @file spi.h
+ * @brief Asynchronous SPI controller driver
+ *
+ * @details This is a fairly complete timer driven, asynchronous high-speed SPI
+ * driver, with support for sending custom commands (including memory card
+ * access), in about 200 lines of code. Feel free to copy and adapt it.
+ *
+ * The way this works is by maintaining a queue of requests to send, each with
+ * its own payload and callback. Timer 2 is configured to trigger an IRQ at
+ * regular intervals. On each tick, the next request in the queue (or a poll
+ * command if no request is pending) is prepared and the first byte is
+ * sent; if the controller asks for more data by pulling /ACK low, the next
+ * byte is sent and the received byte is placed into a buffer. This goes on
+ * until the last byte is exchanged and the controller stops asserting /ACK.
+ *
+ * On the next tick, the response buffer is passed to the request's callback
+ * and reset, and the next request in the queue is sent. This blindly assumes
+ * it only takes one tick for a request/response to be sent, which is the case
+ * for controllers' very small packets but not for memory cards. It is
+ * advisable to call spi_set_poll_rate() to temporarily reduce poll rate while
+ * accessing memory cards.
+ *
+ * Note that this driver completely takes over the SPI bus, so you won't be
+ * able to use any BIOS functions that rely on SPI access (i.e. pad and memory
+ * card APIs) alongside it.
+ */
+
+#pragma once
#include <stdint.h>
#include <stddef.h>
@@ -68,5 +95,3 @@ void SPI_Init(SPI_Callback callback);
#ifdef __cplusplus
}
#endif
-
-#endif
diff --git a/examples/sound/cdstream/main.c b/examples/sound/cdstream/main.c
index 53b88e6..c6d578c 100644
--- a/examples/sound/cdstream/main.c
+++ b/examples/sound/cdstream/main.c
@@ -1,20 +1,18 @@
/*
* PSn00bSDK SPU CD-ROM streaming example
- * (C) 2022 spicyjpeg - MPL licensed
+ * (C) 2022-2023 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,
+ * playback of a large multi-channel audio file from the CD-ROM 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:
+ * A ring buffer takes the place of the stream_data array from the spustream
+ * example. This buffer is filled from the CD-ROM by the main thread and drained
+ * by the SPU IRQ handler, which pulls a single chunk at a time out of it and
+ * transfers it to SPU RAM for playback. The feed_stream() function handles
+ * fetching chunks, which are read once again from an interleaved .VAG file laid
+ * out on the disc as follows:
*
* +--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+----
* | | +--------------------+---------------------+ |
@@ -23,27 +21,28 @@
* +----------+----------+----------+----------+----------+----------+----
* \__________________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.
+ * Note that the ring buffer must be large enough to give the drive enough time
+ * to seek from one chunk to another. A larger buffer will take up more main RAM
+ * but will not influence SPU RAM usage, which depends only on the chunk size
+ * (interleave) and channel count of the .VAG file. Generally, interleave values
+ * in the 2048-4096 byte range work well (the interleaving script in the
+ * spustream directory uses 4096 bytes by default).
*
- * Implementing SPU streaming might seem pointless, but it actually has a
- * number of advantages over CD-DA or XA:
+ * 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.
+ * - 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.
+ * 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
@@ -53,13 +52,14 @@
* - 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.
+ * - 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 <stdbool.h>
#include <stdlib.h>
#include <psxetc.h>
#include <psxapi.h>
@@ -69,9 +69,15 @@
#include <psxcd.h>
#include <hwregs_c.h>
-extern const uint8_t stream_data[];
+#include "stream.h"
+
+// Size of the ring buffer in main RAM in bytes.
+#define RAM_BUFFER_SIZE 0x18000
-#define NUM_CHANNELS 2
+// Minimum number of sectors that will be read from the CD-ROM at once. Higher
+// values will improve efficiency at the cost of requiring a larger buffer in
+// order to prevent underruns and glitches in the audio output.
+#define REFILL_THRESHOLD 24
/* Display/GPU context utilities */
@@ -141,7 +147,8 @@ typedef struct {
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];
+ uint16_t _reserved[5];
+ uint16_t channels; // Little-endian, channel count (stereo if 0)
char name[16];
} VAG_Header;
@@ -154,90 +161,30 @@ typedef struct {
/* 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;
+#define DUMMY_BLOCK_ADDR 0x1000
+#define STREAM_BUFFER_ADDR 0x1010
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;
- }
+ int start_lba, stream_length;
- SpuSetTransferStartAddr(str_ctx.spu_addr);
- SpuWrite(str_ctx.read_buffer, str_ctx.buffer_size * NUM_CHANNELS);
-
- str_ctx.state = STATE_BUFFERING;
-}
+ volatile int next_sector;
+ volatile size_t refill_length;
+} StreamReadContext;
-void spu_dma_handler(void) {
- // Re-enable the SPU IRQ once the new chunk has been fully uploaded.
- SPU_CTRL |= 0x0040;
+static Stream_Context stream_ctx;
+static StreamReadContext read_ctx;
- str_ctx.state = STATE_IDLE;
+void cd_read_handler(CdlIntrResult event, uint8_t *payload) {
+ // Mark the data as valid.
+ if (event != CdlDiskError)
+ Stream_Feed(&stream_ctx, read_ctx.refill_length * 2048);
}
/* 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
+// This isn't actually required for this example, however it is necessary if the
+// stream buffers are going to be allocated into a region of SPU RAM that was
+// previously used (to make sure the IRQ is not going to be triggered by any
// inactive channels).
void reset_spu_channels(void) {
SpuSetKey(0, 0x00ffffff);
@@ -250,30 +197,49 @@ void reset_spu_channels(void) {
SpuSetKey(1, 0x00ffffff);
}
-void feed_stream(void) {
- if (str_ctx.state != STATE_DATA_NEEDED)
- return;
+bool feed_stream(void) {
+ // Do nothing if the drive is already busy reading a chunk.
+ if (CdReadSync(1, 0) > 0)
+ return true;
+
+ // To improve efficiency, do not start refilling immediately but wait until
+ // there is enough space in the buffer (see REFILL_THRESHOLD).
+ if (Stream_GetRefillLength(&stream_ctx) < (REFILL_THRESHOLD * 2048))
+ return false;
- // Start reading the next chunk from the CD.
- int lba = str_ctx.lba + str_ctx.next_chunk * str_ctx.chunk_secs;
+ uint8_t *ptr;
+ size_t refill_length = Stream_GetFeedPtr(&stream_ctx, &ptr) / 2048;
+ // Figure out how much data can be read in one shot. If the end of the file
+ // would be reached before the buffer is full, split the read into two
+ // separate reads.
+ int next_sector = read_ctx.next_sector;
+ int max_length = read_ctx.stream_length - next_sector;
+
+ while (max_length <= 0) {
+ next_sector -= read_ctx.stream_length;
+ max_length += read_ctx.stream_length;
+ }
+
+ if (refill_length > max_length)
+ refill_length = max_length;
+
+ // Start reading the next chunk from the CD-ROM into the buffer.
CdlLOC pos;
- CdIntToPos(lba, &pos);
- CdControl(CdlSetloc, &pos, 0);
+ CdIntToPos(read_ctx.start_lba + next_sector, &pos);
+ CdControl(CdlSetloc, &pos, 0);
CdReadCallback(&cd_read_handler);
- CdRead(str_ctx.chunk_secs, str_ctx.read_buffer, CdlModeSpeed);
+ CdRead(refill_length, (uint32_t *) ptr, CdlModeSpeed);
- str_ctx.state = STATE_READING;
-}
+ read_ctx.next_sector = next_sector + refill_length;
+ read_ctx.refill_length = refill_length;
-void init_stream(const CdlLOC *pos) {
- EnterCriticalSection();
- InterruptCallback(IRQ_SPU, &spu_irq_handler);
- DMACallback(DMA_SPU, &spu_dma_handler);
- ExitCriticalSection();
+ return true;
+}
- // Read the header. Note that in interleaved .VAG files the first sector.
+void setup_stream(const CdlLOC *pos) {
+ // Read the .VAG header from the first sector of the file.
uint32_t header[512];
CdControl(CdlSetloc, pos, 0);
@@ -281,61 +247,42 @@ void init_stream(const CdlLOC *pos) {
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;
+ VAG_Header *vag = (VAG_Header *) header;
+ Stream_Config config;
+
+ int num_channels = vag->channels ? vag->channels : 2;
+ int num_chunks =
+ (SWAP_ENDIAN(vag->size) + vag->interleave - 1) / vag->interleave;
+
+ config.spu_address = STREAM_BUFFER_ADDR;
+ config.channel_mask = 0;
+ config.interleave = vag->interleave;
+ config.buffer_size = RAM_BUFFER_SIZE;
+ config.refill_threshold = 0;
+ config.sample_rate = SWAP_ENDIAN(vag->sample_rate);
+ config.refill_callback = (void *) 0;
+ config.underrun_callback = (void *) 0;
+
+ // Use the first N channels of the SPU and pan them left/right in pairs
+ // (this assumes the stream contains one or more stereo tracks).
+ for (int ch = 0; ch < num_channels; ch++) {
+ config.channel_mask = (config.channel_mask << 1) | 1;
+
+ SPU_CH_VOL_L(ch) = (ch % 2) ? 0x0000 : 0x3fff;
+ SPU_CH_VOL_R(ch) = (ch % 2) ? 0x3fff : 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);
+ Stream_Init(&stream_ctx, &config);
- SpuSetKey(0, bits);
-
- for (int i = 0; i < NUM_CHANNELS; i++)
- SPU_CH_ADDR(i) = getSPUAddr(DUMMY_BLOCK_ADDR);
+ read_ctx.start_lba = CdPosToInt(pos) + 1;
+ read_ctx.stream_length =
+ (num_channels * num_chunks * vag->interleave + 2047) / 2048;
+ read_ctx.next_sector = 0;
+ read_ctx.refill_length = 0;
- SpuSetKey(1, bits);
+ // Ensure the buffer is full before starting playback.
+ while (feed_stream())
+ __asm__ volatile("");
}
/* Main */
@@ -345,8 +292,6 @@ 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();
@@ -366,21 +311,26 @@ int main(int argc, const char* argv[]) {
SHOW_ERROR("FAILED TO FIND STREAM.VAG\n");
SHOW_STATUS("BUFFERING STREAM\n");
- init_stream(&file.pos);
- start_stream();
+ setup_stream(&file.pos);
+ Stream_Start(&stream_ctx);
+
+ int sectors_per_chunk = (stream_ctx.chunk_size + 2047) / 2048;
+ int vag_sample_rate = getSPUSampleRate(stream_ctx.config.sample_rate);
- int paused = 0, sample_rate = getSPUSampleRate(str_ctx.sample_rate);
+ bool paused = false;
+ int sample_rate = vag_sample_rate;
uint16_t last_buttons = 0xffff;
while (1) {
- feed_stream();
+ bool buffering = 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, "BUFFER: %d\n", stream_ctx.db_active);
+ FntPrint(-1, "STATUS: %s\n\n", buffering ? "READING" : "IDLE");
- FntPrint(-1, "POSITION: %d/%d\n", str_ctx.next_chunk, str_ctx.num_chunks);
+ FntPrint(-1, "BUFFERED: %d/%d\n", stream_ctx.buffer.length, stream_ctx.config.buffer_size);
+ FntPrint(-1, "POSITION: %d/%d\n", read_ctx.next_sector, read_ctx.stream_length);
FntPrint(-1, "SMP RATE: %5d HZ\n\n", (sample_rate * 44100) >> 12);
FntPrint(-1, "[START] %s\n", paused ? "RESUME" : "PAUSE");
@@ -407,30 +357,30 @@ int main(int argc, const char* argv[]) {
if ((last_buttons & PAD_START) && !(pad->btn & PAD_START)) {
paused ^= 1;
if (paused)
- stop_stream();
+ Stream_Stop();
else
- start_stream();
+ Stream_Start(&stream_ctx);
}
- if (!(pad->btn & PAD_LEFT))
- str_ctx.next_chunk--;
+ // Note that seeking will only work correctly with .VAG files whose
+ // interleave (chunk size) is a multiple of 2048.
+ if (!(pad->btn & PAD_LEFT) && (read_ctx.next_sector > 0))
+ read_ctx.next_sector -= sectors_per_chunk;
if (!(pad->btn & PAD_RIGHT))
- str_ctx.next_chunk++;
+ read_ctx.next_sector += sectors_per_chunk;
if ((last_buttons & PAD_CIRCLE) && !(pad->btn & PAD_CIRCLE))
- str_ctx.next_chunk = -1;
+ read_ctx.next_sector = 0;
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);
+ sample_rate = vag_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;
- }
+ if (pad->btn != 0xffff)
+ Stream_SetSampleRate(&stream_ctx, (sample_rate * 44100) >> 12);
last_buttons = pad->btn;
}
diff --git a/examples/sound/cdstream/stream.c b/examples/sound/cdstream/stream.c
new file mode 100644
index 0000000..bf73b2f
--- /dev/null
+++ b/examples/sound/cdstream/stream.c
@@ -0,0 +1,264 @@
+/*
+ * PSn00bSDK SPU CD-ROM streaming example (streaming API)
+ * (C) 2022-2023 spicyjpeg - MPL licensed
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <psxspu.h>
+#include <psxetc.h>
+#include <psxapi.h>
+#include <hwregs_c.h>
+
+#include "stream.h"
+
+// 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 _min(x, y) (((x) < (y)) ? (x) : (y))
+
+/* Interrupt handlers */
+
+static volatile Stream_Context *_active_ctx = (void *) 0;
+
+static void _spu_irq_handler(void) {
+ Stream_Context *ctx = _active_ctx;
+
+ // 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 &= ~(1 << 6);
+
+ if (!ctx)
+ return;
+
+ // Ensure enough data is available. If not, re-enable the IRQ (to prevent
+ // the SPU from getting stuck, even though this will produce nasty noises!)
+ // and fire the underrun callback.
+ int length = (int) ctx->buffer.length - (int) ctx->chunk_size;
+
+ if (length < 0) {
+ if (ctx->config.underrun_callback)
+ ctx->config.underrun_callback();
+
+ SPU_CTRL |= 1 << 6; // FIXME: figure out another way around this
+ return;
+ }
+
+ // Pull a chunk from the ring buffer and invoke the refill callback (if any)
+ // once the buffer's length is below the refill threshold.
+ ctx->db_active ^= 1;
+ ctx->buffering = true;
+ ctx->chunk_counter++;
+
+ size_t tail = ctx->buffer.tail;
+ uint8_t *ptr = &ctx->buffer.data[ctx->buffer.tail];
+ ctx->buffer.tail = (tail + ctx->chunk_size) % ctx->config.buffer_size;
+ ctx->buffer.length = length;
+
+ if ((length <= ctx->config.refill_threshold) && !ctx->callback_issued) {
+ if (ctx->config.refill_callback)
+ ctx->config.refill_callback();
+
+ ctx->callback_issued = true;
+ }
+
+ // 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.
+ uint32_t offset = 0;
+ uint32_t address =
+ ctx->config.spu_address + (ctx->db_active ? ctx->chunk_size : 0);
+
+ SPU_IRQ_ADDR = getSPUAddr(address);
+
+ for (uint32_t ch = 0, mask = ctx->config.channel_mask; mask; ch++, mask >>= 1) {
+ if (!(mask & 1))
+ continue;
+
+ SPU_CH_LOOP_ADDR(ch) = getSPUAddr(address + offset);
+ offset += ctx->config.interleave;
+
+ // Make sure this channel's data ends with an appropriate loop flag.
+ //ptr[offset - 15] |= 0x03;
+ }
+
+ // Start uploading the next chunk to the SPU.
+ SpuSetTransferStartAddr(address);
+ SpuWrite((const uint32_t *) ptr, ctx->chunk_size);
+}
+
+static void _spu_dma_handler(void) {
+ // Re-enable the SPU IRQ once the new chunk has been fully uploaded.
+ SPU_CTRL |= 1 << 6;
+
+ _active_ctx->buffering = false;
+}
+
+/* Public API */
+
+void Stream_Init(Stream_Context *ctx, const Stream_Config *config) {
+ memset(ctx, 0, sizeof(Stream_Context));
+ memcpy(&(ctx->config), config, sizeof(Stream_Config));
+
+ ctx->num_channels = 0;
+ for (uint32_t mask = config->channel_mask; mask; mask >>= 1) {
+ if (mask & 1)
+ ctx->num_channels++;
+ }
+
+ assert(ctx->num_channels);
+
+ ctx->chunk_size = ctx->config.interleave * ctx->num_channels;
+ ctx->buffer.data = malloc(config->buffer_size);
+
+ assert(ctx->buffer.data);
+
+ int _exit = EnterCriticalSection();
+ ctx->old_irq_handler = InterruptCallback(IRQ_SPU, &_spu_irq_handler);
+ ctx->old_dma_handler = DMACallback(DMA_SPU, &_spu_dma_handler);
+
+ if (_exit)
+ ExitCriticalSection();
+}
+
+void Stream_Destroy(Stream_Context *ctx) {
+ free(ctx->buffer.data);
+
+ int _exit = EnterCriticalSection();
+ InterruptCallback(IRQ_SPU, ctx->old_irq_handler);
+ DMACallback(DMA_SPU, ctx->old_dma_handler);
+
+ if (_exit)
+ ExitCriticalSection();
+}
+
+bool Stream_Start(Stream_Context *ctx) {
+ if (_active_ctx)
+ return false;
+
+ _active_ctx = ctx;
+
+ // Wait for the first chunk to be buffered and ready to play.
+ if (!ctx->chunk_counter) {
+ _spu_irq_handler();
+ SpuIsTransferCompleted(SPU_TRANSFER_WAIT);
+ }
+
+ uint32_t address =
+ ctx->config.spu_address + (ctx->db_active ? ctx->chunk_size : 0);
+
+ SpuSetKey(0, ctx->config.channel_mask);
+
+ for (uint32_t ch = 0, mask = ctx->config.channel_mask; mask; ch++, mask >>= 1) {
+ if (!(mask & 1))
+ continue;
+
+ SPU_CH_ADDR (ch) = getSPUAddr(address);
+ SPU_CH_FREQ (ch) = getSPUSampleRate(ctx->config.sample_rate);
+ SPU_CH_ADSR1(ch) = 0x00ff;
+ SPU_CH_ADSR2(ch) = 0x0000;
+
+ address += ctx->config.interleave;
+ }
+
+ _spu_irq_handler();
+ SpuSetKey(1, ctx->config.channel_mask);
+
+ return true;
+}
+
+bool Stream_Stop(void) {
+ Stream_Context *ctx = _active_ctx;
+
+ if (!ctx)
+ return false;
+
+ // Prevent the channels from triggering the SPU IRQ by stopping them and
+ // pointing them to the dummy block.
+ SpuSetKey(0, ctx->config.channel_mask);
+
+ for (uint32_t ch = 0, mask = ctx->config.channel_mask; mask; ch++, mask >>= 1) {
+ if (mask & 1)
+ SPU_CH_ADDR(ch) = getSPUAddr(DUMMY_BLOCK_ADDR);
+ }
+
+ SpuSetKey(1, ctx->config.channel_mask);
+
+ _active_ctx = (void *) 0;
+ return true;
+}
+
+void Stream_SetSampleRate(Stream_Context *ctx, int value) {
+ ctx->config.sample_rate = value;
+
+ if (!Stream_IsActive(ctx))
+ return;
+
+ for (uint32_t ch = 0, mask = ctx->config.channel_mask; mask; ch++, mask >>= 1) {
+ if (mask & 1)
+ SPU_CH_FREQ(ch) = getSPUSampleRate(value);
+ }
+}
+
+bool Stream_IsActive(const Stream_Context *ctx) {
+ return (ctx == _active_ctx);
+}
+
+size_t Stream_GetRefillLength(const Stream_Context *ctx) {
+ int unbuf_total = (int) ctx->config.buffer_size - (int) ctx->buffer.length;
+
+ if (unbuf_total <= 0)
+ return 0;
+
+ return unbuf_total;
+}
+
+size_t Stream_GetFeedPtr(const Stream_Context *ctx, uint8_t **ptr) {
+ // Check if filling up the entire buffer would require wrapping around its
+ // boundary. If that is the case, only return the length of the first
+ // contiguous region. The second region will be returned once the first one
+ // has been filled up.
+ FastEnterCriticalSection();
+
+ size_t head = ctx->buffer.head;
+
+ int unbuf_total = (int) ctx->config.buffer_size - (int) ctx->buffer.length;
+ int unbuf_head = (int) ctx->config.buffer_size - (int) head;
+
+ FastExitCriticalSection();
+
+ if (unbuf_total <= 0)
+ return 0;
+
+ *ptr = &(ctx->buffer.data[head]);
+ return (size_t) _min(unbuf_total, unbuf_head);
+}
+
+void Stream_Feed(Stream_Context *ctx, size_t length) {
+ int unbuf_total = (int) ctx->config.buffer_size - (int) ctx->buffer.length;
+ length = _min(length, unbuf_total);
+
+ FastEnterCriticalSection();
+
+ size_t new_length = ctx->buffer.length + length;
+ ctx->buffer.head = (ctx->buffer.head + length) % ctx->config.buffer_size;
+ ctx->buffer.length = new_length;
+
+ if (new_length > ctx->config.refill_threshold)
+ ctx->callback_issued = false;
+
+ FastExitCriticalSection();
+}
diff --git a/examples/sound/cdstream/stream.h b/examples/sound/cdstream/stream.h
new file mode 100644
index 0000000..15e3ec3
--- /dev/null
+++ b/examples/sound/cdstream/stream.h
@@ -0,0 +1,212 @@
+/*
+ * PSn00bSDK SPU CD-ROM streaming example (streaming API)
+ * (C) 2022-2023 spicyjpeg - MPL licensed
+ */
+
+/**
+ * @file stream.h
+ * @brief Helper library for SPU audio streaming
+ *
+ * @details This is a minimal driver for SPU ADPCM streaming, with support for
+ * concurrent streams (although only one can be played at a time due to SPU
+ * limitations), an arbitrary number of channels for each stream and optional
+ * refill and underrun callbacks. Feel free to copy and modify it.
+ *
+ * The driver manages a FIFO (ring buffer) in main RAM. New audio data can be
+ * pushed into the FIFO at any time. When the stream is active, the SPU IRQ
+ * handler will periodically pull a chunk (i.e. an array of fixed-size slices of
+ * audio data, one for each channel) from the FIFO and move it to SPU RAM for
+ * playback.
+ *
+ * As the main buffer is in main RAM, SPU RAM usage is minimal: only a double
+ * buffer holding two chunks is allocated in SPU RAM. The size of each chunk
+ * depends on the specified interleave and number of channels, but not on the
+ * buffer size.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+/* Type definitions */
+
+typedef uint32_t *(*Stream_Callback)(void);
+
+/**
+ * @brief Stream initialization settings structure.
+ *
+ * @details This structure is used to pass settings to Stream_Init() when
+ * initializing a new stream.
+ *
+ * The SPU RAM address is in bytes and must be aligned to 16 bytes. The
+ * 0x0000-0x100f region is reserved for capture buffers and the dummy block and
+ * cannot be used. The channel mask is a bitfield whose bits represent which SPU
+ * channels the stream is going to use: for instance, a value of 0b1101 will
+ * assign the first channel of the stream to SPU channel 0, the second channel
+ * to SPU channel 2 and the third channel to SPU channel 3. The sample rate is
+ * in Hertz, while the interleave and buffer size are in bytes.
+ *
+ * The refill threshold, refill callback and underrun callback are optional. If
+ * provided, the callbacks will be invoked by the SPU IRQ handler once the
+ * FIFO's length goes below the specified threshold and once it reaches zero,
+ * respectively.
+ */
+typedef struct {
+ uint32_t spu_address, channel_mask;
+ size_t interleave, buffer_size, refill_threshold;
+ int sample_rate;
+
+ Stream_Callback refill_callback, underrun_callback;
+} Stream_Config;
+
+typedef struct {
+ uint8_t *data;
+ size_t head, tail, length;
+} Stream_Buffer;
+
+/**
+ * @brief Stream instance object.
+ *
+ * @details This structure represents a single audio stream. An arbitrary number
+ * of streams may be created concurrently, however only one can be active at a
+ * time (as the SPU only provides a single interrupt). With the exception of the
+ * chunk counter, most fields are only used internally and shall not be accessed
+ * directly.
+ */
+typedef struct {
+ Stream_Config config;
+ volatile Stream_Buffer buffer;
+
+ void *old_irq_handler, *old_dma_handler;
+ size_t chunk_size;
+ uint8_t num_channels;
+
+ volatile uint8_t db_active, buffering, callback_issued;
+ volatile uint32_t chunk_counter;
+} Stream_Context;
+
+/* Public API */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Initializes a stream context and allocates its buffers.
+ *
+ * @details Sets up the provided stream context with the given configuration and
+ * allocates the respective FIFO in the heap. The configuration object may be
+ * safely modified or discarded once the stream has been initialized.
+ *
+ * @param ctx
+ * @param config
+ *
+ * @see Stream_Config, Stream_Destroy()
+ */
+void Stream_Init(Stream_Context *ctx, const Stream_Config *config);
+
+/**
+ * @brief Deallocates a stream context's buffers.
+ *
+ * @details Frees the FIFO associated with the given stream context, which must
+ * have been initialized beforehand by calling Stream_Init(). Note that this
+ * function does *not* deallocate the context object itself.
+ *
+ * @param ctx
+ */
+void Stream_Destroy(Stream_Context *ctx);
+
+/**
+ * @brief Starts playback of a stream.
+ *
+ * @details Activates the given stream context and starts playing audio from its
+ * FIFO. This function must be called while no other stream is active and after
+ * the stream's FIFO has been filled up.
+ *
+ * @param ctx
+ * @return True if the stream was started, false if another stream is active
+ *
+ * @see Stream_Stop()
+ */
+bool Stream_Start(Stream_Context *ctx);
+
+/**
+ * @brief Stops playback of any currently active stream.
+ *
+ * @return True if a stream was active and has been stopped, false otherwise
+ */
+bool Stream_Stop(void);
+
+/**
+ * @brief Changes the sampling rate of a stream.
+ *
+ * @details If the stream is currently active the pitch of the SPU channels
+ * assigned to it is changed to match the new value, otherwise the new sampling
+ * rate will be applied once the stream is started.
+ *
+ * @param ctx
+ * @param value
+ */
+void Stream_SetSampleRate(Stream_Context *ctx, int value);
+
+/**
+ * @brief Returns whether or not the given stream is currently active.
+ *
+ * @param ctx
+ * @return True if the stream is active, false otherwise
+ */
+bool Stream_IsActive(const Stream_Context *ctx);
+
+/**
+ * @brief Returns how many bytes in a stream's FIFO are currently empty and can
+ * be filled.
+ *
+ * @param ctx
+ * @return Maximum number of bytes that can currently be pushed into the FIFO
+ *
+ * @see Stream_GetFeedPtr().
+ */
+size_t Stream_GetRefillLength(const Stream_Context *ctx);
+
+/**
+ * @brief Returns a pointer to a region of a stream's FIFO to be filled in.
+ *
+ * @details If the given stream's FIFO is partially or completely empty, returns
+ * the number of bytes that can be filled and writes a pointer to the region to
+ * fill in to the provided location. Stream_Feed() must be called after any new
+ * data is written in order to ensure it gets acknowledged.
+ *
+ * NOTE: if the empty area in the FIFO wraps around its boundary and is not
+ * contiguous, only the pointer to and length of the first contiguous region
+ * will be returned, with the other region being returned after the first one is
+ * filled completely. The length returned by this function may thus be lower
+ * than the value returned by Stream_GetRefillLength().
+ *
+ * @param ctx
+ * @param ptr Pointer to pointer variable to be set
+ * @return Maximum number of bytes that can be written to the FIFO
+ *
+ * @see Stream_Feed()
+ */
+size_t Stream_GetFeedPtr(const Stream_Context *ctx, uint8_t **ptr);
+
+/**
+ * @brief Marks data that has been written into the stream's FIFO as available.
+ *
+ * @details Signals that the specified number of bytes, starting from the
+ * pointer returned by Stream_GetFeedPtr(), are valid and updates the FIFO's
+ * length accordingly. This function does not alter the FIFO's contents, so the
+ * actual data must be written before calling Stream_Feed().
+ *
+ * @param ctx
+ * @param length Number of bytes that have been written
+ *
+ * @see Stream_GetFeedPtr()
+ */
+void Stream_Feed(Stream_Context *ctx, size_t length);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/examples/sound/cdstream/stream.vag b/examples/sound/cdstream/stream.vag
index aab82ba..2407118 100644
--- a/examples/sound/cdstream/stream.vag
+++ b/examples/sound/cdstream/stream.vag
Binary files differ
diff --git a/examples/sound/vagsample/main.c b/examples/sound/vagsample/main.c
index dffa4cc..701bfb5 100644
--- a/examples/sound/vagsample/main.c
+++ b/examples/sound/vagsample/main.c
@@ -94,7 +94,8 @@ typedef struct {
uint32_t interleave; // Unused in mono files
uint32_t size; // Big-endian, in bytes
uint32_t sample_rate; // Big-endian, in Hertz
- uint32_t _reserved[3];
+ uint16_t _reserved[5];
+ uint16_t channels; // Unused in mono files
char name[16];
} VAG_Header;