aboutsummaryrefslogtreecommitdiff
path: root/examples/sound/cdstream
diff options
context:
space:
mode:
authorspicyjpeg <thatspicyjpeg@gmail.com>2022-10-27 16:55:08 +0200
committerspicyjpeg <thatspicyjpeg@gmail.com>2022-10-27 16:55:08 +0200
commitf6c41f3783c4fce49a9899b710ebb50ba9f647ab (patch)
tree7133d4e05008cb11c05995d3c0ce7d938258cd51 /examples/sound/cdstream
parent4dbf47f129a55428b90df2805228fbd481e1d117 (diff)
downloadpsn00bsdk-f6c41f3783c4fce49a9899b710ebb50ba9f647ab.tar.gz
Refactor sound examples, add new spustream example
Diffstat (limited to 'examples/sound/cdstream')
-rw-r--r--examples/sound/cdstream/CMakeLists.txt23
-rw-r--r--examples/sound/cdstream/iso.xml28
-rw-r--r--examples/sound/cdstream/main.c439
-rw-r--r--examples/sound/cdstream/stream.vagbin0 -> 4646912 bytes
-rw-r--r--examples/sound/cdstream/system.cnf4
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
new file mode 100644
index 0000000..a6faf74
--- /dev/null
+++ b/examples/sound/cdstream/stream.vag
Binary files differ
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