FIX: sửa lỗi hiển thị animation của VU meter

This commit is contained in:
2026-08-07 15:40:15 +07:00
parent 58c6ec794d
commit 9e269d3b79
4 changed files with 184 additions and 21 deletions
+91 -6
View File
@@ -13900,6 +13900,12 @@ const App = () => {
selectedItemIdsRef.current = selectedItemIds; selectedItemIdsRef.current = selectedItemIds;
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
const [isPlaying, setIsPlaying] = useState(false); 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 nhy sau khi âm đã dng (user: VU vn animation
// khi hết âm).
const isPlayingRef = useRef(false);
isPlayingRef.current = isPlaying;
const [selectionStart, setSelectionStart] = useState(null); const [selectionStart, setSelectionStart] = useState(null);
const [selectionEnd, setSelectionEnd] = useState(null); const [selectionEnd, setSelectionEnd] = useState(null);
const [selectionFollowsTempo, setSelectionFollowsTempo] = useState(true); const [selectionFollowsTempo, setSelectionFollowsTempo] = useState(true);
@@ -14988,6 +14994,14 @@ const App = () => {
const velFactor = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8; const velFactor = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
const peak = Math.min(1.0, Math.max(0.15, velFactor)); const peak = Math.min(1.0, Math.max(0.15, velFactor));
midiVuActivityRef.current[trackId] = peak; midiVuActivityRef.current[trackId] = peak;
// Log ngun trigger (rate-limited 1/20 fire): xác đnh ai còn fire VU khi
// âm đã ngng (user: VU vn 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; window.triggerMidiVuActivity = triggerMidiVuActivity;
@@ -19349,6 +19363,9 @@ const App = () => {
); );
// Trigger VU meter flash when the note starts playing // Trigger VU meter flash when the note starts playing
setTimeout(() => { setTimeout(() => {
// Guard: stop setTimeout sót không đưc fire (VU nhy
// sau khi âm đã dng user bug 05:45).
if (!isPlayingRef.current) return;
if (window.triggerMidiVuActivity) { if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity(track.id, note.velocity || 0.8); window.triggerMidiVuActivity(track.id, note.velocity || 0.8);
} }
@@ -19480,6 +19497,20 @@ const App = () => {
subCh, subCh,
subTrack.synth_engine subTrack.synth_engine
); );
// Trigger VU meter flash đúng nhp (c subTrack VU
// section tab + parent track VU MAIN): MIDI section đi
// SF masterBus.input (không qua sub-node analyser)
// VU track ch nhy qua midiVuActivity. (B mt khi
// commit gp khôi phc fix 00:30/02:00.)
setTimeout(() => {
// Guard: stop setTimeout sót không đưc fire (VU
// nhy sau khi âm đã dng 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 { } else {
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000; const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
window.SonicSF.playNote( window.SonicSF.playNote(
@@ -19492,16 +19523,12 @@ const App = () => {
subCh, subCh,
subTrack.synth_engine subTrack.synth_engine
); );
}
// Trigger VU meter flash đúng nhp (ging 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 nhy theo section.
if (window.triggerMidiVuActivity) { if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity(subTrack.id, note.velocity || 0.8); window.triggerMidiVuActivity(subTrack.id, note.velocity || 0.8);
window.triggerMidiVuActivity(track.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; const v = Math.abs(data[i] - 128) / 128;
if (v > peak) peak = v; if (v > peak) peak = v;
} }
// Deadband: b noise nn (log 06:00 master peak 0.008 liên tc dù
// không âm master VU nhy nh mãi)
if (peak < 0.01) peak = 0;
setMasterVU(peak); setMasterVU(peak);
setMasterMeterPeak(prev => Math.max(prev * 0.97, peak)); setMasterMeterPeak(prev => Math.max(prev * 0.97, peak));
// Log master VU khi đang nhy (chn đoán 06:00 VU nhy ngoài vùng âm)
if (peak > 0.001) {
try {
console.log('[VU-master] peak=', peak.toFixed(3));
} catch (e) { }
}
} }
// 2. Track VU Meters // 2. Track VU Meters
@@ -24989,20 +25025,69 @@ STRICT CONSTRAINTS:
} }
} }
// MIDI THC: đc t SF output (âm post-FluidSynth) thay velocity
// trigger (GI fire lúc schedule click playhead re-schedule
// VU nhy 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 ti 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 bi triggerMidiVuActivity c khi play // midiVuActivityRef đưc set bi triggerMidiVuActivity c khi play
// MIDI item LN khi ARM + nhn phím MIDI keyboard (preview) nên VU // MIDI item LN khi ARM + nhn phím MIDI keyboard (preview) nên VU
// nhy trong c 2 trưng hp (không chn bi isPlaying). // nhy trong c 2 trưng hp (không chn bi isPlaying).
let midiPeak = isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0; let midiPeak = isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0;
// SF analyser đã tap CH dùng âm THC (velocity trigger = gi b)
if (_sfTapped) midiPeak = 0;
if (midiPeak > 0) { if (midiPeak > 0) {
midiVuActivityRef.current[trackId] = midiPeak * 0.90; // Decay nhanh hơn (0.72/frame tt sau ~0.35s) hết note VU
// dng nhanh, không "vn din animation" khi không còn âm.
midiVuActivityRef.current[trackId] = midiPeak * 0.72;
if (midiVuActivityRef.current[trackId] < 0.01) { if (midiVuActivityRef.current[trackId] < 0.01) {
midiVuActivityRef.current[trackId] = 0; midiVuActivityRef.current[trackId] = 0;
} }
} }
// Gp âm MIDI THC vào audioPeak
if (sfAudioPeak > audioPeak) audioPeak = sfAudioPeak;
// Deadband: b noise nn analyser (log 06:05 audio=0.008 dù không
// âm VU hin th đon nh "v nhưng không animation") hết âm
// VU tt NGAY LP TC (user yêu cu).
if (audioPeak < 0.01) audioPeak = 0;
const peak = Math.max(audioPeak, midiPeak); const peak = Math.max(audioPeak, midiPeak);
const db = peak > 0 ? 20 * Math.log10(peak) : -60; const db = peak > 0 ? 20 * Math.log10(peak) : -60;
// Log chn đoán VU: CH in khi peak > 0 (VU đang nhy tht) xác đnh
// ngun nào còn nhy 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 (peak > 0.001) {
if (key.endsWith('_mixer')) { if (key.endsWith('_mixer')) {
drawMixerVuMeter(canvas, peak); drawMixerVuMeter(canvas, peak);
+39 -11
View File
@@ -609,7 +609,11 @@ var mt2=activeTracksRef.current||tracks;var qTrk=null;for(var qi=0;qi<mt2.length
const cachedSf=(instrumentSelectorData?.soundfonts||[]).find(s=>s.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 cachedSf=(instrumentSelectorData?.soundfonts||[]).find(s=>s.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[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 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[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) 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 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 ở // 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 → // 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). // 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[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[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[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) 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 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<noteEndSec){const durationMs=noteDurSec*1000;const program=track._isSectionClone?track.instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:0;if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,trkCh,track.synth_engine);// Trigger VU meter flash when the note starts playing const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime<noteEndSec){const durationMs=noteDurSec*1000;const program=track._isSectionClone?track.instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:0;if(offsetTime<noteStartSec){const delay=noteStartSec-offsetTime;const startTime=context.currentTime+delay;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durationMs,startTime,program,gainNode,trkCh,track.synth_engine);// Trigger VU meter flash when the note starts playing
setTimeout(()=>{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(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) 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 // 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 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(activeStart<activeEnd&&offsetTime<activeEnd){const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(subNode);if(offsetTime<activeStart){const delay=activeStart-offsetTime;const playOffset=activeStart-clipStartMain;const playDuration=activeEnd-activeStart;source.start(context.currentTime+delay,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}else{const playOffset=offsetTime-clipStartMain;const playDuration=activeEnd-offsetTime;source.start(context.currentTime,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}}});// 2. Play MIDI items in subTrack const activeStart=Math.max(clipStartMain,secStart);const activeEnd=Math.min(clipEndMain,secEnd);if(activeStart<activeEnd&&offsetTime<activeEnd){const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(subNode);if(offsetTime<activeStart){const delay=activeStart-offsetTime;const playOffset=activeStart-clipStartMain;const playDuration=activeEnd-activeStart;source.start(context.currentTime+delay,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}else{const playOffset=offsetTime-clipStartMain;const playDuration=activeEnd-offsetTime;source.start(context.currentTime,playOffset*(clip.speed||1.0),playDuration);activeSourcesRef.current.push(source);}}});// 2. Play MIDI items in subTrack
const subMidiItems=subTrack.midiItems||[];if(window.SonicSF&&subMidiItems.length>0){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 const subMidiItems=subTrack.midiItems||[];if(window.SonicSF&&subMidiItems.length>0){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&&noteStartMain<secEnd&&offsetTime<noteEndMain){const notePlayEndMain=Math.min(noteEndMain,secEnd);const program=subTrack.instrumentProgram;var subCh=assignTrackMidiChannel(subTrack,secTracks);if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,playDurMs,startTime,program,subNode,subCh,subTrack.synth_engine);}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);}// Trigger VU meter flash đúng nhịp (giống main-track midi 19066) if(noteStartMain>=secStart&&noteStartMain<secEnd&&offsetTime<noteEndMain){const notePlayEndMain=Math.min(noteEndMain,secEnd);const program=subTrack.instrumentProgram;var subCh=assignTrackMidiChannel(subTrack,secTracks);if(offsetTime<noteStartMain){const delay=noteStartMain-offsetTime;const startTime=context.currentTime+delay;const playDurMs=(notePlayEndMain-noteStartMain)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,playDurMs,startTime,program,subNode,subCh,subTrack.synth_engine);// Trigger VU meter flash đúng nhịp (cả subTrack — VU
// Cả subTrack (VU section tab) + parent track (VU MAIN section // section tab + parent track VU MAIN): MIDI section đi
// item đang play) — midiVuActivity keyed theo track id → trigger // SF → masterBus.input (không qua sub-node analyser) →
// cả 2 để VU main nhảy theo section. // VU track chỉ nhảy qua midiVuActivity. (Bị mất khi
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) // 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(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 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 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 → // 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/ 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. // 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 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;i<data.length;i++){const v=Math.abs(data[i]-128)/128;if(v>peak)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;i<data.length;i++){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
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 <parent>_sub_<trackId> — 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 <parent>_sub_<trackId> —
// lookup plain key trả undefined → VU không animation. Fallback scan _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) 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;i<data.length;i++){const v=Math.abs(data[i]-128)/128;if(v>audioPeak)audioPeak=v;}}// Section item trên MAIN track: VU theo sub-nodes của track này (key 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;i<data.length;i++){const v=Math.abs(data[i]-128)/128;if(v>audioPeak)audioPeak=v;}}// Section item trên MAIN track: VU theo sub-nodes của track này (key
// <trackId>_sub_*) — âm section đi thẳng masterBus.input (không qua // <trackId>_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. // 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;i<d2.length;i++){const v=Math.abs(d2[i]-128)/128;if(v>audioPeak)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;i<d2.length;i++){const v=Math.abs(d2[i]-128)/128;if(v>audioPeak)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;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 // 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). // 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. // 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 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 try{stopAllPlayback();}catch(e){}setSubTabs([]);setSessionTabs([]);setActiveTab('main');// Reset instrument/channel/route context đã load
+1 -1
View File
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script> <script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script> <script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script> <script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608070535" defer></script> <script src="/static/js/app.precompiled.js?v=202608070610" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016"> <link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style> <style>
:root { :root {
+50
View File
@@ -2533,3 +2533,53 @@
(2) Input End Bar (dock 26436 + statusbar 27846): `setSelectionEnd(t + spb)``setSelectionEnd(t)` (selection tới vạch bar b — khớp hiển thị); `setNumberBar(b beginBar)`. (2) Input End Bar (dock 26436 + statusbar 27846): `setSelectionEnd(t + spb)``setSelectionEnd(t)` (selection tới vạch bar b — khớp hiển thị); `setNumberBar(b beginBar)`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070535), `wiki.md`. Rebuild precompiled (build PASS). - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070535), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → quét 8 bars từ 0 → BARS hiển thị "0 - 8 # 8"; nhập End Bar = 8 → selection tới vạch 8 (8 bars). - **Ghi chú/Test:** hard refresh → quét 8 bars từ 0 → BARS hiển thị "0 - 8 # 8"; nhập End Bar = 8 → selection tới vạch 8 (8 bars).
### [2026-08-07 05:40] Task: MAIN SESSION — play section item có âm nhưng track VU không hiển thị
- **Báo cáo user:** play section item ở MAIN — âm phát ra (track) nhưng VU meter không nhảy.
- **Phân tích:** MIDI section (FluidSynth SF) đi thẳng masterBus.input (midiAudible main rỗng → setOutputDestination(null)) — KHÔNG qua node track/sub-node analyser → audioPeak = 0 — VU track chỉ nhảy qua `triggerMidiVuActivity` (midiVuActivityRef). Trigger cũ (19511-19518) VẪN CÒN (fire lúc schedule — không setTimeout cho note delayed). Chưa rõ vì sao VU đứng yên — cần log.
- **FIX (app.jsx):** (1) bổ sung trigger VU cho nhánh note DELAYED (setTimeout khớp nhịp — subTrack.id + track.id) — nhánh instant đã có; (2) thêm log chẩn đoán VU tick: `[VU] <trackId> audio=... midi=... vNode=... analyser=... canvas=...` (rate-limited ~1.3 lần/giây).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070540), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (section item MIDI) → dán log `[VU]` — xác định: midi= có >0 không (trigger fire?); vNode/analyser/canvas có tồn tại không.
### [2026-08-07 05:45] Task: Track VU vẫn diễn animation khi không còn âm (MAIN/SECTION-TAB)
- **Báo cáo user:** track VU meter không diễn đúng — hết âm vẫn nhảy animation.
- **Phân tích:** VU track (MIDI section) chỉ nhảy qua `midiVuActivityRef` (trigger fire lúc schedule). Trigger cũ fire NGAY lúc schedule (kể cả note delay xa — VU nhảy trước âm / note bị cắt vẫn flash) + TRÙNG với trigger mới (05:40) → VU nhảy sai nhịp/kéo dài. Decay 0.9/frame (~0.75s) — sau note cuối VU còn diễn ~0.75s.
- **FIX (app.jsx):**
(1) XÓA trigger cũ fire-ngay (sau block — chạy cho mọi note cả delayed) — chỉ giữ trigger mới: nhánh delayed (setTimeout delay*1000 — khớp nhịp) + nhánh instant.
(2) Decay nhanh: `midiPeak * 0.90``0.72` (tắt sau ~0.35s — hết note → VU dừng nhanh).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070545), `wiki.md`. Rebuild precompiled (build PASS — triggerMidiVuActivity còn 8 chỗ).
- **Ghi chú/Test:** hard refresh → play MAIN (section MIDI) → hết notes → VU dừng trong ~0.35s; nếu VU VẪN nhảy khi hết âm → dán log `[VU]` (midi= giá trị bao nhiêu khi không còn âm).
### [2026-08-07 05:50] Task: VU vẫn animation khi hết âm — log nguồn trigger [VUTrig]
- **Log user (0545):** âm ngừng nhưng `[VU] 1 audio=0.000 midi=0.058``midi=0.661` — midiPeak TĂNG LẠI → triggerMidiVuActivity vẫn fire định kỳ sau khi hết âm.
- **FIX (app.jsx):** thêm log `[VUTrig] <trackId> vel=... peak=... play=<isPlaying> tab=<activeTab>` vào `triggerMidiVuActivity` (rate-limited 1/20 fire) — xác định AI còn fire (setTimeout sót từ main-midi delayed? re-schedule? keyboard preview?).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070550), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (hết âm — VU vẫn nhảy) → dán log `[VUTrig]` — cho biết track nào + lúc nào (play=?) vẫn fire.
### [2026-08-07 05:55] Task: CHỐT — VU vẫn nhảy khi hết âm = setTimeout VU trigger sót sau stop
- **Log user (0550):** sau STOP (`[Play] click isPlaying=true` → stop) — `[VUTrig] 1 vel=0.83/0.78/0.95 ... play= false`**trigger VU vẫn fire sau khi play đã dừng**.
- **Nguyên nhân:** setTimeout VU trigger của note DELAYED (main-midi 19351 + section delayed — khôi phục 05:40) KHÔNG bị hủy khi stop → fire MUỘN sau stop → midiVuActivity set lại → VU nhảy (decay 0.72 vẫn chưa kịp về 0).
- **FIX (app.jsx):** thêm `isPlayingRef` (App — sync mỗi render) + guard `if (!isPlayingRef.current) return;` ở ĐẦU 2 setTimeout VU trigger (main-midi + section delayed) — stop → isPlaying=false → setTimeout sót không fire → VU dừng ngay khi hết âm/stop.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070555), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (MIDI) → STOP giữa chừng → log `[VUTrig]` KHÔNG còn fire sau stop (chỉ fire lúc play= true); hết bài tự nhiên → VU dừng trong ~0.35s.
### [2026-08-07 06:00] Task: VU vẫn nhảy khi playhead ngoài vùng âm — log nguồn peak>0
- **Báo cáo user:** VU vẫn diễn animation khi playhead di chuyển ra khỏi vùng có âm. Log 0555: track VU `midi=0.000` liên tục (đã dừng đúng) — NHƯNG user vẫn thấy nhảy → nguồn KHÁC (master VU? key khác? peak giữa 2 log rate-limited?).
- **FIX (app.jsx):** đổi log chẩn đoán — (1) `[VU] <trackId> audio=... midi=... key=<key>` CHỈ in khi peak > 0.001 (VU nhảy thật — không rate-limit); (2) thêm `[VU-master] peak=...` (master VU khi nhảy) — xác định VU nào còn nhảy ngoài vùng âm.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070600), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN → playhead ra khỏi vùng âm → nếu VU vẫn nhảy → dán log `[VU]`/`[VU-master]` xuất hiện lúc đó (cho biết track/key/nguồn audio hay midi).
### [2026-08-07 06:05] Task: SECTION-TAB — VU nhảy theo velocity trigger (giả) — đọc âm THỰC từ SF output
- **Log user (0600):** SECTION-TAB — click playhead tới vị trí không âm → VU vẫn nhảy: `[VU] 1 midi= 0.98 → 0.85 → ...` (tăng lại theo chu kỳ — trigger fire lúc schedule/re-schedule) nhưng `[VU-master] peak= 0.008` (âm thực gần im) — **VU GIẢ (velocity trigger), không theo âm thực**. Master VU nhảy nhẹ 0.008 liên tục (noise nền).
- **FIX (app.jsx VU tick):**
(1) Tap analyser trên SF output (`SonicSF.getOutputNode()` — âm MIDI THỰC post-FluidSynth) — `sfAudioPeak` gộp vào audioPeak — VU MIDI theo âm THỰC.
(2) Khi SF analyser đã tap → `midiPeak = 0` (bỏ velocity trigger giả) — hết note → SF im → VU 0 ngay (không nhảy giả sau click playhead/re-schedule).
(3) Master VU deadband: `peak < 0.01 → 0` — hết noise nền 0.008.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070605), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → SECTION-TAB play → click playhead tới vị trí không âm → VU KHÔNG nhảy (SF im → 0); vị trí có âm → VU nhảy theo âm thực; master VU không nhảy noise.
### [2026-08-07 06:10] Task: VU vẽ đoạn ngắn (0.008) khi hết âm — deadband track analyser
- **Log user (0605):** `[VU] 1 audio= 0.008 midi= 0.000` — VU vẽ đoạn nhỏ (~-42dB) dù không âm — noise nền trên track analyser (như master 0.008 — đã deadband 06:05).
- **FIX (app.jsx):** track audioPeak deadband — `if (audioPeak < 0.01) audioPeak = 0;` (sau khi gộp sfAudioPeak) — hết âm → VU tắt NGAY (peak 0 → db -60 → vẽ 0).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070610), `wiki.md`. Rebuild precompiled (build PASS — Deadband ×2: master + track).
- **Ghi chú/Test:** hard refresh → play → playhead ra khỏi vùng âm → VU TẮT NGAY (không còn đoạn 0.008); log `[VU]` không còn audio=0.008.