diff options
| author | spicyjpeg <thatspicyjpeg@gmail.com> | 2022-10-27 16:55:08 +0200 |
|---|---|---|
| committer | spicyjpeg <thatspicyjpeg@gmail.com> | 2022-10-27 16:55:08 +0200 |
| commit | f6c41f3783c4fce49a9899b710ebb50ba9f647ab (patch) | |
| tree | 7133d4e05008cb11c05995d3c0ce7d938258cd51 /examples/sound/spustream | |
| parent | 4dbf47f129a55428b90df2805228fbd481e1d117 (diff) | |
| download | psn00bsdk-f6c41f3783c4fce49a9899b710ebb50ba9f647ab.tar.gz | |
Refactor sound examples, add new spustream example
Diffstat (limited to 'examples/sound/spustream')
| -rw-r--r-- | examples/sound/spustream/CMakeLists.txt | 16 | ||||
| -rw-r--r-- | examples/sound/spustream/convert_stream.py | 112 | ||||
| -rw-r--r-- | examples/sound/spustream/interleave.py | 152 | ||||
| -rw-r--r-- | examples/sound/spustream/iso.xml | 28 | ||||
| -rw-r--r-- | examples/sound/spustream/main.c | 360 | ||||
| -rw-r--r-- | examples/sound/spustream/stream.bin | bin | 4685824 -> 0 bytes | |||
| -rw-r--r-- | examples/sound/spustream/stream.vag | bin | 0 -> 1140736 bytes | |||
| -rw-r--r-- | examples/sound/spustream/system.cnf | 4 |
8 files changed, 293 insertions, 379 deletions
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/iso.xml b/examples/sound/spustream/iso.xml deleted file mode 100644 index 050d673..0000000 --- a/examples/sound/spustream/iso.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?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="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 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 Binary files differdeleted file mode 100644 index e53b726..0000000 --- a/examples/sound/spustream/stream.bin +++ /dev/null diff --git a/examples/sound/spustream/stream.vag b/examples/sound/spustream/stream.vag Binary files differnew file mode 100644 index 0000000..e1cb4f4 --- /dev/null +++ b/examples/sound/spustream/stream.vag diff --git a/examples/sound/spustream/system.cnf b/examples/sound/spustream/system.cnf deleted file mode 100644 index 0c4561a..0000000 --- a/examples/sound/spustream/system.cnf +++ /dev/null @@ -1,4 +0,0 @@ -BOOT=cdrom:\spustrm.exe;1 -TCB=4 -EVENT=10 -STACK=801FFFF0 |
