use serde::{Deserialize, Serialize};
use super::core::{Waveform, stateless_waveform_sample};
use crate::error::{self, Result};
fn default_detune_ratios() -> [f32; 8] {
[1.0; 8]
}
fn default_ratios_dirty() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnisonOscillator {
waveform: Waveform,
frequency: f32,
sample_rate: f32,
num_voices: usize,
detune_cents: f32,
stereo_spread: f32,
phases: [f32; 8],
#[serde(default = "default_detune_ratios")]
detune_ratios: [f32; 8],
#[serde(default = "default_ratios_dirty")]
ratios_dirty: bool,
}
impl UnisonOscillator {
pub fn new(
waveform: Waveform,
frequency: f32,
num_voices: usize,
detune_cents: f32,
sample_rate: f32,
) -> Result<Self> {
if let Some(e) = error::validate_sample_rate(sample_rate) {
return Err(e);
}
if let Some(e) = error::validate_frequency(frequency, sample_rate) {
return Err(e);
}
let nv = num_voices.clamp(1, 8);
let mut phases = [0.0f32; 8];
let mut seed = 12345u32;
for p in phases.iter_mut().take(nv) {
*p = crate::dsp_util::xorshift32_unit_f32(&mut seed);
}
let mut osc = Self {
waveform,
frequency,
sample_rate,
num_voices: nv,
detune_cents: detune_cents.max(0.0),
stereo_spread: 0.5,
phases,
detune_ratios: [1.0; 8],
ratios_dirty: true,
};
osc.recompute_ratios();
Ok(osc)
}
fn recompute_ratios(&mut self) {
if self.num_voices <= 1 {
self.detune_ratios[0] = 1.0;
} else {
for i in 0..self.num_voices {
let t = i as f32 / (self.num_voices - 1) as f32; let cents_offset = (t - 0.5) * self.detune_cents;
self.detune_ratios[i] = (cents_offset / 1200.0).exp2();
}
}
self.ratios_dirty = false;
}
#[inline]
#[must_use]
pub fn next_sample(&mut self) -> f32 {
if self.ratios_dirty {
self.recompute_ratios();
}
let mut sum = 0.0f32;
let nv = self.num_voices;
for i in 0..nv {
let freq = self.frequency * self.detune_ratios[i];
let dt = freq / self.sample_rate;
let t = self.phases[i];
let sample = stateless_waveform_sample(self.waveform, t, dt);
sum += sample;
self.phases[i] += dt;
if self.phases[i] >= 1.0 {
self.phases[i] -= 1.0;
}
}
sum / nv as f32
}
#[inline]
#[must_use]
pub fn next_sample_stereo(&mut self) -> (f32, f32) {
if self.ratios_dirty {
self.recompute_ratios();
}
let mut left = 0.0f32;
let mut right = 0.0f32;
let nv = self.num_voices;
for i in 0..nv {
let freq = self.frequency * self.detune_ratios[i];
let dt = freq / self.sample_rate;
let t = self.phases[i];
let sample = stateless_waveform_sample(self.waveform, t, dt);
if nv > 1 {
let pan = i as f32 / (nv - 1) as f32; let w = self.stereo_spread * 0.5;
let pan_scaled = 0.5 + (pan - 0.5) * w * 2.0;
let angle = pan_scaled * std::f32::consts::FRAC_PI_2;
left += sample * angle.cos();
right += sample * angle.sin();
} else {
left += sample * std::f32::consts::FRAC_1_SQRT_2;
right += sample * std::f32::consts::FRAC_1_SQRT_2;
}
self.phases[i] += dt;
if self.phases[i] >= 1.0 {
self.phases[i] -= 1.0;
}
}
let inv = 1.0 / nv as f32;
(left * inv, right * inv)
}
#[inline]
pub fn fill_buffer(&mut self, buffer: &mut [f32]) {
for sample in buffer.iter_mut() {
*sample = self.next_sample();
}
}
#[inline]
pub fn fill_buffer_stereo(&mut self, left: &mut [f32], right: &mut [f32]) {
for (l, r) in left.iter_mut().zip(right.iter_mut()) {
let (sl, sr) = self.next_sample_stereo();
*l = sl;
*r = sr;
}
}
pub fn set_num_voices(&mut self, n: usize) {
self.num_voices = n.clamp(1, 8);
self.ratios_dirty = true;
}
pub fn set_detune_cents(&mut self, cents: f32) {
self.detune_cents = cents.max(0.0);
self.ratios_dirty = true;
}
pub fn set_stereo_spread(&mut self, spread: f32) {
self.stereo_spread = spread.clamp(0.0, 1.0);
}
pub fn set_frequency(&mut self, freq: f32) -> Result<()> {
if let Some(e) = error::validate_frequency(freq, self.sample_rate) {
return Err(e);
}
self.frequency = freq;
Ok(())
}
#[inline]
#[must_use]
pub fn num_voices(&self) -> usize {
self.num_voices
}
#[inline]
#[must_use]
pub fn frequency(&self) -> f32 {
self.frequency
}
#[inline]
#[must_use]
pub fn detune_cents(&self) -> f32 {
self.detune_cents
}
}
#[cfg(test)]
mod tests {
use super::super::core::Oscillator;
use super::*;
#[test]
fn test_unison_mono() {
let mut uni = UnisonOscillator::new(Waveform::Saw, 440.0, 4, 10.0, 44100.0).unwrap();
let mut buf = [0.0f32; 1024];
uni.fill_buffer(&mut buf);
assert!(buf.iter().all(|s| s.is_finite()));
assert!(buf.iter().any(|&s| s != 0.0));
}
#[test]
fn test_unison_stereo_spread() {
let mut uni = UnisonOscillator::new(Waveform::Saw, 440.0, 4, 10.0, 44100.0).unwrap();
uni.set_stereo_spread(1.0);
let mut left = [0.0f32; 512];
let mut right = [0.0f32; 512];
uni.fill_buffer_stereo(&mut left, &mut right);
let diff: f32 = left
.iter()
.zip(right.iter())
.map(|(l, r)| (l - r).abs())
.sum();
assert!(diff > 0.01, "stereo channels should differ with spread=1.0");
}
#[test]
fn test_unison_single_voice() {
let mut uni = UnisonOscillator::new(Waveform::Sine, 440.0, 1, 0.0, 44100.0).unwrap();
let mut osc = Oscillator::new(Waveform::Sine, 440.0, 44100.0).unwrap();
for _ in 0..100 {
let s = uni.next_sample();
assert!((-1.01..=1.01).contains(&s));
let _ = osc.next_sample();
}
}
#[test]
fn test_unison_serde_roundtrip() {
let uni = UnisonOscillator::new(Waveform::Saw, 440.0, 4, 15.0, 44100.0).unwrap();
let json = serde_json::to_string(&uni).unwrap();
let back: UnisonOscillator = serde_json::from_str(&json).unwrap();
assert_eq!(uni.num_voices(), back.num_voices());
assert!((uni.detune_cents() - back.detune_cents()).abs() < f32::EPSILON);
}
}