diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index d8aea87..b8cc705 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -13500,9 +13500,9 @@ const App = () => { var sfId = se.soundfont_id; if (sfId) { var allTrks = activeTracksRef.current || activeTracks; - var trkIdx = 0; - for (var ti = 0; ti < allTrks.length; ti++) { if (allTrks[ti].id === trackId) { trkIdx = ti; break; } } - var seCh = (se.soundfont_bank === 128 ? 9 : (trkIdx % 16)); + // Use the track's dedicated channel (same as every playback path) so the + // preload matches where piano-roll notes will actually sound. + var seCh = assignTrackMidiChannel(track, allTrks); window.SonicSF.selectInstrument(seCh, se.soundfont_bank || 0, se.soundfont_program || 0, sfId); } } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index c037ef0..781ad59 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -382,7 +382,9 @@ handleSaveSectionTabRef.current(curTab);showToast('Đã lưu Section','success') const st=subTabsRef.current.find(s=>s.id===curTab);if(st){if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);showToast('Đã lưu Piano Roll','success');}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);showToast('Đã lưu Section Tab','success');}else if(st.buffer){const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}showToast('Đã lưu Audio Tab','success');}}}return;}if(!ctrl&&!alt&&e.key==='s'){e.preventDefault();handleSplitTrackRef.current(selectedTrackIdRef.current);return;}if(e.key==='F7'){e.preventDefault();e.stopPropagation();window.__toggleMixerRef();return;}if(e.key==='F6'){e.preventDefault();e.stopPropagation();window.__toggleMediaExplorerRef();return;}};window.addEventListener('keydown',handler,{capture:true});return()=>window.removeEventListener('keydown',handler,{capture:true});},[]);// ── Temp Tab: draw isolated waveform ── useEffect(()=>{if(!tempTabActive||!tempTabBuffer||!tempTabCanvasRef.current)return;const canvas=tempTabCanvasRef.current;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const rect=canvas.getBoundingClientRect();canvas.width=rect.width*dpr;canvas.height=rect.height*dpr;ctx.scale(dpr,dpr);const w=rect.width;const h=rect.height;ctx.fillStyle='#181818';ctx.fillRect(0,0,w,h);const data=tempTabBuffer.getChannelData(0);const sr=tempTabBuffer.sampleRate;const totalSamples=data.length;if(totalSamples===0)return;ctx.strokeStyle='#6ee7b7';ctx.lineWidth=1;for(let px=0;pxmaxVal)maxVal=abs;}const mid=h/2;const peakHeight=maxVal*(h*0.4);ctx.beginPath();ctx.moveTo(px,mid-peakHeight);ctx.lineTo(px,mid+peakHeight);ctx.stroke();}},[tempTabActive,tempTabBuffer]);// ── Sub Tab: open as new tab instead of modal (LOOP_EDITOR_2.md §1) ── const openTempTab=()=>{const useLocal=selectionMode==='local'&&localSelectionTrackId;const trackId=useLocal?localSelectionTrackId:selectedTrackId;const t=tracks.find(x=>x.id===trackId);if(!t||!t.buffer){showToast('Vui lòng chọn track có dữ liệu âm thanh.','warning');return;}if(selLeft===null||selRight===null||selRight<=selLeft){showToast('Vui lòng chọn một khoảng thời gian trên sóng âm.','warning');return;}// Check if a subtab for this track+range already exists -const existing=subTabs.find(s=>s.trackId===trackId&&s.startTime===selLeft&&s.endTime===selRight);if(existing){setActiveTab(existing.id);showToast(`Sub Tab already open.`,'info');return;}const sr=t.buffer.sampleRate;const trackStart=t.startTime||0;const relSelLeft=Math.max(0,selLeft-trackStart);const relSelRight=Math.max(0,selRight-trackStart);const startSample=Math.max(0,Math.floor(relSelLeft*sr));const endSample=Math.min(t.buffer.length,Math.floor(relSelRight*sr));const len=endSample-startSample;if(len<100){showToast('Khoảng chọn quá ngắn.','warning');return;}const ctx=getAudioContext();const numChannels=t.buffer.numberOfChannels||1;const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,startTime:selLeft,endTime:selRight,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditClipInSubTab=(trackId,clipId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default_'+track.id,buffer:track.buffer,startTime:track.startTime||0,name:track.name}]:[];const clip=clips.find(c=>c.id===clipId||clipId==='default'&&c.id==='default_'+trackId);if(!clip||!clip.buffer)return;const resolvedClipId=clip.id==='default'?'default_'+trackId:clip.id;const existing=subTabs.find(s=>s.clipId===resolvedClipId&&s.trackId===trackId);if(existing){setActiveTab(existing.id);showToast(`Sub Tab for "${clip.name}" already open.`,'info');return;}const sr=clip.buffer.sampleRate;const len=clip.buffer.length;const numChannels=clip.buffer.numberOfChannels||1;const ctx=getAudioContext();const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,clipId:resolvedClipId,startTime:clip.startTime,endTime:clip.startTime+clip.buffer.duration,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditMidiInTab=(trackId,midiItemId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const midiItem=(track.midiItems||[]).find(m=>m.id===midiItemId);if(!midiItem)return;const existing=subTabs.find(s=>s.type==='PIANO_ROLL'&&s.target_id===midiItemId);if(existing){setActiveTab(existing.id);showToast('Piano Roll cho nốt MIDI đã được mở.','info');return;}const tabId='midi_'+Date.now();const tabLabel=`Piano Roll: ${midiItem.name||'MIDI'}`;const ctx=getAudioContext();const silentBuffer=ctx.createBuffer(1,128,ctx.sampleRate);const newTab={id:tabId,label:tabLabel,type:'PIANO_ROLL',trackId:trackId,target_id:midiItemId,parent_tab_id:activeTab==='main'?null:activeTab,notes:midiItem.notes||[],duration:midiItem.duration||4,buffer:silentBuffer,instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:track.synth_engine?track.synth_engine.soundfont_program:undefined,instrumentName:track.instrumentName,instrumentId:track.instrumentId,synth_engine:track.synth_engine,currentTime:0,isPlaying:false,isLooping:true,selectionStart:null,selectionEnd:null,viewport_start_bar:0.0,viewport_bar_width:8.0,scroll_y_pitch:60,snap_resolution:snapValue||"1/16",note_selection:[]};setSubTabs(prev=>[...prev,newTab]);setActiveTab(tabId);if(track.synth_engine&&window.SonicSF&&window.SonicSF.selectInstrument){var se=track.synth_engine;var sfId=se.soundfont_id;if(sfId){var allTrks=activeTracksRef.current||activeTracks;var trkIdx=0;for(var ti=0;ti{setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,notes:notes,isDirty:true}:s));};const handleSaveMidiNotes=(tabId,trackId,midiItemId,updatedNotes)=>{const pianoRollTab=subTabs.find(s=>s.id===tabId);const parentTabId=pianoRollTab?pianoRollTab.parent_tab_id:null;if(parentTabId&&parentTabId.startsWith('session_')){setSessionTabs(prev=>prev.map(s=>{if(s.id!==parentTabId)return s;return{...s,tracks:s.tracks.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};})};}));}else{setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};}));}setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,isDirty:false}:s));showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!','success');};const handleSaveSectionTab=tabId=>{const tab=sessionTabs.find(s=>s.id===tabId);if(!tab)return;const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const secondsPerBar=secondsPerBeat*4;const contentTracks=tab.tracks?tab.tracks.filter(tr=>tr.clips?.length>0||tr.midiItems?.length>0):[];let maxEndTime=0;(contentTracks||[]).forEach(tr=>{(tr.clips||[]).forEach(c=>{const end=(c.startTime||0)+(c.buffer?c.buffer.duration/(c.speed||1.0):4);if(end>maxEndTime)maxEndTime=end;});(tr.midiItems||[]).forEach(m=>{const end=(m.startTime||0)+(m.duration||4);if(end>maxEndTime)maxEndTime=end;});});const durationSec=Math.max(maxEndTime,4*secondsPerBar);setTracks(prev=>prev.map(t=>{if(!t.sections||t.sections.length===0)return t;return{...t,sections:t.sections.map(s=>{if(s.sectionId!==tab.sectionId&&s.id!==tab.sectionId)return s;return{...s,name:tab.name,duration:durationSec,tracks:contentTracks};})};}));// Clear isDirty flag +const existing=subTabs.find(s=>s.trackId===trackId&&s.startTime===selLeft&&s.endTime===selRight);if(existing){setActiveTab(existing.id);showToast(`Sub Tab already open.`,'info');return;}const sr=t.buffer.sampleRate;const trackStart=t.startTime||0;const relSelLeft=Math.max(0,selLeft-trackStart);const relSelRight=Math.max(0,selRight-trackStart);const startSample=Math.max(0,Math.floor(relSelLeft*sr));const endSample=Math.min(t.buffer.length,Math.floor(relSelRight*sr));const len=endSample-startSample;if(len<100){showToast('Khoảng chọn quá ngắn.','warning');return;}const ctx=getAudioContext();const numChannels=t.buffer.numberOfChannels||1;const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,startTime:selLeft,endTime:selRight,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditClipInSubTab=(trackId,clipId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default_'+track.id,buffer:track.buffer,startTime:track.startTime||0,name:track.name}]:[];const clip=clips.find(c=>c.id===clipId||clipId==='default'&&c.id==='default_'+trackId);if(!clip||!clip.buffer)return;const resolvedClipId=clip.id==='default'?'default_'+trackId:clip.id;const existing=subTabs.find(s=>s.clipId===resolvedClipId&&s.trackId===trackId);if(existing){setActiveTab(existing.id);showToast(`Sub Tab for "${clip.name}" already open.`,'info');return;}const sr=clip.buffer.sampleRate;const len=clip.buffer.length;const numChannels=clip.buffer.numberOfChannels||1;const ctx=getAudioContext();const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,clipId:resolvedClipId,startTime:clip.startTime,endTime:clip.startTime+clip.buffer.duration,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditMidiInTab=(trackId,midiItemId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const midiItem=(track.midiItems||[]).find(m=>m.id===midiItemId);if(!midiItem)return;const existing=subTabs.find(s=>s.type==='PIANO_ROLL'&&s.target_id===midiItemId);if(existing){setActiveTab(existing.id);showToast('Piano Roll cho nốt MIDI đã được mở.','info');return;}const tabId='midi_'+Date.now();const tabLabel=`Piano Roll: ${midiItem.name||'MIDI'}`;const ctx=getAudioContext();const silentBuffer=ctx.createBuffer(1,128,ctx.sampleRate);const newTab={id:tabId,label:tabLabel,type:'PIANO_ROLL',trackId:trackId,target_id:midiItemId,parent_tab_id:activeTab==='main'?null:activeTab,notes:midiItem.notes||[],duration:midiItem.duration||4,buffer:silentBuffer,instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:track.synth_engine?track.synth_engine.soundfont_program:undefined,instrumentName:track.instrumentName,instrumentId:track.instrumentId,synth_engine:track.synth_engine,currentTime:0,isPlaying:false,isLooping:true,selectionStart:null,selectionEnd:null,viewport_start_bar:0.0,viewport_bar_width:8.0,scroll_y_pitch:60,snap_resolution:snapValue||"1/16",note_selection:[]};setSubTabs(prev=>[...prev,newTab]);setActiveTab(tabId);if(track.synth_engine&&window.SonicSF&&window.SonicSF.selectInstrument){var se=track.synth_engine;var sfId=se.soundfont_id;if(sfId){var allTrks=activeTracksRef.current||activeTracks;// Use the track's dedicated channel (same as every playback path) so the +// preload matches where piano-roll notes will actually sound. +var seCh=assignTrackMidiChannel(track,allTrks);window.SonicSF.selectInstrument(seCh,se.soundfont_bank||0,se.soundfont_program||0,sfId);}}};const handleUpdateMidiNotes=(tabId,notes)=>{setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,notes:notes,isDirty:true}:s));};const handleSaveMidiNotes=(tabId,trackId,midiItemId,updatedNotes)=>{const pianoRollTab=subTabs.find(s=>s.id===tabId);const parentTabId=pianoRollTab?pianoRollTab.parent_tab_id:null;if(parentTabId&&parentTabId.startsWith('session_')){setSessionTabs(prev=>prev.map(s=>{if(s.id!==parentTabId)return s;return{...s,tracks:s.tracks.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};})};}));}else{setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};}));}setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,isDirty:false}:s));showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!','success');};const handleSaveSectionTab=tabId=>{const tab=sessionTabs.find(s=>s.id===tabId);if(!tab)return;const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const secondsPerBar=secondsPerBeat*4;const contentTracks=tab.tracks?tab.tracks.filter(tr=>tr.clips?.length>0||tr.midiItems?.length>0):[];let maxEndTime=0;(contentTracks||[]).forEach(tr=>{(tr.clips||[]).forEach(c=>{const end=(c.startTime||0)+(c.buffer?c.buffer.duration/(c.speed||1.0):4);if(end>maxEndTime)maxEndTime=end;});(tr.midiItems||[]).forEach(m=>{const end=(m.startTime||0)+(m.duration||4);if(end>maxEndTime)maxEndTime=end;});});const durationSec=Math.max(maxEndTime,4*secondsPerBar);setTracks(prev=>prev.map(t=>{if(!t.sections||t.sections.length===0)return t;return{...t,sections:t.sections.map(s=>{if(s.sectionId!==tab.sectionId&&s.id!==tab.sectionId)return s;return{...s,name:tab.name,duration:durationSec,tracks:contentTracks};})};}));// Clear isDirty flag setSessionTabs(prev=>prev.map(s=>s.id===tabId?{...s,isDirty:false}:s));showToast(`Đã lưu nội dung Section "${tab.name}" vào Main Session!`,'success');};// ── Double-click/Edit Section: open Main Session in new tab ── const handleEditSectionInTab=(trackId,sectionId)=>{const track=tracks.find(t=>t.id===trackId);if(!track)return;const section=(track.sections||[]).find(s=>s.id===sectionId);if(!section)return;const existing=sessionTabs.find(s=>s.sectionId===sectionId);if(existing){setActiveTab(existing.id);showToast(`Tab "${section.name}" already open.`,'info');return;}const tabId='session_'+Date.now();const tabName=section.name||'Section';const clonedTracks=section.tracks?JSON.parse(JSON.stringify(section.tracks)).map(t=>({...t,clips:[],sections:[],markers:[],isArmed:false,monitoringEnabled:true,instrumentId:t.instrumentId||null,instrumentProgram:t.instrumentProgram,instrumentName:t.instrumentName||null,_isSectionClone:true})):tracks.filter(t=>t.id===trackId).map(t=>({...t,clips:[],sections:[],midiItems:[],markers:[],isArmed:false,monitoringEnabled:true,instrumentId:null,instrumentProgram:undefined,instrumentName:null,soundfont_id:null,soundfont_bank:undefined,soundfont_program:undefined,instrument_source:null,synth_engine:undefined,_isSectionClone:true}));setSessionTabs(prev=>[...prev,{id:tabId,name:tabName,sectionId:sectionId,tracks:clonedTracks,color:section.color||null}]);setActiveTab(tabId);};// ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ── const applySubTab=tabId=>{const subTab=subTabs.find(s=>s.id===tabId);if(!subTab||!subTab.buffer)return;const track=activeTracks.find(t=>t.id===subTab.trackId);if(!track||!track.buffer)return;const beforeSnap=captureTrackSnapshot(subTab.trackId);// Clone buffer and apply effects diff --git a/app/templates/index.html b/app/templates/index.html index a879ed0..276c5ff 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,7 +24,7 @@ - +