diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index d18a5f3..5aa9f2c 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -127,23 +127,16 @@ async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_ @router.get("/soundfonts/download/{sf_id}") async def download_soundfont_asset(sf_id: str): - import logging as _lg - _log = _lg.getLogger("uvicorn.access") clean_id = sf_id.replace("sf_", "") if sf_id.startswith("sf_") else sf_id - _log.info(f"[DOWNLOAD_DEBUG] sf_id={sf_id} clean_id={clean_id}") for base_dir in [UPLOAD_SF_DIR, SYSTEM_SF_DIR]: if not os.path.isdir(base_dir): - _log.info(f"[DOWNLOAD_DEBUG] dir NOT_FOUND: {base_dir}") continue for ext in [".sf2", ".sf3"]: for fname in os.listdir(base_dir): fbase, fext = os.path.splitext(fname) if fext.lower() == ext and fbase.lower() == clean_id.lower(): full = os.path.join(base_dir, fname) - sz = os.path.getsize(full) - _log.info(f"[DOWNLOAD_DEBUG] SERVING: {full} ext={ext} size={sz}") return FileResponse(full, media_type="application/octet-stream", filename=f"soundfont{ext}") - _log.warning(f"[DOWNLOAD_DEBUG] NOT_FOUND for {sf_id}") raise HTTPException(status_code=404, detail="SoundFont asset not found") diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index 951e5eb..ebcd12e 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -33,199 +33,57 @@ let _synthInstance = null; let _initialized = false; - let _currentSfId = null; - - // ── SpessaSynth addSoundBank wrapper with timeout ── - async function _addBankWithTimeout(synth, buffer, sfId, timeoutMs) { - console.log("[SonicSF][DEBUG] _addBankWithTimeout start sfId=" + sfId + " size=" + buffer.byteLength + " timeout=" + timeoutMs); - return new Promise((resolve) => { - const timer = setTimeout(() => { - console.warn("[SonicSF][DEBUG] _addBankWithTimeout TIMEOUT after " + timeoutMs + "ms for " + sfId); - resolve(false); - }, timeoutMs); - - synth.soundBankManager.addSoundBank(buffer.slice(0), sfId).then(() => { - clearTimeout(timer); - const bl = synth.soundBankManager?.soundBankList?.length ?? 0; - console.log("[SonicSF][DEBUG] _addBankWithTimeout SUCCESS for " + sfId + " banks=" + bl); - resolve(true); - }).catch((err) => { - clearTimeout(timer); - console.error("[SonicSF][DEBUG] _addBankWithTimeout REJECTED for " + sfId + " error=" + err); - resolve(false); - }); - }); - } const SonicSF = { loadedFonts: {}, - // ════════════════════════════════════════════════ - // PHASE 1: Init SpessaSynth - // ════════════════════════════════════════════════ init: async function (audioContext) { if (_initialized && _synthInstance) return; - console.log("[SonicSF][DEBUG] PHASE1: init start"); - if (!window.SpessaSynthClass || !window.__SpessaSynthCDN) { - console.warn("[SonicSF][DEBUG] PHASE1: SpessaSynth CDN not available"); - return; - } + console.log("[SonicSF] SpessaSynth init (oscillator fallback for preview)."); try { - console.log("[SonicSF][DEBUG] PHASE1: loading worklet from " + window.__SpessaSynthCDN + "spessasynth_processor.min.js"); - const procUrl = window.__SpessaSynthCDN + "spessasynth_processor.min.js"; + const procUrl = (window.__SpessaSynthCDN || "https://cdn.jsdelivr.net/npm/spessasynth_lib@4.3.1/dist/") + "spessasynth_processor.min.js"; await audioContext.audioWorklet.addModule(procUrl); - console.log("[SonicSF][DEBUG] PHASE1: worklet loaded, creating WorkletSynthesizer"); - _synthInstance = new window.SpessaSynthClass(audioContext); - console.log("[SonicSF][DEBUG] PHASE1: connecting to destination"); + _synthInstance = new (window.SpessaSynthClass)(audioContext); _synthInstance.connect(audioContext.destination); - console.log("[SonicSF][DEBUG] PHASE1: awaiting isReady"); await _synthInstance.isReady; _initialized = true; - console.log("[SonicSF][DEBUG] PHASE1: SpessaSynth engine ready. isReady resolved."); } catch (e) { - console.warn("[SonicSF][DEBUG] PHASE1: AudioWorklet failed:", e); - try { - console.log("[SonicSF][DEBUG] PHASE1: trying WorkerSynthesizer"); - const mod = await import("https://cdn.jsdelivr.net/npm/spessasynth_lib@4.3.1/dist/index.js"); - _synthInstance = new mod.WorkerSynthesizer(audioContext); - _synthInstance.connect(audioContext.destination); - await _synthInstance.isReady; - _initialized = true; - console.log("[SonicSF][DEBUG] PHASE1: WorkerSynthesizer ready."); - } catch (e2) { - console.error("[SonicSF][DEBUG] PHASE1: All paths failed:", e2); - } + console.warn("[SonicSF] SpessaSynth init failed:", e); } }, - // ════════════════════════════════════════════════ - // PHASE 2: Load SF → IndexedDB → SpessaSynth - // ════════════════════════════════════════════════ loadSoundFont: async function (sfId) { - console.log("[SonicSF][DEBUG] PHASE2: loadSoundFont start sfId=" + sfId + " _initialized=" + _initialized + " _currentSfId=" + _currentSfId); - if (!_initialized || !_synthInstance) { - console.log("[SonicSF][DEBUG] PHASE2: engine not ready, returning false"); - return false; - } - if (_currentSfId === sfId) { - console.log("[SonicSF][DEBUG] PHASE2: already loaded, returning true"); - return true; - } - - let buffer = null; - // Step 2a: Check IndexedDB cache - if (window.SonicSFStorage) { - console.log("[SonicSF][DEBUG] PHASE2a: checking IndexedDB for " + sfId); - buffer = await window.SonicSFStorage.getBuffer(sfId); - if (buffer) { - console.log("[SonicSF][DEBUG] PHASE2a: IndexedDB HIT size=" + buffer.byteLength); - } else { - console.log("[SonicSF][DEBUG] PHASE2a: IndexedDB MISS"); + if (!_initialized || !_synthInstance) return false; + try { + const cache = window.SonicSFStorage; + let buf = cache ? await cache.getBuffer(sfId) : null; + if (!buf) { + const resp = await fetch("/api/v1/plugins/soundfonts/download/" + encodeURIComponent(sfId) + "?t=" + Date.now()); + if (!resp.ok) return false; + buf = await resp.arrayBuffer(); + if (cache) await cache.saveBuffer(sfId, buf); } - } - - // Step 2b: Download from server - if (!buffer) { - try { - const url = "/api/v1/plugins/soundfonts/download/" + encodeURIComponent(sfId) + "?t=" + Date.now(); - console.log("[SonicSF][DEBUG] PHASE2b: fetching from server url=" + url); - const resp = await fetch(url); - console.log("[SonicSF][DEBUG] PHASE2b: server response status=" + resp.status + " type=" + resp.headers.get("content-type") + " size=" + resp.headers.get("content-length")); - if (!resp.ok) throw new Error("HTTP " + resp.status); - buffer = await resp.arrayBuffer(); - console.log("[SonicSF][DEBUG] PHASE2b: downloaded " + buffer.byteLength + " bytes for " + sfId); - - // Step 2c: Save to IndexedDB - if (window.SonicSFStorage) { - console.log("[SonicSF][DEBUG] PHASE2c: saving to IndexedDB"); - await window.SonicSFStorage.saveBuffer(sfId, buffer); - console.log("[SonicSF][DEBUG] PHASE2c: saved to IndexedDB OK"); - } - } catch (e) { - console.error("[SonicSF][DEBUG] PHASE2b: download FAILED for " + sfId + ": " + e); - return false; - } - } - - // Step 2d: Load ArrayBuffer into SpessaSynth Engine - console.log("[SonicSF][DEBUG] PHASE2d: calling addSoundBank for " + sfId + " size=" + buffer.byteLength); - const ok = await _addBankWithTimeout(_synthInstance, buffer, sfId, 15000); - if (ok) { - _currentSfId = sfId; - const bankCount = _synthInstance.soundBankManager?.soundBankList?.length ?? 0; - const sb = _synthInstance.soundBankManager?.soundBankList?.[0]; - console.log("[SonicSF][DEBUG] PHASE2d: addSoundBank SUCCESS. _currentSfId=" + _currentSfId + " total banks=" + bankCount + " sb_exists=" + !!sb); - // soundBank is only stored on the worklet side (not main thread). - // Check selectablePresetList count (proxy for worklet state) - const spl = _synthInstance.soundBankManager?.selectablePresetList?.length ?? 0; - console.log("[SonicSF][DEBUG] PHASE2d: selectablePresetList.length=" + spl); - if (spl === 0) { - // The bank was registered but presets are empty - parser issue. - console.warn("[SonicSF][DEBUG] PHASE2d: presets EMPTY. SpessaSynth parser couldn't extract presets from SF2."); - } - return true; - } else { - console.warn("[SonicSF][DEBUG] PHASE2d: addSoundBank FAILED for " + sfId + ". Clearing IndexedDB cache."); - if (window.SonicSFStorage) { - try { - const db = await window.SonicSFStorage.openDB(); - const tx = db.transaction(window.SonicSFStorage.storeName, "readwrite"); - tx.objectStore(window.SonicSFStorage.storeName).delete(sfId); - } catch (ce) {} - } - _currentSfId = null; + await _synthInstance.soundBankManager.addSoundBank(buf.slice(0), sfId); + await _synthInstance.isReady; + const count = _synthInstance.soundBankManager?.selectablePresetList?.length ?? 0; + if (count === 0) console.warn("[SonicSF] SF parsed but 0 presets (SpessaSynth parser limitation). Using oscillator."); + return count > 0; + } catch (e) { + console.warn("[SonicSF] loadSoundFont failed:", e); return false; } }, - // ════════════════════════════════════════════════ - // PHASE 3: Set up MIDI Channel (bank/program route) - // ════════════════════════════════════════════════ selectInstrument: async function (channel, bank, program, sfId) { - console.log("[SonicSF][DEBUG] PHASE3: selectInstrument ch=" + channel + " bank=" + bank + " prog=" + program + " sf=" + sfId + " _initialized=" + _initialized); - // Auto-init: create AudioContext + init SpessaSynth if not ready - if (!_initialized || !_synthInstance) { - console.log("[SonicSF][DEBUG] PHASE3: engine not ready, triggering init..."); - try { - const ctx = new (window.AudioContext || window.webkitAudioContext)(); - if (ctx.state === 'suspended') await ctx.resume(); - await this.init(ctx); - } catch (e) { - console.error("[SonicSF][DEBUG] PHASE3: auto-init failed:", e); - return; - } - if (!_initialized || !_synthInstance) { - console.warn("[SonicSF][DEBUG] PHASE3: engine still not ready after auto-init"); - return; - } - console.log("[SonicSF][DEBUG] PHASE3: auto-init complete"); - } - // Phase 2 first: ensure SoundFont is loaded - if (sfId) { - const loaded = await this.loadSoundFont(sfId); - if (!loaded) { - console.warn("[SonicSF][DEBUG] PHASE3: loadSoundFont returned false, no bank to select"); - return; - } - } - // Send CC0 + CC32 + Program Change - console.log("[SonicSF][DEBUG] PHASE3: sending controllerChange(ch=" + channel + ", cc=0, val=" + bank + ")"); - try { _synthInstance.controllerChange(channel, 0, bank); } catch (e) { console.warn("[SonicSF][DEBUG] PHASE3: CC0 error:", e); } - console.log("[SonicSF][DEBUG] PHASE3: sending controllerChange(ch=" + channel + ", cc=32, val=0)"); - try { _synthInstance.controllerChange(channel, 32, 0); } catch (e) { console.warn("[SonicSF][DEBUG] PHASE3: CC32 error:", e); } - console.log("[SonicSF][DEBUG] PHASE3: sending programChange(ch=" + channel + ", prog=" + program + ")"); - try { _synthInstance.programChange(channel, program); } catch (e) { console.warn("[SonicSF][DEBUG] PHASE3: progChange error:", e); } - // Update internal SonicSF state + if (!_initialized || !_synthInstance) return; + if (sfId) await this.loadSoundFont(sfId); + try { _synthInstance.controllerChange(channel, 0, bank); } catch (e) {} + try { _synthInstance.controllerChange(channel, 32, 0); } catch (e) {} + try { _synthInstance.programChange(channel, program); } catch (e) {} this.controllerChange(channel, 0, bank); this.programChange(channel, program); - // Verify the sound bank list - const bl = _synthInstance.soundBankManager?.soundBankList?.length ?? 0; - console.log("[SonicSF][DEBUG] PHASE3: complete. total banks=" + bl + " _currentSfId=" + _currentSfId); }, - // ════════════════════════════════════════════════ - // PHASE 4: Play Note - // ════════════════════════════════════════════════ controllerChange: function (channel, controller, value) { if (channel < 0 || channel > 15) return; if (_initialized && _synthInstance) { @@ -268,47 +126,11 @@ }, playNote: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) { - if (_initialized && _synthInstance && _currentSfId) { - if (synthEngine) { - const ch = channel !== undefined ? channel : (synthEngine.soundfont_bank === 128 ? 9 : 0); - try { _synthInstance.controllerChange(ch, 0, synthEngine.soundfont_bank || 0); } catch (e) {} - try { _synthInstance.controllerChange(ch, 32, 0); } catch (e) {} - try { _synthInstance.programChange(ch, synthEngine.soundfont_program || 0); } catch (e) {} - if (channel === undefined) channel = ch; - } - if (channel === undefined) channel = 0; - console.log("[SonicSF][DEBUG] PHASE4: playNote via SpessaSynth ch=" + channel + " note=" + note + " vel=" + velocity + " _currentSfId=" + _currentSfId); - try { - this._playNoteSpessa(note, velocity, durationMs, channel); - return; - } catch (e) { - console.warn("[SonicSF][DEBUG] PHASE4: SpessaSynth play failed, oscillator fallback:", e); - } - } else { - console.log("[SonicSF][DEBUG] PHASE4: playNote via oscillator (init=" + _initialized + " sf=" + _currentSfId + ")"); - } + // Always use oscillator for client preview — reliable + // Server-side FluidSynth render provides authentic SoundFont audio on Export this._playNoteOsc(note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine); }, - _playNoteSpessa: function (note, velocity, durationMs, channel) { - const ctx = getCtx(); - const midiPitch = Math.min(127, Math.max(0, parseInt(note) || 60)); - const midiVel = Math.min(127, Math.max(1, Math.floor( - typeof velocity === 'number' ? (velocity > 1 ? velocity : velocity * 127) : 100 - ))); - const durSec = durationMs / 1000; - console.log("[SonicSF][DEBUG] PHASE4: noteOn ch=" + channel + " pitch=" + midiPitch + " vel=" + midiVel + " banks=" + (_synthInstance.soundBankManager?.soundBankList?.length ?? 0)); - const doNoteOn = () => { - try { - _synthInstance.noteOn(channel, midiPitch, midiVel); - setTimeout(() => { try { _synthInstance.noteOff(channel, midiPitch); } catch (e) {} }, durSec * 1000); - } catch (e) { - console.warn("[SonicSF][DEBUG] PHASE4: noteOn threw:", e); - } - }; - doNoteOn(); - }, - _playNoteOsc: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) { const ctx = getCtx(); const freq = 440 * Math.pow(2, (note - 69) / 12); @@ -333,38 +155,27 @@ prog = chState.program || prog; } if (prog >= 0 && prog <= 7) { - oscType = 'sine'; - decayTime = 0.3; sustainLevel = 0.1; releaseTime = 0.2; + oscType = 'sine'; decayTime = 0.3; sustainLevel = 0.1; releaseTime = 0.2; } else if (prog >= 8 && prog <= 15) { - oscType = 'sine'; - decayTime = 0.1; sustainLevel = 0.0; releaseTime = 0.1; + oscType = 'sine'; decayTime = 0.1; sustainLevel = 0.0; releaseTime = 0.1; } else if (prog >= 16 && prog <= 23) { - oscType = 'sine'; - attackTime = 0.05; sustainLevel = 0.8; releaseTime = 0.1; + oscType = 'sine'; attackTime = 0.05; sustainLevel = 0.8; releaseTime = 0.1; } else if (prog >= 24 && prog <= 31) { - oscType = 'triangle'; - decayTime = 0.4; sustainLevel = 0.2; releaseTime = 0.3; + oscType = 'triangle'; decayTime = 0.4; sustainLevel = 0.2; releaseTime = 0.3; } else if (prog >= 32 && prog <= 39) { - oscType = 'triangle'; - attackTime = 0.02; decayTime = 0.2; sustainLevel = 0.6; releaseTime = 0.2; + oscType = 'triangle'; attackTime = 0.02; decayTime = 0.2; sustainLevel = 0.6; releaseTime = 0.2; } else if (prog >= 40 && prog <= 47) { - oscType = 'sawtooth'; - attackTime = 0.15; sustainLevel = 0.8; releaseTime = 0.5; volFactor = 0.15; + oscType = 'sawtooth'; attackTime = 0.15; sustainLevel = 0.8; releaseTime = 0.5; volFactor = 0.15; } else if (prog >= 48 && prog <= 55) { - oscType = 'sawtooth'; - attackTime = 0.2; sustainLevel = 0.8; releaseTime = 0.6; volFactor = 0.12; + oscType = 'sawtooth'; attackTime = 0.2; sustainLevel = 0.8; releaseTime = 0.6; volFactor = 0.12; } else if (prog >= 56 && prog <= 63) { - oscType = 'sawtooth'; - attackTime = 0.08; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.15; + oscType = 'sawtooth'; attackTime = 0.08; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.15; } else if (prog >= 64 && prog <= 71) { - oscType = 'square'; - attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.2; volFactor = 0.15; + oscType = 'square'; attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.2; volFactor = 0.15; } else if (prog >= 72 && prog <= 79) { - oscType = 'sine'; - attackTime = 0.1; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.2; + oscType = 'sine'; attackTime = 0.1; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.2; } else if (prog >= 80 && prog <= 119) { - oscType = 'sawtooth'; - attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.4; volFactor = 0.15; + oscType = 'sawtooth'; attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.4; volFactor = 0.15; } osc.type = oscType;