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
92 lines
2.9 KiB
Rust
92 lines
2.9 KiB
Rust
//! Example: render a simple melody (pulse lead + triangle bass + noise hihat) to WAV.
|
|
|
|
use soundgen_core::{
|
|
voice::{DutyCycle, NoiseChannel, NoiseMode, PulseChannel, TriangleChannel},
|
|
ChannelRenderer, Envelope, VoiceKind,
|
|
};
|
|
use soundgen_io::write_wav;
|
|
|
|
fn note_freq(n: &str) -> f32 {
|
|
// Note name to frequency: e.g., "A4" = 440 Hz
|
|
let notes = [
|
|
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
|
|
];
|
|
let note = &n[..n.len() - 1];
|
|
let octave: i32 = n[n.len() - 1..].parse().unwrap();
|
|
let semitone = notes.iter().position(|&x| x == note).unwrap() as i32;
|
|
let midi = 12 * (octave + 1) + semitone;
|
|
440.0 * 2.0f32.powf((midi - 69) as f32 / 12.0)
|
|
}
|
|
|
|
fn main() {
|
|
let sr = 44100u32;
|
|
let bpm = 120.0;
|
|
let beat = 60.0 / bpm; // seconds per beat
|
|
let total_beats = 8.0;
|
|
let total_samples = (beat * total_beats * sr as f32) as usize;
|
|
|
|
// Melody: C4 E4 G4 C5 G4 E4 C4 G3
|
|
let melody = ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "G3"];
|
|
// Bass: C3 C3 G3 G3 A2 A2 F2 F2
|
|
let bass = ["C3", "C3", "G3", "G3", "A2", "A2", "F2", "F2"];
|
|
|
|
let samples_per_note = (beat * sr as f32) as usize;
|
|
|
|
let mut stereo = Vec::with_capacity(total_samples * 2);
|
|
|
|
for i in 0..8 {
|
|
// Lead: pulse wave
|
|
let mut lead = ChannelRenderer::new(
|
|
VoiceKind::Pulse(PulseChannel::new(sr as f32, DutyCycle::D50)),
|
|
sr as f32,
|
|
)
|
|
.with_envelope(Envelope::adsr(sr as f32, 0.01, 0.05, 0.6, 0.08))
|
|
.with_volume(0.3)
|
|
.with_pan(-0.3);
|
|
|
|
lead.trigger();
|
|
lead.set_frequency(note_freq(melody[i]));
|
|
|
|
// Bass: triangle
|
|
let mut bass_ch = ChannelRenderer::new(
|
|
VoiceKind::Triangle(TriangleChannel::new(sr as f32)),
|
|
sr as f32,
|
|
)
|
|
.with_envelope(Envelope::adsr(sr as f32, 0.02, 0.1, 0.5, 0.1))
|
|
.with_volume(0.35)
|
|
.with_pan(0.3);
|
|
|
|
bass_ch.trigger();
|
|
bass_ch.set_frequency(note_freq(bass[i]));
|
|
|
|
// Hihat: noise on offbeats
|
|
let has_hihat = i % 2 == 1;
|
|
let mut hihat = ChannelRenderer::new(
|
|
VoiceKind::Noise(NoiseChannel::new(sr as f32, NoiseMode::White)),
|
|
sr as f32,
|
|
)
|
|
.with_envelope(Envelope::adsr(sr as f32, 0.0, 0.02, 0.0, 0.03))
|
|
.with_volume(0.15)
|
|
.with_pan(0.0);
|
|
|
|
if has_hihat {
|
|
hihat.trigger();
|
|
hihat.set_frequency(10000.0);
|
|
}
|
|
|
|
for _ in 0..samples_per_note {
|
|
let (ll, lr) = lead.tick();
|
|
let (bl, br) = bass_ch.tick();
|
|
let (hl, hr) = hihat.tick();
|
|
let l = (ll + bl + hl).tanh();
|
|
let r = (lr + br + hr).tanh();
|
|
stereo.push(l);
|
|
stereo.push(r);
|
|
}
|
|
}
|
|
|
|
let out = std::path::Path::new("play_melody.wav");
|
|
write_wav(out, &stereo, sr).expect("failed to write WAV");
|
|
eprintln!("Wrote {} ({} notes, {:.1}s)", out.display(), 8, beat * 8.0);
|
|
}
|