diff options
| author | spicyjpeg <88942473+spicyjpeg@users.noreply.github.com> | 2021-12-23 21:58:56 +0100 |
|---|---|---|
| committer | spicyjpeg <88942473+spicyjpeg@users.noreply.github.com> | 2021-12-23 21:58:56 +0100 |
| commit | 6c7512ff42805e9399dfee8b67e2f70fa55909d3 (patch) | |
| tree | 946aedf4b4bc1dc548ba82430274150c32ec6e07 /examples | |
| parent | 31fba94b8e8c15c1d127925f01a38efc68933fd1 (diff) | |
| download | psn00bsdk-6c7512ff42805e9399dfee8b67e2f70fa55909d3.tar.gz | |
Add sound/spustream example
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/sound/spustream/CMakeLists.txt | 29 | ||||
| -rw-r--r-- | examples/sound/spustream/convert_stream.py | 112 | ||||
| -rw-r--r-- | examples/sound/spustream/iso.xml | 29 | ||||
| -rw-r--r-- | examples/sound/spustream/main.c | 456 | ||||
| -rw-r--r-- | examples/sound/spustream/system.cnf | 4 |
5 files changed, 630 insertions, 0 deletions
diff --git a/examples/sound/spustream/CMakeLists.txt b/examples/sound/spustream/CMakeLists.txt new file mode 100644 index 0000000..91243cf --- /dev/null +++ b/examples/sound/spustream/CMakeLists.txt @@ -0,0 +1,29 @@ +# PSn00bSDK example CMake script +# (C) 2021 spicyjpeg - MPL licensed + +cmake_minimum_required(VERSION 3.20) + +if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) + set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) +endif() + +project( + spustream + LANGUAGES C + VERSION 1.0.0 + DESCRIPTION "PSn00bSDK SPU custom 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 STATIC ${_sources}) +#psn00bsdk_add_cd_image(spustream_iso spustream iso.xml DEPENDS spustream) + +install( + FILES + #${PROJECT_BINARY_DIR}/spustream.bin + #${PROJECT_BINARY_DIR}/spustream.cue + ${PROJECT_BINARY_DIR}/spustream.exe + TYPE BIN +) diff --git a/examples/sound/spustream/convert_stream.py b/examples/sound/spustream/convert_stream.py new file mode 100644 index 0000000..1b1696f --- /dev/null +++ b/examples/sound/spustream/convert_stream.py @@ -0,0 +1,112 @@ +#!/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/iso.xml b/examples/sound/spustream/iso.xml new file mode 100644 index 0000000..3807046 --- /dev/null +++ b/examples/sound/spustream/iso.xml @@ -0,0 +1,29 @@ +<?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 ="SPUSTREAM" + volume_set ="SPUSTREAM" + 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="SPUSTRM.EXE" type="data" source="spustream.exe" /> + <file name="SPUSTRM.MAP" type="data" source="spustream.map" /> + + <file name="STREAM.BIN" type="data" source="${PROJECT_SOURCE_DIR}/stream.bin" /> + + <dummy sectors="1024"/> + </directory_tree> + </track> + + <!--<track type="audio" source="track2.wav" />--> +</iso_project> diff --git a/examples/sound/spustream/main.c b/examples/sound/spustream/main.c new file mode 100644 index 0000000..2032df5 --- /dev/null +++ b/examples/sound/spustream/main.c @@ -0,0 +1,456 @@ +/* + * PSn00bSDK SPU audio streaming example + * (C) 2021 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. + * + * 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_CHANNELS[n].loop_addr) once 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: + * + * /================================================\ + * | /==================\ | + * v Loop point | v Loop point | + * +-------+----------------+----------------+----------------+----------------+ + * | 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. + * + * 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________________________/ + * + * Such file isn't provided as PSn00bSDK doesn't yet have a tool for audio + * transcoding. 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. + */ + +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <psxetc.h> +#include <psxapi.h> +#include <psxgpu.h> +#include <psxpad.h> +#include <psxspu.h> +#include <psxcd.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 26624 // (26624 / 16 * 28) / 44100 = 1.05 seconds + +#define NUM_CHANNELS 2 +#define CHANNEL_MASK 0x03 + +/* Register definitions */ + +// For some reason SpuVoiceRaw doesn't actually match the layout of SPU +// registers, so here we go. +typedef struct { + uint16_t vol_left; + uint16_t vol_right; + uint16_t freq; + uint16_t addr; + uint32_t adsr_param; + uint16_t _reserved; + uint16_t loop_addr; +} SPUCHANNEL; + +#define SPU_CTRL *((volatile uint16_t *) 0x1f801daa) +#define SPU_IRQ_ADDR *((volatile uint16_t *) 0x1f801da4) +#define SPU_KEY_ON *((volatile uint32_t *) 0x1f801d88) +#define SPU_KEY_OFF *((volatile uint32_t *) 0x1f801d8c) + +// SPU RAM is addressed in 8-byte units, using 16-bit pointers. +#define SPU_CHANNELS ((volatile SPUCHANNEL *) 0x1f801c00) +#define SPU_RAM_ADDR(x) ((uint16_t) (((uint32_t) (x)) >> 3)) + +/* 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; +} DB; + +typedef struct { + DB db[2]; + uint32_t db_active; +} CONTEXT; + +void init_context(CONTEXT *ctx) { + DB *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(CONTEXT *ctx) { + DB *db; + + DrawSync(0); + VSync(0); + ctx->db_active ^= 1; + + db = &(ctx->db[ctx->db_active]); + PutDrawEnv(&(db->draw)); + PutDispEnv(&(db->disp)); + SetDispMask(1); +} + +/* Stream interrupt handlers */ + +// This is a silent looping sample used 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"). It is 64 bytes as that is the minimum size for SPU DMA +// transfers, however only the first 16 bytes are kept. The rest is going to be +// overwritten by chunks. +// https://problemkaputt.de/psx-spx.htm#spuinterrupt +const uint8_t SPU_DUMMY_BLOCK[] = { + 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +// The first 4 KB of SPU RAM are reserved for capture buffers, so we have to +// place stream buffers after those. Sony's SPU library additionally places a +// dummy sample at 0x1000; we are going to do the same with the block above. +#define DUMMY_BLOCK_ADDR 0x1000 +#define BUFFER_START_ADDR 0x1010 +#define CHUNK_SIZE (BUFFER_SIZE * NUM_CHANNELS) + +typedef struct { + uint32_t lba; + uint32_t length; + uint32_t pos; + + uint32_t spu_addr; + uint32_t spu_pos; + uint32_t db_active; +} STREAMCONTEXT; + +static volatile 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 uint8_t sector_buffer[2048]; + +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. + SPU_CTRL &= 0xffbf; + + str_ctx.db_active ^= 1; + str_ctx.spu_pos = 0; + + // 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); + + // Configure to SPU to trigger an IRQ once the buffer 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); + + for (uint32_t i = 0; i < NUM_CHANNELS; i++) + SPU_CHANNELS[i].loop_addr = SPU_RAM_ADDR(str_ctx.spu_addr + 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); +} + +void cd_event_handler(int32_t 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 (uint32_t i = 0; i < NUM_CHANNELS; i++) { + if ( + str_ctx.spu_pos >= (BUFFER_SIZE * i - 2048) && + str_ctx.spu_pos < (BUFFER_SIZE * i) + ) + sector[(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. + uint32_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. + // TODO TODO: preload first sector + if (str_ctx.spu_pos >= CHUNK_SIZE) { + CdControlF(CdlPause, 0); + SPU_CTRL |= 0x0040; + } +} + +/* Stream helpers */ + +void init_spu_channels(void) { + // Upload the dummy block to the SPU and play it on all channels, locking + // them up and stopping them from messing with the SPU interrupt. + // TODO: is this really necessary? (needs testing on real hardware) + SpuSetTransferStartAddr(DUMMY_BLOCK_ADDR); + SpuWrite(SPU_DUMMY_BLOCK, 64); + + SPU_KEY_OFF = 0x00ffffff; + + for (uint32_t i = 0; i < 24; i++) + SPU_CHANNELS[i].addr = SPU_RAM_ADDR(DUMMY_BLOCK_ADDR); + + SPU_KEY_ON = 0x00ffffff; +} + +void init_stream(CdlFILE *file) { + EnterCriticalSection(); + InterruptCallback(9, &spu_irq_handler); + CdReadyCallback(&cd_event_handler); + ExitCriticalSection(); + + // 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; + + // Ensure at least one chunk is in SPU RAM by invoking the SPU 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) + __asm__("nop"); +} + +void start_stream(void) { + SPU_KEY_OFF = CHANNEL_MASK; + + for (uint32_t i = 0; i < NUM_CHANNELS; i++) { + SPU_CHANNELS[i].addr = SPU_RAM_ADDR(BUFFER_START_ADDR + BUFFER_SIZE * i); + SPU_CHANNELS[i].freq = SAMPLE_RATE; + SPU_CHANNELS[i].adsr_param = 0xdfee80ff; // or 0x9fc080ff, 0xdff18087 + } + + // 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_CHANNELS[0].vol_left = 0x3fff; + SPU_CHANNELS[0].vol_right = 0x0000; + SPU_CHANNELS[1].vol_left = 0x0000; + SPU_CHANNELS[1].vol_right = 0x3fff; + + SPU_KEY_ON = CHANNEL_MASK; + spu_irq_handler(); +} + +/* Main */ + +static CONTEXT 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(); + init_spu_channels(); + + SHOW_STATUS("LOCATING 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); + start_stream(); + + // Set up controller polling. + uint8_t pad_buff[2][34]; + InitPAD(pad_buff[0], 34, pad_buff[1], 34); + StartPAD(); + ChangeClearPAD(0); + + uint16_t sample_rate = SAMPLE_RATE; + uint16_t last_buttons = 0xffff; + + while (1) { + FntPrint(-1, "PLAYING SPU STREAM\n"); + if (str_ctx.spu_pos >= CHUNK_SIZE) + FntPrint(-1, "STATUS: IDLE\n\n"); + else if (!str_ctx.spu_pos) + FntPrint(-1, "STATUS: SEEKING\n\n"); + else + FntPrint(-1, "STATUS: BUFFERING\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, "SMP RATE=%5d HZ\n\n", (sample_rate * 44100) >> 12); + + 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 != 4) && (pad->type != 5) && (pad->type != 7)) + continue; + + // 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; + if (!(pad->btn & PAD_RIGHT)) + str_ctx.pos += 16; + if ((last_buttons & PAD_CIRCLE) && !(pad->btn & PAD_CIRCLE)) + str_ctx.pos = 0; + + if (!(pad->btn & PAD_DOWN) && (sample_rate > 0x400)) + sample_rate -= 0x40; + if (!(pad->btn & PAD_UP) && (sample_rate < 0x2000)) + sample_rate += 0x40; + if ((last_buttons & PAD_CROSS) && !(pad->btn & PAD_CROSS)) + sample_rate = SAMPLE_RATE; + + // Only set the sample rate registers if necessary. + if (pad->btn != 0xffff) { + for (uint32_t i = 0; i < NUM_CHANNELS; i++) + SPU_CHANNELS[i].freq = sample_rate; + } + + last_buttons = pad->btn; + } + + return 0; +} diff --git a/examples/sound/spustream/system.cnf b/examples/sound/spustream/system.cnf new file mode 100644 index 0000000..0c4561a --- /dev/null +++ b/examples/sound/spustream/system.cnf @@ -0,0 +1,4 @@ +BOOT=cdrom:\spustrm.exe;1 +TCB=4 +EVENT=10 +STACK=801FFFF0 |
