Skip to content

Commit

Permalink
Fixes for MIDI editor + synth designer chorus effect
Browse files Browse the repository at this point in the history
  • Loading branch information
Ameobea committed Jan 3, 2024
1 parent 29e6fd7 commit 94f9951
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 19 deletions.
76 changes: 76 additions & 0 deletions engine/wavetable/src/fm/effects/chorus.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::f32::consts::PI;

use super::Effect;
use crate::fm::{ParamSource, SAMPLE_RATE};
use dsp::circular_buffer::CircularBuffer;

const MAX_CHORUS_DELAY_SAMPLES: usize = SAMPLE_RATE / 20; // 50ms
const NUM_TAPS: usize = 3;

const TWO_PI: f32 = PI * 2.;

#[derive(Clone)]
pub struct ChorusEffect {
pub buffer: Box<CircularBuffer<MAX_CHORUS_DELAY_SAMPLES>>,
pub modulation_depth: ParamSource,
pub last_modulation_depth: f32,
pub wet: ParamSource,
pub last_wet: f32,
pub dry: ParamSource,
pub last_dry: f32,
pub lfo_rate: ParamSource,
pub last_lfo_rate: f32,
pub lfo_phases: [f32; NUM_TAPS],
}

impl Effect for ChorusEffect {
fn get_params<'a>(&'a mut self, buf: &mut [Option<&'a mut crate::fm::ParamSource>; 4]) {
buf[0] = Some(&mut self.modulation_depth);
buf[1] = Some(&mut self.wet);
buf[2] = Some(&mut self.dry);
buf[3] = Some(&mut self.lfo_rate);
}

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]),
0.95,
);
let wet = dsp::smooth(
&mut self.last_wet,
dsp::clamp(0., 1., rendered_params[1]),
0.95,
);
let dry = dsp::smooth(
&mut self.last_dry,
dsp::clamp(0., 1., rendered_params[2]),
0.95,
);
let lfo_rate = dsp::smooth(
&mut self.last_lfo_rate,
dsp::clamp(0., 20., rendered_params[3]),
0.95,
);

// Update LFO phases
for i in 0..NUM_TAPS {
self.lfo_phases[i] += TWO_PI * lfo_rate / SAMPLE_RATE as f32 * (i as f32 + 1.0);
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;
chorus_sample += self.buffer.read_interpolated(-delay_samples);
}

chorus_sample /= NUM_TAPS as f32;
self.buffer.set(sample);

(sample * dry) + (chorus_sample * wet)
}
}
83 changes: 83 additions & 0 deletions engine/wavetable/src/fm/effects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use super::{uninit, ParamSource, RenderRawParams, FRAME_SIZE};

pub mod bitcrusher;
pub mod butterworth_filter;
pub mod chorus;
pub mod comb_filter;
pub mod compressor;
pub mod delay;
Expand All @@ -20,6 +21,7 @@ pub mod wavefolder;
use self::{
bitcrusher::Bitcrusher,
butterworth_filter::{ButterworthFilter, ButterworthFilterMode},
chorus::ChorusEffect,
compressor::CompressorEffect,
delay::Delay,
moog::MoogFilter,
Expand Down Expand Up @@ -81,6 +83,7 @@ pub enum EffectInstance {
MoogFilter(MoogFilter),
CombFilter(CombFilter),
Compressor(CompressorEffect),
Chorus(ChorusEffect),
}

impl EffectInstance {
Expand Down Expand Up @@ -342,6 +345,46 @@ impl EffectInstance {

EffectInstance::Compressor(compressor)
},
10 => {
let chorus = ChorusEffect {
buffer: Box::new(CircularBuffer::new()),
modulation_depth: ParamSource::from_parts(
param_1_type,
param_1_int_val,
param_1_float_val,
param_1_float_val_2,
param_1_float_val_3,
),
wet: ParamSource::from_parts(
param_2_type,
param_2_int_val,
param_2_float_val,
param_2_float_val_2,
param_2_float_val_3,
),
dry: ParamSource::from_parts(
param_3_type,
param_3_int_val,
param_3_float_val,
param_3_float_val_2,
param_3_float_val_3,
),
lfo_phases: [0.; 3],
lfo_rate: ParamSource::from_parts(
param_4_type,
param_4_int_val,
param_4_float_val,
param_4_float_val_2,
param_4_float_val_3,
),
last_dry: 0.,
last_wet: 0.,
last_modulation_depth: 0.,
last_lfo_rate: 0.,
};

EffectInstance::Chorus(chorus)
},
_ => panic!("Invalid effect type: {}", effect_type),
}
}
Expand Down Expand Up @@ -592,6 +635,43 @@ impl EffectInstance {
));
return true;
},
9 => false, // TODO
10 => {
let chorus = match self {
EffectInstance::Chorus(chorus) => chorus,
_ => return false,
};

chorus.modulation_depth.replace(ParamSource::from_parts(
param_1_type,
param_1_int_val,
param_1_float_val,
param_1_float_val_2,
param_1_float_val_3,
));
chorus.wet.replace(ParamSource::from_parts(
param_2_type,
param_2_int_val,
param_2_float_val,
param_2_float_val_2,
param_2_float_val_3,
));
chorus.dry.replace(ParamSource::from_parts(
param_3_type,
param_3_int_val,
param_3_float_val,
param_3_float_val_2,
param_3_float_val_3,
));
chorus.lfo_rate.replace(ParamSource::from_parts(
param_4_type,
param_4_int_val,
param_4_float_val,
param_4_float_val_2,
param_4_float_val_3,
));
return true;
},
_ => false,
}
}
Expand All @@ -610,6 +690,7 @@ impl Effect for EffectInstance {
EffectInstance::MoogFilter(e) => e.apply(rendered_params, base_frequency, sample),
EffectInstance::CombFilter(e) => e.apply(rendered_params, base_frequency, sample),
EffectInstance::Compressor(e) => e.apply(rendered_params, base_frequency, sample),
EffectInstance::Chorus(e) => e.apply(rendered_params, base_frequency, sample),
}
}

Expand All @@ -631,6 +712,7 @@ impl Effect for EffectInstance {
EffectInstance::MoogFilter(e) => e.apply_all(rendered_params, base_frequencies, samples),
EffectInstance::CombFilter(e) => e.apply_all(rendered_params, base_frequencies, samples),
EffectInstance::Compressor(e) => e.apply_all(rendered_params, base_frequencies, samples),
EffectInstance::Chorus(e) => e.apply_all(rendered_params, base_frequencies, samples),
}
}

Expand All @@ -646,6 +728,7 @@ impl Effect for EffectInstance {
EffectInstance::MoogFilter(e) => e.get_params(buf),
EffectInstance::CombFilter(e) => e.get_params(buf),
EffectInstance::Compressor(e) => e.get_params(buf),
EffectInstance::Chorus(e) => e.get_params(buf),
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/controls/adsr2/adsr2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ interface StepHandleConfiguratorCtx {
}

PIXI.utils.skipHello();
if (PIXI.settings.RENDER_OPTIONS) {
PIXI.settings.RENDER_OPTIONS.hello = false;
}

/**
* Controls the properties of a ramp curve. Can be dragged, but must be bounded by the marks that define
Expand Down
67 changes: 66 additions & 1 deletion src/fmSynth/ConfigureEffects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const EFFECT_TYPE_SETTING = {
'moog filter',
'comb filter',
'compressor',
'chorus',
] as Effect['type'][],
};

Expand Down Expand Up @@ -103,10 +104,16 @@ const buildDefaultEffect = (type: Effect['type']): Effect => {
};
}
case 'compressor': {
return { type };
}
case 'chorus':
return {
type,
dry: { type: 'constant', value: 0.5 },
wet: { type: 'constant', value: 0.5 },
lfoRate: { type: 'constant', value: 0.5 },
modulationDepth: { type: 'constant', value: 0.5 },
};
}
}
};

Expand All @@ -128,6 +135,7 @@ const delayTheme = { ...baseTheme, background2: 'rgb(13,107,89)' };
const moogFilterTheme = { ...baseTheme, background2: 'rgb(49,69,120)' };
const combFilterTheme = { ...baseTheme, background2: 'rgb(36,64,21)' };
const compressorTheme = { ...baseTheme, background2: 'rgb(16,24,21)' };
const chorusTheme = { ...baseTheme, background2: 'rgb(181,97,184)' };

const ThemesByType: { [K in Effect['type']]: { [key: string]: any } } = {
'spectral warping': spectralWarpTheme,
Expand All @@ -140,6 +148,7 @@ const ThemesByType: { [K in Effect['type']]: { [key: string]: any } } = {
'moog filter': moogFilterTheme,
'comb filter': combFilterTheme,
compressor: compressorTheme,
chorus: chorusTheme,
};

const EMPTY_ADSRS: AdsrParams[] = [];
Expand Down Expand Up @@ -566,6 +575,61 @@ const ConfigureCompressor: EffectConfigurator<'compressor'> = ({
vcId,
}) => <>Compressor params TODO</>;

const ConfigureChorus: EffectConfigurator<'chorus'> = ({
state,
onChange,
adsrs,
onAdsrChange,
vcId,
}) => (
<>
<ConfigureParamSource
title='dry'
adsrs={adsrsMemoHelper(state.dry, adsrs)}
onAdsrChange={onAdsrChange}
theme={chorusTheme}
min={0}
max={1}
state={state.dry}
onChange={useCallback(dry => onChange({ dry }), [onChange])}
vcId={vcId}
/>
<ConfigureParamSource
title='wet'
adsrs={adsrsMemoHelper(state.wet, adsrs)}
onAdsrChange={onAdsrChange}
theme={chorusTheme}
min={0}
max={1}
state={state.wet}
onChange={useCallback(wet => onChange({ wet }), [onChange])}
vcId={vcId}
/>
<ConfigureParamSource
title='lfo rate'
adsrs={adsrsMemoHelper(state.lfoRate, adsrs)}
onAdsrChange={onAdsrChange}
theme={chorusTheme}
min={0}
max={1}
state={state.lfoRate}
onChange={useCallback(lfoRate => onChange({ lfoRate }), [onChange])}
vcId={vcId}
/>
<ConfigureParamSource
title='modulation depth'
adsrs={adsrsMemoHelper(state.modulationDepth, adsrs)}
onAdsrChange={onAdsrChange}
theme={chorusTheme}
min={0}
max={1}
state={state.modulationDepth}
onChange={useCallback(modulationDepth => onChange({ modulationDepth }), [onChange])}
vcId={vcId}
/>
</>
);

interface EffectManagementProps {
effectIx: number;
isBypassed: boolean;
Expand Down Expand Up @@ -661,6 +725,7 @@ const EFFECT_CONFIGURATOR_BY_EFFECT_TYPE: { [K in Effect['type']]: EffectConfigu
'moog filter': React.memo(ConfigureMoogFilter),
'comb filter': React.memo(ConfigureCombFilter),
compressor: React.memo(ConfigureCompressor),
chorus: React.memo(ConfigureChorus),
};

interface ConfigureEffectSpecificProps {
Expand Down
18 changes: 17 additions & 1 deletion src/fmSynth/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ export type EffectInner =
| {
type: 'compressor';
// TODO: Params
}
| {
type: 'chorus';
modulationDepth: ParamSource;
wet: ParamSource;
dry: ParamSource;
lfoRate: ParamSource;
};

export type Effect = EffectInner & {
Expand All @@ -77,7 +84,7 @@ type EncodedEffect = [
EncodedParamSource | null,
EncodedParamSource | null,
EncodedParamSource | null,
EncodedParamSource | null
EncodedParamSource | null,
];

export const encodeEffect = (effect: Effect | null): EncodedEffect => {
Expand Down Expand Up @@ -176,6 +183,15 @@ export const encodeEffect = (effect: Effect | null): EncodedEffect => {
case 'compressor': {
return [9, null, null, null, null];
}
case 'chorus': {
return [
10,
encodeParamSource(effect.modulationDepth),
encodeParamSource(effect.wet),
encodeParamSource(effect.dry),
encodeParamSource(effect.lfoRate),
];
}
default: {
throw new UnimplementedError(`Effect not handled yet: ${(effect as any).type}`);
}
Expand Down
1 change: 1 addition & 0 deletions src/graphEditor/nodes/CustomAudio/NoiseGen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ export class NoiseGenNode {
this.awpNode = new AudioWorkletNode(this.ctx, 'noise-generator-audio-worklet-node-processor', {
numberOfOutputs: 1,
numberOfInputs: 0,
channelCount: 1,
channelInterpretation: 'discrete',
channelCountMode: 'explicit',
});
Expand Down
Loading

0 comments on commit 94f9951

Please sign in to comment.