Skip to content

Commit 0ba3adc

Browse files
committed
fix: Robust trigger mode switching and voice choking
1 parent 4ac54bc commit 0ba3adc

4 files changed

Lines changed: 63 additions & 13 deletions

File tree

components/WaveformEditor.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface WaveformEditorProps {
1111
}
1212

1313
export const WaveformEditor: React.FC<WaveformEditorProps> = ({ isUltraSampleMode = false }) => {
14-
const { currentChannel, selectedPadId, pads, samples, updatePad, stopPad, setAppMode, setRecordingModalOpen } = usePadStore();
14+
const { currentChannel, selectedPadId, pads, samples, updatePad, stopPadExplicit, setAppMode, setRecordingModalOpen } = usePadStore();
1515
const { audioContext, micAnalyser, isRecording } = useAudioStore();
1616
const { setBpm } = useSequencerStore();
1717
const selectedPadIndex = parseInt(selectedPadId.split('-')[1]);
@@ -456,9 +456,8 @@ export const WaveformEditor: React.FC<WaveformEditorProps> = ({ isUltraSampleMod
456456
};
457457

458458
const setTriggerMode = (mode: TriggerMode) => {
459-
if (activePad?.triggerMode === 'ONE_SHOT' && mode !== 'ONE_SHOT') {
460-
stopPad(selectedPadIndex);
461-
}
459+
// Force stop all voices and clear UI state when switching trigger modes
460+
stopPadExplicit(selectedPadIndex);
462461
updatePad(selectedPadIndex, { triggerMode: mode });
463462
};
464463

public/assets/worklets/VoiceProcessor.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class VoiceProcessor extends AudioWorkletProcessor {
2323
case 'RELEASE_PAD':
2424
this.releaseVoice(data.padId, data.startTime);
2525
break;
26+
case 'STOP_PAD':
27+
this.stopPadVoices(data.padId, data.startTime);
28+
break;
2629
case 'UPDATE_PAD_START_END':
2730
this.updateVoiceBoundaries(data);
2831
break;
@@ -107,16 +110,13 @@ class VoiceProcessor extends AudioWorkletProcessor {
107110
// For GATE and LOOP modes, we enforce monophonic behavior per pad (Choke Group).
108111
// If a pad is re-triggered, the existing voice must stop with a very fast fade (5ms)
109112
// to avoid overlaps and digital clicks.
110-
if (triggerMode === 'GATE' || triggerMode === 'LOOP') {
111-
for (const voice of this.voices) {
112-
if (voice.padId === padId && !voice.finished) {
113-
voice.envelope.phase = 'release';
114-
voice.envelope.releaseT = 0;
115-
voice.envelope.release = 0.005; // 5ms forced release
116-
}
113+
// Every pad is monophonic (choke previous voice of same pad)
114+
for (const voice of this.voices) {
115+
if (voice.padId === padId && !voice.finished) {
116+
voice.envelope.phase = 'release';
117+
voice.envelope.releaseT = 0;
118+
voice.envelope.release = 0.005; // 5ms forced release to avoid clicks
117119
}
118-
} else {
119-
this.releaseVoice(padId);
120120
}
121121

122122
if (this.voices.length >= 32) {
@@ -175,13 +175,30 @@ class VoiceProcessor extends AudioWorkletProcessor {
175175

176176
for (const voice of this.voices) {
177177
if (voice.padId === padId && !voice.finished && voice.envelope.phase !== 'release') {
178+
// Standard release (e.g. key up) only affects GATE and LOOP
178179
if (voice.triggerMode === 'GATE' || voice.triggerMode === 'LOOP') {
179180
voice.releaseAtFrame = releaseAtFrame;
180181
}
181182
}
182183
}
183184
}
184185

186+
stopPadVoices(padId, startTime) {
187+
const sr = sampleRate || 44100;
188+
const releaseAtFrame = startTime !== undefined ? Math.floor(startTime * sr) : currentFrame;
189+
190+
for (const voice of this.voices) {
191+
if (voice.padId === padId && !voice.finished) {
192+
// Forced release (STOP) affects all modes
193+
voice.envelope.phase = 'release';
194+
voice.envelope.releaseT = 0;
195+
voice.envelope.release = 0.005; // Fast 5ms fade
196+
voice.envelope.levelAtRelease = voice.envelope.levelAtRelease || 0.5;
197+
voice.releaseAtFrame = undefined; // Process immediately
198+
}
199+
}
200+
}
201+
185202
process(inputs, outputs, parameters) {
186203
const output = outputs[0];
187204
const channelL = output[0];

stores/audioStore.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface AudioState {
2323
removeSampleFromWorklet: (id: string) => void;
2424
triggerPad: (data: any) => void;
2525
stopPad: (padId: string, startTime?: number) => void;
26+
stopPadExplicit: (padId: string) => void;
2627
updatePadStartEnd: (padId: string, start: number, end: number) => void;
2728
updatePadParams: (padId: string, params: { cutoff?: number, resonance?: number, pitch?: number, volume?: number, pan?: number, mute?: boolean }) => void;
2829
stopAll: () => void;
@@ -160,6 +161,16 @@ export const useAudioStore = create<AudioState>((set, get) => ({
160161
}
161162
},
162163

164+
stopPadExplicit: (padId: string) => {
165+
const { workletNode } = get();
166+
if (workletNode) {
167+
workletNode.port.postMessage({
168+
type: 'STOP_PAD',
169+
data: { padId }
170+
});
171+
}
172+
},
173+
163174
updatePadStartEnd: (padId: string, start: number, end: number) => {
164175
const { workletNode } = get();
165176
if (workletNode) {

stores/padStore.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface PadState {
2323
loadSample: (index: number, url: string, name: string) => Promise<void>;
2424
triggerPad: (index: number, velocity?: number, pitchOverrideMultiplier?: number, startTime?: number, channelId?: ChannelId) => void;
2525
stopPad: (index: number, startTime?: number, channelId?: ChannelId) => void;
26+
stopPadExplicit: (index: number, channelId?: ChannelId) => void;
2627
toggleMute: (index: number) => void;
2728
toggleSolo: (index: number) => void;
2829
syncMuteStates: () => void;
@@ -452,6 +453,28 @@ export const usePadStore = create<PadState>((set, get) => ({
452453
useAudioStore.getState().stopPad(id, startTime);
453454
},
454455

456+
stopPadExplicit: (index: number, channelId?: ChannelId) => {
457+
const { pads, currentChannel } = get();
458+
const effectiveChannel = channelId || currentChannel;
459+
const id = `${effectiveChannel}-${index}`;
460+
const pad = pads[id];
461+
if (pad) {
462+
set(state => ({
463+
pads: {
464+
...state.pads,
465+
[id]: {
466+
...pad,
467+
isHeld: false,
468+
lastTriggerTime: undefined,
469+
lastTriggerDuration: undefined,
470+
lastStopTime: undefined
471+
}
472+
}
473+
}));
474+
}
475+
useAudioStore.getState().stopPadExplicit(id);
476+
},
477+
455478
loadSamplePack: async (packId) => {
456479
set({ currentSamplePackId: packId });
457480
await dbService.saveMetadata('current_sample_pack_id', packId);

0 commit comments

Comments
 (0)