diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index ebcd12e..c3c60a6 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -2,8 +2,8 @@ 'use strict'; const activeOscillators = {}; - let __gainNode = null; + const getCtx = () => { if (typeof getAudioContext === 'function') { const ctx = getAudioContext(); @@ -33,23 +33,29 @@ let _synthInstance = null; let _initialized = false; + let _initPromise = null; const SonicSF = { loadedFonts: {}, init: async function (audioContext) { if (_initialized && _synthInstance) return; - console.log("[SonicSF] SpessaSynth init (oscillator fallback for preview)."); - try { - const procUrl = (window.__SpessaSynthCDN || "https://cdn.jsdelivr.net/npm/spessasynth_lib@4.3.1/dist/") + "spessasynth_processor.min.js"; - await audioContext.audioWorklet.addModule(procUrl); - _synthInstance = new (window.SpessaSynthClass)(audioContext); - _synthInstance.connect(audioContext.destination); - await _synthInstance.isReady; - _initialized = true; - } catch (e) { - console.warn("[SonicSF] SpessaSynth init failed:", e); - } + if (_initPromise) return _initPromise; + _initPromise = (async () => { + if (!window.SpessaSynthClass || !window.__SpessaSynthCDN) return; + try { + if (audioContext.state === 'suspended') await audioContext.resume(); + const procUrl = window.__SpessaSynthCDN + "spessasynth_processor.min.js"; + await audioContext.audioWorklet.addModule(procUrl); + _synthInstance = new window.SpessaSynthClass(audioContext); + _synthInstance.connect(audioContext.destination); + await _synthInstance.isReady; + _initialized = true; + } catch (e) { + console.warn("[SonicSF] SpessaSynth init failed:", e); + } + })(); + return _initPromise; }, loadSoundFont: async function (sfId) { @@ -58,16 +64,15 @@ 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()); + const url = "/api/v1/plugins/soundfonts/download/" + encodeURIComponent(sfId) + "?t=" + Date.now(); + const resp = await fetch(url); if (!resp.ok) return false; buf = await resp.arrayBuffer(); if (cache) await cache.saveBuffer(sfId, buf); } 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; + return true; } catch (e) { console.warn("[SonicSF] loadSoundFont failed:", e); return false; @@ -75,8 +80,14 @@ }, selectInstrument: async function (channel, bank, program, sfId) { - if (!_initialized || !_synthInstance) return; - if (sfId) await this.loadSoundFont(sfId); + if (!_initialized || !_synthInstance) { + const ctx = new (window.AudioContext || window.webkitAudioContext)(); + await this.init(ctx); + } + if (sfId) { + const ok = await this.loadSoundFont(sfId); + if (!ok) return; + } try { _synthInstance.controllerChange(channel, 0, bank); } catch (e) {} try { _synthInstance.controllerChange(channel, 32, 0); } catch (e) {} try { _synthInstance.programChange(channel, program); } catch (e) {} @@ -126,11 +137,37 @@ }, playNote: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) { - // Always use oscillator for client preview — reliable - // Server-side FluidSynth render provides authentic SoundFont audio on Export + if (_initialized && _synthInstance) { + this._playNoteSpessa(note, velocity, durationMs, startTime, program, channel, synthEngine); + return; + } this._playNoteOsc(note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine); }, + _playNoteSpessa: function (note, velocity, durationMs, startTime, program, channel, synthEngine) { + 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 + ))); + 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; + const durSec = durationMs / 1000; + try { + _synthInstance.noteOn(channel, midiPitch, midiVel); + setTimeout(() => { try { _synthInstance.noteOff(channel, midiPitch); } catch (e) {} }, durSec * 1000); + } catch (e) { + console.warn("[SonicSF] SpessaSynth noteOn error, oscillator fallback:", e); + this._playNoteOsc(note, velocity, durationMs, startTime, program, null, channel, synthEngine); + } + }, + _playNoteOsc: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) { const ctx = getCtx(); const freq = 440 * Math.pow(2, (note - 69) / 12);