Skip to content

Commit

Permalink
Fix some issues with FM synth filter migration
Browse files Browse the repository at this point in the history
  • Loading branch information
Ameobea committed Jan 15, 2024
1 parent 8be8f7f commit 405c012
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 103 deletions.
14 changes: 12 additions & 2 deletions engine/wavetable/src/fm/filter/dynabandpass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, center_frequency - bandwidth / 2.);
let lowpass_freq = dsp::clamp(10., NYQUIST, center_frequency + bandwidth / 2.);
let highpass_freq = dsp::clamp(10., NYQUIST - 10., center_frequency - bandwidth / 2.);
let lowpass_freq = dsp::clamp(10., NYQUIST - 10., center_frequency + bandwidth / 2.);
(lowpass_freq, highpass_freq)
}

Expand Down Expand Up @@ -105,5 +105,15 @@ 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
);
}
}
}
50 changes: 36 additions & 14 deletions engine/wavetable/src/fm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,8 @@ pub struct FMSynthContext {
pub operator_base_frequency_sources: [ParamSource; OPERATOR_COUNT],
pub base_frequency_input_buffer: Vec<[f32; FRAME_SIZE]>,
pub output_buffers: Vec<[f32; FRAME_SIZE]>,
pub main_output_buffer: [f32; FRAME_SIZE],
pub master_gain: f32,
pub frequency_multiplier: f32,
pub most_recent_gated_voice_ix: usize,
pub adsr_phase_buf: [f32; 256],
Expand Down Expand Up @@ -1784,6 +1786,22 @@ impl FMSynthContext {
output_buffer[i] *= gain;
}
}

// Mix all voices together
self
.main_output_buffer
.copy_from_slice(unsafe { self.output_buffers.get_unchecked(0) });
for voice_ix in 1..self.voices.len() {
let voice_output = unsafe { self.output_buffers.get_unchecked(voice_ix) };
for i in 0..FRAME_SIZE {
self.main_output_buffer[i] += voice_output[i];
}
}

// Apply master gain
for i in 0..FRAME_SIZE {
self.main_output_buffer[i] *= self.master_gain;
}
}

pub fn update_operator_enabled_statuses(&mut self) {
Expand Down Expand Up @@ -1817,12 +1835,6 @@ impl FMSynthContext {
}
}
}

fn clear_output_buffer(&mut self, voice_ix: usize) {
self.base_frequency_input_buffer[voice_ix].fill(0.);
let buf = &mut self.output_buffers[voice_ix];
buf.fill(0.);
}
}

#[no_mangle]
Expand All @@ -1840,13 +1852,15 @@ pub unsafe extern "C" fn init_fm_synth_ctx(voice_count: usize) -> *mut FMSynthCo
operator_base_frequency_sources: uninit(),
base_frequency_input_buffer: Vec::with_capacity(voice_count),
output_buffers: Vec::with_capacity(voice_count),
main_output_buffer: uninit(),
frequency_multiplier: 1.,
most_recent_gated_voice_ix: 0,
adsr_phase_buf: [0.; 256],
detune: None,
wavetables: Vec::new(),
sample_mapping_manager: SampleMappingManager::default(),
polysynth: uninit(),
master_gain: 1.,
}));

std::ptr::write(
Expand Down Expand Up @@ -1912,9 +1926,9 @@ pub unsafe extern "C" fn fm_synth_generate(
ctx: *mut FMSynthContext,
cur_bpm: f32,
cur_frame_start_beat: f32,
) -> *const [f32; FRAME_SIZE] {
) -> *const f32 {
(*ctx).generate(cur_bpm, cur_frame_start_beat);
(*ctx).output_buffers.as_ptr()
(*ctx).main_output_buffer.as_ptr()
}

#[no_mangle]
Expand Down Expand Up @@ -2374,12 +2388,6 @@ unsafe fn gate_voice_inner(ctx: *mut FMSynthContext, voice_ix: usize, midi_numbe
}
}

#[no_mangle]
pub unsafe extern "C" fn fm_synth_clear_output_buffer(ctx: *mut FMSynthContext, voice_ix: usize) {
let ctx = &mut (*ctx);
ctx.clear_output_buffer(voice_ix)
}

#[no_mangle]
pub unsafe extern "C" fn fm_synth_set_frequency_multiplier(
ctx: *mut FMSynthContext,
Expand Down Expand Up @@ -2684,6 +2692,12 @@ pub extern "C" fn fm_synth_set_mapped_sample_config(
)
}

#[no_mangle]
pub extern "C" fn fm_synth_set_master_gain(ctx: *mut FMSynthContext, gain: f32) {
let ctx = unsafe { &mut *ctx };
ctx.master_gain = gain;
}

#[no_mangle]
pub extern "C" fn fm_synth_set_filter_bypassed(ctx: *mut FMSynthContext, bypassed: bool) {
let ctx = unsafe { &mut *ctx };
Expand Down Expand Up @@ -2745,3 +2759,11 @@ pub extern "C" fn fm_synth_set_filter_gain(
voice.filter_module.set_gain(param_source.clone())
}
}

#[no_mangle]
pub extern "C" fn fm_synth_get_filter_param_buffers_ptr(
ctx: *mut FMSynthContext,
) -> *mut [f32; FRAME_SIZE] {
let ctx = unsafe { &mut *ctx };
ctx.filter_param_buffers.as_mut_ptr()
}
78 changes: 53 additions & 25 deletions public/FMSynthAWP.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ class FMSynthAWP extends AudioWorkletProcessor {
defaultValue: 0,
automationRate: 'a-rate',
})),
{
name: 'filter_cutoff_freq',
defaultValue: 0,
automationRate: 'a-rate',
},
{
name: 'filter_q',
defaultValue: 0,
automationRate: 'a-rate',
},
{
name: 'filter_gain',
defaultValue: 0,
automationRate: 'a-rate',
},
];
}

Expand Down Expand Up @@ -332,16 +347,6 @@ class FMSynthAWP extends AudioWorkletProcessor {
}
break;
}
case 'clearOutputBuffer': {
if (!this.wasmInstance) {
console.warn('Tried to clear output buffer before Wasm instance loaded');
return;
}

this.wasmInstance.exports.fm_synth_clear_output_buffer(this.ctxPtr, evt.data.voiceIx);
this.tacentVoiceFlags[evt.data.voiceIx] = 1;
break;
}
case 'setFrequencyMultiplier': {
if (!this.wasmInstance) {
console.warn('Tried to set frequency multiplier before Wasm instance loaded');
Expand Down Expand Up @@ -379,7 +384,6 @@ class FMSynthAWP extends AudioWorkletProcessor {
}

const { Q, controlSource } = evt.data;
console.log(Q);
this.wasmInstance.exports.fm_synth_set_filter_q(this.ctxPtr, Q, controlSource);
break;
}
Expand Down Expand Up @@ -407,6 +411,15 @@ class FMSynthAWP extends AudioWorkletProcessor {
this.wasmInstance.exports.fm_synth_set_filter_gain(this.ctxPtr, gain, controlSource);
break;
}
case 'setMasterGain': {
if (!this.wasmInstance) {
console.warn('Tried to set master gain before Wasm instance loaded');
return;
}

this.wasmInstance.exports.fm_synth_set_master_gain(this.ctxPtr, evt.data.masterGain);
break;
}
case 'shutdown': {
this.shutdown = true;
break;
Expand Down Expand Up @@ -481,7 +494,6 @@ class FMSynthAWP extends AudioWorkletProcessor {
log_raw: (ptr, len, _level) => this.handleWasmPanic(ptr, len),
debug1: (v1, v2, v3) => console.log({ v1, v2, v3 }),
on_gate_cb: (midiNumber, voiceIx) => {
this.tacentVoiceFlags[voiceIx] = 0;
this.port.postMessage({ type: 'onGate', midiNumber, voiceIx });
},
on_ungate_cb: (midiNumber, voiceIx) =>
Expand All @@ -493,7 +505,6 @@ class FMSynthAWP extends AudioWorkletProcessor {
this.wasmInstance.exports.memory.grow(1024 * 4);
this.ctxPtr = this.wasmInstance.exports.init_fm_synth_ctx(VOICE_COUNT);
this.wasmMemoryBuffer = new Float32Array(this.wasmInstance.exports.memory.buffer);
this.tacentVoiceFlags = new Uint8Array(VOICE_COUNT).fill(1);

outputWeights.forEach((paramSource, operatorIx) =>
this.wasmInstance.exports.fm_synth_set_output_weight_value(
Expand Down Expand Up @@ -637,6 +648,33 @@ class FMSynthAWP extends AudioWorkletProcessor {
}
}

// TODO: Only copy filter param buffers if they're being used
const filterParamBuffersPtr = this.wasmInstance.exports.fm_synth_get_filter_param_buffers_ptr(
this.ctxPtr
);
const filterParamBuffer = wasmMemory.subarray(
filterParamBuffersPtr / BYTES_PER_F32,
filterParamBuffersPtr / BYTES_PER_F32 + 4 * FRAME_SIZE
);
const qParam = params['filter_q'];
const cutoffParam = params['filter_cutoff_freq'];
const gainParam = params['filter_gain'];
if (qParam.length === 1) {
filterParamBuffer.fill(qParam[0], 0, FRAME_SIZE);
} else {
filterParamBuffer.set(qParam, 0);
}
if (cutoffParam.length === 1) {
filterParamBuffer.fill(cutoffParam[0], FRAME_SIZE, FRAME_SIZE * 2);
} else {
filterParamBuffer.set(cutoffParam, FRAME_SIZE);
}
if (gainParam.length === 1) {
filterParamBuffer.fill(gainParam[0], FRAME_SIZE * 2, FRAME_SIZE * 3);
} else {
filterParamBuffer.set(gainParam, FRAME_SIZE * 2);
}

const outputsPtr = this.wasmInstance.exports.fm_synth_generate(
this.ctxPtr,
globalThis.globalTempoBPM,
Expand All @@ -649,18 +687,8 @@ class FMSynthAWP extends AudioWorkletProcessor {
0
);
wasmMemory = this.getWasmMemoryBuffer();
for (let voiceIx = 0; voiceIx < VOICE_COUNT; voiceIx++) {
const voiceIsTacent = this.tacentVoiceFlags[voiceIx];
if (voiceIsTacent) {
continue;
}

const outputSlice = wasmMemory.subarray(
(outputsPtr + voiceIx * OUTPUT_BYTES_PER_OPERATOR) / 4,
(outputsPtr + voiceIx * OUTPUT_BYTES_PER_OPERATOR + OUTPUT_BYTES_PER_OPERATOR) / 4
);
outputs[voiceIx]?.[0]?.set(outputSlice);
}
const outputSlice = wasmMemory.subarray(outputsPtr / 4, outputsPtr / 4 + FRAME_SIZE);
outputs[0]?.[0]?.set(outputSlice);

// Copy current ADSR phases to shared buffer
if (this.audioThreadDataBuffer && this.adsrPhasesBufPtr) {
Expand Down
17 changes: 14 additions & 3 deletions src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { MIDINode } from 'src/patchNetwork/midiNode';
import { mkContainerCleanupHelper, mkContainerRenderHelper } from 'src/reactUtils';
import { getSample, hashSampleDescriptor, type SampleDescriptor } from 'src/sampleLibrary';
import { getSentry } from 'src/sentry';
import { AsyncOnce, dbToLinear, normalizeEnvelope } from 'src/util';
import { AsyncOnce, normalizeEnvelope } from 'src/util';
import { EventScheduleInitialized } from 'src/eventScheduler';
import type { FilterParams } from 'src/redux/modules/synthDesigner';
import { buildDefaultFilter } from 'src/synthDesigner/filterHelpersLight';
Expand Down Expand Up @@ -172,6 +172,7 @@ export default class FMSynth implements ForeignNode {
private onInitializedCBs: ((inst: FMSynth) => void)[] = [];
private audioThreadDataBuffer: Float32Array | null = null;
private detune: ParamSource | null = null;
private masterGain = 1;
public midiControlValuesCache: MIDIControlValuesCache;
private wavetableState: WavetableState = { wavetableBanks: [] };
private wavetableBackendIxByName: string[] = [];
Expand Down Expand Up @@ -368,9 +369,8 @@ export default class FMSynth implements ForeignNode {
EventScheduleInitialized,
] as const);
this.awpHandle = new AudioWorkletNode(this.ctx, 'fm-synth-audio-worklet-processor', {
// First `VOICE_COUNT` outputs are for the actual voice outputs
numberOfInputs: 0,
numberOfOutputs: VOICE_COUNT,
numberOfOutputs: 1,
channelCount: 1,
channelInterpretation: 'discrete',
channelCountMode: 'explicit',
Expand Down Expand Up @@ -420,6 +420,7 @@ export default class FMSynth implements ForeignNode {
this.handleDetuneChange(this.detune);
this.setFilterBypassed(this.filterBypassed);
this.setFilterParams(this.filterParams);
this.setMasterGain(this.masterGain);
this.sampleMappingStore.subscribe(this.handleSampleMappingStateChange);

for (const cb of this.onInitializedCBs) {
Expand Down Expand Up @@ -740,6 +741,12 @@ export default class FMSynth implements ForeignNode {
}
}

public setMasterGain(newMasterGain: number) {
this.masterGain = newMasterGain;
// Old behavior was to have a base gain of 1, but FM synth multiplies by it directly
this.awpHandle?.port.postMessage({ type: 'setMasterGain', masterGain: 1 + newMasterGain });
}

public onInitialized(): Promise<FMSynth> {
if (this.awpHandle) {
return Promise.resolve(this);
Expand Down Expand Up @@ -964,6 +971,9 @@ export default class FMSynth implements ForeignNode {
if (!R.isNil(params.filterParams)) {
this.setFilterParams(params.filterParams);
}
if (!R.isNil(params.masterGain)) {
this.masterGain = params.masterGain;
}
}

public shutdown() {
Expand Down Expand Up @@ -998,6 +1008,7 @@ export default class FMSynth implements ForeignNode {
sampleMappingState: serializeSampleMappingState(get(this.sampleMappingStore)),
useLegacyWavetableControls: this.useLegacyWavetableControls,
filterParamControlSources: this.filterParamControlSources,
masterGain: this.masterGain,
};
}

Expand Down
Loading

0 comments on commit 405c012

Please sign in to comment.