diff options
| author | spicyjpeg <thatspicyjpeg@gmail.com> | 2023-06-20 11:40:13 +0200 |
|---|---|---|
| committer | spicyjpeg <thatspicyjpeg@gmail.com> | 2023-06-20 11:40:13 +0200 |
| commit | ced21c69f7b399dce169069334c1424e5254d582 (patch) | |
| tree | f8d7f3bb4eed04410881334992f664e6e0edcb64 /examples | |
| parent | eaea5649a0803cc4bfeb6d21ee9f4098d4b493fc (diff) | |
| download | psn00bsdk-ced21c69f7b399dce169069334c1424e5254d582.tar.gz | |
Update io/pads and sound/cdstream examples
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/io/pads/spi.c | 23 | ||||
| -rw-r--r-- | examples/io/pads/spi.h | 33 | ||||
| -rw-r--r-- | examples/sound/cdstream/main.c | 334 | ||||
| -rw-r--r-- | examples/sound/cdstream/stream.c | 264 | ||||
| -rw-r--r-- | examples/sound/cdstream/stream.h | 212 | ||||
| -rw-r--r-- | examples/sound/cdstream/stream.vag | bin | 4687872 -> 4646912 bytes | |||
| -rw-r--r-- | examples/sound/vagsample/main.c | 3 |
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 Binary files differindex aab82ba..2407118 100644 --- a/examples/sound/cdstream/stream.vag +++ b/examples/sound/cdstream/stream.vag 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; |
