Skip to content

Commit

Permalink
More misc. fixes and optimizations
Browse files Browse the repository at this point in the history
 * Optimize moog filter implementation to hopefully be faster
 * Fix incorrectness in moog filter implementation
 * Clear internal buffers for FM synth effects when gating voices
 * Fix off by one in circular buffer implementation (lol)
 * Optimize sawtooth oscillator in FM synth to be faster
   * Idk why I was using a lookup table at all since ... it's a sawtooth wave
  • Loading branch information
Ameobea committed Jan 17, 2024
1 parent 26dc1f8 commit 6a90d32
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 99 deletions.
19 changes: 13 additions & 6 deletions engine/dsp/src/circular_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl<const LENGTH: usize> CircularBuffer<LENGTH> {
#[inline]
pub fn get(&self, ix: isize) -> f32 {
debug_assert!(ix <= 0);
let ix = (self.head as isize + ix) % ((LENGTH - 1) as isize);
let ix = (self.head as isize + ix) % (LENGTH as isize);

if ix >= 0 {
self.buffer[ix as usize]
Expand All @@ -40,19 +40,26 @@ impl<const LENGTH: usize> CircularBuffer<LENGTH> {

#[inline]
pub fn read_interpolated(&self, sample_ix: f32) -> f32 {
debug_assert!(sample_ix <= 0.);
if cfg!(debug_assertions) {
if sample_ix > 0. {
panic!("sample_ix must be <= 0; found: {sample_ix}");
}
}
if sample_ix == 0. {
if cfg!(debug_assertions) {
return self.buffer[self.head];
} else {
return *unsafe { self.buffer.get_unchecked(self.head) };
}
}
let base_ix = sample_ix.trunc();
let next_ix = base_ix + (1. * sample_ix.signum());
let base_ix = sample_ix.trunc() as isize;
let next_ix = base_ix - 1;

let base_val = self.get(base_ix as isize);
let next_val = self.get(next_ix as isize);
let base_val = self.get(base_ix);
let next_val = self.get(next_ix);
crate::mix(1. - sample_ix.fract().abs(), base_val, next_val)
}

#[inline]
pub fn fill(&mut self, val: f32) { self.buffer.fill(val); }
}
19 changes: 13 additions & 6 deletions engine/dsp/src/oscillator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ pub trait PhasedOscillator {

fn set_phase(&mut self, new_phase: f32);

fn update_phase(&mut self, frequency: f32) {
// 1 phase corresponds to 1 period of the waveform. 1 phase is passed every (SAMPLE_RATE /
// frequency) samples.
let phase = self.get_phase();
// if frequency.is_normal() && frequency.abs() > 0.001 {
fn compute_new_phase(phase: f32, frequency: f32) -> f32 {
let mut new_phase = (phase + (1. / (SAMPLE_RATE as f32 / frequency))).fract();
if new_phase < 0. {
new_phase = 1. + new_phase;
}
new_phase
}

fn compute_new_phase_oversampled(phase: f32, oversample_multiplier: f32, frequency: f32) -> f32 {
Self::compute_new_phase(phase, frequency / oversample_multiplier)
}

fn update_phase(&mut self, frequency: f32) {
// 1 phase corresponds to 1 period of the waveform. 1 phase is passed every (SAMPLE_RATE /
// frequency) samples.
let phase = self.get_phase();
let new_phase = Self::compute_new_phase(phase, frequency);
self.set_phase(new_phase);
// }
}

fn update_phase_oversampled(&mut self, oversample_multiplier: f32, frequency: f32) {
Expand Down
18 changes: 11 additions & 7 deletions engine/wavetable/src/fm/effects/chorus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,37 +34,39 @@ impl Effect for ChorusEffect {
fn apply(&mut self, rendered_params: &[f32], _base_frequency: f32, sample: f32) -> f32 {
let depth = dsp::smooth(
&mut self.last_modulation_depth,
dsp::clamp(0., 1., rendered_params[0]),
dsp::clamp(0., 1., unsafe { *rendered_params.get_unchecked(0) }),
0.95,
);
let wet = dsp::smooth(
&mut self.last_wet,
dsp::clamp(0., 1., rendered_params[1]),
dsp::clamp(0., 1., unsafe { *rendered_params.get_unchecked(1) }),
0.95,
);
let dry = dsp::smooth(
&mut self.last_dry,
dsp::clamp(0., 1., rendered_params[2]),
dsp::clamp(0., 1., unsafe { *rendered_params.get_unchecked(2) }),
0.95,
);
let lfo_rate_hz = dsp::smooth(
&mut self.last_lfo_rate,
dsp::clamp(0., 20., rendered_params[3]),
dsp::clamp(0., 20., unsafe { *rendered_params.get_unchecked(3) }),
0.95,
);

// Update LFO phases
for i in 0..NUM_TAPS {
self.lfo_phases[i] += lfo_rate_hz * TWO_PI / SAMPLE_RATE as f32;
while self.lfo_phases[i] > TWO_PI {
self.lfo_phases[i] += lfo_rate_hz / SAMPLE_RATE as f32;
if self.lfo_phases[i] > TWO_PI {
self.lfo_phases[i] -= TWO_PI;
}
}

let mut chorus_sample = 0.0;
for &phase in &self.lfo_phases {
let lfo = phase.sin();
let delay_samples = ((MAX_CHORUS_DELAY_SAMPLES as f32) / 2.0) * depth * lfo;
// scale from [-1, 1] to [0, 1]
let lfo = (lfo + 1.) / 2.;
let delay_samples = (MAX_CHORUS_DELAY_SAMPLES as f32) * depth * lfo;
chorus_sample += self.buffer.read_interpolated(-delay_samples);
}

Expand All @@ -73,4 +75,6 @@ impl Effect for ChorusEffect {

(sample * dry) + (chorus_sample * wet)
}

fn reset(&mut self) { self.buffer.fill(0.); }
}
5 changes: 5 additions & 0 deletions engine/wavetable/src/fm/effects/comb_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,9 @@ impl Effect for CombFilter {
samples[sample_ix] = output;
}
}

fn reset(&mut self) {
self.input_buffer.fill(0.);
self.feedback_buffer.fill(0.);
}
}
2 changes: 2 additions & 0 deletions engine/wavetable/src/fm/effects/delay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ impl Effect for Delay {

(sample * dry) + (delayed_sample * wet)
}

fn reset(&mut self) { self.buffer.fill(0.); }
}
29 changes: 29 additions & 0 deletions engine/wavetable/src/fm/effects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ pub trait Effect {
);
}
}

/// Resets the effect to its initial state. Called after a voice is freshly gated.
///
/// Useful for effects with internal state like delay lines.
fn reset(&mut self) {}
}

#[derive(Clone)]
Expand Down Expand Up @@ -737,6 +742,22 @@ impl Effect for EffectInstance {
EffectInstance::Chorus(e) => e.get_params(buf),
}
}

fn reset(&mut self) {
match self {
EffectInstance::SpectralWarping(e) => e.reset(),
EffectInstance::Wavecruncher(e) => e.reset(),
EffectInstance::Bitcrusher(e) => e.reset(),
EffectInstance::Wavefolder(e) => e.reset(),
EffectInstance::SoftClipper(e) => e.reset(),
EffectInstance::ButterworthFilter(e) => e.reset(),
EffectInstance::Delay(e) => e.reset(),
EffectInstance::MoogFilter(e) => e.reset(),
EffectInstance::CombFilter(e) => e.reset(),
EffectInstance::Compressor(e) => e.reset(),
EffectInstance::Chorus(e) => e.reset(),
}
}
}

#[derive(Clone)]
Expand Down Expand Up @@ -858,6 +879,14 @@ impl EffectChain {
self.effects[effect_ix - 1] = self.effects[effect_ix].take();
}
}

pub fn reset(&mut self) {
for effect in self.effects.iter_mut() {
if let Some(effect) = effect {
effect.inst.reset();
}
}
}
}

/// Given an arbitrary effect, queries the effect for its current list of parameters. Then, renders
Expand Down
Loading

0 comments on commit 6a90d32

Please sign in to comment.