soundlog
soundlog — builder, parser and stream-processor for retro sound-chip register-write logs
soundlog is a small crate for building and parsing register-write
logs for retro sound chips. It currently supports the VGM
(Video Game Music) file format.
Key features:
- Builder API to construct VGM documents programmatically.
- Parser support to read VGM data into a structured
VgmDocument. - Type-safe APIs: chip specifications and VGM commands are modeled as Rust types to help prevent invalid register writes at compile time.
- Stream processing:
VgmStreamprovides a low-memory, iterator-based processor that can accept either chunked binary input (viapush_chunk) or a pre-parsedVgmDocument(viafrom_document) and yields parsedVgmCommandvalues as they become available. - Memory limits: Configurable limits for data block accumulation (default 32 MiB) and parsing buffer size (default 64 MiB) prevent unbounded memory growth from untrusted input.
VgmStream overview
VgmStream is designed for streaming/real-time consumption of VGM data:
- It yields
VgmCommandvalues wrapped in stream results as it parses input and as it generates writes from DAC streams. - It understands DAC stream control commands (e.g.
SetupStreamControl,SetStreamData,SetStreamFrequency,StartStream,StartStreamFastCall,StopStream) and will expand stream-generated writes into the output timeline at the correct sample positions. - It also supports YM2612 direct DAC writes and expands them into corresponding
Ym2612Port0Address2AWriteAndWaitNcommands on the stream timeline. - During
Waitcommands, the internal scheduler finds upcoming stream- generated writes and splits waits as necessary so that generated chip writes are interleaved with parsed commands. This avoids emitting large bursts and preserves per-sample timing when multiple DAC streams are active concurrently. - DataBlock compression (e.g. bit-packed and DPCM streams) is automatically decompressed and expanded by the crate so compressed streams and their associated decompression tables are applied transparently.
- Memory limits are enforced to protect against malicious or malformed files:
- Data block size limit (default 32 MiB, configurable via
set_max_data_block_size()) - Parsing buffer size limit (default 64 MiB, configurable via
set_max_buffer_size())
- Data block size limit (default 32 MiB, configurable via
Examples
VgmBuilder as builder
use ;
use ;
use ;
use Gd3;
let mut builder = new;
// Register the chip's master clock in the VGM header (in Hz)
builder.register_chip;
// Append chip register writes using a chip-specific spec
builder.add_chip_write;
// Append a VGM command (example: wait)
builder.add_vgm_command;
// ... add more commands
// Set GD3 metadata for the document
builder.set_gd3;
// Finalize the document
let document: VgmDocument = builder.finalize;
// `into()` converts the finalized `VgmDocument` into VGM-format binary bytes
let bytes: = document.into;
VgmDocument as parser
use ;
use ;
// Read VGM bytes from somewhere
let bytes: = /* read a .vgm file */ Vecnew;
// For this example we construct a VGM byte sequence using the builder
// and then parse it back.
let mut b = new;
b.add_vgm_command;
b.add_vgm_command;
let doc = b.finalize;
let bytes: = .into;
// Parse the bytes into a `VgmDocument`
let document: VgmDocument =
.try_into
.expect;
// Example: map commands to their sample counts and sum them.
let total_wait: u32 = document
.iter
.map
.sum;
assert_eq!;
VgmStream::from_document
The from_document constructor is convenient when you already have a
parsed VgmDocument (for example: constructed programmatically via the
VgmBuilder). The stream will expand DAC-stream-generated writes into
the emitted command sequence and split waits so emitted writes are
interleaved at the correct sample positions. All wait commands
(WaitSamples, WaitNSample, Wait735Samples, Wait882Samples) are converted
to WaitSamples for consistent processing.
use ;
use StreamResult;
use ;
use Ym2612Spec;
use ;
// Build a minimal document that contains a data block and stream control
// commands. (Builder helpers for data blocks / stream setup exist on the
// `VgmBuilder` type; see the vgm module docs for details.)
let mut b = new;
// Example: append a YM2612 chip register write using the chip-specific spec
b.add_chip_write;
// (pseudo-code) append data block, configure stream and start it
// b.add_data_block(...);
// b.add_vgm_command(SetupStreamControl { /* ... */ });
// b.add_vgm_command(StartStream { /* ... */ });
b.add_vgm_command;
b.add_vgm_command; // 6 samples (5+1)
b.add_vgm_command; // 735 samples
b.add_vgm_command; // 882 samples
let doc: VgmDocument = b.finalize;
// Create a stream from the parsed document. The iterator will yield
// parsed commands as well as any stream-generated writes expanded into
// the timeline.
let mut stream = from_document;
stream.set_loop_count; // Prevent infinite loops
while let Some = stream.next
VgmStream — feeding raw byte chunks
Note: apart from providing input via push_chunk, handling the stream is the same as the from_document example above — iterate over the stream and handle StreamResult variants (Command, NeedsMoreData, EndOfStream, Err) in the same way.
Important: Always set a loop count limit for untrusted input to prevent infinite loops.
use VgmStream;
use StreamResult;
let mut parser = new;
parser.set_loop_count; // Prevent infinite loops
let chunks = vec!;
for chunk in chunks
License
MIT License