From e31a20ed93d7c421cf0df2b6e65960fa5073a896 Mon Sep 17 00:00:00 2001 From: espadonne Date: Thu, 2 Apr 2026 19:32:40 -0400 Subject: [PATCH 1/2] handle OfflineAudioContext unavailable in Firefox/Safari workers Worker transfers fetched ArrayBuffer back to main thread for decoding instead of erroring out and triggering a re-fetch that may 503. --- .../audio/DAW/Multitrack/AudioProcessor.js | 84 +++++++++++++------ 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/components/audio/DAW/Multitrack/AudioProcessor.js b/components/audio/DAW/Multitrack/AudioProcessor.js index 5434e3c..21d57ad 100644 --- a/components/audio/DAW/Multitrack/AudioProcessor.js +++ b/components/audio/DAW/Multitrack/AudioProcessor.js @@ -117,6 +117,28 @@ class AudioProcessor { processing.reject(new Error(data.error)); this.processingQueue.delete(clipId); break; + + case 'decode-failed': + // Worker fetched audio but couldn't decode (Firefox/Safari lack + // OfflineAudioContext in workers). Decode on main thread using + // the already-fetched data to avoid a second network request. + debugLog('AudioProcessor', `🔄 Main-thread decode for ${clipId} (worker transferred data)`); + processing.onProgress?.('decoding', 70); + { + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + audioCtx.decodeAudioData(data.arrayBuffer) + .then(audioBuffer => { + const peaks = this.generateSimplePeaks(audioBuffer); + processing.resolve({ duration: audioBuffer.duration, peaks, method: 'worker-fetch-main-decode' }); + }) + .catch(err => { + processing.reject(new Error('Main-thread decode failed: ' + err.message)); + }) + .finally(() => { + this.processingQueue.delete(clipId); + }); + } + break; } } @@ -451,32 +473,42 @@ class AudioProcessor { self.postMessage({ type: 'progress', clipId, stage: 'decoding', progress: 60 }); console.log('🔧 Worker: Starting audio decode for', clipId); - // Use OfflineAudioContext in worker - const decodeStart = performance.now(); - const sampleRate = WORKER_CONSTANTS.DEFAULT_SAMPLE_RATE; - const offlineCtx = new OfflineAudioContext(2, sampleRate, sampleRate); - const audioBuffer = await offlineCtx.decodeAudioData(arrayBuffer); - const decodeTime = Math.round(performance.now() - decodeStart); - console.log('🔧 Worker: Decode completed in', decodeTime + 'ms for', clipId, '(duration:', audioBuffer.duration.toFixed(2) + 's)'); - - self.postMessage({ type: 'progress', clipId, stage: 'generating-peaks', progress: 85 }); - console.log('🌊 Worker: Generating peaks for', clipId); - - // Generate peaks - const peaksStart = performance.now(); - const peaks = generatePeaks(audioBuffer, WORKER_CONSTANTS.DEFAULT_SAMPLES_PER_PIXEL); - const peaksTime = Math.round(performance.now() - peaksStart); - console.log('🌊 Worker: Peaks generated in', peaksTime + 'ms for', clipId, '(' + peaks.length + ' samples)'); - - const totalTime = Math.round(performance.now() - startTime); - console.log('✅ Worker: Completed', clipId, 'in', totalTime + 'ms total'); - - self.postMessage({ - type: 'success', - clipId, - duration: audioBuffer.duration, - peaks - }); + // OfflineAudioContext is not available in Web Workers on Firefox/Safari. + // If decode fails, transfer the fetched data back so the main thread + // can decode without re-fetching. + try { + const decodeStart = performance.now(); + const sampleRate = WORKER_CONSTANTS.DEFAULT_SAMPLE_RATE; + const offlineCtx = new OfflineAudioContext(2, sampleRate, sampleRate); + const audioBuffer = await offlineCtx.decodeAudioData(arrayBuffer); + const decodeTime = Math.round(performance.now() - decodeStart); + console.log('🔧 Worker: Decode completed in', decodeTime + 'ms for', clipId, '(duration:', audioBuffer.duration.toFixed(2) + 's)'); + + self.postMessage({ type: 'progress', clipId, stage: 'generating-peaks', progress: 85 }); + console.log('🌊 Worker: Generating peaks for', clipId); + + const peaksStart = performance.now(); + const peaks = generatePeaks(audioBuffer, WORKER_CONSTANTS.DEFAULT_SAMPLES_PER_PIXEL); + const peaksTime = Math.round(performance.now() - peaksStart); + console.log('🌊 Worker: Peaks generated in', peaksTime + 'ms for', clipId, '(' + peaks.length + ' samples)'); + + const totalTime = Math.round(performance.now() - startTime); + console.log('✅ Worker: Completed', clipId, 'in', totalTime + 'ms total'); + + self.postMessage({ + type: 'success', + clipId, + duration: audioBuffer.duration, + peaks + }); + } catch (decodeError) { + console.warn('⚠️ Worker: Decode failed for', clipId, '- transferring data to main thread:', decodeError.message); + self.postMessage({ + type: 'decode-failed', + clipId, + arrayBuffer: arrayBuffer + }, [arrayBuffer]); + } } catch (error) { const totalTime = Math.round(performance.now() - startTime); From 3455fd29a155e09d1bb0c066aa74f28adce1ccb8 Mon Sep 17 00:00:00 2001 From: espadonne Date: Thu, 2 Apr 2026 19:39:35 -0400 Subject: [PATCH 2/2] wire logOperation to multitrack ClipEQ preset selection ClipEQ (multitrack EQ) is a separate component from EQ.js (single-track). It had no logOperation at all, so selecting Warm Bass in multitrack never logged eq_preset_applied. --- components/audio/DAW/Multitrack/ClipEffectParametersModal.js | 4 +++- components/audio/DAW/Multitrack/ClipEffectsRack.js | 1 + components/audio/DAW/Multitrack/effects/ClipEQ.js | 5 ++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/audio/DAW/Multitrack/ClipEffectParametersModal.js b/components/audio/DAW/Multitrack/ClipEffectParametersModal.js index bcd418b..2f7420e 100644 --- a/components/audio/DAW/Multitrack/ClipEffectParametersModal.js +++ b/components/audio/DAW/Multitrack/ClipEffectParametersModal.js @@ -68,7 +68,8 @@ export default function ClipEffectParametersModal({ clipId, effectId, effectType, - currentParameters + currentParameters, + logOperation = null }) { const { tracks, updateTrack } = useMultitrack(); @@ -121,6 +122,7 @@ export default function ClipEffectParametersModal({ diff --git a/components/audio/DAW/Multitrack/ClipEffectsRack.js b/components/audio/DAW/Multitrack/ClipEffectsRack.js index c6fa78d..a4ce5bd 100644 --- a/components/audio/DAW/Multitrack/ClipEffectsRack.js +++ b/components/audio/DAW/Multitrack/ClipEffectsRack.js @@ -675,6 +675,7 @@ export default function ClipEffectsRack({ show, onHide, selectedClipId, logOpera effectId={selectedEffect.id} effectType={selectedEffect.type} currentParameters={selectedEffect.parameters} + logOperation={logOperation} /> )} diff --git a/components/audio/DAW/Multitrack/effects/ClipEQ.js b/components/audio/DAW/Multitrack/effects/ClipEQ.js index b46d395..d2ab840 100644 --- a/components/audio/DAW/Multitrack/effects/ClipEQ.js +++ b/components/audio/DAW/Multitrack/effects/ClipEQ.js @@ -244,7 +244,7 @@ function calculateFrequencyResponse(bands, sampleRate, numPoints = 512) { /** * Clip EQ Component */ -export default function ClipEQ({ parameters, onParametersChange }) { +export default function ClipEQ({ parameters, onParametersChange, logOperation = null }) { const canvasRef = useRef(null); const [eqBands, setEqBands] = useState(parameters.bands || EQPresets.flat.bands); const [outputGain, setOutputGain] = useState(parameters.outputGain || 0); @@ -396,6 +396,9 @@ export default function ClipEQ({ parameters, onParametersChange }) { setEqBands(preset.bands.map(band => ({ ...band }))); setOutputGain(preset.outputGain); onParametersChange({ bands: preset.bands, outputGain: preset.outputGain }); + if (logOperation && presetKey !== 'flat') { + logOperation('eq_preset_applied', { presetName: preset.name }); + } } }} className="bg-secondary text-white border-0"