diff options
| author | spicyjpeg <thatspicyjpeg@gmail.com> | 2022-10-27 16:55:08 +0200 |
|---|---|---|
| committer | spicyjpeg <thatspicyjpeg@gmail.com> | 2022-10-27 16:55:08 +0200 |
| commit | f6c41f3783c4fce49a9899b710ebb50ba9f647ab (patch) | |
| tree | 7133d4e05008cb11c05995d3c0ce7d938258cd51 /examples/sound/cdstream | |
| parent | 4dbf47f129a55428b90df2805228fbd481e1d117 (diff) | |
| download | psn00bsdk-f6c41f3783c4fce49a9899b710ebb50ba9f647ab.tar.gz | |
Refactor sound examples, add new spustream example
Diffstat (limited to 'examples/sound/cdstream')
| -rw-r--r-- | examples/sound/cdstream/CMakeLists.txt | 23 | ||||
| -rw-r--r-- | examples/sound/cdstream/iso.xml | 28 | ||||
| -rw-r--r-- | examples/sound/cdstream/main.c | 439 | ||||
| -rw-r--r-- | examples/sound/cdstream/stream.vag | bin | 0 -> 4646912 bytes | |||
| -rw-r--r-- | examples/sound/cdstream/system.cnf | 4 |
5 files changed, 494 insertions, 0 deletions
diff --git a/examples/sound/cdstream/CMakeLists.txt b/examples/sound/cdstream/CMakeLists.txt new file mode 100644 index 0000000..e569449 --- /dev/null +++ b/examples/sound/cdstream/CMakeLists.txt @@ -0,0 +1,23 @@ +# PSn00bSDK example CMake script +# (C) 2021 spicyjpeg - MPL licensed + +cmake_minimum_required(VERSION 3.21) + +project( + cdstream + LANGUAGES C + VERSION 1.0.0 + DESCRIPTION "PSn00bSDK SPU CD audio streaming example" + HOMEPAGE_URL "http://lameguy64.net/?page=psn00bsdk" +) + +file(GLOB _sources *.c) +psn00bsdk_add_executable(cdstream GPREL ${_sources}) +psn00bsdk_add_cd_image(cdstream_iso cdstream iso.xml DEPENDS cdstream) + +install( + FILES + ${PROJECT_BINARY_DIR}/cdstream.bin + ${PROJECT_BINARY_DIR}/cdstream.cue + TYPE BIN +) diff --git a/examples/sound/cdstream/iso.xml b/examples/sound/cdstream/iso.xml new file mode 100644 index 0000000..66f1f74 --- /dev/null +++ b/examples/sound/cdstream/iso.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<iso_project + image_name="${CD_IMAGE_NAME}.bin" + cue_sheet="${CD_IMAGE_NAME}.cue" +> + <track type="data"> + <identifiers + system ="PLAYSTATION" + volume ="CDSTREAM" + volume_set ="CDSTREAM" + publisher ="MEIDOTEK" + data_preparer ="PSN00BSDK ${PSN00BSDK_VERSION}" + application ="PLAYSTATION" + copyright ="README.TXT;1" + /> + + <directory_tree> + <file name="SYSTEM.CNF" type="data" source="${PROJECT_SOURCE_DIR}/system.cnf" /> + <file name="CDSTREAM.EXE" type="data" source="cdstream.exe" /> + + <file name="STREAM.VAG" type="data" source="${PROJECT_SOURCE_DIR}/stream.vag" /> + + <dummy sectors="1024"/> + </directory_tree> + </track> + + <!--<track type="audio" source="track2.wav" />--> +</iso_project> diff --git a/examples/sound/cdstream/main.c b/examples/sound/cdstream/main.c new file mode 100644 index 0000000..636ef10 --- /dev/null +++ b/examples/sound/cdstream/main.c @@ -0,0 +1,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(int 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) = 0x80ff; + SPU_CH_ADSR2(i) = 0x1fee; + } + + // 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; +} diff --git a/examples/sound/cdstream/stream.vag b/examples/sound/cdstream/stream.vag Binary files differnew file mode 100644 index 0000000..a6faf74 --- /dev/null +++ b/examples/sound/cdstream/stream.vag diff --git a/examples/sound/cdstream/system.cnf b/examples/sound/cdstream/system.cnf new file mode 100644 index 0000000..11ca055 --- /dev/null +++ b/examples/sound/cdstream/system.cnf @@ -0,0 +1,4 @@ +BOOT=cdrom:\cdstream.exe;1 +TCB=4 +EVENT=10 +STACK=801FFFF0 |
