diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 172debe..c5abe80 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -13900,6 +13900,12 @@ const App = () => { selectedItemIdsRef.current = selectedItemIds; const [currentTime, setCurrentTime] = useState(0); const [isPlaying, setIsPlaying] = useState(false); + // Ref theo dõi isPlaying — setTimeout VU trigger (main-midi delayed + section + // delayed) check ref này trước khi fire: stop → isPlaying=false → setTimeout + // sót không fire → VU không nhảy sau khi âm đã dừng (user: VU vẫn animation + // khi hết âm). + const isPlayingRef = useRef(false); + isPlayingRef.current = isPlaying; const [selectionStart, setSelectionStart] = useState(null); const [selectionEnd, setSelectionEnd] = useState(null); const [selectionFollowsTempo, setSelectionFollowsTempo] = useState(true); @@ -14988,6 +14994,14 @@ const App = () => { 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; + // Log nguồn trigger (rate-limited 1/20 fire): xác định ai còn fire VU khi + // âm đã ngừng (user: VU vẫn animation sau khi hết âm). + try { + const _tf = (window.__trigFrame = (window.__trigFrame || 0) + 1); + if (_tf % 20 === 1) { + console.log('[VUTrig]', trackId, 'vel=', velocity, 'peak=', peak.toFixed(2), 'play=', isPlaying, 'tab=', activeTabRef.current); + } + } catch (e) { } }; window.triggerMidiVuActivity = triggerMidiVuActivity; @@ -19349,6 +19363,9 @@ const App = () => { ); // Trigger VU meter flash when the note starts playing setTimeout(() => { + // ⚠️ Guard: stop → setTimeout sót không được fire (VU nhảy + // sau khi âm đã dừng — user bug 05:45). + if (!isPlayingRef.current) return; if (window.triggerMidiVuActivity) { window.triggerMidiVuActivity(track.id, note.velocity || 0.8); } @@ -19480,6 +19497,20 @@ const App = () => { subCh, subTrack.synth_engine ); + // Trigger VU meter flash đúng nhịp (cả subTrack — VU + // section tab + parent track — VU MAIN): MIDI section đi + // SF → masterBus.input (không qua sub-node analyser) → + // VU track chỉ nhảy qua midiVuActivity. (Bị mất khi + // commit gộp — khôi phục fix 00:30/02:00.) + setTimeout(() => { + // ⚠️ Guard: stop → setTimeout sót không được fire (VU + // nhảy sau khi âm đã dừng — user bug 05:45). + if (!isPlayingRef.current) return; + if (window.triggerMidiVuActivity) { + window.triggerMidiVuActivity(subTrack.id, note.velocity || 0.8); + window.triggerMidiVuActivity(track.id, note.velocity || 0.8); + } + }, delay * 1000); } else { const remainingDurMs = (notePlayEndMain - offsetTime) * 1000; window.SonicSF.playNote( @@ -19492,14 +19523,10 @@ const App = () => { subCh, subTrack.synth_engine ); - } - // Trigger VU meter flash đúng nhịp (giống main-track midi 19066) - // Cả subTrack (VU section tab) + parent track (VU MAIN — section - // item đang play) — midiVuActivity keyed theo track id → trigger - // cả 2 để VU main nhảy theo section. - if (window.triggerMidiVuActivity) { - window.triggerMidiVuActivity(subTrack.id, note.velocity || 0.8); - window.triggerMidiVuActivity(track.id, note.velocity || 0.8); + if (window.triggerMidiVuActivity) { + window.triggerMidiVuActivity(subTrack.id, note.velocity || 0.8); + window.triggerMidiVuActivity(track.id, note.velocity || 0.8); + } } } }); @@ -24932,8 +24959,17 @@ STRICT CONSTRAINTS: const v = Math.abs(data[i] - 128) / 128; if (v > peak) peak = v; } + // Deadband: bỏ noise nền (log 06:00 — master peak 0.008 liên tục dù + // không âm → master VU nhảy nhẹ mãi) + if (peak < 0.01) peak = 0; setMasterVU(peak); setMasterMeterPeak(prev => Math.max(prev * 0.97, peak)); + // Log master VU khi đang nhảy (chẩn đoán 06:00 — VU nhảy ngoài vùng âm) + if (peak > 0.001) { + try { + console.log('[VU-master] peak=', peak.toFixed(3)); + } catch (e) { } + } } // 2. Track VU Meters @@ -24989,20 +25025,69 @@ STRICT CONSTRAINTS: } } + // MIDI THỰC: đọc từ SF output (âm post-FluidSynth) — thay velocity + // trigger (GIẢ — fire lúc schedule — click playhead → re-schedule → + // VU nhảy dù âm không ra — log 06:00: midi=0.98 nhưng master=0.008). + // Track MIDI đi SF → masterBus (không qua node analyser) → tap tại SF + // output gain. + let sfAudioPeak = 0; + let _sfTapped = false; + if (audioPeak <= 0.001) { + try { + if (window.SonicSF && window.SonicSF.getOutputNode) { + const sfNode = window.SonicSF.getOutputNode(); + if (sfNode) { + if (!sfNode.__vuAnalyser) { + const _sa = getAudioContext().createAnalyser(); + _sa.fftSize = 256; + _sa.smoothingTimeConstant = 0.2; + sfNode.connect(_sa); + sfNode.__vuAnalyser = _sa; + } + _sfTapped = true; + const d3 = new Uint8Array(128); + sfNode.__vuAnalyser.getByteTimeDomainData(d3); + for (let i = 0; i < d3.length; i++) { + const v = Math.abs(d3[i] - 128) / 128; + if (v > sfAudioPeak) sfAudioPeak = v; + } + } + } + } catch (e) { } + } + // midiVuActivityRef được set bởi triggerMidiVuActivity — cả khi play // MIDI item LẪN khi ARM + nhấn phím MIDI keyboard (preview) — nên VU // nhảy trong cả 2 trường hợp (không chặn bởi isPlaying). let midiPeak = isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0; + // SF analyser đã tap → CHỈ dùng âm THỰC (velocity trigger = giả — bỏ) + if (_sfTapped) midiPeak = 0; if (midiPeak > 0) { - midiVuActivityRef.current[trackId] = midiPeak * 0.90; + // Decay nhanh hơn (0.72/frame ≈ tắt sau ~0.35s) — hết note → VU + // dừng nhanh, không "vẫn diễn animation" khi không còn âm. + midiVuActivityRef.current[trackId] = midiPeak * 0.72; if (midiVuActivityRef.current[trackId] < 0.01) { midiVuActivityRef.current[trackId] = 0; } } + // Gộp âm MIDI THỰC vào audioPeak + if (sfAudioPeak > audioPeak) audioPeak = sfAudioPeak; + // Deadband: bỏ noise nền analyser (log 06:05 — audio=0.008 dù không + // âm → VU hiển thị đoạn nhỏ "vẽ nhưng không animation") — hết âm → + // VU tắt NGAY LẬP TỨC (user yêu cầu). + if (audioPeak < 0.01) audioPeak = 0; const peak = Math.max(audioPeak, midiPeak); const db = peak > 0 ? 20 * Math.log10(peak) : -60; + // Log chẩn đoán VU: CHỈ in khi peak > 0 (VU đang nhảy thật) — xác định + // nguồn nào còn nhảy khi playhead ngoài vùng âm (user bug 06:00). + if (peak > 0.001) { + try { + console.log('[VU]', trackId, 'audio=', audioPeak.toFixed(3), 'midi=', midiPeak.toFixed(3), 'key=', key); + } catch (e) { } + } + if (peak > 0.001) { if (key.endsWith('_mixer')) { drawMixerVuMeter(canvas, peak); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 447153a..03e130a 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -609,7 +609,11 @@ var mt2=activeTracksRef.current||tracks;var qTrk=null;for(var qi=0;qis.id===instrumentId||s.id===sfIdParam);if(cachedSf&&cachedSf.presets){setSfPresets(cachedSf.presets);}else{window.SonicAPI.listSoundfontInstruments(sfIdParam).then(data=>setSfPresets(data.presets||[])).catch(e=>{console.error('listSoundfontInstruments failed:',e);setSfPresets([]);});}}else{setTrackInstrumentWithUndo(trackId,instrumentId,displayName);}};const[activeTool,setActiveTool]=useState('select');// 'select' | 'grab' | 'razor' const[snapValue,onSnapChangeue]=useState('1');// 'free', '1', '1/2', '1/4', '1/8', '1/16', '1/32' const snapTime=(time,snapValue,bpmVal)=>{if(snapValue==='free')return time;const beatDuration=60/parseFloat(bpmVal||120);let divisor=1;if(snapValue==='4')divisor=4;else if(snapValue==='1')divisor=1;else if(snapValue==='1/2')divisor=0.5;else if(snapValue==='1/4')divisor=0.25;else if(snapValue==='1/8')divisor=0.125;else if(snapValue==='1/16')divisor=0.0625;else if(snapValue==='1/32')divisor=0.03125;const gridSpacing=beatDuration*divisor;return Math.round(time/gridSpacing)*gridSpacing;};const snapValueRef=useRef(snapValue);snapValueRef.current=snapValue;const bpmRef=useRef(bpm);bpmRef.current=bpm;// BMP for Tempo Track - LOOP_EDITOR_2.md §6 -const[selectedTrackId,setSelectedTrackId]=useState('1');const[selectedItemIds,setSelectedItemIds]=useState(new Set());const selectedItemIdsRef=useRef(new Set());selectedItemIdsRef.current=selectedItemIds;const[currentTime,setCurrentTime]=useState(0);const[isPlaying,setIsPlaying]=useState(false);const[selectionStart,setSelectionStart]=useState(null);const[selectionEnd,setSelectionEnd]=useState(null);const[selectionFollowsTempo,setSelectionFollowsTempo]=useState(true);const selectionRef=useRef({start:null,end:null});selectionRef.current={start:selectionStart,end:selectionEnd};const[selectionMode,setSelectionMode]=useState(null);// 'global' (from ruler) | 'local' (from track) +const[selectedTrackId,setSelectedTrackId]=useState('1');const[selectedItemIds,setSelectedItemIds]=useState(new Set());const selectedItemIdsRef=useRef(new Set());selectedItemIdsRef.current=selectedItemIds;const[currentTime,setCurrentTime]=useState(0);const[isPlaying,setIsPlaying]=useState(false);// Ref theo dõi isPlaying — setTimeout VU trigger (main-midi delayed + section +// delayed) check ref này trước khi fire: stop → isPlaying=false → setTimeout +// sót không fire → VU không nhảy sau khi âm đã dừng (user: VU vẫn animation +// khi hết âm). +const isPlayingRef=useRef(false);isPlayingRef.current=isPlaying;const[selectionStart,setSelectionStart]=useState(null);const[selectionEnd,setSelectionEnd]=useState(null);const[selectionFollowsTempo,setSelectionFollowsTempo]=useState(true);const selectionRef=useRef({start:null,end:null});selectionRef.current={start:selectionStart,end:selectionEnd};const[selectionMode,setSelectionMode]=useState(null);// 'global' (from ruler) | 'local' (from track) const[sweepSelect,setSweepSelect]=useState(null);const isSweepingRef=useRef(false);const sweepStartRef=useRef(0);const sweepTrackIdRef=useRef(null);const sweepStartYRef=useRef(0);const sweepEndYRef=useRef(0);const sweepSelectRef=useRef(null);const pendingDragRef=useRef(null);// { trackId, itemType, itemId, clickOffset, startX, startY } const handleSectionItemDragStartRef=useRef(null);const[localSelectionTrackId,setLocalSelectionTrackId]=useState(null);const[localSelectionStart,setLocalSelectionStart]=useState(null);const[localSelectionEnd,setLocalSelectionEnd]=useState(null);const[zoom,setZoom]=useState(()=>{// Persist zoom qua reload (user yêu cầu giữ kích thước items sau refresh) try{const v=parseFloat(localStorage.getItem('sf_zoom'));if(isFinite(v)&&v>0)return v;}catch(e){}return 100;});React.useEffect(()=>{try{localStorage.setItem('sf_zoom',String(zoom));}catch(e){}// Zoom TIMELINE (App) — serialize project dùng cái này (window.__currentZoom @@ -734,7 +738,9 @@ mainResumeRef.current=null;}}},[activeTab]);const[selectionCleared,setSelectionC // Detect MAIN/SECTION play vừa DỪNG (hết notes/projectEnd/stop): nếu đang ở // PIANO ROLL tab (không play tab) → playhead về 0 — user: main play hết → // không còn midi note → piano roll phải về đầu (không giữ local cuối). -const prevIsPlayingRef=useRef(false);useEffect(()=>{const wasPlaying=prevIsPlayingRef.current;prevIsPlayingRef.current=isPlaying;if(wasPlaying&&!isPlaying){const _tabId=activeTabRef.current;if(_tabId&&_tabId.startsWith('midi_')){const _prSt=subTabsRef.current.find(s=>s.id===_tabId);if(_prSt&&!_prSt.isPlaying){setSubTabs(prev=>prev.map(s=>s.id===_tabId?{...s,currentTime:0}:s));}}}},[isPlaying]);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) ── +const prevIsPlayingRef=useRef(false);useEffect(()=>{const wasPlaying=prevIsPlayingRef.current;prevIsPlayingRef.current=isPlaying;if(wasPlaying&&!isPlaying){const _tabId=activeTabRef.current;if(_tabId&&_tabId.startsWith('midi_')){const _prSt=subTabsRef.current.find(s=>s.id===_tabId);if(_prSt&&!_prSt.isPlaying){setSubTabs(prev=>prev.map(s=>s.id===_tabId?{...s,currentTime:0}:s));}}}},[isPlaying]);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;// Log nguồn trigger (rate-limited 1/20 fire): xác định ai còn fire VU khi +// âm đã ngừng (user: VU vẫn animation sau khi hết âm). +try{const _tf=window.__trigFrame=(window.__trigFrame||0)+1;if(_tf%20===1){console.log('[VUTrig]',trackId,'vel=',velocity,'peak=',peak.toFixed(2),'play=',isPlaying,'tab=',activeTabRef.current);}}catch(e){}};window.triggerMidiVuActivity=triggerMidiVuActivity;// ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ── const[tempTabActive,setTempTabActive]=useState(false);const[tempTabBuffer,setTempTabBuffer]=useState(null);const[tempTabTrackId,setTempTabTrackId]=useState(null);const[tempTabOrigStart,setTempTabOrigStart]=useState(0);const[tempTabOrigEnd,setTempTabOrigEnd]=useState(0);const tempTabCanvasRef=useRef(null);// Effect parameters for temp tab 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' @@ -1092,7 +1098,9 @@ ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);var trkCh=assignT if(track.synth_engine&&track.synth_engine.type==='soundfont'&&track.synth_engine.soundfont_id){window.SonicSF.selectInstrument(trkCh,track.synth_engine.soundfont_bank||0,track.synth_engine.soundfont_program||0,track.synth_engine.soundfont_id);}const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const itemEndSec=item.startTime+(item.duration||4);const notes=item.notes||[];notes.forEach(note=>{// note start/duration is in beats (for MIDI items) const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;if(noteStartSec>=itemEndSec)return;// skip notes outside item's visual duration const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime{if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(track.id,note.velocity||0.8);}},delay*1000);}else{const playOffset=offsetTime-noteStartSec;const remainingDurMs=(noteEndSec-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,trkCh,track.synth_engine);// Trigger VU meter flash instantly +setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire (VU nhảy +// sau khi âm đã dừng — user bug 05:45). +if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(track.id,note.velocity||0.8);}},delay*1000);}else{const playOffset=offsetTime-noteStartSec;const remainingDurMs=(noteEndSec-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,trkCh,track.synth_engine);// Trigger VU meter flash instantly if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(track.id,note.velocity||0.8);}}}});});}// If track has instrumentId set but no MIDI items, create scheduled oscillators if(track.instrumentId&&midiItems.length===0&&track.buffer){// Play audio normally (the buffer has the audio data) // This block is intentionally empty - audio clips already play above @@ -1104,11 +1112,14 @@ const subNodeObj=activeTrackNodesRef.current[track.id+'_sub_'+subTrack.id];if(su const subClips=subTrack.clips||[];subClips.forEach(clip=>{if(!clip.buffer)return;const clipStartLocal=clip.startTime||0;const clipDurLocal=clip.buffer.duration/(clip.speed||1.0);const clipStartMain=secStart+clipStartLocal;const clipEndMain=clipStartMain+clipDurLocal;// Only play within the bounds of [secStart, secEnd] const activeStart=Math.max(clipStartMain,secStart);const activeEnd=Math.min(clipEndMain,secEnd);if(activeStart0){subMidiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSecLocal=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteStartMain=secStart+noteStartSecLocal;const noteEndMain=noteStartMain+noteDurSec;// Only play if the note starts inside the Section item bounds and hasn't finished yet -if(noteStartMain>=secStart&¬eStartMain=secStart&¬eStartMain{// ⚠️ Guard: stop → setTimeout sót không được fire (VU +// nhảy sau khi âm đã dừng — user bug 05:45). +if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity(track.id,note.velocity||0.8);}},delay*1000);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity(track.id,note.velocity||0.8);}}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2) const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();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,speed:track.speed||1.0}]:[];// Get or create persistent gain & panner for real-time control const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);// NOTE: do NOT connect gainNode → pannerNode directly here — the track // node already wires gainNode → fxEntry → [FX modules] → fxLegacyIn → @@ -1347,17 +1358,34 @@ const prefsRef=useRef({});prefsRef.current={showAIPanel,showExportPanel,showSele const masterVUAnimRef=useRef(null);useEffect(()=>{function tick(){try{// Khi STOP (không play, không recording): vẽ VU RỖNG — không đọc analyser/ // activity → hết animation dính + master không full sau khi dừng play. const isRecActive=activeAudioRecordersRef.current&&Object.keys(activeAudioRecordersRef.current).length>0;if(!isPlaying&&!isRecActive){setMasterVU(0);setMasterMeterPeak(0);Object.keys(trackVuRefs.current).forEach(k=>{const c=trackVuRefs.current[k];if(!c)return;if(k.endsWith('_mixer'))drawMixerVuMeter(c,0);else drawVuMeter(c,-60);});masterVUAnimRef.current=requestAnimationFrame(tick);return;}// 1. Master VU Meter -if(masterBus&&masterBus.analyser){const data=new Uint8Array(128);masterBus.analyser.getByteTimeDomainData(data);let peak=0;for(let i=0;ipeak)peak=v;}setMasterVU(peak);setMasterMeterPeak(prev=>Math.max(prev*0.97,peak));}// 2. Track VU Meters +if(masterBus&&masterBus.analyser){const data=new Uint8Array(128);masterBus.analyser.getByteTimeDomainData(data);let peak=0;for(let i=0;ipeak)peak=v;}// Deadband: bỏ noise nền (log 06:00 — master peak 0.008 liên tục dù +// không âm → master VU nhảy nhẹ mãi) +if(peak<0.01)peak=0;setMasterVU(peak);setMasterMeterPeak(prev=>Math.max(prev*0.97,peak));// Log master VU khi đang nhảy (chẩn đoán 06:00 — VU nhảy ngoài vùng âm) +if(peak>0.001){try{console.log('[VU-master] peak=',peak.toFixed(3));}catch(e){}}}// 2. Track VU Meters const trackNodes=activeTrackNodesRef.current||{};const activeKeys=Object.keys(trackVuRefs.current);activeKeys.forEach(key=>{const trackId=key.replace('_mixer','');const isRecordingThisTrack=activeAudioRecordersRef.current&&activeAudioRecordersRef.current[trackId];if(isRecordingThisTrack)return;const node=trackNodes[trackId];const canvas=trackVuRefs.current[key];if(!canvas)return;// Section tab: track nodes nằm dưới key _sub_ — // lookup plain key trả undefined → VU không animation. Fallback scan _sub_. const vNode=node||function(){for(var k in trackNodes){if(k.endsWith('_sub_'+trackId))return trackNodes[k];}return null;}();// Only animate when real audio actually passes through the track (solo/mute aware) var vuTracks=activeTracksRef.current||[];var anySoloVU=vuTracks.some(function(t){return t.solo;});var vuTrack=vuTracks.find(function(t){return t.id===trackId;});var isAudible=vuTrack?anySoloVU?!!vuTrack.solo:!vuTrack.muted:true;let audioPeak=0;if(isAudible&&vNode&&vNode.analyserNode){const analyser=vNode.analyserNode;const data=new Uint8Array(128);analyser.getByteTimeDomainData(data);for(let i=0;iaudioPeak)audioPeak=v;}}// Section item trên MAIN track: VU theo sub-nodes của track này (key // _sub_*) — âm section đi thẳng masterBus.input (không qua // node chính) → node chính không có tín hiệu → VU track đứng yên. -if(audioPeak<=0.001){for(var sk in trackNodes){if(sk.indexOf(trackId+'_sub_')===0){const sn=trackNodes[sk];if(sn&&sn.analyserNode){const d2=new Uint8Array(128);sn.analyserNode.getByteTimeDomainData(d2);for(let i=0;iaudioPeak)audioPeak=v;}}}}}// midiVuActivityRef được set bởi triggerMidiVuActivity — cả khi play +if(audioPeak<=0.001){for(var sk in trackNodes){if(sk.indexOf(trackId+'_sub_')===0){const sn=trackNodes[sk];if(sn&&sn.analyserNode){const d2=new Uint8Array(128);sn.analyserNode.getByteTimeDomainData(d2);for(let i=0;iaudioPeak)audioPeak=v;}}}}}// MIDI THỰC: đọc từ SF output (âm post-FluidSynth) — thay velocity +// trigger (GIẢ — fire lúc schedule — click playhead → re-schedule → +// VU nhảy dù âm không ra — log 06:00: midi=0.98 nhưng master=0.008). +// Track MIDI đi SF → masterBus (không qua node analyser) → tap tại SF +// output gain. +let sfAudioPeak=0;let _sfTapped=false;if(audioPeak<=0.001){try{if(window.SonicSF&&window.SonicSF.getOutputNode){const sfNode=window.SonicSF.getOutputNode();if(sfNode){if(!sfNode.__vuAnalyser){const _sa=getAudioContext().createAnalyser();_sa.fftSize=256;_sa.smoothingTimeConstant=0.2;sfNode.connect(_sa);sfNode.__vuAnalyser=_sa;}_sfTapped=true;const d3=new Uint8Array(128);sfNode.__vuAnalyser.getByteTimeDomainData(d3);for(let i=0;isfAudioPeak)sfAudioPeak=v;}}}}catch(e){}}// midiVuActivityRef được set bởi triggerMidiVuActivity — cả khi play // MIDI item LẪN khi ARM + nhấn phím MIDI keyboard (preview) — nên VU // nhảy trong cả 2 trường hợp (không chặn bởi isPlaying). -let midiPeak=isAudible?midiVuActivityRef.current[trackId]||0:0;if(midiPeak>0){midiVuActivityRef.current[trackId]=midiPeak*0.90;if(midiVuActivityRef.current[trackId]<0.01){midiVuActivityRef.current[trackId]=0;}}const peak=Math.max(audioPeak,midiPeak);const db=peak>0?20*Math.log10(peak):-60;if(peak>0.001){if(key.endsWith('_mixer')){drawMixerVuMeter(canvas,peak);}else{drawVuMeter(canvas,db);}}else{if(key.endsWith('_mixer')){drawMixerVuMeter(canvas,0);}else{drawVuMeter(canvas,-60);}}});}catch(e){console.warn('VU tick error:',e);}masterVUAnimRef.current=requestAnimationFrame(tick);}masterVUAnimRef.current=requestAnimationFrame(tick);return()=>{if(masterVUAnimRef.current)cancelAnimationFrame(masterVUAnimRef.current);};},[isPlaying]);const handleOpenProject=async(projectId,projectName)=>{setAppWarningModal({title:"Mở dự án",message:"Bạn có muốn mở dự án này? Các thay đổi chưa lưu trên workspace hiện tại sẽ bị mất.",isAlert:false,onConfirm:async()=>{try{const proj=await window.SonicAPI.getCloudProject(projectId);const parsed=JSON.parse(proj.data_json);let restoredTracks=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(parsed.main_session){const result=deserializeProjectFromSchema(parsed);restoredTracks=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;// Bỏ qua zoom ≤ 1 (data cũ lưu 1.0 — lỗi __currentZoom của Media +let midiPeak=isAudible?midiVuActivityRef.current[trackId]||0:0;// SF analyser đã tap → CHỈ dùng âm THỰC (velocity trigger = giả — bỏ) +if(_sfTapped)midiPeak=0;if(midiPeak>0){// Decay nhanh hơn (0.72/frame ≈ tắt sau ~0.35s) — hết note → VU +// dừng nhanh, không "vẫn diễn animation" khi không còn âm. +midiVuActivityRef.current[trackId]=midiPeak*0.72;if(midiVuActivityRef.current[trackId]<0.01){midiVuActivityRef.current[trackId]=0;}}// Gộp âm MIDI THỰC vào audioPeak +if(sfAudioPeak>audioPeak)audioPeak=sfAudioPeak;// Deadband: bỏ noise nền analyser (log 06:05 — audio=0.008 dù không +// âm → VU hiển thị đoạn nhỏ "vẽ nhưng không animation") — hết âm → +// VU tắt NGAY LẬP TỨC (user yêu cầu). +if(audioPeak<0.01)audioPeak=0;const peak=Math.max(audioPeak,midiPeak);const db=peak>0?20*Math.log10(peak):-60;// Log chẩn đoán VU: CHỈ in khi peak > 0 (VU đang nhảy thật) — xác định +// nguồn nào còn nhảy khi playhead ngoài vùng âm (user bug 06:00). +if(peak>0.001){try{console.log('[VU]',trackId,'audio=',audioPeak.toFixed(3),'midi=',midiPeak.toFixed(3),'key=',key);}catch(e){}}if(peak>0.001){if(key.endsWith('_mixer')){drawMixerVuMeter(canvas,peak);}else{drawVuMeter(canvas,db);}}else{if(key.endsWith('_mixer')){drawMixerVuMeter(canvas,0);}else{drawVuMeter(canvas,-60);}}});}catch(e){console.warn('VU tick error:',e);}masterVUAnimRef.current=requestAnimationFrame(tick);}masterVUAnimRef.current=requestAnimationFrame(tick);return()=>{if(masterVUAnimRef.current)cancelAnimationFrame(masterVUAnimRef.current);};},[isPlaying]);const handleOpenProject=async(projectId,projectName)=>{setAppWarningModal({title:"Mở dự án",message:"Bạn có muốn mở dự án này? Các thay đổi chưa lưu trên workspace hiện tại sẽ bị mất.",isAlert:false,onConfirm:async()=>{try{const proj=await window.SonicAPI.getCloudProject(projectId);const parsed=JSON.parse(proj.data_json);let restoredTracks=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(parsed.main_session){const result=deserializeProjectFromSchema(parsed);restoredTracks=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;// Bỏ qua zoom ≤ 1 (data cũ lưu 1.0 — lỗi __currentZoom của Media // Explorer) — áp dụng mới giữ zoom hiện tại/localStorage. if(result.zoom>1&&setZoom)setZoom(result.zoom);if(result.masteringSettings)setMasteringSettings(result.masteringSettings);}else{restoredTracks=(parsed.tracks||[]).map(function(t){const rest=Object.assign({},t);delete rest.height;return Object.assign({},rest,{buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null});});}setTracks(restoredTracks);loadAudioBuffersForTracks(restoredTracks).catch(function(err){console.warn('loadAudioBuffersForTracks error:',err);});trackMidiChannelsRef.current={};preloadTrackInstruments(restoredTracks).catch(function(err){console.warn('preloadTrackInstruments error:',err);});setBpm(restoredBpm.toString());setSelectedTrackId(restoredTracks[0]?.id||'1');setProjectName(proj.name);setCurrentProjectId(proj.id);if(restoredSessionTabs.length>0)setSessionTabs(restoredSessionTabs);if(restoredSubTabs.length>0)setSubTabs(restoredSubTabs);localStorage.setItem('sonic_project_name',proj.name);localStorage.setItem('sonic_project_id',proj.id);showToast('Đã nạp dự án "'+proj.name+'" thành công!',"success");}catch(e){showToast(e.message||"Lỗi khi nạp dự án","error");}}});};return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"h-full w-full flex flex-col bg-[#1e1e1e]"},/*#__PURE__*/React.createElement("header",{className:"h-9 bg-[#2e2e2e] border-b border-[#181818] flex items-center px-1 shrink-0 select-none"},[{label:'File',items:[{label:'New Project',icon:'file-plus',shortcut:'Ctrl+N',action:()=>{// New project: đóng hết tab + reset MỌI trạng thái về mặc định try{stopAllPlayback();}catch(e){}setSubTabs([]);setSessionTabs([]);setActiveTab('main');// Reset instrument/channel/route context đã load diff --git a/app/templates/index.html b/app/templates/index.html index 94646e4..66f79ac 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,7 +24,7 @@ - +