use crate::audio::{
env::{EnvStage, Envelope},
filter::SvFilter,
osc::{Lfo, Oscillator, detune_hz, midi_to_hz},
};
use crate::params::{LfoTarget, MidiNote, SynthParams};
pub struct Voice {
pub active: bool,
pub target_note: MidiNote,
pub target_freq: f32,
pub current_freq: f32,
pub osc: Oscillator,
pub env: Envelope,
pub filter: SvFilter,
pub lfo: Lfo,
pub glide_coeff: f32,
}
impl Default for Voice {
fn default() -> Self {
Self::new()
}
}
impl Voice {
#[must_use]
pub fn new() -> Self {
Self {
active: false,
target_note: MidiNote::A4,
target_freq: 440.0,
current_freq: 440.0,
osc: Oscillator::default(),
env: Envelope::default(),
filter: SvFilter::default(),
lfo: Lfo::default(),
glide_coeff: 0.0,
}
}
pub fn update_glide(&mut self, glide_time: f32, sample_rate: f32) {
self.glide_coeff = if glide_time < 1e-4 {
0.0
} else {
(-1.0_f32 / (glide_time * sample_rate)).exp()
};
}
pub fn note_on(&mut self, note: impl Into<MidiNote>, params: &SynthParams, sample_rate: f32) {
let note = note.into();
let legato = self.active;
self.target_note = note;
let base = midi_to_hz(note);
self.target_freq = detune_hz(base, params.osc.detune);
if !legato {
self.current_freq = self.target_freq;
}
self.active = true;
self.update_glide(params.global.glide_time, sample_rate);
self.env.note_on(legato);
}
pub fn note_off(&mut self) {
self.env.note_off();
}
pub fn panic(&mut self) {
self.active = false;
self.env.reset();
self.filter.reset();
}
pub fn process(&mut self, params: &SynthParams, sample_rate: f32) -> f32 {
if !self.active && !self.env.is_active() {
return 0.0;
}
if self.env.stage == EnvStage::Idle && !self.env.is_active() {
self.active = false;
}
let lfo_val = self.lfo.next(params.lfo.lfo_rate, sample_rate); let lfo_depth = params.lfo.lfo_depth;
let gc = self.glide_coeff;
self.current_freq = self.target_freq + (self.current_freq - self.target_freq) * gc;
let freq = self.current_freq;
let modded_freq = match params.lfo.lfo_target {
LfoTarget::Pitch => freq * 2.0_f32.powf(lfo_val * lfo_depth * 0.1),
_ => freq,
};
let final_freq = detune_hz(modded_freq, 0.0);
let pw = match params.lfo.lfo_target {
LfoTarget::PulseWidth => {
(params.osc.pulse_width + lfo_val * lfo_depth * 0.4).clamp(0.05, 0.95)
}
_ => params.osc.pulse_width,
};
let osc_out = self.osc.next_sample(
final_freq,
sample_rate,
params.osc.waveform,
pw,
params.osc.noise_mix,
);
let env_val = self.env.process(
params.env.attack,
params.env.decay,
params.env.sustain,
params.env.release,
params.env.env_reverse,
sample_rate,
);
let vol_mod = match params.lfo.lfo_target {
LfoTarget::Volume => 1.0 - lfo_val * lfo_depth * 0.5,
_ => 1.0,
};
let cutoff_mod = match params.lfo.lfo_target {
LfoTarget::Cutoff => (params.filter.cutoff * 2.0_f32.powf(lfo_val * lfo_depth * 2.0))
.clamp(20.0, 18000.0),
_ => params.filter.cutoff,
};
let filtered = self.filter.process(
osc_out * env_val,
params.filter.filter_mode,
cutoff_mod,
params.filter.resonance,
params.filter.drive,
sample_rate,
);
filtered * env_val * vol_mod * params.global.volume
}
}