diff --git a/engine/dsp/src/filters/biquad.rs b/engine/dsp/src/filters/biquad.rs index ec220178..9f9f3cc5 100644 --- a/engine/dsp/src/filters/biquad.rs +++ b/engine/dsp/src/filters/biquad.rs @@ -64,7 +64,7 @@ impl BiquadFilter { ) -> (f32, f32, f32, f32, f32) { // From: https://webaudio.github.io/web-audio-api/#filters-characteristics let computed_frequency = freq; - let normalized_freq = computed_frequency / NYQUIST; + let normalized_freq = crate::clamp(0.005, 0.99, computed_frequency / NYQUIST); let w0 = PI * normalized_freq; #[allow(non_snake_case)] let A = 10.0_f32.powf(gain / 40.); diff --git a/engine/wavetable/src/fm/filter/dynabandpass.rs b/engine/wavetable/src/fm/filter/dynabandpass.rs index 765d259c..c7ceea37 100644 --- a/engine/wavetable/src/fm/filter/dynabandpass.rs +++ b/engine/wavetable/src/fm/filter/dynabandpass.rs @@ -39,8 +39,8 @@ fn compute_modified_dynabandpass_filter_bandwidth( fn compute_filter_cutoff_frequencies(center_frequency: f32, base_bandwidth: f32) -> (f32, f32) { let bandwidth = compute_modified_dynabandpass_filter_bandwidth(10., base_bandwidth, center_frequency); - let highpass_freq = dsp::clamp(10., NYQUIST - 10., center_frequency - bandwidth / 2.); - let lowpass_freq = dsp::clamp(10., NYQUIST - 10., center_frequency + bandwidth / 2.); + let highpass_freq = dsp::clamp(10., NYQUIST - 100., center_frequency - bandwidth / 2.); + let lowpass_freq = dsp::clamp(10., NYQUIST - 100., center_frequency + bandwidth / 2.); (lowpass_freq, highpass_freq) } @@ -105,15 +105,5 @@ impl DynabandpassFilter { &PRECOMPUTED_BASE_Q_FACTORS, &self.highpass_cutoff_freqs, ); - - if frame - .iter() - .any(|&sample| sample.is_nan() || sample < -10. || sample > 10.) - { - panic!( - "{:?} \n\n\n {:?} \n\n\n {:?}", - self.lowpass_cutoff_freqs, self.highpass_cutoff_freqs, frame - ); - } } } diff --git a/engine/wavetable/src/fm/mod.rs b/engine/wavetable/src/fm/mod.rs index a19012ab..d3227648 100644 --- a/engine/wavetable/src/fm/mod.rs +++ b/engine/wavetable/src/fm/mod.rs @@ -26,6 +26,8 @@ use self::{ }; extern "C" { + pub(crate) fn log_panic(ptr: *const u8, len: usize); + pub(crate) fn log_err(ptr: *const u8, len: usize); fn on_gate_cb(midi_number: usize, voice_ix: usize); @@ -33,6 +35,8 @@ extern "C" { fn on_ungate_cb(midi_number: usize, voice_ix: usize); } +pub fn log_err_str(s: &str) { unsafe { log_err(s.as_ptr(), s.len()) } } + pub static mut MIDI_CONTROL_VALUES: [f32; 1024] = [0.; 1024]; const GAIN_ENVELOPE_PHASE_BUF_INDEX: usize = 255; const FILTER_ENVELOPE_PHASE_BUF_INDEX: usize = 254; @@ -1726,6 +1730,8 @@ pub struct FMSynthContext { PolySynth)>, Box)>>, } +static mut DID_LOG_NAN: bool = false; + impl FMSynthContext { pub fn generate(&mut self, cur_bpm: f32, cur_frame_start_beat: f32) { for (voice_ix, voice) in self.voices.iter_mut().enumerate() { @@ -1802,6 +1808,25 @@ impl FMSynthContext { for i in 0..FRAME_SIZE { self.main_output_buffer[i] *= self.master_gain; } + + let mut found_nan = false; + for sample in &mut self.main_output_buffer { + if sample.is_nan() || !sample.is_finite() { + found_nan = true; + *sample = 0.; + } + } + + if found_nan { + if unsafe { !DID_LOG_NAN } { + unsafe { + DID_LOG_NAN = true; + } + log_err_str(&format!( + "NaN, Inf, or -Inf detected in output buffer from FM synth" + )); + } + } } pub fn update_operator_enabled_statuses(&mut self) { diff --git a/engine/wavetable/src/lib.rs b/engine/wavetable/src/lib.rs index 1841335b..e32cee81 100644 --- a/engine/wavetable/src/lib.rs +++ b/engine/wavetable/src/lib.rs @@ -202,7 +202,7 @@ pub fn init_wavetable( waveform_length: usize, base_frequency: f32, ) -> *mut WaveTable { - common::set_raw_panic_hook(crate::fm::log_err); + common::set_raw_panic_hook(crate::fm::log_panic); let settings = WaveTableSettings { waveforms_per_dimension, diff --git a/public/FMSynthAWP.js b/public/FMSynthAWP.js index 6daa0574..5e7ec800 100644 --- a/public/FMSynthAWP.js +++ b/public/FMSynthAWP.js @@ -431,13 +431,27 @@ class FMSynthAWP extends AudioWorkletProcessor { }; } - handleWasmPanic = (ptr, len) => { + readStringFromWasmMemory = (ptr, len) => { const mem = new Uint8Array(this.getWasmMemoryBuffer().buffer); const slice = mem.subarray(ptr, ptr + len); - const str = String.fromCharCode(...slice); + return String.fromCharCode(...slice); + }; + + handleWasmPanic = (ptr, len) => { + const str = this.readStringFromWasmMemory(ptr, len); throw new Error(str); }; + logErr = (ptr, len) => { + const str = this.readStringFromWasmMemory(ptr, len); + console.error(str); + }; + + log = (ptr, len) => { + const str = this.readStringFromWasmMemory(ptr, len); + console.log(str); + }; + setOperatorState(operatorIx, mappedSamplesByMIDINumber) { const entries = Object.entries(mappedSamplesByMIDINumber); this.wasmInstance.exports.fm_synth_set_mapped_sample_midi_number_count( @@ -490,8 +504,9 @@ class FMSynthAWP extends AudioWorkletProcessor { async initWasm(wasmBytes, modulationMatrix, outputWeights, adsrs) { const importObject = { env: { - log_err: this.handleWasmPanic, - log_raw: (ptr, len, _level) => this.handleWasmPanic(ptr, len), + log_panic: this.handleWasmPanic, + log_err: (ptr, len) => this.logErr(ptr, len), + log_raw: (ptr, len, _level) => this.log(ptr, len), debug1: (v1, v2, v3) => console.log({ v1, v2, v3 }), on_gate_cb: (midiNumber, voiceIx) => { this.port.postMessage({ type: 'onGate', midiNumber, voiceIx }); diff --git a/src/fmDemo/FMSynthDemo.tsx b/src/fmDemo/FMSynthDemo.tsx index 2ccbb9b7..20f97820 100644 --- a/src/fmDemo/FMSynthDemo.tsx +++ b/src/fmDemo/FMSynthDemo.tsx @@ -9,10 +9,13 @@ import { createRoot } from 'react-dom/client'; import './fmDemo.scss'; import { mkControlPanelADSR2WithSize } from 'src/controls/adsr2/ControlPanelADSR2'; -import FilterConfig, { FilterContainer } from 'src/fmDemo/FilterConfig'; +import FilterConfig from 'src/fmDemo/FilterConfig'; import { Presets, type SerializedFMSynthDemoState } from 'src/fmDemo/presets'; import { ConnectedFMSynthUI } from 'src/fmSynth/FMSynthUI'; -import FMSynth, { type Adsr } from 'src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth'; +import FMSynth, { + FilterParamControlSource, + type Adsr, +} from 'src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth'; import 'src/index.scss'; import { MIDIInput } from 'src/midiKeyboard/midiInput'; import { MidiKeyboard } from 'src/midiKeyboard/MidiKeyboard'; @@ -28,8 +31,6 @@ import { FilterType } from 'src/synthDesigner/FilterType'; initGlobals(); -const VOICE_COUNT = 10; - const GlobalState: { octaveOffset: number; globalVolume: number; @@ -66,11 +67,6 @@ if (!environmentIsValid) { const ctx = new AudioContext(); const mainGain = new GainNode(ctx); mainGain.gain.value = 0.1; -const filters = new Array(VOICE_COUNT).fill(null).map(() => { - const filter = new FilterContainer(ctx, GlobalState.filterParams); - filter.getOutput().connect(mainGain); - return filter; -}); // Disable context menu on mobile that can be caused by long holds on keys if (window.screen.width < 1000) { @@ -167,21 +163,8 @@ GlobalState.filterADSREnabled = serialized!.filterADSREnabled ?? true; GlobalState.selectedMIDIInputName = serialized!.selectedMIDIInputName; GlobalState.lastLoadedPreset = serialized!.lastLoadedPreset; -const voiceGains = new Array(VOICE_COUNT).fill(null).map((_i, voiceIx) => { - const gain = new GainNode(ctx); - gain.gain.value = 1; - const filterBypassed = serialized?.filterBypassed ?? false; - if (filterBypassed) { - gain.connect(mainGain); - } else { - gain.connect(filters[voiceIx].getInput()); - } - return gain; -}); - if (!R.isNil(serialized?.filterParams)) { GlobalState.filterParams = serialized!.filterParams; - filters.forEach(filter => filter.setAll(GlobalState.filterParams)); } const baseTheme = { @@ -219,15 +202,16 @@ const synth = new FMSynth(ctx, undefined, { midiNode: midiInputNode, onInitialized: () => { const awpNode = synth.getAWPNode()!; - - voiceGains.forEach((voiceGain, voiceIx) => awpNode.connect(voiceGain, voiceIx)); - - filters.forEach((filter, i) => { - awpNode.connect(filter.csns.frequency, VOICE_COUNT + i, 0); - // Filter is overridden if ADSR is disabled, meaning that the frequency slider from the UI - // controls the fliter's frequency completely - filter.csns.frequency.setIsOverridden(!GlobalState.filterADSREnabled); - }); + awpNode.connect(mainGain); + + synth.setFilterParams(GlobalState.filterParams); + synth.setFilterBypassed(GlobalState.filterBypassed); + synth.handleFilterFrequencyChange( + GlobalState.filterParams.frequency, + GlobalState.filterADSREnabled + ? FilterParamControlSource.Envelope + : FilterParamControlSource.Manual + ); }, }); @@ -328,19 +312,9 @@ const MainControlPanel: React.FC = () => { ); }; -const bypassFilter = () => { - voiceGains.forEach((node, voiceIx) => { - node.disconnect(filters[voiceIx].getInput()); - node.connect(mainGain); - }); -}; +const bypassFilter = () => synth.setFilterBypassed(true); -const unBypassFilter = () => { - voiceGains.forEach((node, voiceIx) => { - node.disconnect(mainGain); - node.connect(filters[voiceIx].getInput()); - }); -}; +const unBypassFilter = () => synth.setFilterBypassed(false); interface PresetsControlPanelProps { setOctaveOffset: (newOctaveOffset: number) => void; @@ -398,7 +372,7 @@ const PresetsControlPanel: React.FC = ({ lenSamples: { type: 'constant', value: filterEnvelope.lenSamples }, }); GlobalState.filterParams = preset.filterParams; - filters.forEach(filter => filter.setAll(GlobalState.filterParams)); + synth.setFilterParams(GlobalState.filterParams); if (preset.filterBypassed !== GlobalState.filterBypassed) { if (preset.filterBypassed) { bypassFilter(); @@ -407,9 +381,12 @@ const PresetsControlPanel: React.FC = ({ } } GlobalState.filterBypassed = preset.filterBypassed; - if (GlobalState.filterADSREnabled !== (preset.filterADSREnabled ?? false)) { - filters.forEach(filter => filter.csns.frequency.setIsOverridden(!preset.filterADSREnabled)); - } + synth.handleFilterFrequencyChange( + preset.filterParams.frequency, + preset.filterADSREnabled + ? FilterParamControlSource.Envelope + : FilterParamControlSource.Manual + ); GlobalState.filterADSREnabled = preset.filterADSREnabled ?? false; // Disconnect main output to avoid any horrific artifacts while we're switching @@ -683,7 +660,6 @@ const FMSynthDemo: React.FC = () => { isHidden={false} /> { lenSamples: { type: 'constant', value: envelope.lenSamples }, }); if (GlobalState.filterADSREnabled !== enableADSR) { - filters.forEach(filter => filter.csns.frequency.setIsOverridden(!enableADSR)); + synth.handleFilterFrequencyChange( + params.frequency, + enableADSR ? FilterParamControlSource.Envelope : FilterParamControlSource.Manual + ); } GlobalState.filterADSREnabled = enableADSR; diff --git a/src/fmDemo/FilterConfig.tsx b/src/fmDemo/FilterConfig.tsx index d3a75aa8..b3f768c4 100644 --- a/src/fmDemo/FilterConfig.tsx +++ b/src/fmDemo/FilterConfig.tsx @@ -68,7 +68,7 @@ export class FilterContainer { } const handleFilterChange = ( - filters: FilterContainer[], + synth: FMSynth, state: { params: FilterParams; envelope: Adsr; bypass: boolean; enableADSR: boolean }, key: string, val: any @@ -76,14 +76,20 @@ const handleFilterChange = ( const newState = { ...state, envelope: { ...state.envelope }, params: { ...state.params } }; switch (key) { case 'frequency': + newState.params[key] = val; + synth.handleFilterFrequencyChange(val); + break; case 'Q': + newState.params[key] = val; + synth.handleFilterQChange(val); + break; case 'gain': newState.params[key] = val; - filters.forEach(filter => filter.set(key, val)); + synth.handleFilterGainChange(val); break; case 'type': { - filters.forEach(filter => filter.setType(val)); newState.params.type = val; + synth.handleFilterTypeChange(val); break; } case 'enable envelope': { @@ -120,7 +126,6 @@ interface FilterConfigProps { bypass: boolean; enableADSR: boolean; }; - filters: FilterContainer[]; onChange: (params: FilterParams, envelope: Adsr, bypass: boolean, enableADSR: boolean) => void; vcId: string | undefined; adsrDebugName?: string; @@ -129,7 +134,6 @@ interface FilterConfigProps { const FilterConfig: React.FC = ({ initialState, - filters, onChange, vcId, adsrDebugName, @@ -183,7 +187,7 @@ const FilterConfig: React.FC = ({ settings={settings} state={controlPanelState} onChange={(key: string, val: any) => { - const newState = handleFilterChange(filters, state, key, val); + const newState = handleFilterChange(synth, state, key, val); onChange(newState.params, newState.envelope, newState.bypass, newState.enableADSR); setState(newState); }} diff --git a/src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx b/src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx index c87b0cd0..adbe0542 100644 --- a/src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx +++ b/src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx @@ -45,7 +45,6 @@ import { buildDefaultFilter } from 'src/synthDesigner/filterHelpersLight'; import { FilterType } from 'src/synthDesigner/FilterType'; const OPERATOR_COUNT = 8; -const VOICE_COUNT = 10; const ctx = new AudioContext(); @@ -695,36 +694,42 @@ export default class FMSynth implements ForeignNode { }); } - public handleFilterQChange(newManualQ: number, controlSource: FilterParamControlSource) { + public handleFilterQChange(newManualQ: number, controlSource?: FilterParamControlSource) { this.filterParams.Q = newManualQ; - this.filterParamControlSources.Q = controlSource; + if (!R.isNil(controlSource)) { + this.filterParamControlSources.Q = controlSource; + } this.awpHandle?.port.postMessage({ type: 'setFilterQ', Q: newManualQ, - controlSource: controlSource, + controlSource: this.filterParamControlSources.Q, }); } public handleFilterFrequencyChange( newManualFrequency: number, - controlSource: FilterParamControlSource + controlSource?: FilterParamControlSource ) { this.filterParams.frequency = newManualFrequency; - this.filterParamControlSources.frequency = controlSource; + if (!R.isNil(controlSource)) { + this.filterParamControlSources.frequency = controlSource; + } this.awpHandle?.port.postMessage({ type: 'setFilterFrequency', frequency: newManualFrequency, - controlSource: controlSource, + controlSource: this.filterParamControlSources.frequency, }); } - public handleFilterGainChange(newManualGain: number, controlSource: FilterParamControlSource) { + public handleFilterGainChange(newManualGain: number, controlSource?: FilterParamControlSource) { this.filterParams.gain = newManualGain; - this.filterParamControlSources.gain = controlSource; + if (!R.isNil(controlSource)) { + this.filterParamControlSources.gain = controlSource; + } this.awpHandle?.port.postMessage({ type: 'setFilterGain', gain: newManualGain, - controlSource: controlSource, + controlSource: this.filterParamControlSources.gain, }); } @@ -768,7 +773,13 @@ export default class FMSynth implements ForeignNode { const oldAdsr = adsrIx === -1 ? this.gainEnvelope : adsrIx === -2 ? this.filterEnvelope : this.adsrs[adsrIx]; - const isLenOnlyChange = oldAdsr && !R.equals(oldAdsr.lenSamples, newAdsrRaw.lenSamples); + const isLenOnlyChange = + oldAdsr && + !R.equals(oldAdsr.lenSamples, newAdsrRaw.lenSamples) && + R.equals(oldAdsr.steps, newAdsrRaw.steps) && + oldAdsr.releasePoint === newAdsrRaw.releasePoint && + oldAdsr.loopPoint === newAdsrRaw.loopPoint && + oldAdsr.logScale === newAdsrRaw.logScale; const newAdsr = { ...R.clone({ ...newAdsrRaw, audioThreadData: undefined }), audioThreadData: { diff --git a/src/graphEditor/nodes/util.ts b/src/graphEditor/nodes/util.ts index 972bf718..d2efa8eb 100644 --- a/src/graphEditor/nodes/util.ts +++ b/src/graphEditor/nodes/util.ts @@ -159,6 +159,10 @@ export class OverridableAudioParam extends GainNode implements AudioNode { this.overrideStatusChangeCbs.push(cb); } + public deregisterOverrideStatusChangeCb(cb: (isOverridden: boolean) => void) { + this.overrideStatusChangeCbs = this.overrideStatusChangeCbs.filter(c => c !== cb); + } + /** * Replaces the currently wrapped param with the new one provided, disconnecting the old one and re-connecting the new * one in its place. diff --git a/src/redux/modules/synthDesigner.ts b/src/redux/modules/synthDesigner.ts index 7d772ffd..a2dff568 100644 --- a/src/redux/modules/synthDesigner.ts +++ b/src/redux/modules/synthDesigner.ts @@ -50,6 +50,7 @@ export interface SynthModule { filterEnvelope: Adsr; filterADSRLength: number; pitchMultiplier: number; + filterOverrideStatusChangeCbs?: FilterOverrideStatusChangeCBs; } const ctx = new AudioContext(); @@ -93,6 +94,18 @@ const connectOscillators = (connect: boolean, synth: SynthModule) => { const disposeSynthModule = (synth: SynthModule) => { synth.fmSynth.shutdown(); synth.outerGainNode.disconnect(); + if (synth.filterOverrideStatusChangeCbs) { + console.log('de-registering'); + synth.filterCSNs.frequency.deregisterOverrideStatusChangeCb( + synth.filterOverrideStatusChangeCbs.handleFrequencyOverrideStatusChange + ); + synth.filterCSNs.Q.deregisterOverrideStatusChangeCb( + synth.filterOverrideStatusChangeCbs.handleQOverrideStatusChange + ); + synth.filterCSNs.gain.deregisterOverrideStatusChangeCb( + synth.filterOverrideStatusChangeCbs.handleGainOverrideStatusChange + ); + } }; const connectFMSynth = (stateKey: string, synthIx: number) => { @@ -145,12 +158,18 @@ interface InitAndConnectFilterCSNsArgs { fmSynth: FMSynth; } +interface FilterOverrideStatusChangeCBs { + handleFrequencyOverrideStatusChange: (isOverridden: boolean) => void; + handleQOverrideStatusChange: (isOverridden: boolean) => void; + handleGainOverrideStatusChange: (isOverridden: boolean) => void; +} + const initAndConnectFilterCSNs = ({ filterCSNs, synthIx, stateKey, fmSynth, -}: InitAndConnectFilterCSNsArgs) => { +}: InitAndConnectFilterCSNsArgs): FilterOverrideStatusChangeCBs => { const getState = SynthDesignerStateByStateKey.get(stateKey)?.getState; if (!getState) { throw new Error(`Failed to get state for stateKey=${stateKey}`); @@ -167,7 +186,6 @@ const initAndConnectFilterCSNs = ({ ); const handleFrequencyOverrideStatusChange = (isOverridden: boolean) => { - console.log({ isOverridden }); const targetSynth = getState().synthDesigner.synths[synthIx]; const controlSource = isOverridden ? targetSynth.filterEnvelopeEnabled @@ -198,6 +216,12 @@ const initAndConnectFilterCSNs = ({ }; filterCSNs.gain.registerOverrideStatusChangeCb(handleGainOverrideStatusChange); handleGainOverrideStatusChange(filterCSNs.gain.getIsOverridden()); + + return { + handleFrequencyOverrideStatusChange, + handleQOverrideStatusChange, + handleGainOverrideStatusChange, + }; }; const buildDefaultSynthModule = ( @@ -219,14 +243,16 @@ const buildDefaultSynthModule = ( new FMSynth(ctx, undefined, { filterEnvelope: filterEnvelope ? normalizeEnvelope(filterEnvelope) : filterEnvelope, onInitialized: () => { - const getState = SynthDesignerStateByStateKey.get(stateKey)?.getState; - if (!getState) { + const state = SynthDesignerStateByStateKey.get(stateKey); + if (!state) { throw new Error(`Failed to get state for stateKey=${stateKey}`); } + const { getState, dispatch, actionCreators } = state; const pitchMultiplier = getState().synthDesigner.synths[synthIx].pitchMultiplier; fmSynth.setFrequencyMultiplier(pitchMultiplier); - initAndConnectFilterCSNs({ filterCSNs, synthIx, stateKey, fmSynth }); + const cbs = initAndConnectFilterCSNs({ filterCSNs, synthIx, stateKey, fmSynth }); + dispatch(actionCreators.synthDesigner.SET_FILTER_OVERRIDE_STATUS_CHANGE_CBS(synthIx, cbs)); connectFMSynth(stateKey, synthIx); }, @@ -299,13 +325,15 @@ export const deserializeSynthModule = ( onInitialized: () => { fmSynth.setFrequencyMultiplier(pitchMultiplier); - const getState = SynthDesignerStateByStateKey.get(stateKey)?.getState; - if (!getState) { + const state = SynthDesignerStateByStateKey.get(stateKey); + if (!state) { throw new Error(`Failed to get state for stateKey=${stateKey}`); } + const { getState, dispatch, actionCreators } = state; const targetSynth = getState().synthDesigner.synths[synthIx]; const filterCSNs = targetSynth.filterCSNs; - initAndConnectFilterCSNs({ filterCSNs, synthIx, stateKey, fmSynth }); + const cbs = initAndConnectFilterCSNs({ filterCSNs, synthIx, stateKey, fmSynth }); + dispatch(actionCreators.synthDesigner.SET_FILTER_OVERRIDE_STATUS_CHANGE_CBS(synthIx, cbs)); connectFMSynth(stateKey, synthIx); }, @@ -709,6 +737,20 @@ const actionGroups = { actionCreator: (ctx: PolysynthContext) => ({ type: 'SET_POLYSYNTH_CTX', ctx }), subReducer: (state: SynthDesignerState, { ctx }) => ({ ...state, polysynthCtx: ctx }), }), + SET_FILTER_OVERRIDE_STATUS_CHANGE_CBS: buildActionGroup({ + actionCreator: ( + synthIx: number, + cbs: FilterOverrideStatusChangeCBs | undefined + ): { type: 'SET_FILTER_OVERRIDE_STATUS_CHANGE_CBS'; synthIx: number; cbs: any } => ({ + type: 'SET_FILTER_OVERRIDE_STATUS_CHANGE_CBS', + synthIx, + cbs, + }), + subReducer: (state: SynthDesignerState, { synthIx, cbs }) => { + const targetSynth = getSynth(synthIx, state.synths); + return setSynth(synthIx, { ...targetSynth, filterOverrideStatusChangeCbs: cbs }, state); + }, + }), }; interface SynthDesignerStateMapValue extends ReturnType { diff --git a/src/synthDesigner/SynthModule.tsx b/src/synthDesigner/SynthModule.tsx index 33b797ba..118892f6 100644 --- a/src/synthDesigner/SynthModule.tsx +++ b/src/synthDesigner/SynthModule.tsx @@ -150,7 +150,10 @@ const SynthControlPanelInner: React.FC = props => { case 'gain envelope': { setGainEnvelope(val); const fmSynth = getState().synthDesigner.synths[props.index].fmSynth; - fmSynth.handleAdsrChange(-1, val); + fmSynth.handleAdsrChange(-1, { + ...val, + lenSamples: { type: 'constant', value: msToSamples(gainADSRLengthMs) }, + }); return; } case 'adsr length ms': {