Files
Soundgen/crates/soundgen-core/src/mixer.rs
T
Emil c7d6c40683 Initial release: 8-bit sound synthesizer with LLM/MCP integration
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
2026-06-21 22:07:05 +03:00

169 lines
4.6 KiB
Rust

//! Stereo mixer — sums multiple voices with per-channel volume and pan.
use crate::voice::VoiceKind;
use crate::Generator;
/// A single channel in the mixer: voice + amplitude + pan.
pub struct MixerChannel {
pub voice: VoiceKind,
pub volume: f32,
/// -1.0 = full left, 0.0 = center, 1.0 = full right.
pub pan: f32,
}
pub struct Mixer {
channels: Vec<MixerChannel>,
sample_rate: f32,
}
impl Mixer {
pub fn new(sample_rate: f32) -> Self {
Self {
channels: Vec::new(),
sample_rate,
}
}
pub fn push(&mut self, voice: VoiceKind, volume: f32, pan: f32) {
self.channels.push(MixerChannel {
voice,
volume: volume.clamp(0.0, 1.0),
pan: pan.clamp(-1.0, 1.0),
});
}
pub fn sample_rate(&self) -> f32 {
self.sample_rate
}
pub fn len(&self) -> usize {
self.channels.len()
}
pub fn is_empty(&self) -> bool {
self.channels.is_empty()
}
pub fn reset(&mut self) {
for ch in &mut self.channels {
ch.voice.reset();
}
}
/// Produce one stereo sample (left, right).
#[inline]
pub fn tick_stereo(&mut self) -> (f32, f32) {
let mut left = 0.0f32;
let mut right = 0.0f32;
for ch in &mut self.channels {
let sample = ch.voice.tick() * ch.volume;
// Equal-power panning
let pan_norm = (ch.pan + 1.0) * 0.5; // 0 = left, 1 = right
let left_gain = (1.0 - pan_norm).sqrt();
let right_gain = pan_norm.sqrt();
left += sample * left_gain;
right += sample * right_gain;
}
// Soft clip to prevent harsh clipping
let left = left.tanh();
let right = right.tanh();
(left, right)
}
/// Produce one mono sample (sum of all channels).
#[inline]
pub fn tick_mono(&mut self) -> f32 {
let (l, r) = self.tick_stereo();
(l + r) * 0.5
}
}
/// Render a mixer to a stereo Vec (interleaved L, R, L, R, ...).
pub fn render_stereo(mixer: &mut Mixer, n_samples: usize) -> Vec<f32> {
let mut out = Vec::with_capacity(n_samples * 2);
for _ in 0..n_samples {
let (l, r) = mixer.tick_stereo();
out.push(l);
out.push(r);
}
out
}
/// Render a mixer to a mono Vec.
pub fn render_mono(mixer: &mut Mixer, n_samples: usize) -> Vec<f32> {
let mut out = Vec::with_capacity(n_samples);
for _ in 0..n_samples {
out.push(mixer.tick_mono());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generator::Voice;
use crate::voice::{DutyCycle, PulseChannel};
#[test]
fn test_mixer_empty() {
let mut m = Mixer::new(44100.0);
let (l, r) = m.tick_stereo();
assert_eq!(l, 0.0);
assert_eq!(r, 0.0);
}
#[test]
fn test_mixer_single_channel() {
let mut m = Mixer::new(44100.0);
let mut pulse = PulseChannel::new(44100.0, DutyCycle::D50);
pulse.note_on(440.0, 1.0);
m.push(VoiceKind::Pulse(pulse), 0.5, 0.0);
let (l, r) = m.tick_stereo();
// Center pan, volume 0.5
assert!(l.abs() > 0.0 || r.abs() > 0.0);
}
#[test]
fn test_mixer_pan_left() {
let mut m = Mixer::new(44100.0);
let mut pulse = PulseChannel::new(44100.0, DutyCycle::D50);
pulse.note_on(440.0, 1.0);
m.push(VoiceKind::Pulse(pulse), 1.0, -1.0); // full left
let (l, r) = m.tick_stereo();
assert!(l.abs() > 0.0);
// Right should be near zero (equal power pan at -1.0 → right_gain = 0)
assert!(r.abs() < 0.01);
}
#[test]
fn test_mixer_pan_right() {
let mut m = Mixer::new(44100.0);
let mut pulse = PulseChannel::new(44100.0, DutyCycle::D50);
pulse.note_on(440.0, 1.0);
m.push(VoiceKind::Pulse(pulse), 1.0, 1.0); // full right
let (l, r) = m.tick_stereo();
assert!(r.abs() > 0.0);
assert!(l.abs() < 0.01);
}
#[test]
fn test_render_mono_length() {
let mut m = Mixer::new(44100.0);
let mut pulse = PulseChannel::new(44100.0, DutyCycle::D50);
pulse.note_on(440.0, 1.0);
m.push(VoiceKind::Pulse(pulse), 0.5, 0.0);
let out = render_mono(&mut m, 1000);
assert_eq!(out.len(), 1000);
}
#[test]
fn test_render_stereo_length() {
let mut m = Mixer::new(44100.0);
let mut pulse = PulseChannel::new(44100.0, DutyCycle::D50);
pulse.note_on(440.0, 1.0);
m.push(VoiceKind::Pulse(pulse), 0.5, 0.0);
let out = render_stereo(&mut m, 1000);
assert_eq!(out.len(), 2000); // interleaved
}
}