|
|
|
@@ -10,13 +10,17 @@ const trackMidiChannelsRef={current:{}};const ensureTrackMidiChannel=(track,trac
|
|
|
|
|
(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;let masterBus=null;// { input, compressor, analyser, output, masteringActive }
|
|
|
|
|
function makeDistortionCurve(k){const n_samples=44100;const curve=new Float32Array(n_samples);for(let i=0;i<n_samples;++i){const x=i*2/n_samples-1;curve[i]=Math.atan(x*k)/(Math.atan(k)||1);}return curve;}function applyMasteringSettings(s){if(!masterBus||!audioCtx)return;const now=audioCtx.currentTime;// 1. EQ Settings
|
|
|
|
|
masterBus.eqLowFilter.gain.setTargetAtTime(s.eqActive?s.eqLowGain:0,now,0.01);masterBus.eqMid1Filter.gain.setTargetAtTime(s.eqActive?s.eqMid1Gain:0,now,0.01);masterBus.eqMid2Filter.gain.setTargetAtTime(s.eqActive?s.eqMid2Gain:0,now,0.01);masterBus.eqHighFilter.gain.setTargetAtTime(s.eqActive?s.eqHighGain:0,now,0.01);// 2. Imager Settings (Mid/Side matrix width control for each band)
|
|
|
|
|
const updateImagerBand=(w,active,gainLL,gainRL,gainLR,gainRR)=>{const widthVal=s.imagerActive&&active?w:0;const g1=1+widthVal/200;const g2=-widthVal/200;gainLL.gain.setTargetAtTime(g1,now,0.01);gainRR.gain.setTargetAtTime(g1,now,0.01);gainRL.gain.setTargetAtTime(g2,now,0.01);gainLR.gain.setTargetAtTime(g2,now,0.01);};updateImagerBand(s.w1,true,masterBus.gainLL1,masterBus.gainRL1,masterBus.gainLR1,masterBus.gainRR1);updateImagerBand(s.w2,true,masterBus.gainLL2,masterBus.gainRL2,masterBus.gainLR2,masterBus.gainRR2);updateImagerBand(s.w3,true,masterBus.gainLL3,masterBus.gainRL3,masterBus.gainLR3,masterBus.gainRR3);updateImagerBand(s.w4,true,masterBus.gainLL4,masterBus.gainRL4,masterBus.gainLR4,masterBus.gainRR4);// 3. Maximizer Settings
|
|
|
|
|
const boostLinear=s.maximizerActive?Math.pow(10,s.maxGain/20):1.0;masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear,now,0.01);// Soft Clipper
|
|
|
|
|
if(s.maximizerActive&&s.maxSoftClip>0){const k=1+s.maxSoftClip/100*10;masterBus.maximizerSoftClipper.curve=makeDistortionCurve(k);}else{masterBus.maximizerSoftClipper.curve=null;}// Upward Compressor
|
|
|
|
|
const upwardGainLinear=s.maximizerActive&&s.maxUpward>0?Math.pow(10,s.maxUpward/20)-1.0:0.0;masterBus.upwardGain.gain.setTargetAtTime(upwardGainLinear,now,0.01);// Limiter Threshold
|
|
|
|
|
const ceilingVal=s.maximizerActive?s.ceiling:-0.1;masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal,now,0.01);}function initMasterBus(ctx){if(masterBus)return masterBus;// Create EQ filters
|
|
|
|
|
function makeDistortionCurve(k){const n_samples=44100;const curve=new Float32Array(n_samples);for(let i=0;i<n_samples;++i){const x=i*2/n_samples-1;curve[i]=Math.atan(x*k)/(Math.atan(k)||1);}return curve;}function applyMasteringSettings(s){if(!masterBus||!audioCtx||!s)return;const now=audioCtx.currentTime;// Clamp every parameter so a stale/incomplete settings object can never push
|
|
|
|
|
// NaN or an extreme value into the biquad filters — that puts the master
|
|
|
|
|
// chain into a bad state and silences ALL audio (the "mất soundfont" symptom).
|
|
|
|
|
const clamp=(v,lo,hi)=>{const n=Number(v);if(!isFinite(n))return 0;// missing/NaN → neutral, never a filter-breaking value
|
|
|
|
|
return Math.min(hi,Math.max(lo,n));};// 1. EQ Settings
|
|
|
|
|
masterBus.eqLowFilter.gain.setTargetAtTime(s.eqActive?clamp(s.eqLowGain,-24,24):0,now,0.01);masterBus.eqMid1Filter.gain.setTargetAtTime(s.eqActive?clamp(s.eqMid1Gain,-24,24):0,now,0.01);masterBus.eqMid2Filter.gain.setTargetAtTime(s.eqActive?clamp(s.eqMid2Gain,-24,24):0,now,0.01);masterBus.eqHighFilter.gain.setTargetAtTime(s.eqActive?clamp(s.eqHighGain,-24,24):0,now,0.01);// 2. Imager Settings (Mid/Side matrix width control for each band)
|
|
|
|
|
const updateImagerBand=(w,active,gainLL,gainRL,gainLR,gainRR)=>{const widthVal=s.imagerActive&&active?clamp(w,-100,100):0;const g1=1+widthVal/200;const g2=-widthVal/200;gainLL.gain.setTargetAtTime(g1,now,0.01);gainRR.gain.setTargetAtTime(g1,now,0.01);gainRL.gain.setTargetAtTime(g2,now,0.01);gainLR.gain.setTargetAtTime(g2,now,0.01);};updateImagerBand(s.w1,true,masterBus.gainLL1,masterBus.gainRL1,masterBus.gainLR1,masterBus.gainRR1);updateImagerBand(s.w2,true,masterBus.gainLL2,masterBus.gainRL2,masterBus.gainLR2,masterBus.gainRR2);updateImagerBand(s.w3,true,masterBus.gainLL3,masterBus.gainRL3,masterBus.gainLR3,masterBus.gainRR3);updateImagerBand(s.w4,true,masterBus.gainLL4,masterBus.gainRL4,masterBus.gainLR4,masterBus.gainRR4);// 3. Maximizer Settings
|
|
|
|
|
const boostLinear=s.maximizerActive?Math.pow(10,clamp(s.maxGain,-60,30)/20):1.0;masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear,now,0.01);// Soft Clipper
|
|
|
|
|
if(s.maximizerActive&&s.maxSoftClip>0){const k=1+clamp(s.maxSoftClip,0,100)/100*10;masterBus.maximizerSoftClipper.curve=makeDistortionCurve(k);}else{masterBus.maximizerSoftClipper.curve=null;}// Upward Compressor
|
|
|
|
|
const upwardGainLinear=s.maximizerActive&&s.maxUpward>0?Math.pow(10,clamp(s.maxUpward,0,30)/20)-1.0:0.0;masterBus.upwardGain.gain.setTargetAtTime(upwardGainLinear,now,0.01);// Limiter Threshold
|
|
|
|
|
const ceilingVal=s.maximizerActive?clamp(s.ceiling,-60,0):-0.1;masterBus.maximizerCompressor.threshold.setTargetAtTime(ceilingVal,now,0.01);}function initMasterBus(ctx){if(masterBus)return masterBus;// Create EQ filters
|
|
|
|
|
const eqLowFilter=ctx.createBiquadFilter();eqLowFilter.type='lowshelf';eqLowFilter.frequency.value=100;const eqMid1Filter=ctx.createBiquadFilter();eqMid1Filter.type='peaking';eqMid1Filter.frequency.value=822;eqMid1Filter.Q.value=0.7;const eqMid2Filter=ctx.createBiquadFilter();eqMid2Filter.type='peaking';eqMid2Filter.frequency.value=3200;eqMid2Filter.Q.value=1.2;const eqHighFilter=ctx.createBiquadFilter();eqHighFilter.type='highshelf';eqHighFilter.frequency.value=10000;// Create Stereo Imager nodes
|
|
|
|
|
const imagerInput=ctx.createGain();const imagerOutput=ctx.createGain();// Imager Crossover Filters
|
|
|
|
|
const f1_lp=ctx.createBiquadFilter();f1_lp.type='lowpass';f1_lp.frequency.value=100;const f2_hp=ctx.createBiquadFilter();f2_hp.type='highpass';f2_hp.frequency.value=100;const f2_lp=ctx.createBiquadFilter();f2_lp.type='lowpass';f2_lp.frequency.value=1000;const f3_hp=ctx.createBiquadFilter();f3_hp.type='highpass';f3_hp.frequency.value=1000;const f3_lp=ctx.createBiquadFilter();f3_lp.type='lowpass';f3_lp.frequency.value=6000;const f4_hp=ctx.createBiquadFilter();f4_hp.type='highpass';f4_hp.frequency.value=6000;const split1=ctx.createChannelSplitter(2);const split2=ctx.createChannelSplitter(2);const split3=ctx.createChannelSplitter(2);const split4=ctx.createChannelSplitter(2);const merge1=ctx.createChannelMerger(2);const merge2=ctx.createChannelMerger(2);const merge3=ctx.createChannelMerger(2);const merge4=ctx.createChannelMerger(2);const gainLL1=ctx.createGain();const gainRL1=ctx.createGain();const gainLR1=ctx.createGain();const gainRR1=ctx.createGain();const gainLL2=ctx.createGain();const gainRL2=ctx.createGain();const gainLR2=ctx.createGain();const gainRR2=ctx.createGain();const gainLL3=ctx.createGain();const gainRL3=ctx.createGain();const gainLR3=ctx.createGain();const gainRR3=ctx.createGain();const gainLL4=ctx.createGain();const gainRL4=ctx.createGain();const gainLR4=ctx.createGain();const gainRR4=ctx.createGain();// Connections for Imager DSP
|
|
|
|
@@ -34,8 +38,11 @@ eqLowFilter.connect(eqMid1Filter);eqMid1Filter.connect(eqMid2Filter);eqMid2Filte
|
|
|
|
|
eqHighFilter.connect(imagerInput);// Connect Imager to Maximizer
|
|
|
|
|
imagerOutput.connect(maximizerBoostGain);// Setup default non-mastered routing:
|
|
|
|
|
// input -> compressor -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination
|
|
|
|
|
masterBus.input.connect(masterBus.compressor);masterBus.compressor.connect(masterBus.inputAnalyser);masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.outputAnalyser.connect(masterBus.output);masterBus.output.connect(masterBus.analyser);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}function toggleMasteringOnMaster(activate,isBypassed){if(!masterBus)return;// Disconnect the dynamic junction
|
|
|
|
|
masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnect();if(activate&&!isBypassed){// Active routing: inputAnalyser -> EQ -> Imager -> Maximizer -> outputAnalyser
|
|
|
|
|
masterBus.input.connect(masterBus.compressor);masterBus.compressor.connect(masterBus.inputAnalyser);masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.outputAnalyser.connect(masterBus.output);masterBus.output.connect(masterBus.analyser);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}let _lastMasteringActive=null;function toggleMasteringOnMaster(activate,isBypassed){if(!masterBus)return;const active=!!(activate&&!isBypassed);// Idempotent: don't disconnect/reconnect the mastering chain on every call
|
|
|
|
|
// (getAudioContext invokes this constantly). Rapid re-connection while audio
|
|
|
|
|
// flows destabilizes the biquad filters → "BiquadFilterNode: state is bad".
|
|
|
|
|
if(_lastMasteringActive===active&&masterBus.masteringActive===active)return;_lastMasteringActive=active;// Disconnect the dynamic junction
|
|
|
|
|
masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnect();if(active){// Active routing: inputAnalyser -> EQ -> Imager -> Maximizer -> outputAnalyser
|
|
|
|
|
masterBus.inputAnalyser.connect(masterBus.eqLowFilter);masterBus.maximizerCompressor.connect(masterBus.outputAnalyser);masterBus.masteringActive=true;}else{// Bypassed routing: inputAnalyser -> outputAnalyser
|
|
|
|
|
masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.masteringActive=false;}}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(!masterBus){initMasterBus(audioCtx);}if(window.currentMasteringSettings){toggleMasteringOnMaster(window.currentMasteringSettings.masterConnected,window.currentMasteringSettings.isBypassed);applyMasteringSettings(window.currentMasteringSettings);}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
|
|
|
|
|