fix: MIDI preview dừng file cũ khi click file midi khác

This commit is contained in:
2026-08-02 17:18:08 +07:00
parent 1de6bd6b57
commit 7000529d8d
4 changed files with 10 additions and 2 deletions
+3
View File
@@ -9361,6 +9361,9 @@ const MediaExplorerPanel = ({ height }) => {
}
if (rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = null;
if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') {
try { window.SonicSF.stopAll(); } catch (e) {}
}
setIsPlaying(false);
setIsPaused(false);
}, []);
+1 -1
View File
@@ -237,7 +237,7 @@ const MEDIA_LIBRARY_SAMPLES=[{name:"MIDI_Loop_01.mid",events:95,lengthQn:16,time
setComputerMode('server');if(computerRoots)return;setComputerRoots(null);try{const resp=await fetch(`${API_BASE_URL}/api/v1/media/computer`);if(!resp.ok)throw new Error('HTTP '+resp.status);const data=await resp.json();const roots=data.roots||[];setComputerRoots(roots.length?roots:[{path:'/',name:'Root (/)',is_dir:true}]);if(!roots.length)window.showToast&&window.showToast('Không tìm thấy ổ đĩa nào','warning');}catch(e){setComputerRoots([{path:'/',name:'Root (/)',is_dir:true}]);window.showToast&&window.showToast('Không thể truy cập My Computer: '+e.message,'error');}};const browseClientDir=async entry=>{const handle=entry&&entry.handle;if(!handle||!handle.entries)return;const dirs=[];const files=[];const parentPath=entry.path;try{for await(const[name,h]of handle.entries()){if(name.startsWith('.'))continue;if(h.kind==='directory'){dirs.push({name,path:parentPath+'/'+name,is_dir:true,handle:h});}else{const ext=(name.split('.').pop()||'').toLowerCase();const kind=ext==='mid'||ext==='midi'?'midi':['wav','mp3','ogg','flac','aiff','aif','m4a','aac','opus'].includes(ext)?'audio':'other';let size=0;try{const f=await h.getFile();size=f.size;}catch(e2){}files.push({name,path:parentPath+'/'+name,is_dir:false,size_mb:size?+(size/1048576).toFixed(2):0,ext:'.'+ext,kind,handle:h});}}dirs.sort((a,b)=>a.name.localeCompare(b.name));files.sort((a,b)=>a.name.localeCompare(b.name));setComputerPath(parentPath);setComputerFiles(files);setSelected(null);setCurrentTime(0);stopMediaPlayback();setComputerTree(prev=>({...prev,[parentPath]:{handle,parent:entry.parent||null,dirs,expanded:true}}));}catch(e){window.showToast&&window.showToast('Không thể đọc thư mục: '+e.message,'error');}};const browseComputerDir=async entry=>{if(!entry)return;if(computerMode==='client'||entry.handle){if(entry.handle&&entry.handle.entries){const parentInfo=computerTree[entry.path];await browseClientDir({...entry,parent:parentInfo?parentInfo.parent:null});return;}}const path=entry.path||entry;if(!path)return;try{const resp=await fetch(`${API_BASE_URL}/api/v1/media/browse?path=${encodeURIComponent(path)}`);if(!resp.ok)throw new Error('HTTP '+resp.status);const data=await resp.json();setComputerPath(data.path);setComputerFiles(data.files||[]);setSelected(null);setCurrentTime(0);stopMediaPlayback();setComputerTree(prev=>({...prev,[path]:{dirs:data.dirs||[],expanded:true}}));}catch(e){window.showToast&&window.showToast('Không thể mở thư mục: '+e.message,'error');}};const toggleComputerDir=async entry=>{if(!entry)return;const path=entry.path||entry;const node=computerTree[path];if(node&&node.expanded){setComputerTree(prev=>({...prev,[path]:{...prev[path],expanded:false}}));}else{await browseComputerDir(entry);}};const goComputerParent=()=>{if(!computerPath)return;if(computerMode==='client'||clientRoot){const node=computerTree[computerPath];const parentHandle=node&&node.parent;if(parentHandle){const parentEntry=computerRoots&&computerRoots[0]&&parentHandle===clientRoot?computerRoots[0]:{name:parentHandle.name,path:computerPath.split('/').slice(0,-1).join('/')||'root',is_dir:true,handle:parentHandle};browseComputerDir({...parentEntry,parent:parentHandle===clientRoot?null:computerTree[parentEntry.path]?computerTree[parentEntry.path].parent:null});}return;}const isUnix=computerPath.startsWith('/');const parts=computerPath.split(/[\\/]/).filter(Boolean);parts.pop();if(isUnix){browseComputerDir(parts.length?'/'+parts.join('/'):'/');}else{// Windows: quay về root ổ đĩa nếu đã lên tới đỉnh
browseComputerDir(parts.length?parts.join('\\'):computerPath.split(/[\\/]/)[0]+'\\');}};const readLocalFileBuffer=async f=>{if(f&&f.handle&&typeof f.handle.getFile==='function'){const file=await f.handle.getFile();return await file.arrayBuffer();}const url=filePreviewUrl(f);if(!url)return null;const resp=await fetch(url);return await resp.arrayBuffer();};const filePreviewUrl=f=>{if(f&&f.path&&!(f.handle&&f.handle.getFile))return`${API_BASE_URL}/api/v1/media/file?path=${encodeURIComponent(f.path)}`;const fid=f&&(f.file_id||f.fileId);return fid?`${API_BASE_URL}/api/v1/audio/download/${fid}`:null;};const toggleSynthDropdown=()=>{if(synthOpen){setSynthOpen(false);return;}setSynthOpen(true);if(synthListRef.current)return;setSynthLoading(true);(window.SonicAPI&&window.SonicAPI.listPlugins?window.SonicAPI.listPlugins():Promise.resolve({soundfonts:[]})).then(async data=>{const sfonts=data&&data.soundfonts||[];if(!sfonts.length){setSynthList([]);setSynthLoading(false);return;}const results=await Promise.all(sfonts.map(sf=>{const baseId=String(sf.id||'').replace('sf_','');return(window.SonicAPI.listSoundfontInstruments?window.SonicAPI.listSoundfontInstruments(baseId):Promise.resolve({presets:[]})).then(r=>({sf,presets:r&&r.presets||[]})).catch(()=>({sf,presets:[]}));}));setSynthList(results);setSynthLoading(false);}).catch(()=>{setSynthList([]);setSynthLoading(false);});};const selectSynthInst=inst=>{setSynthInst(inst);setSynthOpen(false);localStorage.setItem('studio_media_explorer_synth',JSON.stringify(inst));};const playMidiPreview=async(f,token)=>{// Play real MIDI file through selected synth instrument (SonicSF)
if(!f||!window.SonicSF)return;try{const buf=await readLocalFileBuffer(f);if(!buf)return;if(selectTokenRef.current!==(token||selectTokenRef.current))return;const midiResult=(typeof parseMidiFile==='function'?parseMidiFile:window.parseMidiFile)(buf);if(!midiResult||!midiResult.length)return;const ctx=getAudioContext();const bpmVal=parseInt(synthInst&&synthInst.bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=ctx.currentTime+0.05;const program=synthInst?synthInst.program:undefined;const sfId=synthInst?synthInst.sfId:undefined;const bank=synthInst?synthInst.bank:0;if(synthInst&&window.SonicSF.selectInstrument){try{await window.SonicSF.selectInstrument(0,bank,program,sfId);}catch(e2){}}const prog=synthInst&&synthInst.program!==undefined?synthInst.program:undefined;const eng=synthInst?{soundfont_id:sfId,soundfont_bank:bank,soundfont_program:prog!==undefined?prog:0}:undefined;midiResult.forEach(track=>{(track.notes||[]).forEach(note=>{const startSec=(note.start_beat||0)*secondsPerBeat;const durMs=Math.max(80,(note.duration_beats||1)*secondsPerBeat*1000);window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,startWallTime+startSec,prog,null,0,eng);});});// Keep a fake clock so canvas playhead animates; loop uses playStateRef
playStateRef.current={source:null,ctx,startedAt:startWallTime,fakeStart:startWallTime,midiTotal:midiResult[0].duration||4};setMidiNotes(midiResult[0].notes||[]);setMidiTotal(midiResult[0].duration||4);setIsPlaying(true);setIsPaused(false);startCanvasClock();}catch(e){console.error('MIDI preview failed',e);}};const stopMediaPlayback=React.useCallback(()=>{if(playStateRef.current){try{if(playStateRef.current.source)playStateRef.current.source.stop();}catch(e){}try{if(playStateRef.current.source)playStateRef.current.source.disconnect();}catch(e){}playStateRef.current=null;}if(rafRef.current)cancelAnimationFrame(rafRef.current);rafRef.current=null;setIsPlaying(false);setIsPaused(false);},[]);const playSelected=async(f,token)=>{if(!f)return;stopMediaPlayback();if(isMidiFile(f)){if(f.handle||f.path||f.file_id||f.fileId){// Real MIDI file: play through selected synth instrument
playStateRef.current={source:null,ctx,startedAt:startWallTime,fakeStart:startWallTime,midiTotal:midiResult[0].duration||4};setMidiNotes(midiResult[0].notes||[]);setMidiTotal(midiResult[0].duration||4);setIsPlaying(true);setIsPaused(false);startCanvasClock();}catch(e){console.error('MIDI preview failed',e);}};const stopMediaPlayback=React.useCallback(()=>{if(playStateRef.current){try{if(playStateRef.current.source)playStateRef.current.source.stop();}catch(e){}try{if(playStateRef.current.source)playStateRef.current.source.disconnect();}catch(e){}playStateRef.current=null;}if(rafRef.current)cancelAnimationFrame(rafRef.current);rafRef.current=null;if(window.SonicSF&&typeof window.SonicSF.stopAll==='function'){try{window.SonicSF.stopAll();}catch(e){}}setIsPlaying(false);setIsPaused(false);},[]);const playSelected=async(f,token)=>{if(!f)return;stopMediaPlayback();if(isMidiFile(f)){if(f.handle||f.path||f.file_id||f.fileId){// Real MIDI file: play through selected synth instrument
await playMidiPreview(f,token);return;}playStateRef.current={source:null,ctx:null,fakeStart:performance.now()/1000};setIsPlaying(true);setIsPaused(false);startCanvasClock();return;}const fid=f.file_id||f.fileId;const buf=await readLocalFileBuffer(f);if(selectTokenRef.current!==(token||selectTokenRef.current))return;if(!buf)return;try{const ctx=getAudioContext();const decoded=await ctx.decodeAudioData(buf);if(selectTokenRef.current!==(token||selectTokenRef.current))return;setAudioBuffer(decoded);const src=ctx.createBufferSource();src.buffer=decoded;src.loop=isLooping;src.playbackRate.value=rate;const gain=ctx.createGain();const linear=volumeDb<=-50?0:Math.pow(10,volumeDb/20);gain.gain.value=linear;src.connect(gain);gain.connect(ctx.destination);src.start();const startedAt=ctx.currentTime;playStateRef.current={source:src,ctx,startedAt};setIsPlaying(true);setIsPaused(false);startCanvasClock();}catch(e){console.error('Preview failed',e);}};const togglePause=()=>{const st=playStateRef.current;if(!st)return;if(isPaused){if(st.ctx)st.ctx.resume();setIsPaused(false);}else{if(st.ctx)st.ctx.suspend();setIsPaused(true);}};const startCanvasClock=()=>{if(rafRef.current)cancelAnimationFrame(rafRef.current);const tick=()=>{rafRef.current=requestAnimationFrame(tick);let t=currentTime;const st=playStateRef.current;if(st&&!isPaused){t=st.ctx?st.ctx.currentTime-st.startedAt:performance.now()/1000-st.fakeStart;}setCurrentTime(t);drawCanvas(t);};tick();};const drawCanvas=t=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const w=canvas.clientWidth,h=canvas.clientHeight;if(!w||!h)return;canvas.width=w*2;canvas.height=h*2;ctx.scale(2,2);ctx.clearRect(0,0,w,h);ctx.fillStyle='#181818';ctx.fillRect(0,0,w,h);const f=selected;if(!f){ctx.fillStyle='#555';ctx.font='12px JetBrains Mono, monospace';ctx.fillText('No file selected',10,h/2);return;}const dur=fileDuration(f);if(isMidiFile(f)){ctx.strokeStyle='#333';for(let y=0;y<h-14;y+=10){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(w,y);ctx.stroke();}ctx.strokeStyle='#444';const totalBeats=midiNotes&&midiNotes.length?Math.max(midiTotal*(f.bpm||120)/60,4):f.lengthQn||16;const beats=totalBeats;for(let b=0;b<=beats;b+=4){const x=b/beats*w;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h-14);ctx.stroke();}ctx.fillStyle='#9ca3af';if(midiNotes&&midiNotes.length){// Real piano-roll: rows = pitches (48..84), columns = beats
const pitchMin=48,pitchMax=84;const pitchRange=Math.max(1,pitchMax-pitchMin);midiNotes.forEach(n=>{const x=n.start_beat/beats*w;const nw=Math.max(3,n.duration_beats/beats*w);const y=h-14-8-(Math.min(pitchMax,Math.max(pitchMin,n.pitch))-pitchMin)/pitchRange*(h-30);ctx.fillRect(x,y,nw,5);});}else{const events=f.events||76;for(let i=0;i<events;i++){const nx=(i*17+76)%95/100*(w-20);const ny=(i*13+76)%(h-30)+4;ctx.fillRect(nx,ny,Math.max(8,(i%5+1)*10),3);}}}else{const pk=peaks&&peaks.length>0?peaks:null;if(pk){const mid=h/2;ctx.fillStyle='#22c55e';for(let i=0;i<pk.length;i++){const x=i/pk.length*w;const ph=Math.max(2,pk[i]*(h/2-4));ctx.fillRect(x,mid-ph,Math.max(1,w/pk.length),ph*2);}}else{ctx.fillStyle='#666';ctx.font='11px monospace';ctx.fillText('Waveform unavailable',10,h/2);}}// ruler
ctx.fillStyle='#111';ctx.fillRect(0,h-14,w,14);ctx.fillStyle='#888';ctx.font='9px JetBrains Mono, monospace';const totalBeats=Math.max(4,Math.ceil(dur*(f.bpm||120)/60)||16);for(let b=0;b<=totalBeats;b+=4){const x=b/totalBeats*w;ctx.fillText(String(Math.floor(b/4)),x+2,h-3);}// playhead