From e79ee2d76c1b9ccbd1b721956bd4ec171243126d Mon Sep 17 00:00:00 2001 From: ChuxiJ Date: Fri, 27 Mar 2026 20:42:53 +0800 Subject: [PATCH] feat: expose filter envelope controls in synth editor --- .../pianoroll/SynthInstrumentEditor.tsx | 100 +++++++++++++++++- tests/unit/SynthInstrumentEditor.test.tsx | 24 ++++- 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/src/components/pianoroll/SynthInstrumentEditor.tsx b/src/components/pianoroll/SynthInstrumentEditor.tsx index 57e9edec..021e2467 100644 --- a/src/components/pianoroll/SynthInstrumentEditor.tsx +++ b/src/components/pianoroll/SynthInstrumentEditor.tsx @@ -135,7 +135,7 @@ function renderSubtractiveEditor( instrument: SubtractiveTrackInstrument, onInstrumentChange: (instrument: SubtractiveTrackInstrument) => void, ) { - const { oscillator, ampEnvelope, filter, lfo, unison, glideTime, outputGain } = instrument.settings; + const { oscillator, ampEnvelope, filter, filterEnvelope, lfo, unison, glideTime, outputGain } = instrument.settings; const updateSettings = (settings: SubtractiveTrackInstrument['settings']) => { onInstrumentChange({ @@ -311,7 +311,7 @@ function renderSubtractiveEditor(
@@ -486,6 +486,101 @@ function renderSubtractiveEditor( /> +
+
+
Filter Envelope
+
Per-note cutoff contour
+
+
+ updateSettings({ + ...instrument.settings, + filterEnvelope: { + ...filterEnvelope, + amount: value, + }, + })} + /> + updateSettings({ + ...instrument.settings, + filterEnvelope: { + ...filterEnvelope, + attack: value, + }, + })} + /> + updateSettings({ + ...instrument.settings, + filterEnvelope: { + ...filterEnvelope, + decay: value, + }, + })} + /> +
+
+ updateSettings({ + ...instrument.settings, + filterEnvelope: { + ...filterEnvelope, + sustain: value, + }, + })} + /> + updateSettings({ + ...instrument.settings, + filterEnvelope: { + ...filterEnvelope, + release: value, + }, + })} + /> +
+
+ +
LFO and Glide
+
Unison
{ - it('updates subtractive oscillator waveform and filter cutoff through canonical instrument state', () => { + it('updates subtractive oscillator waveform, filter cutoff, and filter envelope through canonical instrument state', () => { const onInstrumentChange = vi.fn(); render( @@ -32,6 +32,28 @@ describe('SynthInstrumentEditor', () => { filter: expect.objectContaining({ cutoffHz: 3200 }), }), })); + + fireEvent.contextMenu(screen.getByLabelText('Filter Env Amt knob')); + fireEvent.change(screen.getByLabelText('Filter Env Amt exact value'), { target: { value: '0.42' } }); + fireEvent.keyDown(screen.getByLabelText('Filter Env Amt exact value'), { key: 'Enter' }); + + expect(onInstrumentChange).toHaveBeenLastCalledWith(expect.objectContaining({ + kind: 'subtractive', + settings: expect.objectContaining({ + filterEnvelope: expect.objectContaining({ amount: 0.42 }), + }), + })); + + fireEvent.contextMenu(screen.getByLabelText('Filt Env Release knob')); + fireEvent.change(screen.getByLabelText('Filt Env Release exact value'), { target: { value: '1.8' } }); + fireEvent.keyDown(screen.getByLabelText('Filt Env Release exact value'), { key: 'Enter' }); + + expect(onInstrumentChange).toHaveBeenLastCalledWith(expect.objectContaining({ + kind: 'subtractive', + settings: expect.objectContaining({ + filterEnvelope: expect.objectContaining({ release: 1.8 }), + }), + })); }); it('updates FM fallback preset and modulation index through canonical instrument state', () => {