FIX: Clone AI Var kế thừa midiChannel của track gốc → 2 track DÙNG CHUNG channel
This commit is contained in:
@@ -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&&time<c.startTime+c.buffer.duration)?.id;if(!targetClipId)return;const clip=clips.find(c=>c.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;i<data.length;i++){if(offset+i<newData.length){newData[offset+i]+=data[i];}}});let maxPeak=0;for(let i=0;i<newData.length;i++){const abs=Math.abs(newData[i]);if(abs>maxPeak)maxPeak=abs;}if(maxPeak>1.0){for(let i=0;i<newData.length;i++)newData[i]/=maxPeak;}const mergedClip={id:'clip_merged_'+Date.now(),name:`${track.name.replace(' (Part 1)','').replace(' (Part 2)','')} (Glued)`,buffer:newBuffer,startTime:minStart};setTracks(prev=>prev.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) ──
|
||||
|
||||
Reference in New Issue
Block a user