From b3ced7a7b3b6206535dfd00b41e058d837f94c31 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 5 Aug 2026 15:49:44 +0700 Subject: [PATCH] =?UTF-8?q?FIX:=20Clone=20AI=20Var=20k=E1=BA=BF=20th?= =?UTF-8?q?=E1=BB=ABa=20midiChannel=20c=E1=BB=A7a=20track=20g=E1=BB=91c=20?= =?UTF-8?q?=E2=86=92=202=20track=20D=C3=99NG=20CHUNG=20channel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 72 ++++++++++++++++++++--- app/static/js/app.precompiled.js | 33 ++++++++--- app/static/js/services/soundfontPlayer.js | 39 ++++++++---- app/templates/index.html | 4 +- wiki.md | 33 +++++++++++ 5 files changed, 150 insertions(+), 31 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 085cc7e..e32e88f 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -112,6 +112,20 @@ const trackMasteringBypassMap = {}; const trackAudioBypassMap = {}; const trackMidiBypassMap = {}; +// Mastering chain ON? (masterConnected && !isBypassed) +const masteringChainOn = () => !!(window.currentMasteringSettings && window.currentMasteringSettings.masterConnected && !window.currentMasteringSettings.isBypassed); +// ♪ bypass hiệu lực CHỈ khi mastering chain TẮT — khi chain ON, MỌI track +// (solo/preview/play) PHẢI đi qua mastering chain (user requirement: âm phải +// qua chain để đủ lớn). Chain OFF → theo ♪ maps như cũ. +const effMidiBypass = (track) => { + if (masteringChainOn()) return false; + return trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : !!(track.midiBypass ?? track.masteringBypass); +}; +const effAudioBypass = (track) => { + if (masteringChainOn()) return false; + return trackAudioBypassMap[track.id] !== undefined ? !!trackAudioBypassMap[track.id] : !!(track.audioBypass ?? track.masteringBypass); +}; + // Build the dual routing for one track: routeGain -> mastering chain (normal), // dryGain -> dry bus (bypass). Gains start at complementary 1/0 values. function createMasteringRoute(ctx, track, bus) { @@ -121,9 +135,11 @@ function createMasteringRoute(ctx, track, bus) { let bypass = false; if (track && track.id && trackAudioBypassMap[track.id] !== undefined) { bypass = !!trackAudioBypassMap[track.id]; - } else { - bypass = !!(track && (track.audioBypass ?? track.masteringBypass)); + } else if (track) { + bypass = !!(track.audioBypass ?? track.masteringBypass); } + // Mastering chain ON → MỌI track qua chain (♪ bị override — user requirement) + if (masteringChainOn()) bypass = false; const routeGain = ctx.createGain(); const dryGain = ctx.createGain(); const masterDest = bus ? bus.input : ctx.destination; @@ -1012,7 +1028,7 @@ function buildOfflineTrackNode(track, ctx, nodeMap) { sfOut.connect(sfPan); const sfRouteGain = ctx.createGain(); const sfDryGain = ctx.createGain(); - const sfBypass = trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : !!(track.midiBypass ?? track.masteringBypass); + const sfBypass = effMidiBypass(track); sfRouteGain.gain.value = sfBypass ? 0 : 1; sfDryGain.gain.value = sfBypass ? 1 : 0; sfPan.connect(sfRouteGain); @@ -14519,9 +14535,10 @@ const App = () => { list.forEach(t => { // ♪ state → SF mastering route (sfRouteGain/sfDryGain), live on existing nodes const sn = activeTrackNodesRef.current[t.id]; - if (sn && sn.sfRouteGain && sn.sfDryGain && (sn.sfRouteGain.gain.value > 0) !== !trackMidiBypassMap[t.id]) { - sn.sfRouteGain.gain.value = trackMidiBypassMap[t.id] ? 0 : 1; - sn.sfDryGain.gain.value = trackMidiBypassMap[t.id] ? 1 : 0; + if (sn && sn.sfRouteGain && sn.sfDryGain && (sn.sfRouteGain.gain.value > 0) !== !effMidiBypass(t)) { + const _b = effMidiBypass(t); + sn.sfRouteGain.gain.value = _b ? 0 : 1; + sn.sfDryGain.gain.value = _b ? 1 : 0; } const sig = (t.muted ? '1' : '0') + (t.solo ? '1' : '0') + ':' + (t.volumeDb ?? 0); if (trackMuteSoloSigRef.current[t.id] === sig) return; @@ -14630,6 +14647,28 @@ const App = () => { if (audioCtx && masterBus) { toggleMasteringOnMaster(masteringSettings.masterConnected, masteringSettings.isBypassed); applyMasteringSettings(masteringSettings); + // Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị + // override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp + // ngay lên node đang phát (không chờ tracks effect). + try { + const _list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks; + _list.forEach(_t => { + const _n = activeTrackNodesRef.current[_t.id]; + if (_n) { + if (_n.sfRouteGain && _n.sfDryGain) { + const _b = effMidiBypass(_t); + _n.sfRouteGain.gain.value = _b ? 0 : 1; + _n.sfDryGain.gain.value = _b ? 1 : 0; + } + if (_n.route && _n.route.routeGain && _n.route.dryGain) { + const _ab = effAudioBypass(_t); + _n.route.routeGain.gain.value = _ab ? 0 : 1; + _n.route.dryGain.gain.value = _ab ? 1 : 0; + } + } + }); + updateSfRouting(); + } catch (e) {} } }, [masteringSettings]); @@ -18221,7 +18260,7 @@ const App = () => { // ♪ button: bypass Mastering FX Chain for the soundfont ONLY (track FX // Rack modules are still applied — PWR controls those). sfRouteGain → // masterBus.input (mastering), sfDryGain → dry bus (skip mastering). - const sfBypass = trackMidiBypassMap[track.id] !== undefined ? !!trackMidiBypassMap[track.id] : !!(track.midiBypass ?? track.masteringBypass); + const sfBypass = effMidiBypass(track); sfRouteGain = context.createGain(); sfDryGain = context.createGain(); sfRouteGain.gain.value = sfBypass ? 0 : 1; @@ -18327,6 +18366,14 @@ const App = () => { if (_prNode && window.SonicSF && window.SonicSF.setOutputDestination) { if (_prNode.sfEntry) { window.SonicSF.setOutputDestination(_prNode.sfEntry); + // Ép route qua mastering chain NGAY tại thời điểm routing (node có + // thể tạo khi mastering OFF → route dry — sửa ngay nếu chain ON). + if (_prNode.sfRouteGain && _prNode.sfDryGain) { + const _prTrk = list.find(t => t.id === _activeSub.trackId); + const _b = effMidiBypass(_prTrk || { id: _activeSub.trackId }); + _prNode.sfRouteGain.gain.value = _b ? 0 : 1; + _prNode.sfDryGain.gain.value = _b ? 1 : 0; + } return; } if (_prNode.gainNode) { @@ -18344,6 +18391,12 @@ const App = () => { // sfDryGain) to skip or include the Mastering FX Chain. if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) { window.SonicSF.setOutputDestination(node.sfEntry); + // Ép route qua mastering chain NGAY tại thời điểm routing. + if (node.sfRouteGain && node.sfDryGain) { + const _b = effMidiBypass(t.id); + node.sfRouteGain.gain.value = _b ? 0 : 1; + node.sfDryGain.gain.value = _b ? 1 : 0; + } return; } if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) { @@ -22990,6 +23043,11 @@ STRICT CONSTRAINTS: ...(srcTrack || {}), id: newTrackId, name: '[AI Var] ' + title, + // KHÔNG kế thừa midiChannel của track gốc: nếu dùng chung channel, + // solo/mute track gốc gửi CC7=0 trên channel đó → clone (cùng + // channel) bị NHỎ/CÂM. ensureTrackMidiChannel/assignTrackMidiChannel + // sẽ cấp channel RIÊNG cho track mới. + midiChannel: undefined, buffer: null, startTime: 0, clips: [], diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 9eabb46..fe2db18 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -21,12 +21,17 @@ if(window.history.replaceState){window.history.replaceState({},document.title,wi let serverFileIdMap={};let audioCtx;let masterBus=null;// { input, compressor, analyser, output, masteringActive, dryInput, dryOutput } // Per-track mastering-bypass state (trackId -> bool), kept in sync with the // tracks state so ANY audio path can route without holding the track object. -const trackMasteringBypassMap={};const trackAudioBypassMap={};const trackMidiBypassMap={};// Build the dual routing for one track: routeGain -> mastering chain (normal), +const trackMasteringBypassMap={};const trackAudioBypassMap={};const trackMidiBypassMap={};// Mastering chain ON? (masterConnected && !isBypassed) +const masteringChainOn=()=>!!(window.currentMasteringSettings&&window.currentMasteringSettings.masterConnected&&!window.currentMasteringSettings.isBypassed);// ♪ bypass hiệu lực CHỈ khi mastering chain TẮT — khi chain ON, MỌI track +// (solo/preview/play) PHẢI đi qua mastering chain (user requirement: âm phải +// qua chain để đủ lớn). Chain OFF → theo ♪ maps như cũ. +const effMidiBypass=track=>{if(masteringChainOn())return false;return trackMidiBypassMap[track.id]!==undefined?!!trackMidiBypassMap[track.id]:!!(track.midiBypass??track.masteringBypass);};const effAudioBypass=track=>{if(masteringChainOn())return false;return trackAudioBypassMap[track.id]!==undefined?!!trackAudioBypassMap[track.id]:!!(track.audioBypass??track.masteringBypass);};// Build the dual routing for one track: routeGain -> mastering chain (normal), // dryGain -> dry bus (bypass). Gains start at complementary 1/0 values. function createMasteringRoute(ctx,track,bus){// Prefer the live bypass map (synced from the tracks state on every render), // falling back to the track object — this guarantees the A-button toggle is // picked up even if a stale track object is passed in. -let bypass=false;if(track&&track.id&&trackAudioBypassMap[track.id]!==undefined){bypass=!!trackAudioBypassMap[track.id];}else{bypass=!!(track&&(track.audioBypass??track.masteringBypass));}const routeGain=ctx.createGain();const dryGain=ctx.createGain();const masterDest=bus?bus.input:ctx.destination;const dryDest=bus&&bus.dryInput?bus.dryInput:ctx.destination;routeGain.gain.value=bypass?0:1;dryGain.gain.value=bypass?1:0;routeGain.connect(masterDest);dryGain.connect(dryDest);const routeObj={routeGain,dryGain};routeObj._trackId=track&&track.id;routeObj._bypass=bypass;return routeObj;}// Live-toggle a route. HARD switch: cancel any pending automation and assign +let bypass=false;if(track&&track.id&&trackAudioBypassMap[track.id]!==undefined){bypass=!!trackAudioBypassMap[track.id];}else if(track){bypass=!!(track.audioBypass??track.masteringBypass);}// Mastering chain ON → MỌI track qua chain (♪ bị override — user requirement) +if(masteringChainOn())bypass=false;const routeGain=ctx.createGain();const dryGain=ctx.createGain();const masterDest=bus?bus.input:ctx.destination;const dryDest=bus&&bus.dryInput?bus.dryInput:ctx.destination;routeGain.gain.value=bypass?0:1;dryGain.gain.value=bypass?1:0;routeGain.connect(masterDest);dryGain.connect(dryDest);const routeObj={routeGain,dryGain};routeObj._trackId=track&&track.id;routeObj._bypass=bypass;return routeObj;}// Live-toggle a route. HARD switch: cancel any pending automation and assign // .value directly (instant, cannot be delayed by the automation queue). function setMasteringRoute(route,bypass){if(!route)return;const on=!!bypass;try{const ctx=typeof getAudioContext==='function'?getAudioContext():null;if(!ctx)return;const t=ctx.currentTime;route.routeGain.gain.cancelScheduledValues(t);route.dryGain.gain.cancelScheduledValues(t);route.routeGain.gain.value=on?0:1;route.dryGain.gain.value=on?1:0;route._bypass=on;console.log('[Bypass] track',route._trackId,'audioBypass='+on,'→',on?'DRY BUS (bỏ mastering + bỏ track FX)':'MASTERING CHAIN (qua FX + mastering)');}catch(e){console.warn('setMasteringRoute error:',e);}}// Realtime mute/solo: audible linear gain for a track given the full track list // of the CURRENT context. Solo semantics: if ANY track is soloed, only soloed @@ -162,7 +167,7 @@ const f1=ctx.createBiquadFilter();f1.type='lowshelf';f1.frequency.value=clampF(1 // mastering chain — the exported file therefore matches what you hear. function buildOfflineTrackNode(track,ctx,nodeMap){if(!track||nodeMap[track.id])return nodeMap[track.id]||null;const gainNode=ctx.createGain();const volDb=track.volumeDb??0;gainNode.gain.setValueAtTime(volDb<=-50?0:Math.pow(10,volDb/20),0);const pannerNode=ctx.createStereoPanner();pannerNode.pan.setValueAtTime((track.pan??0)/100,0);const analyserNode=ctx.createAnalyser();analyserNode.fftSize=2048;pannerNode.connect(analyserNode);if(!masterBus)initMasterBus(ctx);const route=createMasteringRoute(ctx,track,masterBus);analyserNode.connect(route.routeGain);analyserNode.connect(route.dryGain);let fxStopFn;const fxEntry=ctx.createGain();const fxLegacyIn=ctx.createGain();gainNode.connect(fxEntry);const fxChain=track.fxChain||[];const fxEnabled=track.fxActive!==false;const chainMods=fxEnabled?fxChain.filter(m=>m&&m.active!==false).map(m=>{try{return createTrackFxModule(m.type||m,ctx,m.params);}catch(e){return null;}}).filter(Boolean):[];let fxChainTail=fxEntry;chainMods.forEach(mod=>{fxChainTail.connect(mod.input);fxChainTail=mod.output;});fxChainTail.connect(fxLegacyIn);if(fxEnabled&&track.fxType==='chorus'){const fxInput=ctx.createGain();fxLegacyIn.connect(fxInput);const chorus=createChorusNode(ctx,fxInput,pannerNode);fxStopFn=chorus.stop;}else if(fxEnabled&&track.fxType==='reverb'){const fxInput=ctx.createGain();fxLegacyIn.connect(fxInput);createReverbNode(ctx,fxInput,pannerNode);}else{fxLegacyIn.connect(pannerNode);}const node={gainNode,pannerNode,analyserNode,route,fxStopFn,sfEntry:null,sfOut:null,sfRouteGain:null,sfDryGain:null};// Soundfont (MIDI cache) chain — mirrors the live node so cached MIDI buffers // flow through the track FX modules + mastering exactly like playback. -if(track.midiItems&&track.midiItems.length>0){const sfEntry=ctx.createGain();sfEntry.gain.setValueAtTime(1,0);const sfOut=ctx.createGain();const sfPan=ctx.createStereoPanner();sfPan.pan.setValueAtTime((track.pan??0)/100,0);sfOut.connect(sfPan);const sfRouteGain=ctx.createGain();const sfDryGain=ctx.createGain();const sfBypass=trackMidiBypassMap[track.id]!==undefined?!!trackMidiBypassMap[track.id]:!!(track.midiBypass??track.masteringBypass);sfRouteGain.gain.value=sfBypass?0:1;sfDryGain.gain.value=sfBypass?1:0;sfPan.connect(sfRouteGain);sfPan.connect(sfDryGain);sfRouteGain.connect(masterBus.input);sfDryGain.connect(masterBus.dryInput);const sfMods=fxEnabled?fxChain.filter(m=>m&&m.active!==false).map(m=>{try{return createTrackFxModule(m.type||m,ctx,m.params);}catch(e){return null;}}).filter(Boolean):[];let sfTail=sfEntry;sfMods.forEach(mod=>{sfTail.connect(mod.input);sfTail=mod.output;});sfTail.connect(sfOut);node.sfEntry=sfEntry;node.sfOut=sfOut;node.sfRouteGain=sfRouteGain;node.sfDryGain=sfDryGain;}nodeMap[track.id]=node;return node;}// Track-EQ preset library (unified_fx_rack_panel.md §III.1) — band gains only. +if(track.midiItems&&track.midiItems.length>0){const sfEntry=ctx.createGain();sfEntry.gain.setValueAtTime(1,0);const sfOut=ctx.createGain();const sfPan=ctx.createStereoPanner();sfPan.pan.setValueAtTime((track.pan??0)/100,0);sfOut.connect(sfPan);const sfRouteGain=ctx.createGain();const sfDryGain=ctx.createGain();const sfBypass=effMidiBypass(track);sfRouteGain.gain.value=sfBypass?0:1;sfDryGain.gain.value=sfBypass?1:0;sfPan.connect(sfRouteGain);sfPan.connect(sfDryGain);sfRouteGain.connect(masterBus.input);sfDryGain.connect(masterBus.dryInput);const sfMods=fxEnabled?fxChain.filter(m=>m&&m.active!==false).map(m=>{try{return createTrackFxModule(m.type||m,ctx,m.params);}catch(e){return null;}}).filter(Boolean):[];let sfTail=sfEntry;sfMods.forEach(mod=>{sfTail.connect(mod.input);sfTail=mod.output;});sfTail.connect(sfOut);node.sfEntry=sfEntry;node.sfOut=sfOut;node.sfRouteGain=sfRouteGain;node.sfDryGain=sfDryGain;}nodeMap[track.id]=node;return node;}// Track-EQ preset library (unified_fx_rack_panel.md §III.1) — band gains only. const TRACK_EQ_PRESETS={flat:{name:'Flat / Reset',g:[0,0,0,0]},vocal_clarity:{name:'Vocal Unmask & Clarity',g:[-2.5,-1.8,3.2,2.0]},bass_punch:{name:'EDM Low-End Punch',g:[4.0,-3.0,1.5,1.0]},warm_tape:{name:'Warm Vintage Analog',g:[2.0,1.0,-2.0,-3.0]},guitar_edge:{name:'Guitar Cut & Presence',g:[-3.0,-2.0,3.5,1.5]}};// Dynamic signal-chain reconstruction (mastering_expand.md §II.3): // disconnect every module boundary, then wire the ACTIVE modules in series // between inputAnalyser (chain input) and outputAnalyser (chain output). @@ -624,7 +629,7 @@ useEffect(()=>{const all=[...(tracks||[]),...(sessionTabs||[]).reduce((acc,s)=>a // KHÔNG được trigger re-sync/re-route (tránh mất âm khi play sau đó — // updateSfRouting chạy thừa có thể đặt SF destination sai thời điểm). const audioSig=list.map(t=>(t.muted?'1':'0')+(t.solo?'1':'0')+':'+(t.volumeDb??0)+':'+(t.audioBypass?'1':'0')+(t.midiBypass?'1':'0')+':'+(t.midiItems||[]).length+':'+(t.clips||[]).length+':'+(t.sections||[]).length).join('|');if(trackAudioSyncSigRef.current===audioSig)return;trackAudioSyncSigRef.current=audioSig;list.forEach(t=>{// ♪ state → SF mastering route (sfRouteGain/sfDryGain), live on existing nodes -const sn=activeTrackNodesRef.current[t.id];if(sn&&sn.sfRouteGain&&sn.sfDryGain&&sn.sfRouteGain.gain.value>0!==!trackMidiBypassMap[t.id]){sn.sfRouteGain.gain.value=trackMidiBypassMap[t.id]?0:1;sn.sfDryGain.gain.value=trackMidiBypassMap[t.id]?1:0;}const sig=(t.muted?'1':'0')+(t.solo?'1':'0')+':'+(t.volumeDb??0);if(trackMuteSoloSigRef.current[t.id]===sig)return;trackMuteSoloSigRef.current[t.id]=sig;const audible=computeTrackAudibleGain(list,t)>0;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(list,t);if(node)setTrackNodeGain(node,audibleGain);if(node&&node.sfEntry){try{const g=typeof audibleGain==='number'&&isFinite(audibleGain)&&!isNaN(audibleGain)?audibleGain:1.0;node.sfEntry.gain.setTargetAtTime(g,getAudioContext().currentTime,0.02);}catch(e){}}// MIDI tracks: mirror the gain decision onto the channel CC7 volume so +const sn=activeTrackNodesRef.current[t.id];if(sn&&sn.sfRouteGain&&sn.sfDryGain&&sn.sfRouteGain.gain.value>0!==!effMidiBypass(t)){const _b=effMidiBypass(t);sn.sfRouteGain.gain.value=_b?0:1;sn.sfDryGain.gain.value=_b?1:0;}const sig=(t.muted?'1':'0')+(t.solo?'1':'0')+':'+(t.volumeDb??0);if(trackMuteSoloSigRef.current[t.id]===sig)return;trackMuteSoloSigRef.current[t.id]=sig;const audible=computeTrackAudibleGain(list,t)>0;const node=activeTrackNodesRef.current[t.id];const audibleGain=computeTrackAudibleGain(list,t);if(node)setTrackNodeGain(node,audibleGain);if(node&&node.sfEntry){try{const g=typeof audibleGain==='number'&&isFinite(audibleGain)&&!isNaN(audibleGain)?audibleGain:1.0;node.sfEntry.gain.setTargetAtTime(g,getAudioContext().currentTime,0.02);}catch(e){}}// MIDI tracks: mirror the gain decision onto the channel CC7 volume so // FluidSynth-rendered notes respect mute/solo too. if(t.midiItems&&t.midiItems.length>0||t.type==='MIDI'){try{if(window.SonicSF&&typeof window.SonicSF.controllerChange==='function'){const ch=ensureTrackMidiChannel(t,list);window.SonicSF.controllerChange(ch,7,audible?100:0);}}catch(e){}}trackAudibleRef.current[t.id]=audible;});updateSfRouting();},[tracks,sessionTabs]);useEffect(()=>{updateSfRouting();},[activeTab]);const[selectionCleared,setSelectionCleared]=useState(false);// LOOP_EDITOR_2.md §4.2 const midiVuActivityRef=useRef({});const triggerMidiVuActivity=(trackId,velocity)=>{if(!trackId)return;const velFactor=typeof velocity==='number'?velocity>1?velocity/127:velocity:0.8;const peak=Math.min(1.0,Math.max(0.15,velFactor));midiVuActivityRef.current[trackId]=peak;};window.triggerMidiVuActivity=triggerMidiVuActivity;// ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ── @@ -632,7 +637,10 @@ const[tempTabActive,setTempTabActive]=useState(false);const[tempTabBuffer,setTem const[tempTabEffects,setTempTabEffects]=useState({reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0});// ── Auth / User State ── const[currentUser,setCurrentUser]=useState(null);const[authModalOpen,setAuthModalOpen]=useState(false);const[authMode,setAuthMode]=useState('login');// 'login' | 'register' | 'force_change' const[isMandatoryLogin,setIsMandatoryLogin]=useState(false);const[profileModalOpen,setProfileModalOpen]=useState(false);const[systemManagerModalOpen,setSystemManagerModalOpen]=useState(false);const[aiConfigModalOpen,setAiConfigModalOpen]=useState(false);const[aiPresetModalOpen,setAiPresetModalOpen]=useState(false);const[aiPresetVersion,setAiPresetVersion]=useState(0);const[showMasteringModal,setShowMasteringModal]=useState(false);// Unified FX Rack target context (unified_fx_rack_panel.md): { trackId, trackName } | null -const[fxRackTarget,setFxRackTarget]=useState(null);window.__openFxRack=(trackId,trackName)=>setFxRackTarget({trackId,trackName});const[masteringSettings,setMasteringSettings]=useState({masterConnected:false,activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:DEFAULT_MASTER_CHAIN.map(m=>({...m})),compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0});useEffect(()=>{window.currentMasteringSettings=masteringSettings;if(audioCtx&&masterBus){toggleMasteringOnMaster(masteringSettings.masterConnected,masteringSettings.isBypassed);applyMasteringSettings(masteringSettings);}},[masteringSettings]);const[pluginManagerModalOpen,setPluginManagerModalOpen]=useState(false);const[pluginsData,setPluginsData]=useState(null);const loadAudioBuffersForTracks=async tracksList=>{let hasLoadedAny=false;const loadBuffer=async url=>{const res=await fetch(url);if(!res.ok)return null;const blob=await res.blob();return await window.SonicAudio.decodeAudioFile(blob);};const tryLoad=async fileId=>{if(!fileId)return null;try{const result=await loadBuffer('/static/audio/uploads/'+fileId);if(result)return result;}catch(_){}try{const result=await loadBuffer(`${API_AUDIO}/download/${fileId}`);if(result)return result;}catch(_){}return null;};const updatedTracks=await Promise.all(tracksList.map(async t=>{let trackBuffer=t.buffer;let trackChannelInfo=t.channelInfo;if(t.serverFileId&&!trackBuffer){const result=await tryLoad(t.serverFileId);if(result){trackBuffer=result.audioBuffer;trackChannelInfo=result.channelInfo;hasLoadedAny=true;}}const updatedClips=await Promise.all((t.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||t.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));if(trackBuffer&&updatedClips.length===0){const clipId=`default_${t.id}`;return{...t,buffer:trackBuffer,channelInfo:trackChannelInfo,clips:[{id:clipId,buffer:trackBuffer,startTime:t.startTime||0,name:t.name,speed:1.0}]};}return{...t,buffer:trackBuffer,channelInfo:trackChannelInfo,clips:updatedClips};}));if(hasLoadedAny){setTracks(prev=>{// Merge by TRACK ID (not array index): if the state changed between the +const[fxRackTarget,setFxRackTarget]=useState(null);window.__openFxRack=(trackId,trackName)=>setFxRackTarget({trackId,trackName});const[masteringSettings,setMasteringSettings]=useState({masterConnected:false,activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:DEFAULT_MASTER_CHAIN.map(m=>({...m})),compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0});useEffect(()=>{window.currentMasteringSettings=masteringSettings;if(audioCtx&&masterBus){toggleMasteringOnMaster(masteringSettings.masterConnected,masteringSettings.isBypassed);applyMasteringSettings(masteringSettings);// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị +// override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp +// ngay lên node đang phát (không chờ tracks effect). +try{const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(_n){if(_n.sfRouteGain&&_n.sfDryGain){const _b=effMidiBypass(_t);_n.sfRouteGain.gain.value=_b?0:1;_n.sfDryGain.gain.value=_b?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=effAudioBypass(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}}});updateSfRouting();}catch(e){}}},[masteringSettings]);const[pluginManagerModalOpen,setPluginManagerModalOpen]=useState(false);const[pluginsData,setPluginsData]=useState(null);const loadAudioBuffersForTracks=async tracksList=>{let hasLoadedAny=false;const loadBuffer=async url=>{const res=await fetch(url);if(!res.ok)return null;const blob=await res.blob();return await window.SonicAudio.decodeAudioFile(blob);};const tryLoad=async fileId=>{if(!fileId)return null;try{const result=await loadBuffer('/static/audio/uploads/'+fileId);if(result)return result;}catch(_){}try{const result=await loadBuffer(`${API_AUDIO}/download/${fileId}`);if(result)return result;}catch(_){}return null;};const updatedTracks=await Promise.all(tracksList.map(async t=>{let trackBuffer=t.buffer;let trackChannelInfo=t.channelInfo;if(t.serverFileId&&!trackBuffer){const result=await tryLoad(t.serverFileId);if(result){trackBuffer=result.audioBuffer;trackChannelInfo=result.channelInfo;hasLoadedAny=true;}}const updatedClips=await Promise.all((t.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||t.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));if(trackBuffer&&updatedClips.length===0){const clipId=`default_${t.id}`;return{...t,buffer:trackBuffer,channelInfo:trackChannelInfo,clips:[{id:clipId,buffer:trackBuffer,startTime:t.startTime||0,name:t.name,speed:1.0}]};}return{...t,buffer:trackBuffer,channelInfo:trackChannelInfo,clips:updatedClips};}));if(hasLoadedAny){setTracks(prev=>{// Merge by TRACK ID (not array index): if the state changed between the // fetch start and now (e.g. another project opened), index-based merging // would scramble tracks and drop items into the wrong track. updatedTracks // is authoritative; patch clip buffers from prev by matching clip ids. @@ -827,7 +835,7 @@ sfEntry.gain.value=1;sfOut.connect(sfPan);// Feed the soundfont into the shared sfOut.connect(scopeSplitter);// ♪ button: bypass Mastering FX Chain for the soundfont ONLY (track FX // Rack modules are still applied — PWR controls those). sfRouteGain → // masterBus.input (mastering), sfDryGain → dry bus (skip mastering). -const sfBypass=trackMidiBypassMap[track.id]!==undefined?!!trackMidiBypassMap[track.id]:!!(track.midiBypass??track.masteringBypass);sfRouteGain=context.createGain();sfDryGain=context.createGain();sfRouteGain.gain.value=sfBypass?0:1;sfDryGain.gain.value=sfBypass?1:0;sfPan.connect(sfRouteGain);sfPan.connect(sfDryGain);sfRouteGain.connect(masterBus.input);sfDryGain.connect(masterBus.dryInput);sfOut.connect(sfAnalyser);const sfModsBuilt=fxEnabled?fxChain.filter(m=>m&&m.active!==false).map(m=>{try{return createTrackFxModule(m.type||m,context,m.params);}catch(e){return null;}}).filter(Boolean):[];sfMods=sfModsBuilt;let sfTail=sfEntry;sfMods.forEach(mod=>{sfTail.connect(mod.input);sfTail=mod.output;});sfTail.connect(sfOut);}node={gainNode,pannerNode,fxStopFn,analyserNode,route,fxEntry,fxLegacyIn,scopeAnalyserL,scopeAnalyserR,sfEntry,sfOut,sfPan,sfAnalyser,sfRouteGain,sfDryGain,fxMods:chainMods,sfMods};// Realtime mute/solo: apply the track's current mute/solo/volume state to +const sfBypass=effMidiBypass(track);sfRouteGain=context.createGain();sfDryGain=context.createGain();sfRouteGain.gain.value=sfBypass?0:1;sfDryGain.gain.value=sfBypass?1:0;sfPan.connect(sfRouteGain);sfPan.connect(sfDryGain);sfRouteGain.connect(masterBus.input);sfDryGain.connect(masterBus.dryInput);sfOut.connect(sfAnalyser);const sfModsBuilt=fxEnabled?fxChain.filter(m=>m&&m.active!==false).map(m=>{try{return createTrackFxModule(m.type||m,context,m.params);}catch(e){return null;}}).filter(Boolean):[];sfMods=sfModsBuilt;let sfTail=sfEntry;sfMods.forEach(mod=>{sfTail.connect(mod.input);sfTail=mod.output;});sfTail.connect(sfOut);}node={gainNode,pannerNode,fxStopFn,analyserNode,route,fxEntry,fxLegacyIn,scopeAnalyserL,scopeAnalyserR,sfEntry,sfOut,sfPan,sfAnalyser,sfRouteGain,sfDryGain,fxMods:chainMods,sfMods};// Realtime mute/solo: apply the track's current mute/solo/volume state to // the fresh node so items of muted/soloed tracks start correctly. const trackList=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:[track];const nodeAudibleGain=computeTrackAudibleGain(trackList,track);setTrackNodeGain(node,nodeAudibleGain);if(node.sfEntry){try{const g=typeof nodeAudibleGain==='number'&&isFinite(nodeAudibleGain)&&!isNaN(nodeAudibleGain)?nodeAudibleGain:1.0;node.sfEntry.gain.setTargetAtTime(g,context.currentTime,0.02);}catch(e){}}activeTrackNodesRef.current[track.id]=node;console.log('[Bypass] node created track',track.id,'initial audioBypass=',!!trackAudioBypassMap[track.id],'routeGain=',node.route.routeGain.gain.value,'dryGain=',node.route.dryGain.gain.value);updateSfRouting();}return node.gainNode;};// Per-context audio graph reconstruction (unified_fx_rack_panel.md §III.2): // re-routes ONLY this track's fx chain (fxEntry → active modules → fxLegacyIn), @@ -843,10 +851,13 @@ const updateSfRouting=()=>{try{const list=activeTracksRef.current&&activeTracksR // TRACK đang edit (sfEntry → FX Rack riêng, fallback gainNode) để fader/ // pan/FX áp đúng cho notes đang nghe — kể cả khi project có >1 track MIDI // audible (bình thường rơi về masterBus.input, mất FX track). -const _activeTabId=activeTabRef.current;const _activeSub=subTabsRef.current?subTabsRef.current.find(s=>s.id===_activeTabId):null;if(_activeSub&&_activeSub.type==='PIANO_ROLL'){const _prNode=activeTrackNodesRef.current[_activeSub.trackId];if(_prNode&&window.SonicSF&&window.SonicSF.setOutputDestination){if(_prNode.sfEntry){window.SonicSF.setOutputDestination(_prNode.sfEntry);return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);if(midiAudible.length===1){const t=midiAudible[0];const node=activeTrackNodesRef.current[t.id];// SF ALWAYS enters the track's own FX chain (sfEntry → sfModules when +const _activeTabId=activeTabRef.current;const _activeSub=subTabsRef.current?subTabsRef.current.find(s=>s.id===_activeTabId):null;if(_activeSub&&_activeSub.type==='PIANO_ROLL'){const _prNode=activeTrackNodesRef.current[_activeSub.trackId];if(_prNode&&window.SonicSF&&window.SonicSF.setOutputDestination){if(_prNode.sfEntry){window.SonicSF.setOutputDestination(_prNode.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing (node có +// thể tạo khi mastering OFF → route dry — sửa ngay nếu chain ON). +if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_activeSub.trackId);const _b=effMidiBypass(_prTrk||{id:_activeSub.trackId});_prNode.sfRouteGain.gain.value=_b?0:1;_prNode.sfDryGain.gain.value=_b?1:0;}return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);if(midiAudible.length===1){const t=midiAudible[0];const node=activeTrackNodesRef.current[t.id];// SF ALWAYS enters the track's own FX chain (sfEntry → sfModules when // PWR ON). The ♪ button only switches the post-FX route (sfRouteGain / // sfDryGain) to skip or include the Mastering FX Chain. -if(node&&node.sfEntry&&window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(node.sfEntry);return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(node.gainNode);return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── MIDI preview cache (bounce nhanh offline) ── +if(node&&node.sfEntry&&window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(node.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing. +if(node.sfRouteGain&&node.sfDryGain){const _b=effMidiBypass(t.id);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain.value=_b?1:0;}return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(node.gainNode);return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── MIDI preview cache (bounce nhanh offline) ── // Khi preview: capture tiếng đàn (SF output — pre track-FX) vào cache theo // track. Khi export: nếu cache đủ độ dài → render OFFLINE NHANH bằng cache // (qua FX Rack + Mastering như playback) — không cần chờ bounce realtime. @@ -1090,7 +1101,11 @@ updateActiveTracks(prev=>prev.map(t=>t.id===selMidi.trackId?{...t,midiItems:[... // TUÂN THỦ RULES như track gốc: clone toàn bộ cấu trúc track nguồn // (synth_engine, instrumentProgram/instrumentName, volumeDb, pan, // fxActive, bypass flags, color...) rồi reset phần content. -const newTrackId='track_ai_var_'+Date.now();const srcTrack=curTracks[srcIdx];const newTrack={...(srcTrack||{}),id:newTrackId,name:'[AI Var] '+title,buffer:null,startTime:0,clips:[],sections:[],midiItems:[newItem],muted:false,solo:false,markers:[],serverFileId:null,synth_engine:srcTrack&&srcTrack.synth_engine?srcTrack.synth_engine:{type:'soundfont',soundfont_id:sf,soundfont_bank:sfBank,soundfont_program:sfProg},instrumentProgram:srcTrack?srcTrack.instrumentProgram:sfProg,instrumentName:srcTrack?srcTrack.instrumentName:'AI '+title};updateActiveTracks(prev=>{const copy=[...prev];copy.splice(srcIdx+1,0,newTrack);return copy;});setAiActionLog(prev=>[...prev,{type:'status',text:` ✅ [Variation] "${newItem.name}" (${newNotes.length} notes) → track mới "[AI Var] ${title}" ngay dưới (A/B) — kế thừa synth/instrument của track gốc.`,time:Date.now()}]);}setCanvasRedrawCount(n=>n+1);setTimeout(()=>lucide.createIcons(),200);}catch(err){setAiActionLog(prev=>[...prev,{type:'error',text:` ❌ Lỗi: ${err.message}`,time:Date.now()}]);showToast(`AI Error: ${err.message}`,'error');}finally{setAiProcessing(false);}};// Alias cũ giữ cho nút UI hiện tại (Compose) hoạt động — mode mặc định Variation +const newTrackId='track_ai_var_'+Date.now();const srcTrack=curTracks[srcIdx];const newTrack={...(srcTrack||{}),id:newTrackId,name:'[AI Var] '+title,// KHÔNG kế thừa midiChannel của track gốc: nếu dùng chung channel, +// solo/mute track gốc gửi CC7=0 trên channel đó → clone (cùng +// channel) bị NHỎ/CÂM. ensureTrackMidiChannel/assignTrackMidiChannel +// sẽ cấp channel RIÊNG cho track mới. +midiChannel:undefined,buffer:null,startTime:0,clips:[],sections:[],midiItems:[newItem],muted:false,solo:false,markers:[],serverFileId:null,synth_engine:srcTrack&&srcTrack.synth_engine?srcTrack.synth_engine:{type:'soundfont',soundfont_id:sf,soundfont_bank:sfBank,soundfont_program:sfProg},instrumentProgram:srcTrack?srcTrack.instrumentProgram:sfProg,instrumentName:srcTrack?srcTrack.instrumentName:'AI '+title};updateActiveTracks(prev=>{const copy=[...prev];copy.splice(srcIdx+1,0,newTrack);return copy;});setAiActionLog(prev=>[...prev,{type:'status',text:` ✅ [Variation] "${newItem.name}" (${newNotes.length} notes) → track mới "[AI Var] ${title}" ngay dưới (A/B) — kế thừa synth/instrument của track gốc.`,time:Date.now()}]);}setCanvasRedrawCount(n=>n+1);setTimeout(()=>lucide.createIcons(),200);}catch(err){setAiActionLog(prev=>[...prev,{type:'error',text:` ❌ Lỗi: ${err.message}`,time:Date.now()}]);showToast(`AI Error: ${err.message}`,'error');}finally{setAiProcessing(false);}};// Alias cũ giữ cho nút UI hiện tại (Compose) hoạt động — mode mặc định Variation const handleAiComposeToNextTrack=()=>handleAiComposeFromItem('SIMILAR_VARIATION');// ── Split Track at Playhead ── const handleSplitTrackAtTime=(trackId,clipId,time)=>{const track=tracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name}]:[];const targetClipId=clipId||clips.find(c=>time>=c.startTime&&timec.id===targetClipId);if(!clip||!clip.buffer)return;const relTime=Math.max(0,time-clip.startTime);const sr=clip.buffer.sampleRate;const cutSample=Math.floor(relTime*sr);const originalData=clip.buffer.getChannelData(0);if(cutSample<=0||cutSample>=originalData.length){showToast("Vị trí cắt nằm ngoài dải âm thanh của clip.","warning");return;}const beforeSnap=captureTrackSnapshot(trackId);const ctx=getAudioContext();const b1=ctx.createBuffer(1,cutSample,sr);b1.copyToChannel(originalData.subarray(0,cutSample),0);const b2=ctx.createBuffer(1,originalData.length-cutSample,sr);b2.copyToChannel(originalData.subarray(cutSample),0);const clip1={id:'clip_'+Date.now()+'_p1',name:`${clip.name.replace(' (Part 1)','').replace(' (Part 2)','')} (Part 1)`,buffer:b1,startTime:clip.startTime};const clip2={id:'clip_'+Date.now()+'_p2',name:`${clip.name.replace(' (Part 1)','').replace(' (Part 2)','')} (Part 2)`,buffer:b2,startTime:clip.startTime+cutSample/sr};setTracks(prev=>prev.map(t=>{if(t.id===trackId){const remainingClips=clips.filter(c=>c.id!==targetClipId);const updatedClips=[...remainingClips,clip1,clip2];return{...t,clips:updatedClips,buffer:updatedClips[0]?.buffer||null,startTime:updatedClips[0]?.startTime||0,name:updatedClips[0]?.name||t.name};}return t;}));setTimeout(()=>{const afterSnap=captureTrackSnapshot(trackId);pushAction('SPLIT_CLIP',trackId,beforeSnap,afterSnap);},50);showToast(`Đã chia nhỏ clip tại ${formatTime(time)}.`,"info");};const handleSplitTrack=trackId=>{handleSplitTrackAtTime(trackId,null,currentTime);};handleSplitTrackRef.current=handleSplitTrack;// ── Glue (Merge) Clips on Selected Track ── const handleGlueTracks=()=>{const track=tracks.find(t=>t.id===selectedTrackId);if(!track){showToast('Vui lòng chọn một track để thực hiện gộp (glue).','warning');return;}const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name}]:[];if(clips.length<2){showToast('Cần ít nhất 2 clip trên track này để gộp (glue).','warning');return;}const beforeSnap=captureTrackSnapshot(track.id);const ctx=getAudioContext();const sr=clips[0].buffer.sampleRate;let minStart=Infinity;let maxEnd=-Infinity;clips.forEach(c=>{const start=c.startTime||0;const end=start+c.buffer.duration;minStart=Math.min(minStart,start);maxEnd=Math.max(maxEnd,end);});const newDur=maxEnd-minStart;const newBuffer=ctx.createBuffer(1,Math.ceil(newDur*sr),sr);const newData=newBuffer.getChannelData(0);clips.forEach(c=>{const data=c.buffer.getChannelData(0);const offset=Math.floor(((c.startTime||0)-minStart)*sr);for(let i=0;imaxPeak)maxPeak=abs;}if(maxPeak>1.0){for(let i=0;iprev.map(t=>{if(t.id===track.id){return{...t,clips:[mergedClip],buffer:newBuffer,startTime:minStart,name:mergedClip.name};}return t;}));setTimeout(()=>{const afterSnap=captureTrackSnapshot(track.id);pushAction('GLUE',track.id,beforeSnap,afterSnap);},50);showToast(`Đã gộp ${clips.length} clips thành công.`,'success');};// ── DAW Command Registration for AI (28_AI_PANEL.md §1 & §2) ── diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index 15f4c7a..5bfa9f4 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -24,6 +24,7 @@ let _gainNode = null; let _pendingOutputDestination = null; let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream + let _validPercCache = {}; // { sfId: [bank, prog] | null } — preset percussion hợp lệ let _scheduledNotes = []; let _loadPromises = {}; let _sfloadSeq = 0; @@ -562,22 +563,34 @@ console.log('[SonicSF] selectProgram for channel:', ch, 'sfHandle:', sfHandle, 'bank:', finalBank, 'prog:', finalProg); if (sfHandle !== undefined) { try { - var _selRet = _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg); - // Percussion (bank 128) preset không tồn tại trong - // font → fallback preset trống hợp lệ (GM kit 48 → - // preset đầu font) — nếu không, noteon trên preset - // rỗng = CÂM (track percussion "1 âm đầu rồi câm"). - if (_selRet !== 0 && finalBank === 128) { - var _cands = [[128, 48], [0, 0], [128, 1], [0, 48]]; - for (var _ci = 0; _ci < _cands.length; _ci++) { - try { - if (_fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, _cands[_ci][0], _cands[_ci][1]) === 0) { - finalBank = _cands[_ci][0]; finalProg = _cands[_ci][1]; - break; + // Percussion (bank 128): tìm preset HỢP LỆ trong + // font — quét bank 128 + bank 0 (0-127) MỘT LẦN, + // cache theo sfId. Trước đây chỉ thử 4 preset cố + // định → font không có → cache channel = (128,0) + // INVALID → note sau skip re-select (progAlreadySet) + // → noteon preset rỗng = CÂM ("1 âm đầu rồi câm"). + if (finalBank === 128) { + var _vKey = finalSfId || ('h' + sfHandle); + if (_validPercCache[_vKey] === undefined) { + var _found = null; + for (var _b = 0; _b < 2 && !_found; _b++) { + var _bk = _b === 0 ? 128 : 0; + for (var _p = 0; _p < 128 && !_found; _p++) { + try { + if (_fluidModule._fluid_synth_program_select(_synthPtr, 9, sfHandle, _bk, _p) === 0) { + _found = [_bk, _p]; + } + } catch (e) {} } - } catch (e) {} + } + _validPercCache[_vKey] = _found; + } + if (_validPercCache[_vKey]) { + finalBank = _validPercCache[_vKey][0]; + finalProg = _validPercCache[_vKey][1]; } } + var _selRet = _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg); } catch (e) {} } else { try { _fluidModule._fluid_synth_bank_select(_synthPtr, ch, finalBank); } catch (e) {} diff --git a/app/templates/index.html b/app/templates/index.html index fadd55d..a0b1331 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -16,7 +16,7 @@ - + @@ -24,7 +24,7 @@ - +