Soundgen is a Rust workspace for generating 8-bit/chiptune sound effects, UI sounds, and ambient textures for game audio assets. It features JSON-first sound specs, an MCP server for LLM tool-use, an egui GUI editor, and a runtime library with NES-authentic DAC emulation. Features: - 6 voice types: pulse, triangle, noise, DPCM, wavetable, FM - Effects: ADSR envelope, frequency sweep, biquad filter, vibrato - 13 built-in presets (SFX/UI/ambient) as JSON data files - MCP server: list_presets, generate_sfx, render_sound tools - Pattern-based sequencer (JSON song format) - egui GUI: virtual keyboard, preset browser, channel editor, undo/redo - Runtime: NES nonlinear DAC + SoundBank for game embedding - 90 tests, 0 warnings Crates: - soundgen-core: synthesis engine (no I/O) - soundgen-fmt: SoundSpec JSON schema + PresetRegistry - soundgen-io: WAV writer + audio playback - soundgen-seq: sequencer (patterns, songs) - soundgen-cli: gen/render/render-song/list-presets - soundgen-mcp: MCP server for LLM integration - soundgen-gui: egui editor - soundgen-runtime: NES DAC + SoundBank
113 lines
3.9 KiB
Rust
113 lines
3.9 KiB
Rust
//! Example: render a song (JSON) to WAV using the sequencer.
|
|
//!
|
|
//! Song JSON format:
|
|
//! ```json
|
|
//! {
|
|
//! "bpm": 120,
|
|
//! "rows_per_beat": 4,
|
|
//! "tracks": [
|
|
//! { "type": "pulse", "duty": 50, "volume": 0.4 }
|
|
//! ],
|
|
//! "patterns": [
|
|
//! { "rows": [ { "notes": [{"frequency": 440}] }, {"notes": [null]} ] }
|
|
//! ],
|
|
//! "pattern_order": [0]
|
|
//! }
|
|
//! ```
|
|
|
|
use soundgen_io::write_wav;
|
|
use soundgen_seq::{render_song, Song};
|
|
|
|
fn main() {
|
|
// Build a simple chiptune melody programmatically
|
|
let song_json = r#"{
|
|
"bpm": 130,
|
|
"rows_per_beat": 4,
|
|
"sample_rate": 44100,
|
|
"tracks": [
|
|
{
|
|
"type": "pulse", "duty": 50,
|
|
"volume": 0.35, "pan": -0.2,
|
|
"envelope": { "attack": 0.005, "decay": 0.03, "sustain": 0.6, "release": 0.05 }
|
|
},
|
|
{
|
|
"type": "triangle",
|
|
"volume": 0.4, "pan": 0.3,
|
|
"envelope": { "attack": 0.01, "decay": 0.05, "sustain": 0.5, "release": 0.08 }
|
|
},
|
|
{
|
|
"type": "noise", "mode": "white", "frequency": 8000,
|
|
"volume": 0.15, "pan": 0.0,
|
|
"envelope": { "attack": 0.0, "decay": 0.02, "sustain": 0.0, "release": 0.02 }
|
|
}
|
|
],
|
|
"patterns": [
|
|
{
|
|
"rows": [
|
|
{ "notes": [
|
|
{"frequency": 523.25, "velocity": 1.0},
|
|
{"frequency": 261.63, "velocity": 1.0},
|
|
{"frequency": 8000, "velocity": 1.0}
|
|
]},
|
|
{ "notes": [null, null, null] },
|
|
{ "notes": [
|
|
{"frequency": 659.25, "velocity": 1.0},
|
|
{"frequency": 261.63, "velocity": 1.0},
|
|
null
|
|
]},
|
|
{ "notes": [null, null, {"frequency": 8000, "velocity": 1.0}] },
|
|
|
|
{ "notes": [
|
|
{"frequency": 783.99, "velocity": 1.0},
|
|
{"frequency": 329.63, "velocity": 1.0},
|
|
null
|
|
]},
|
|
{ "notes": [null, null, null] },
|
|
{ "notes": [
|
|
{"frequency": 1046.50, "velocity": 1.0},
|
|
{"frequency": 329.63, "velocity": 1.0},
|
|
{"frequency": 8000, "velocity": 1.0}
|
|
]},
|
|
{ "notes": [null, null, null] },
|
|
|
|
{ "notes": [
|
|
{"frequency": 659.25, "velocity": 1.0},
|
|
{"frequency": 196.00, "velocity": 1.0},
|
|
null
|
|
]},
|
|
{ "notes": [null, null, {"frequency": 8000, "velocity": 1.0}] },
|
|
{ "notes": [
|
|
{"frequency": 587.33, "velocity": 1.0},
|
|
{"frequency": 196.00, "velocity": 1.0},
|
|
null
|
|
]},
|
|
{ "notes": [null, null, null] },
|
|
|
|
{ "notes": [
|
|
{"frequency": 523.25, "velocity": 1.0},
|
|
{"frequency": 261.63, "velocity": 1.0},
|
|
{"frequency": 8000, "velocity": 1.0}
|
|
]},
|
|
{ "notes": [null, null, null] },
|
|
{ "notes": [null, null, null] },
|
|
{ "notes": [null, null, null] }
|
|
]
|
|
}
|
|
],
|
|
"pattern_order": [0, 0]
|
|
}"#;
|
|
|
|
let song: Song = serde_json::from_str(song_json).expect("parse song");
|
|
let samples = render_song(&song);
|
|
|
|
let out = std::path::Path::new("render_song.wav");
|
|
write_wav(out, &samples, song.sample_rate).expect("write WAV");
|
|
eprintln!(
|
|
"Wrote {} ({:.1}s, {} patterns, {} tracks)",
|
|
out.display(),
|
|
song.duration(),
|
|
song.pattern_order.len(),
|
|
song.tracks.len()
|
|
);
|
|
}
|