aboutsummaryrefslogtreecommitdiff
path: root/examples
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
parent4dbf47f129a55428b90df2805228fbd481e1d117 (diff)
downloadpsn00bsdk-f6c41f3783c4fce49a9899b710ebb50ba9f647ab.tar.gz
Refactor sound examples, add new spustream example
Diffstat (limited to 'examples')
-rw-r--r--examples/README.md7
-rw-r--r--examples/sound/cdstream/CMakeLists.txt23
-rw-r--r--examples/sound/cdstream/iso.xml (renamed from examples/sound/spustream/iso.xml)8
-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.cnf (renamed from examples/sound/spustream/system.cnf)2
-rw-r--r--examples/sound/spustream/CMakeLists.txt16
-rw-r--r--examples/sound/spustream/convert_stream.py112
-rw-r--r--examples/sound/spustream/interleave.py152
-rw-r--r--examples/sound/spustream/main.c360
-rw-r--r--examples/sound/spustream/stream.binbin4685824 -> 0 bytes
-rw-r--r--examples/sound/spustream/stream.vagbin0 -> 1140736 bytes
-rw-r--r--examples/sound/vagsample/3dfx.vagbin227936 -> 227968 bytes
-rw-r--r--examples/sound/vagsample/main.c455
-rw-r--r--examples/sound/vagsample/proyt.vagbin189264 -> 189248 bytes
15 files changed, 959 insertions, 615 deletions
diff --git a/examples/README.md b/examples/README.md
index ade94b0..ae601f1 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -26,8 +26,9 @@ Additional information may be found in the source code of each example.
| [`lowlevel/cartrom`](./lowlevel/cartrom) | ROM firmware for cheat devices written using GNU GAS | ROM | 4 |
| [`mdec/mdecimage`](./mdec/mdecimage) | Displays a (raw) MDEC format image | EXE | |
| [`mdec/strvideo`](./mdec/strvideo) | Plays a .STR video file using the MDEC | CD | 1 |
-| [`sound/spustream`](./sound/spustream) | Custom (non XA) CD-ROM audio streaming using the SPU | CD | |
-| [`sound/vagsample`](./sound/vagsample) | Demonstrates playing VAG sound files using the SPU | EXE | |
+| [`sound/cdstream`](./sound/cdstream) | Streams an interleaved .VAG file from the CD-ROM | CD | |
+| [`sound/spustream`](./sound/spustream) | Streams an interleaved .VAG file from main RAM | EXE | |
+| [`sound/vagsample`](./sound/vagsample) | Loads and plays .VAG sound files using the SPU | EXE | |
| [`system/childexec`](./system/childexec) | Loading a child program and returning to parent | EXE | |
| [`system/console`](./system/console) | TTY based text console that interrupts gameplay | EXE | |
| [`system/dynlink`](./system/dynlink) | Demonstrates dynamically linked libraries | CD | |
@@ -85,4 +86,4 @@ are for rebuilding the examples *after* the SDK has been installed.
CD images for each example.
-----------------------------------------
-_Last updated on 2022-10-16 by spicyjpeg_
+_Last updated on 2022-10-27 by spicyjpeg_
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/spustream/iso.xml b/examples/sound/cdstream/iso.xml
index 050d673..66f1f74 100644
--- a/examples/sound/spustream/iso.xml
+++ b/examples/sound/cdstream/iso.xml
@@ -6,8 +6,8 @@
<track type="data">
<identifiers
system ="PLAYSTATION"
- volume ="SPUSTREAM"
- volume_set ="SPUSTREAM"
+ volume ="CDSTREAM"
+ volume_set ="CDSTREAM"
publisher ="MEIDOTEK"
data_preparer ="PSN00BSDK ${PSN00BSDK_VERSION}"
application ="PLAYSTATION"
@@ -16,9 +16,9 @@
<directory_tree>
<file name="SYSTEM.CNF" type="data" source="${PROJECT_SOURCE_DIR}/system.cnf" />
- <file name="SPUSTRM.EXE" type="data" source="spustream.exe" />
+ <file name="CDSTREAM.EXE" type="data" source="cdstream.exe" />
- <file name="STREAM.BIN" type="data" source="${PROJECT_SOURCE_DIR}/stream.bin" />
+ <file name="STREAM.VAG" type="data" source="${PROJECT_SOURCE_DIR}/stream.vag" />
<dummy sectors="1024"/>
</directory_tree>
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/spustream/system.cnf b/examples/sound/cdstream/system.cnf
index 0c4561a..11ca055 100644
--- a/examples/sound/spustream/system.cnf
+++ b/examples/sound/cdstream/system.cnf
@@ -1,4 +1,4 @@
-BOOT=cdrom:\spustrm.exe;1
+BOOT=cdrom:\cdstream.exe;1
TCB=4
EVENT=10
STACK=801FFFF0
diff --git a/examples/sound/spustream/CMakeLists.txt b/examples/sound/spustream/CMakeLists.txt
index 63d113b..465e291 100644
--- a/examples/sound/spustream/CMakeLists.txt
+++ b/examples/sound/spustream/CMakeLists.txt
@@ -5,20 +5,16 @@ cmake_minimum_required(VERSION 3.21)
project(
spustream
- LANGUAGES C
+ LANGUAGES C ASM
VERSION 1.0.0
- DESCRIPTION "PSn00bSDK SPU custom streaming example"
+ DESCRIPTION "PSn00bSDK SPU audio streaming example"
HOMEPAGE_URL "http://lameguy64.net/?page=psn00bsdk"
)
-# TODO: add rules to actually generate a valid STREAM.BIN file
file(GLOB _sources *.c)
psn00bsdk_add_executable(spustream GPREL ${_sources})
-psn00bsdk_add_cd_image(spustream_iso spustream iso.xml DEPENDS spustream)
+#psn00bsdk_add_cd_image(spustream_iso spustream iso.xml DEPENDS spustream)
-install(
- FILES
- ${PROJECT_BINARY_DIR}/spustream.bin
- ${PROJECT_BINARY_DIR}/spustream.cue
- TYPE BIN
-)
+psn00bsdk_target_incbin(spustream PRIVATE stream_data stream.vag)
+
+install(FILES ${PROJECT_BINARY_DIR}/spustream.exe TYPE BIN)
diff --git a/examples/sound/spustream/convert_stream.py b/examples/sound/spustream/convert_stream.py
deleted file mode 100644
index 1b1696f..0000000
--- a/examples/sound/spustream/convert_stream.py
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env python3
-# Simple .VAG to STREAM.BIN interleaving tool
-# (C) 2021 spicyjpeg - MPL licensed
-
-import sys
-from warnings import warn
-from struct import Struct
-from itertools import zip_longest
-from argparse import ArgumentParser, FileType
-
-VAG_HEADER = Struct("> 4s I 4x 2I 12x 16s")
-VAG_MAGIC = b"VAGp"
-SAMPLE_RATE = 44100
-BUFFER_SIZE = 26624 # (26624 / 16 * 28) / 44100 = 1.05 seconds
-ALIGN_SIZE = 2048
-
-## Helpers
-
-def align(data, size):
- chunks = (len(data) + size - 1) // size
-
- return data.ljust(chunks * size, b"\x00")
-
-def set_loop_flag(data):
- last_block = bytearray(data[-16:])
- last_block[1] = 0x03 # Jump to loop point + sustain
-
- return data[:-16] + last_block
-
-## .VAG file reader
-
-def read_vag(_file, chunk_size):
- with _file:
- header = _file.read(VAG_HEADER.size)
- (
- magic,
- version,
- size,
- sample_rate,
- name
- ) = VAG_HEADER.unpack(header)
-
- #if magic != VAG_MAGIC:
- #raise RuntimeError(f"{_file.name} is not a valid .VAG file")
- if sample_rate != SAMPLE_RATE:
- warn(RuntimeWarning(f"{_file.name} sample rate is not {SAMPLE_RATE} Hz"))
-
- for i in range(0, size, chunk_size):
- chunk = _file.read(chunk_size)
-
- if len(chunk) % 16:
- warn(RuntimeWarning(f"{_file.name} is not 16-byte aligned, trimming"))
- chunk = chunk[0:len(chunk) // 16 * 16]
-
- chunk = set_loop_flag(chunk)
-
- yield chunk.ljust(chunk_size, b"\x00")
-
-## Main
-
-def get_args():
- parser = ArgumentParser(
- description = "Generates interleaved stream data from one or more .VAG files."
- )
- parser.add_argument(
- "input_file",
- nargs = "+",
- type = FileType("rb"),
- help = f"mono input files for each channel (must be {SAMPLE_RATE} Hz .VAG)"
- )
- parser.add_argument(
- "-o", "--output",
- type = FileType("wb"),
- default = "stream.bin",
- help = "where to output converted stream data (stream.bin by default)",
- metavar = "file"
- )
- parser.add_argument(
- "-b", "--buffer-size",
- type = int,
- default = BUFFER_SIZE,
- help = f"size of each interleaved chunk (one per channel, default {BUFFER_SIZE})",
- metavar = "bytes"
- )
- parser.add_argument(
- "-a", "--align",
- type = int,
- default = ALIGN_SIZE,
- help = f"align each group of chunks to N bytes (default {ALIGN_SIZE})",
- metavar = "bytes"
- )
-
- return parser.parse_args()
-
-def main():
- args = get_args()
- if args.buffer_size % 16:
- raise ValueError("buffer size must be a multiple of 16 bytes")
-
- interleave = zip_longest(
- *( read_vag(_file, args.buffer_size) for _file in args.input_file ),
- fillvalue = b"\x00" * args.buffer_size
- )
-
- with args.output as _file:
- for chunks in interleave:
- data = b"".join(chunks)
-
- _file.write(align(data, args.align))
-
-if __name__ == "__main__":
- main()
diff --git a/examples/sound/spustream/interleave.py b/examples/sound/spustream/interleave.py
new file mode 100644
index 0000000..4e68974
--- /dev/null
+++ b/examples/sound/spustream/interleave.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+# Simple .VAG interleaving tool
+# (C) 2021-2022 spicyjpeg - MPL licensed
+
+import os, sys
+from warnings import warn
+from struct import Struct
+from itertools import zip_longest
+from argparse import ArgumentParser, FileType
+
+VAG_HEADER = Struct("> 4s I 4s 2I 12x 16s")
+VAG_MAGIC = b"VAGp"
+VAGI_MAGIC = b"VAGi"
+VAG_VERSION = 0x20
+BUFFER_SIZE = 0x1000
+CHUNK_ALIGN = 0x800
+
+## Helpers
+
+def align(data, size):
+ chunks = (len(data) + size - 1) // size
+
+ return data.ljust(chunks * size, b"\x00")
+
+def get_loop_offset(data):
+ for index, flag in enumerate(data[1::16]):
+ if flag & 0x01:
+ return index * 16
+
+ return len(data) - 16
+
+## .VAG file reader
+
+class VAGReader:
+ def __init__(self, _file):
+ self.file = _file
+ header = _file.read(VAG_HEADER.size)
+
+ (
+ magic, _, _,
+ self.size,
+ self.sample_rate,
+ self.name
+ ) = VAG_HEADER.unpack(header)
+
+ if magic == VAGI_MAGIC:
+ raise RuntimeError(f"{_file.name} is an interleaved .VAG file (must be mono)")
+ if magic != VAG_MAGIC:
+ raise RuntimeError(f"{_file.name} is not a valid .VAG file")
+
+ def read(self, chunk_size):
+ for _ in range(0, self.size, chunk_size):
+ chunk = self.file.read(chunk_size)
+
+ if len(chunk) < 16:
+ break
+ if len(chunk) % 16:
+ warn(RuntimeWarning(f"{self.file.name} is not 16-byte aligned, trimming"))
+ chunk = chunk[0:(len(chunk) // 16) * 16]
+
+ # If there already is an end flag in the chunk replace it with a
+ # loop flag, otherwise add a new loop flag at the end.
+ end = get_loop_offset(chunk)
+ chunk = bytearray(chunk)
+
+ chunk[end + 1] = 0x03 # Jump to loop point + sustain
+ yield chunk.ljust(chunk_size, b"\x00")
+
+## Main
+
+def get_args():
+ parser = ArgumentParser(
+ description = "Generates interleaved audio stream data from one or more .VAG files."
+ )
+ parser.add_argument(
+ "input_file",
+ nargs = "+",
+ type = FileType("rb"),
+ help = "mono input files for each channel in .VAG format"
+ )
+ parser.add_argument(
+ "output_file",
+ type = FileType("wb"),
+ help = "where to output converted stream data"
+ )
+ parser.add_argument(
+ "-b", "--buffer-size",
+ type = int,
+ default = BUFFER_SIZE,
+ help = f"size of each channel buffer in each chunk (default {BUFFER_SIZE})",
+ metavar = "bytes"
+ )
+ parser.add_argument(
+ "-a", "--align",
+ type = int,
+ default = CHUNK_ALIGN,
+ help = f"pad each chunk to a multiple of the given size (default {CHUNK_ALIGN})",
+ metavar = "bytes"
+ )
+ parser.add_argument(
+ "-r", "--raw",
+ action = "store_true",
+ help = "do not add an interleaved .VAG header to the output file"
+ )
+
+ return parser.parse_args()
+
+def main():
+ args = get_args()
+ if args.buffer_size % 16:
+ raise ValueError("buffer size must be a multiple of 16 bytes")
+ if args.buffer_size % args.align:
+ warn(RuntimeWarning(f"buffer size should be a multiple of {args.align}"))
+
+ input_files = tuple(map(VAGReader, args.input_file))
+ size = input_files[0].size
+ sample_rate = input_files[0].sample_rate
+
+ if (not args.raw) and (len(input_files) != 2):
+ warn(RuntimeWarning("interleaved .VAG only supports stereo (2 input files)"))
+
+ for vag in input_files[1:]:
+ if vag.size != size:
+ warn(RuntimeWarning(f"{vag.file.name} has a different file size"))
+ if vag.sample_rate != sample_rate:
+ warn(RuntimeWarning(f"{vag.file.name} has a different sample rate"))
+
+ interleave = zip_longest(
+ *( vag.read(args.buffer_size) for vag in input_files ),
+ fillvalue = b"\x00" * args.buffer_size
+ )
+
+ with args.output_file as _file:
+ if not args.raw:
+ header = VAG_HEADER.pack(
+ VAGI_MAGIC,
+ VAG_VERSION,
+ args.buffer_size.to_bytes(4, "little"),
+ size,
+ sample_rate,
+ os.path.basename(_file.name).encode()[0:16]
+ )
+
+ _file.write(align(header, args.align))
+
+ for chunks in interleave:
+ data = b"".join(chunks)
+
+ _file.write(align(data, args.align))
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/sound/spustream/main.c b/examples/sound/spustream/main.c
index 1fee883..68cf9b0 100644
--- a/examples/sound/spustream/main.c
+++ b/examples/sound/spustream/main.c
@@ -1,111 +1,60 @@
/*
- * PSn00bSDK SPU audio streaming example
- * (C) 2021 spicyjpeg - MPL licensed
+ * PSn00bSDK SPU .VAG streaming example
+ * (C) 2022 spicyjpeg - MPL licensed
*
- * This example demonstrates how to play a large multi-channel audio file
- * "manually" by streaming it through the SPU, without having to rely on the CD
- * drive's ability to play audio tracks or XA files.
+ * This example shows how to play arbitrarily long sounds, which normally would
+ * not fit into SPU RAM in their entirety, by streaming them to the SPU from
+ * main RAM. In this example audio data is streamed from an in-memory file,
+ * however the code can easily be modified to stream from the CD instead (see
+ * the cdstream example).
*
- * The way this works is by splitting the audio file into a series of ~1 second
- * "chunks", each of which in turn is an array of concatenated buffers holding
- * SPU ADPCM data (one for each channel, so a stereo stream would have 2
- * buffers per chunk). All buffers in a chunk are played simultaneously using
- * multiple SPU channels; each buffer has the loop flag set at the end, so each
- * channel will jump to its loop address (SPU_CH_LOOP_ADDR(n)) once the chunk
- * is played.
+ * The way SPU streaming works is by splitting the audio data into a series of
+ * small "chunks", each of which in turn is an array of concatenated buffers
+ * holding SPU ADPCM data (one for each channel, so a stereo stream would have
+ * 2 buffers per chunk). All buffers in a chunk are played simultaneously using
+ * multiple SPU channels; each buffer has the loop flag set at the end, so the
+ * SPU will jump to the loop point set in the SPU_CH_LOOP_ADDR registers after
+ * the chunk is played.
*
- * Since the loop point doesn't necessarily have to be within the chunk itself,
- * we can abuse it to "queue" another set of buffers to be played immediately
- * after the currently playing chunk. This allows us to fetch a chunk from the
- * CD, upload it to SPU RAM (2048 bytes at a time to avoid having to keep
- * another large buffer in main RAM) and queue it for playback while a
- * previously buffered chunk is playing in the background. SPU RAM always holds
- * two chunks, one of which is played while the other one is buffered. This is
- * the layout used in this example:
+ * As the loop point doesn't necessarily have to be within the chunk itself, it
+ * can be used to "queue" another chunk to be played immediately after the
+ * current one. This allows for double buffering: two chunks are always kept in
+ * SPU RAM and one is overwritten with while the other is playing. Chunks are
+ * laid out in SPU RAM as follows:
*
- * /================================================\
- * | /==================\ |
- * v Loop point | v Loop point |
+ * ________________________________________________
+ * / __________________ \
+ * | / \ |
+ * v Loop point | Loop flag v Loop point | Loop flag
* +-------+----------------+----------------+----------------+----------------+
* | Dummy | Left buffer 0 | Right buffer 0 | Left buffer 1 | Right buffer 1 |
* +-------+----------------+----------------+----------------+----------------+
* \____________Chunk 0____________/ \____________Chunk 1____________/
*
- * It's pretty much the same thing as GPU double buffering (aka page flipping),
- * just with chunks instead of framebuffers.
+ * In order to keep streaming continuously we need to know when each chunk
+ * actually starts playing. The SPU can be configured to trigger an interrupt
+ * whenever a specific address in SPU RAM is read by a channel, so we can just
+ * point it to the beginning of the buffered chunk's first buffer and wait
+ * until the IRQ is fired before loading the next chunk.
*
- * We need to know when the chunk we've buffered actually starts playing in
- * order to start buffering the next one. The SPU can be configured to trigger
- * an interrupt whenever a specific address in SPU RAM is read by a channel, so
- * we can just point it to the beginning of the buffered chunk's first buffer.
- * The interrupt callback will then kick off CD reading and adjust the loop/IRQ
- * addresses to the ones of the chunk that is going to be buffered next.
- *
- * Chunks are read from a STREAM.BIN file which is just a series of sector
- * aligned chunks, arranged as follows:
- *
- * +--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+----
- * | +--------------------------+--------------------------+ |
- * | | Left channel data | Right channel data | Padding | ...
- * | +--------------------------+--------------------------+ |
- * +----------+----------+----------+----------+----------+----------+----
- * \________________________Chunk________________________/
- *
- * A Python script is included to generate STREAM.BIN from one or more SPU
- * ADPCM (.VAG) files, one for each channel (the .VAG format only supports
- * mono).
- *
- * Of course SPU streaming isn't the only way to play music, as the CD drive
- * can play CD-DA tracks and XA files natively with zero CPU overhead. However
- * streaming has a number of advantages over CD audio 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 of course), or even interpolated for
- * effects like tape stops or DJ scratches.
- * - 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. crossfading between tracks (not possible with XA) or controlling
- * volume and panning of each individual instrument.
- * - 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).
- * - Thanks to the idle periods it is possible to seek back to the beginning of
- * the stream and preload the first chunk before the end is reached, allowing
- * the track to be looped seamlessly without having to resort to tricks like
- * filler samples.
- * - Unlike XA, SPU streaming can be used on some PS1-based arcade boards such
- * as the Konami System 573. These systems usually use IDE/SCSI CD drives or
- * flash memory, neither of which supports XA playback.
+ * Chunks are read from a special type of .VAG file which has been interleaved
+ * ahead-of-time and already contains the loop flags required to make streaming
+ * work. A Python script is provided to generate such file from one or more
+ * mono .VAG files.
*/
#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
+#include <stddef.h>
#include <psxetc.h>
#include <psxapi.h>
#include <psxgpu.h>
#include <psxpad.h>
#include <psxspu.h>
-#include <psxcd.h>
#include <hwregs_c.h>
-// To maximize STREAM.BIN packing efficiency and get rid of padding between
-// chunks, buffer size should be a multiple of sector size (2048 bytes). Buffer
-// size can be increased to get more idle time between CD reads, however it is
-// usually best to keep it to 1-2 seconds as SPU RAM is only 512 KB.
-#define SAMPLE_RATE 0x1000 // 44100 Hz
-#define BUFFER_SIZE 0x6800 // (0x6800 / 16 * 28) / 44100 = 1.05 seconds
-
-#define NUM_CHANNELS 2
-#define CHANNEL_MASK 0x03
+extern const uint8_t stream_data[];
-#define SPU_RAM_ADDR(x) ((uint16_t) (((uint32_t) (x)) >> 3))
+#define NUM_CHANNELS 2
/* Display/GPU context utilities */
@@ -167,159 +116,137 @@ void display(RenderContext *ctx) {
SetDispMask(1);
}
-/* Stream interrupt handlers */
+/* .VAG header structure */
-// The first 4 KB of SPU RAM are reserved for capture buffers, so we have to
-// place stream buffers after those. A dummy sample is additionally placed by
-// default by the SPU library at 0x1000; it is going to be used here to keep
-// unused SPU channels busy, preventing them from accidentally triggering the
-// SPU RAM interrupt and throwing off the timing (all channels are always
-// reading sample data, even when "stopped").
+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
-#define CHUNK_SIZE (BUFFER_SIZE * NUM_CHANNELS)
+#define DUMMY_BLOCK_ADDR 0x1000
+#define BUFFER_START_ADDR 0x1010
+
+typedef enum {
+ STATE_IDLE,
+ STATE_BUFFERING
+} StreamState;
typedef struct {
- int lba, length;
+ const uint8_t *data;
+ int buffer_size, num_chunks, sample_rate;
- volatile int pos;
- volatile int spu_addr, spu_pos;
- volatile int db_active;
+ volatile int next_chunk, spu_addr;
+ volatile int8_t db_active, state;
} StreamContext;
static StreamContext str_ctx;
-// This buffer is used by cd_event_handler() as a temporary area for sectors
-// read from the CD and uploaded to SPU RAM. Due to DMA limitations it can't be
-// allocated on the stack (especially not in the interrupt callbacks' stack,
-// whose size is very limited).
-static uint32_t sector_buffer[512];
-
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 buffer is ready.
+ // enable it again once the chunk is ready.
SPU_CTRL &= 0xffbf;
- str_ctx.db_active ^= 1;
- str_ctx.spu_pos = 0;
+ int chunk_size = str_ctx.buffer_size * NUM_CHANNELS;
+ int chunk = (str_ctx.next_chunk + 1) % (uint32_t) str_ctx.num_chunks;
- // Align the sector counter to the size of a chunk (to prevent glitches
- // after seeking) and reset it if it exceeds the stream's length.
- str_ctx.pos %= str_ctx.length;
- str_ctx.pos -= str_ctx.pos % ((CHUNK_SIZE + 2047) / 2048);
+ str_ctx.db_active ^= 1;
+ str_ctx.state = STATE_BUFFERING;
+ str_ctx.next_chunk = chunk;
- // Configure to SPU to trigger an IRQ once the buffer that is going to be
+ // 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
- // buffer rather than actually looping when they encounter the loop flag at
- // the end of the currently playing buffer.
- str_ctx.spu_addr = BUFFER_START_ADDR + CHUNK_SIZE * str_ctx.db_active;
- SPU_IRQ_ADDR = SPU_RAM_ADDR(str_ctx.spu_addr);
+ // 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) = SPU_RAM_ADDR(str_ctx.spu_addr + BUFFER_SIZE * i);
+ SPU_CH_LOOP_ADDR(i) = getSPUAddr(addr + str_ctx.buffer_size * i);
- // Start loading the next chunk. cd_event_handler() will be called
- // repeatedly for each sector until the entire chunk is read.
- CdlLOC pos;
- CdIntToPos(str_ctx.lba + str_ctx.pos, &pos);
- CdControlF(CdlReadN, &pos);
+ // Start uploading the next chunk to the SPU.
+ SpuSetTransferStartAddr(addr);
+ SpuWrite((const uint32_t *) &str_ctx.data[chunk * chunk_size], chunk_size);
}
-void cd_event_handler(int event, uint8_t *payload) {
- // Ignore all events other than a sector being ready.
- // TODO: read errors should be handled properly
- if (event != CdlDataReady)
- return;
-
- // Fetch the sector that has been read from the drive.
- CdGetSector(sector_buffer, 512);
- str_ctx.pos++;
-
- // Set loop flags to make sure the buffer will loop (actually jump to the
- // other buffer, as we're overriding loop addresses) at the end.
- // NOTE: this isn't actually necessary here as the stream converter script
- // already sets these flags in the file.
- /*for (int i = 0; i < NUM_CHANNELS; i++) {
- if (
- str_ctx.spu_pos >= (BUFFER_SIZE * i - 2048) &&
- str_ctx.spu_pos < (BUFFER_SIZE * i)
- )
- sector_buffer[(BUFFER_SIZE * i - str_ctx.spu_pos) - 15] = 0x03;
- }*/
-
- // Copy the sector to SPU RAM, appending it to the buffer that is not
- // playing currently. As the left and right buffers are adjacent, we can
- // just treat the chunk as a single blob of data and copy it as-is; we only
- // have to trim the padding at the end (if any) to avoid overwriting other
- // data in SPU RAM.
- size_t length = CHUNK_SIZE - str_ctx.spu_pos;
- if (length > 2048)
- length = 2048;
-
- SpuSetTransferStartAddr(str_ctx.spu_addr + str_ctx.spu_pos);
- SpuWrite(sector_buffer, length);
- str_ctx.spu_pos += length;
-
- // If the buffer has been filled completely, stop reading and re-enable the
- // SPU IRQ.
- if (str_ctx.spu_pos >= CHUNK_SIZE) {
- CdControlF(CdlPause, 0);
- SPU_CTRL |= 0x0040;
- }
+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;
}
-/* Stream helpers */
+/* 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) {
- SPU_KEY_OFF = 0x00ffffff;
+ SpuSetKey(0, 0x00ffffff);
for (int i = 0; i < 24; i++) {
- SPU_CH_ADDR(i) = SPU_RAM_ADDR(DUMMY_BLOCK_ADDR);
+ SPU_CH_ADDR(i) = getSPUAddr(DUMMY_BLOCK_ADDR);
SPU_CH_FREQ(i) = 0x1000;
}
- SPU_KEY_ON = 0x00ffffff;
+ SpuSetKey(1, 0x00ffffff);
}
-void init_stream(CdlFILE *file) {
+void init_stream(const VAG_Header *vag) {
EnterCriticalSection();
- InterruptCallback(9, &spu_irq_handler);
- CdReadyCallback(&cd_event_handler);
+ InterruptCallback(IRQ_SPU, &spu_irq_handler);
+ DMACallback(DMA_SPU, &spu_dma_handler);
ExitCriticalSection();
- // Configure the CD drive to read 2048-byte sectors at 2x speed.
- uint8_t mode = CdlModeSpeed;
- CdControl(CdlSetmode, (const uint8_t *) &mode, 0);
+ int buf_size = vag->interleave;
+
+ str_ctx.data = &((const uint8_t *) vag)[2048];
+ 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);
- // Set the initial LBA of the stream file, which is going to be incremented
- // as the stream is played.
- str_ctx.lba = CdPosToInt(&(file->pos));
- str_ctx.length = file->size / 2048;
- str_ctx.pos = 0;
+ str_ctx.db_active = 1;
+ str_ctx.next_chunk = -1;
- // Ensure at least one chunk is in SPU RAM by invoking the SPU IRQ handler
+ // Ensure at least one chunk is in SPU RAM by invoking the IRQ handler
// manually and blocking until the chunk has loaded.
- str_ctx.db_active = 1;
spu_irq_handler();
-
- while (str_ctx.spu_pos < CHUNK_SIZE)
+ while (str_ctx.state != STATE_IDLE)
__asm__ volatile("");
}
void start_stream(void) {
- uint32_t addr = BUFFER_START_ADDR + CHUNK_SIZE * str_ctx.db_active;
+ int bits = 0x00ffffff >> (24 - NUM_CHANNELS);
for (int i = 0; i < NUM_CHANNELS; i++) {
- SPU_CH_ADDR(i) = SPU_RAM_ADDR(addr + BUFFER_SIZE * i);
- SPU_CH_FREQ(i) = SAMPLE_RATE;
- SPU_CH_ADSR(i) = 0x1fee80ff;
+ 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
@@ -330,35 +257,31 @@ void start_stream(void) {
SPU_CH_VOL_L(1) = 0x0000;
SPU_CH_VOL_R(1) = 0x3fff;
- SPU_KEY_ON = CHANNEL_MASK;
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) {
- SPU_KEY_OFF = CHANNEL_MASK;
+ int bits = 0x00ffffff >> (24 - NUM_CHANNELS);
+
+ SpuSetKey(0, bits);
for (int i = 0; i < NUM_CHANNELS; i++)
- SPU_CH_ADDR(i) = SPU_RAM_ADDR(DUMMY_BLOCK_ADDR);
+ SPU_CH_ADDR(i) = getSPUAddr(DUMMY_BLOCK_ADDR);
- SPU_KEY_ON = CHANNEL_MASK;
+ 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"); }
-
int main(int argc, const char* argv[]) {
init_context(&ctx);
-
- SHOW_STATUS("INITIALIZING\n");
SpuInit();
- CdInit();
reset_spu_channels();
// Set up controller polling.
@@ -367,34 +290,19 @@ int main(int argc, const char* argv[]) {
StartPAD();
ChangeClearPAD(0);
- SHOW_STATUS("OPENING STREAM FILE\n");
-
- CdlFILE file;
- if (!CdSearchFile(&file, "\\STREAM.BIN"))
- SHOW_ERROR("FAILED TO FIND STREAM.BIN\n");
-
- SHOW_STATUS("BUFFERING STREAM\n");
- init_stream(&file);
+ init_stream((const VAG_Header *) stream_data);
start_stream();
- int paused = 0;
+ int paused = 0, sample_rate = getSPUSampleRate(str_ctx.sample_rate);
- uint16_t sample_rate = SAMPLE_RATE;
uint16_t last_buttons = 0xffff;
while (1) {
FntPrint(-1, "PLAYING SPU STREAM\n\n");
+ FntPrint(-1, "BUFFER: %d\n", str_ctx.db_active);
+ FntPrint(-1, "STATUS: %s\n\n", str_ctx.state ? "BUFFERING" : "IDLE");
- FntPrint(-1, "BUFFER: %d\nSTATUS: ", str_ctx.db_active);
- if (str_ctx.spu_pos >= CHUNK_SIZE)
- FntPrint(-1, "IDLE\n\n");
- else if (str_ctx.spu_pos)
- FntPrint(-1, "BUFFERING\n\n");
- else
- FntPrint(-1, "SEEKING\n\n");
-
- FntPrint(-1, "POSITION: %5d/%5d\n", str_ctx.pos, str_ctx.length);
- FntPrint(-1, "BUFFERED: %5d/%5d\n", str_ctx.spu_pos, CHUNK_SIZE);
+ 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");
@@ -411,7 +319,11 @@ int main(int argc, const char* argv[]) {
PADTYPE *pad = (PADTYPE *) pad_buff[0];
if (pad->stat)
continue;
- if ((pad->type != 4) && (pad->type != 5) && (pad->type != 7))
+ 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)) {
@@ -422,21 +334,19 @@ int main(int argc, const char* argv[]) {
start_stream();
}
- // Seeking by an arbitrary number of sectors isn't a problem as
- // spu_irq_handler() always realigns the counter.
if (!(pad->btn & PAD_LEFT))
- str_ctx.pos -= 16;
+ str_ctx.next_chunk--;
if (!(pad->btn & PAD_RIGHT))
- str_ctx.pos += 16;
+ str_ctx.next_chunk++;
if ((last_buttons & PAD_CIRCLE) && !(pad->btn & PAD_CIRCLE))
- str_ctx.pos = 0;
+ 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 = SAMPLE_RATE;
+ sample_rate = getSPUSampleRate(str_ctx.sample_rate);
// Only set the sample rate registers if necessary.
if (pad->btn != 0xffff) {
diff --git a/examples/sound/spustream/stream.bin b/examples/sound/spustream/stream.bin
deleted file mode 100644
index e53b726..0000000
--- a/examples/sound/spustream/stream.bin
+++ /dev/null
Binary files differ
diff --git a/examples/sound/spustream/stream.vag b/examples/sound/spustream/stream.vag
new file mode 100644
index 0000000..e1cb4f4
--- /dev/null
+++ b/examples/sound/spustream/stream.vag
Binary files differ
diff --git a/examples/sound/vagsample/3dfx.vag b/examples/sound/vagsample/3dfx.vag
index 9284a9a..3a006bc 100644
--- a/examples/sound/vagsample/3dfx.vag
+++ b/examples/sound/vagsample/3dfx.vag
Binary files differ
diff --git a/examples/sound/vagsample/main.c b/examples/sound/vagsample/main.c
index c79e68e..6a60c19 100644
--- a/examples/sound/vagsample/main.c
+++ b/examples/sound/vagsample/main.c
@@ -1,280 +1,215 @@
-/*
- * LibPSn00b Example Programs
+/*
+ * PSn00bSDK SPU .VAG playback example
+ * (C) 2021-2022 Lameguy64, spicyjpeg - MPL licensed
*
- * VAG Playback Example
- * 2019-2021 Meido-Tek Productions / PSn00bSDK Project
+ * This example demonstrates basic usage of the SPU. Two mono audio samples (in
+ * the standard PS1 .VAG format) are uploaded from main memory to SPU RAM and
+ * played on one of the 24 channels by manipulating the SPU's registers. The
+ * .VAG header is parsed to obtain the sample rate and data size, while the
+ * actual audio data does not need any processing as it is already encoded in
+ * the ADPCM format expected by the SPU.
*
- * This example program demonstrates the basic use of the SPU; uploading sound
- * clips to SPU RAM and playing it back on one of 24 SPU voices (and possibly
- * leave you with ears ringing from the cacophony).
- *
- * The PS1 SPU only supports playing back of specially encoded ADPCM samples
- * natively and can play them at sample rates of up to 44.1KHz, so sound files
- * will have to be converted to 'VAG' format before it can be used on the PS1.
- * While it is possible to play plain PCM samples on the SPU, this requires
- * some special trickery that involves abusing the echo buffer and is not
- * supported by the (half-baked) SPU library of PSn00bSDK.
- *
- * Additionally, the SPU can only play ADPCM samples from its own local memory
- * called the SPU RAM, so sound samples will have to be uploaded to SPU RAM
- * before it can be played by the SPU.
- *
- * The included sound clips are by HighTreason610 (0proyt) and
- * Lameguy64 (threedeeffeggzz) respectively.
- *
- * Example by Lameguy64
- *
- *
- * Changelog:
- *
- * October 6, 2021 - Initial version
+ * Note that PSn00bSDK does not yet provide any tool for SPU ADPCM encoding, so
+ * you will have to use an external program to convert your samples to .VAG.
*
+ * The included sound clips are by HighTreason610 (proyt.vag) and Lameguy64
+ * (3dfx.vag) respectively.
*/
-
-#include <stdio.h>
+
#include <stdint.h>
-#include <psxetc.h>
-#include <psxgte.h>
#include <psxgpu.h>
-#include <psxpad.h>
#include <psxapi.h>
+#include <psxpad.h>
#include <psxspu.h>
#include <hwregs_c.h>
-extern const unsigned char proyt[];
-extern const int proyt_size;
-extern const unsigned char tdfx[];
-extern const int tdfx_size;
-
-// Define display/draw environments for double buffering
-DISPENV disp[2];
-DRAWENV draw[2];
-int db;
-
-unsigned char pad_buff[2][34];
-
-// SPU addresses of the uploaded sound clips
-int proyt_addr;
-int tdfx_addr;
-
-// Init function
-void init(void)
-{
- int addr_temp;
-
- // This not only resets the GPU but it also installs the library's
- // ISR subsystem to the kernel
+extern const uint8_t proyt[];
+extern const uint8_t tdfx[];
+
+/* 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);
-
- // Define display environments, first on top and second on bottom
- SetDefDispEnv(&disp[0], 0, 0, 320, 240);
- SetDefDispEnv(&disp[1], 0, 240, 320, 240);
-
- // Define drawing environments, first on bottom and second on top
- SetDefDrawEnv(&draw[0], 0, 240, 320, 240);
- SetDefDrawEnv(&draw[1], 0, 0, 320, 240);
-
- // Set and enable clear color
- setRGB0(&draw[0], 0, 96, 0);
- setRGB0(&draw[1], 0, 96, 0);
- draw[0].isbg = 1;
- draw[1].isbg = 1;
-
- // Clear double buffer counter
- db = 0;
-
- // Apply the GPU environments
- PutDispEnv(&disp[db]);
- PutDrawEnv(&draw[db]);
-
- // Load test font
+ 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);
-
- // Open up a test font text stream of 100 characters
- FntOpen(0, 8, 320, 224, 0, 100);
-
- // Initialize the SPU
- SpuInit();
-
- // Set SPU transfer mode to DMA (only mode currently supported)
+ 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; // 0x70474156 ("VAGp") for mono files
+ uint32_t version;
+ 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];
+ 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) \
+)
+
+/* Helper functions */
+
+// 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
+// samples must be placed after those.
+#define ALLOC_START_ADDR 0x1010
+
+static int next_channel = 0;
+static int next_sample_addr = ALLOC_START_ADDR;
+
+int upload_sample(const void *data, int size) {
+ // Round the size up to the nearest multiple of 64, as SPU DMA transfers
+ // are done in 64-byte blocks.
+ int _addr = next_sample_addr;
+ int _size = (size + 63) & 0xffffffc0;
+
SpuSetTransferMode(SPU_TRANSFER_BY_DMA);
-
- // Set SPU transfer address (start address for sample upload)
- addr_temp = 0x1000;
- SpuSetTransferStartAddr(addr_temp);
-
- // Upload first sound clip and wait for transfer to finish
- SpuWrite((const uint32_t *) &proyt[48], proyt_size-48);
- SpuIsTransferCompleted(SPU_TRANSFER_WAIT);
-
- // Obtain the address of the sound and advance address for the next one
- // Samples are addressed in 8-byte units, so it'll have to be divided by 8
- proyt_addr = addr_temp/8;
- addr_temp += proyt_size-48;
-
- printf("proyt.vag\t= %02x\n", proyt_addr);
-
- // Upload second sound clip
- SpuSetTransferStartAddr(addr_temp);
- SpuWrite((const uint32_t *) &tdfx[48], tdfx_size-48);
+ SpuSetTransferStartAddr(_addr);
+
+ SpuWrite((const uint32_t *) data, _size);
SpuIsTransferCompleted(SPU_TRANSFER_WAIT);
-
- // Obtain the address of the second sound clip
- tdfx_addr = addr_temp/8;
- addr_temp += tdfx_size-48;
-
- printf("3dfx.vag\t= %02x\n", tdfx_addr);
-
- // Begin pad polling
- InitPAD( pad_buff[0], 34, pad_buff[1], 34 );
+
+ next_sample_addr = _addr + _size;
+ return _addr;
+}
+
+void play_sample(int addr, int sample_rate) {
+ int ch = next_channel;
+
+ // Make sure the channel is stopped.
+ SpuSetKey(0, 1 << ch);
+
+ // Set the channel's sample rate and start address. Note that the SPU
+ // expects the sample rate to be in 4.12 fixed point format (with
+ // 1.0 = 44100 Hz) and the address in 8-byte units; psxspu.h provides the
+ // getSPUSampleRate() and getSPUAddr() macros to convert values to these
+ // units.
+ SPU_CH_FREQ(ch) = getSPUSampleRate(sample_rate);
+ SPU_CH_ADDR(ch) = getSPUAddr(addr);
+
+ // Set the channel's volume and ADSR parameters (0x80ff and 0x1fee are
+ // dummy values that disable the ADSR envelope entirely).
+ SPU_CH_VOL_L(ch) = 0x3fff;
+ SPU_CH_VOL_R(ch) = 0x3fff;
+ SPU_CH_ADSR1(ch) = 0x80ff;
+ SPU_CH_ADSR2(ch) = 0x1fee;
+
+ // Start the channel.
+ SpuSetKey(1, 1 << ch);
+
+ next_channel = (ch + 1) % 24;
+}
+
+/* Main */
+
+static RenderContext ctx;
+
+int main(int argc, const char* argv[]) {
+ init_context(&ctx);
+ SpuInit();
+
+ // Upload the samples to the SPU and parse their headers.
+ VAG_Header *proyt_vag = (VAG_Header *) proyt;
+ VAG_Header *tdfx_vag = (VAG_Header *) tdfx;
+
+ int proyt_addr = upload_sample(&proyt_vag[1], SWAP_ENDIAN(proyt_vag->size));
+ int tdfx_addr = upload_sample(&tdfx_vag[1], SWAP_ENDIAN(tdfx_vag->size));
+ int proyt_sr = SWAP_ENDIAN(proyt_vag->sample_rate);
+ int tdfx_sr = SWAP_ENDIAN(tdfx_vag->sample_rate);
+
+ // Set up controller polling.
+ uint8_t pad_buff[2][34];
+ InitPAD(pad_buff[0], 34, pad_buff[1], 34);
StartPAD();
ChangeClearPAD(0);
-} /* init */
-
-// Display function
-void display(void)
-{
- // Flip buffer index
- db = !db;
-
- // Wait for all drawing to complete
- DrawSync(0);
-
- // Wait for vertical sync to cap the logic to 60fps (or 50 in PAL mode)
- // and prevent screen tearing
- VSync(0);
- // Switch pages
- PutDispEnv(&disp[db]);
- PutDrawEnv(&draw[db]);
-
- // Enable display output, ResetGraph() disables it by default
- SetDispMask(1);
-
-} /* main */
-
-// Main function, program entrypoint
-int main(int argc, const char *argv[])
-{
- int counter,nextchan;
- int cross_pressed;
- int circle_pressed;
- PADTYPE *pad;
-
- // Init stuff
- init();
-
- // Main loop
- counter = 0;
- nextchan = 0;
- cross_pressed = 0;
- circle_pressed = 0;
-
- while(1)
- {
- pad = (PADTYPE*)&pad_buff[0][0];
-
- if( pad->stat == 0 )
- {
- // For digital pad, dual-analog and dual-shock
- if( ( pad->type == 0x4 ) || ( pad->type == 0x5 ) || ( pad->type == 0x7 ) )
- {
- // Plays the first sound
- if( !(pad->btn&PAD_CROSS) )
- {
- if( !cross_pressed )
- {
- // Voice frequency
- // (800h = 22.05KHz)
- SPU_CH_FREQ(nextchan) = 0x800;
- // Voice start playback address
- // (transfer address / 8)
- SPU_CH_ADDR(nextchan) = proyt_addr;
- // Voice loop address
- // (transfer address / 8)
- SPU_CH_LOOP_ADDR(nextchan) = proyt_addr;
- // Voice volume and envelope
- SPU_CH_VOL_L(nextchan) = 0x3fff;
- SPU_CH_VOL_R(nextchan) = 0x3fff;
- SPU_CH_ADSR(nextchan) = 0x1fee80ff;
-
- // Set voice to key-off to allow restart
- SPU_KEY_OFF = 1 << nextchan;
- // Set voice to key-on
- SPU_KEY_ON = 1 << nextchan;
-
- // Advance to next voice
- nextchan++;
- if( nextchan > 23 )
- nextchan = 0;
-
- cross_pressed = 1;
- }
- }
- else
- {
- cross_pressed = 0;
- }
-
- // Plays the second sound
- if( !(pad->btn&PAD_CIRCLE) )
- {
- if( !circle_pressed )
- {
- // Voice frequency
- // (1000h = 44.1KHz)
- SPU_CH_FREQ(nextchan) = 0x1000;
- // Voice start playback address
- // (transfer address / 8)
- SPU_CH_ADDR(nextchan) = tdfx_addr;
- // Voice loop address
- // (transfer address / 8)
- SPU_CH_LOOP_ADDR(nextchan) = tdfx_addr;
- // Voice volume and envelope
- SPU_CH_VOL_L(nextchan) = 0x3fff;
- SPU_CH_VOL_R(nextchan) = 0x3fff;
- SPU_CH_ADSR(nextchan) = 0x1fee80ff;
-
- // Set voice to key-off to allow restart
- SPU_KEY_OFF = 1 << nextchan;
- // Set voice to key-on
- SPU_KEY_ON = 1 << nextchan;
-
- // Advance to next voice
- nextchan++;
- if( nextchan > 23 )
- nextchan = 0;
-
- circle_pressed = 1;
- }
- }
- else
- {
- circle_pressed = 0;
- }
- }
- }
- else
- {
- cross_pressed = 0;
- circle_pressed = 0;
- }
-
- // Print the obligatory hello world and counter to show that the
- // program isn't locking up to the last created text stream
- FntPrint(-1, "VAG SAMPLE - PRESS X OR O TO PLAY\n");
- FntPrint(-1, "COUNTER=%d\n", counter);
-
- // Draw the last created text stream
+ uint16_t last_buttons = 0xffff;
+
+ while (1) {
+ FntPrint(-1, "SPU SAMPLE PLAYBACK DEMO\n\n");
+ FntPrint(-1, "[X] PLAY FIRST SAMPLE\n");
+ FntPrint(-1, "[O] PLAY SECOND SAMPLE\n");
+
FntFlush(-1);
-
- // Update display
- display();
-
- // Increment the counter
- counter++;
+ 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_CROSS) && !(pad->btn & PAD_CROSS))
+ play_sample(proyt_addr, proyt_sr);
+ if ((last_buttons & PAD_CIRCLE) && !(pad->btn & PAD_CIRCLE))
+ play_sample(tdfx_addr, tdfx_sr);
+
+ last_buttons = pad->btn;
}
-
+
return 0;
-
-} /* main */
+}
diff --git a/examples/sound/vagsample/proyt.vag b/examples/sound/vagsample/proyt.vag
index 663828d..b8d68d6 100644
--- a/examples/sound/vagsample/proyt.vag
+++ b/examples/sound/vagsample/proyt.vag
Binary files differ