FIX: sửa lỗi hiển thị animation của VU meter
This commit is contained in:
@@ -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[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<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(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(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
|
||||
if(noteStartMain>=secStart&¬eStartMain<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)
|
||||
// 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);}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2)
|
||||
if(noteStartMain>=secStart&¬eStartMain<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
|
||||
// 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(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;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> —
|
||||
// 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;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
|
||||
// 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
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user