//! 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, 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 { 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 { 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 } }