fix: SpessaSynth init retry + default SF3 auto-load

- getAudioContext() calls SonicSF.init on every call (not just first)
- soundfontPlayer.init retries after 1s if SpessaSynth module not loaded yet
- Auto-loads sgm_v2.01 SF3 on successful init
- Fixes race condition: module script loads after regular scripts
This commit is contained in:
2026-07-26 17:59:15 +07:00
parent 29c4c45791
commit 23601007bf
3 changed files with 16 additions and 6 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ const{useState,useRef,useEffect,useMemo,useCallback}=React;// ── FastAPI Bac
const API_BASE_URL=window.location.origin;const API_AUDIO=`${API_BASE_URL}/api/v1/audio`;const API_MULTITRACK=`${API_BASE_URL}/api/v1/multitrack`;const API_TASKS=`${API_BASE_URL}/api/v1/audio/tasks`;// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink(){try{const params=new URLSearchParams(window.location.search);const sfsParam=params.get('sfs');if(!sfsParam)return;const decoded=JSON.parse(decodeURIComponent(sfsParam));window.__pendingSfsProject=decoded;// consumed after auth in App
if(window.history.replaceState){window.history.replaceState({},document.title,window.location.pathname);}}catch(e){window.__pendingSfsProject=null;}})();// Storage for server-side file IDs mapped to track IDs
let serverFileIdMap={};let audioCtx;function getAudioContext(){if(!audioCtx){audioCtx=new(window.AudioContext||window.webkitAudioContext)();if(window.SonicAudio&&window.SonicAudio.initAudioWorklet){window.SonicAudio.initAudioWorklet();}if(window.SonicSF&&window.SonicSF.init){window.SonicSF.init(audioCtx);}}if(audioCtx.state==='suspended'){audioCtx.resume();}return audioCtx;}const formatTime=secs=>{if(isNaN(secs)||secs<0)return"0:00.000";const m=Math.floor(secs/60);const s=Math.floor(secs%60);const ms=Math.floor(secs%1*1000).toString().padStart(3,'0');return`${m}:${s.toString().padStart(2,'0')}.${ms}`;};const formatTimeSimple=secs=>{if(isNaN(secs)||secs<0)return"0.00s";return`${secs.toFixed(2)}s`;};const formatBeat=(secs,bpmVal)=>{if(isNaN(secs)||secs<0)return"0.1.1";const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const bar=Math.floor(secs/barDuration);const beat=Math.floor(secs%barDuration/beatDuration)+1;const sub=Math.floor(secs%beatDuration/(beatDuration/4))+1;return`${bar}.${beat}.${sub}`;};const midiPitchToName=pitch=>{const names=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;return names[pitch%12]+octave;};const getBeatMarkers=(maxDur,bpmVal)=>{const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const markers=[];for(let t=0;t<=maxDur;t+=beatDuration){const isBar=Math.abs(t%barDuration)<0.001||Math.abs(t%barDuration-barDuration)<0.001;markers.push({time:t,isBar,beatNum:Math.floor(t/beatDuration)+1});}return markers;};const findZeroCrossing=(buffer,targetTime)=>{if(!buffer)return targetTime;const sampleRate=buffer.sampleRate;const data=buffer.getChannelData(0);const targetSample=Math.floor(targetTime*sampleRate);const windowSize=Math.floor(0.04*sampleRate);const start=Math.max(0,targetSample-windowSize);const end=Math.min(data.length-2,targetSample+windowSize);let bestSample=targetSample;let minDistance=Infinity;for(let i=start;i<=end;i++){if(data[i]>=0&&data[i+1]<=0||data[i]<=0&&data[i+1]>=0){const dist=Math.abs(i-targetSample);if(dist<minDistance){minDistance=dist;bestSample=i;}}}return bestSample/sampleRate;};class ClientMIDIRecorder{constructor(audioContext,bpm=120,timeSigNumerator=4){this.audioCtx=audioContext;this.bpm=bpm;this.timeSigNum=timeSigNumerator;this.isRecording=false;this.tempMidiItemId=null;this.activeNotes=new Map();// Store pitch -> { noteId, startBeat, velocity }
let serverFileIdMap={};let audioCtx;function getAudioContext(){if(!audioCtx){audioCtx=new(window.AudioContext||window.webkitAudioContext)();if(window.SonicAudio&&window.SonicAudio.initAudioWorklet){window.SonicAudio.initAudioWorklet();}}if(audioCtx.state==='suspended'){audioCtx.resume();}if(window.SonicSF&&window.SonicSF.init){window.SonicSF.init(audioCtx);}return audioCtx;}const formatTime=secs=>{if(isNaN(secs)||secs<0)return"0:00.000";const m=Math.floor(secs/60);const s=Math.floor(secs%60);const ms=Math.floor(secs%1*1000).toString().padStart(3,'0');return`${m}:${s.toString().padStart(2,'0')}.${ms}`;};const formatTimeSimple=secs=>{if(isNaN(secs)||secs<0)return"0.00s";return`${secs.toFixed(2)}s`;};const formatBeat=(secs,bpmVal)=>{if(isNaN(secs)||secs<0)return"0.1.1";const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const bar=Math.floor(secs/barDuration);const beat=Math.floor(secs%barDuration/beatDuration)+1;const sub=Math.floor(secs%beatDuration/(beatDuration/4))+1;return`${bar}.${beat}.${sub}`;};const midiPitchToName=pitch=>{const names=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;return names[pitch%12]+octave;};const getBeatMarkers=(maxDur,bpmVal)=>{const beatDuration=60/bpmVal;const barDuration=beatDuration*4;const markers=[];for(let t=0;t<=maxDur;t+=beatDuration){const isBar=Math.abs(t%barDuration)<0.001||Math.abs(t%barDuration-barDuration)<0.001;markers.push({time:t,isBar,beatNum:Math.floor(t/beatDuration)+1});}return markers;};const findZeroCrossing=(buffer,targetTime)=>{if(!buffer)return targetTime;const sampleRate=buffer.sampleRate;const data=buffer.getChannelData(0);const targetSample=Math.floor(targetTime*sampleRate);const windowSize=Math.floor(0.04*sampleRate);const start=Math.max(0,targetSample-windowSize);const end=Math.min(data.length-2,targetSample+windowSize);let bestSample=targetSample;let minDistance=Infinity;for(let i=start;i<=end;i++){if(data[i]>=0&&data[i+1]<=0||data[i]<=0&&data[i+1]>=0){const dist=Math.abs(i-targetSample);if(dist<minDistance){minDistance=dist;bestSample=i;}}}return bestSample/sampleRate;};class ClientMIDIRecorder{constructor(audioContext,bpm=120,timeSigNumerator=4){this.audioCtx=audioContext;this.bpm=bpm;this.timeSigNum=timeSigNumerator;this.isRecording=false;this.tempMidiItemId=null;this.activeNotes=new Map();// Store pitch -> { noteId, startBeat, velocity }
this.recordedNotes=[];this.recStartAudioTime=0.0;this.recStartBar=0.0;this.selectedMidiInputId=null;// Compute round-trip browser latency
this.latencyCompSec=(this.audioCtx.baseLatency||0)+(this.audioCtx.outputLatency||0);}start(startBar=0.0,selectedMidiInputId=null){this.isRecording=true;this.recordedNotes=[];this.activeNotes.clear();this.recStartBar=startBar;this.recStartAudioTime=this.audioCtx.currentTime;this.selectedMidiInputId=selectedMidiInputId;}handleMIDIMessage(event,sourceInputId=null){if(!this.isRecording)return;if(this.selectedMidiInputId&&this.selectedMidiInputId!=='ALL'&&sourceInputId&&sourceInputId!==this.selectedMidiInputId){console.log(`[DevLog] [MIDI Rec] Ignoring input message from "${sourceInputId}" (Selected: "${this.selectedMidiInputId}")`);return;}const[status,pitch,velocity]=event.data;const command=status>>4;// Apply latency compensation formula
const currentTimeSec=Math.max(0,this.audioCtx.currentTime-this.recStartAudioTime-this.latencyCompSec);const secondsPerBeat=60.0/this.bpm;const currentBeat=currentTimeSec/secondsPerBeat;// Command 0x9: Note On