fix: MIDI tab real-time instrument change - restart playback

This commit is contained in:
2026-07-26 11:07:37 +07:00
parent 5b500ebdf3
commit e57f6d96d9
3 changed files with 17 additions and 1 deletions
+11
View File
@@ -6445,6 +6445,17 @@ const App = () => {
if (s.trackId !== trackId) return s; if (s.trackId !== trackId) return s;
return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId }; return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId };
})); }));
const playingSub = subTabs.find(s => s.trackId === trackId && s.type === 'PIANO_ROLL' && s.isPlaying);
if (playingSub) {
stopAllPlayback();
setTimeout(() => {
const context = getAudioContext();
startOffsetTimeRef.current = playingSub.currentTime || 0;
startAudioTimeRef.current = context.currentTime;
schedulePianoRollMidi(playingSub, playingSub.currentTime || 0);
startSubTabPlayback(playingSub, playingSub.currentTime || 0);
}, 50);
}
setTimeout(() => lucide.createIcons(), 50); setTimeout(() => lucide.createIcons(), 50);
}; };
const setTrackInstrument = (trackId, instrumentId, displayName) => { const setTrackInstrument = (trackId, instrumentId, displayName) => {
+1 -1
View File
@@ -170,7 +170,7 @@ const[hoveredTrackId,setHoveredTrackId]=useState(null);const openPanel=id=>{if(i
setSynthCategory('soundfont');setSelectedSoundFontId(track.instrumentId);setSfPresets(null);const sfIdParam=track.instrumentId.replace('sf_','');window.SonicAPI.listSoundfontInstruments(sfIdParam).then(data=>setSfPresets(data.presets||[])).catch(()=>setSfPresets([]));}else{// Reset synth state and always reload plugin data setSynthCategory('soundfont');setSelectedSoundFontId(track.instrumentId);setSfPresets(null);const sfIdParam=track.instrumentId.replace('sf_','');window.SonicAPI.listSoundfontInstruments(sfIdParam).then(data=>setSfPresets(data.presets||[])).catch(()=>setSfPresets([]));}else{// Reset synth state and always reload plugin data
setSynthCategory(null);setSelectedSoundFontId(null);window.SonicAPI.listPlugins().then(data=>setInstrumentSelectorData(data)).catch(e=>console.error('listPlugins failed:',e));}};const closeInstrumentSelector=()=>{setInstrumentSelectorTrackId(null);setSynthCategory(null);setSelectedSoundFontId(null);};const[synthCategory,setSynthCategory]=useState(null);// 'vst' | 'soundfont' setSynthCategory(null);setSelectedSoundFontId(null);window.SonicAPI.listPlugins().then(data=>setInstrumentSelectorData(data)).catch(e=>console.error('listPlugins failed:',e));}};const closeInstrumentSelector=()=>{setInstrumentSelectorTrackId(null);setSynthCategory(null);setSelectedSoundFontId(null);};const[synthCategory,setSynthCategory]=useState(null);// 'vst' | 'soundfont'
const[selectedSoundFontId,setSelectedSoundFontId]=useState(null);const[sfPresets,setSfPresets]=useState(null);// presets from SoundFont const[selectedSoundFontId,setSelectedSoundFontId]=useState(null);const[sfPresets,setSfPresets]=useState(null);// presets from SoundFont
const[instrumentDropdownTrackId,setInstrumentDropdownTrackId]=useState(null);const[instrumentDropdownBtnRect,setInstrumentDropdownBtnRect]=useState(null);const[instrumentSearchQuery,setInstrumentSearchQuery]=useState('');const filteredInstruments=useMemo(()=>{if(!instrumentSelectorData||!instrumentSearchQuery)return{soundfonts:instrumentSelectorData?.soundfonts||[],vst:instrumentSelectorData?.vst_instruments||[]};const q=instrumentSearchQuery.toLowerCase();return{soundfonts:(instrumentSelectorData.soundfonts||[]).filter(sf=>(sf.display||sf.name||sf.id).toLowerCase().includes(q)),vst:(instrumentSelectorData.vst_instruments||[]).filter(v=>(v.name||v.id).toLowerCase().includes(q))};},[instrumentSearchQuery,instrumentSelectorData]);useEffect(()=>{if(!instrumentSelectorData){window.SonicAPI.listPlugins().then(data=>setInstrumentSelectorData(data)).catch(()=>{});}},[]);useEffect(()=>{if(!instrumentDropdownTrackId)return;const handler=e=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSearchQuery('');};document.addEventListener('click',handler);return()=>document.removeEventListener('click',handler);},[instrumentDropdownTrackId]);const GM_INSTRUMENTS=["Acoustic Grand Piano","Bright Acoustic Piano","Electric Grand Piano","Honky-tonk Piano","Electric Piano 1","Electric Piano 2","Harpsichord","Clavi","Celesta","Glockenspiel","Music Box","Vibraphone","Marimba","Xylophone","Tubular Bells","Dulcimer","Drawbar Organ","Percussive Organ","Rock Organ","Church Organ","Reed Organ","Accordion","Harmonica","Tango Accordion","Acoustic Guitar (nylon)","Acoustic Guitar (steel)","Electric Guitar (jazz)","Electric Guitar (clean)","Electric Guitar (muted)","Overdriven Guitar","Distortion Guitar","Guitar harmonics","Acoustic Bass","Electric Bass (finger)","Electric Bass (pick)","Fretless Bass","Slap Bass 1","Slap Bass 2","Synth Bass 1","Synth Bass 2","Violin","Viola","Cello","Contrabass","Tremolo Strings","Pizzicato Strings","Orchestral Harp","Timpani","String Ensemble 1","String Ensemble 2","Synth Strings 1","Synth Strings 2","Choir Aahs","Voice Oohs","Synth Voice","Orchestra Hit","Trumpet","Trombone","Tuba","Muted Trumpet","French Horn","Brass Section","Synth Brass 1","Synth Brass 2","Soprano Sax","Alto Sax","Tenor Sax","Baritone Sax","Oboe","English Horn","Bassoon","Clarinet","Piccolo","Flute","Recorder","Pan Flute","Blown Bottle","Shakuhachi","Whistle","Ocarina","Lead 1 (square)","Lead 2 (sawtooth)","Lead 3 (calliope)","Lead 4 (chiff)","Lead 5 (charang)","Lead 6 (voice)","Lead 7 (fifths)","Lead 8 (bass+lead)","Pad 1 (new age)","Pad 2 (warm)","Pad 3 (polysynth)","Pad 4 (choir)","Pad 5 (bowed)","Pad 6 (metallic)","Pad 7 (halo)","Pad 8 (sweep)","FX 1 (rain)","FX 2 (soundtrack)","FX 3 (crystal)","FX 4 (atmosphere)","FX 5 (brightness)","FX 6 (goblins)","FX 7 (echoes)","FX 8 (sci-fi)","Sitar","Banjo","Shamisen","Koto","Kalimba","Bag pipe","Fiddle","Shanai","Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal","Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"];const setTrackInstrumentWithProgram=(trackId,instrumentId,programNumber,displayName)=>{updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,instrumentId,instrumentProgram:programNumber!==undefined?programNumber:undefined,instrumentName:displayName};}));setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSelectorTrackId(null);setSynthCategory(null);setSelectedSoundFontId(null);setSubTabs(prev=>prev.map(s=>{if(s.trackId!==trackId)return s;return{...s,instrumentProgram:programNumber!==undefined?programNumber:undefined,instrumentName:displayName,instrumentId};}));setTimeout(()=>lucide.createIcons(),50);};const setTrackInstrument=(trackId,instrumentId,displayName)=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);if(instrumentId&&instrumentId.startsWith('sf_')){// Set instrument on track immediately so Synth button shows the name const[instrumentDropdownTrackId,setInstrumentDropdownTrackId]=useState(null);const[instrumentDropdownBtnRect,setInstrumentDropdownBtnRect]=useState(null);const[instrumentSearchQuery,setInstrumentSearchQuery]=useState('');const filteredInstruments=useMemo(()=>{if(!instrumentSelectorData||!instrumentSearchQuery)return{soundfonts:instrumentSelectorData?.soundfonts||[],vst:instrumentSelectorData?.vst_instruments||[]};const q=instrumentSearchQuery.toLowerCase();return{soundfonts:(instrumentSelectorData.soundfonts||[]).filter(sf=>(sf.display||sf.name||sf.id).toLowerCase().includes(q)),vst:(instrumentSelectorData.vst_instruments||[]).filter(v=>(v.name||v.id).toLowerCase().includes(q))};},[instrumentSearchQuery,instrumentSelectorData]);useEffect(()=>{if(!instrumentSelectorData){window.SonicAPI.listPlugins().then(data=>setInstrumentSelectorData(data)).catch(()=>{});}},[]);useEffect(()=>{if(!instrumentDropdownTrackId)return;const handler=e=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSearchQuery('');};document.addEventListener('click',handler);return()=>document.removeEventListener('click',handler);},[instrumentDropdownTrackId]);const GM_INSTRUMENTS=["Acoustic Grand Piano","Bright Acoustic Piano","Electric Grand Piano","Honky-tonk Piano","Electric Piano 1","Electric Piano 2","Harpsichord","Clavi","Celesta","Glockenspiel","Music Box","Vibraphone","Marimba","Xylophone","Tubular Bells","Dulcimer","Drawbar Organ","Percussive Organ","Rock Organ","Church Organ","Reed Organ","Accordion","Harmonica","Tango Accordion","Acoustic Guitar (nylon)","Acoustic Guitar (steel)","Electric Guitar (jazz)","Electric Guitar (clean)","Electric Guitar (muted)","Overdriven Guitar","Distortion Guitar","Guitar harmonics","Acoustic Bass","Electric Bass (finger)","Electric Bass (pick)","Fretless Bass","Slap Bass 1","Slap Bass 2","Synth Bass 1","Synth Bass 2","Violin","Viola","Cello","Contrabass","Tremolo Strings","Pizzicato Strings","Orchestral Harp","Timpani","String Ensemble 1","String Ensemble 2","Synth Strings 1","Synth Strings 2","Choir Aahs","Voice Oohs","Synth Voice","Orchestra Hit","Trumpet","Trombone","Tuba","Muted Trumpet","French Horn","Brass Section","Synth Brass 1","Synth Brass 2","Soprano Sax","Alto Sax","Tenor Sax","Baritone Sax","Oboe","English Horn","Bassoon","Clarinet","Piccolo","Flute","Recorder","Pan Flute","Blown Bottle","Shakuhachi","Whistle","Ocarina","Lead 1 (square)","Lead 2 (sawtooth)","Lead 3 (calliope)","Lead 4 (chiff)","Lead 5 (charang)","Lead 6 (voice)","Lead 7 (fifths)","Lead 8 (bass+lead)","Pad 1 (new age)","Pad 2 (warm)","Pad 3 (polysynth)","Pad 4 (choir)","Pad 5 (bowed)","Pad 6 (metallic)","Pad 7 (halo)","Pad 8 (sweep)","FX 1 (rain)","FX 2 (soundtrack)","FX 3 (crystal)","FX 4 (atmosphere)","FX 5 (brightness)","FX 6 (goblins)","FX 7 (echoes)","FX 8 (sci-fi)","Sitar","Banjo","Shamisen","Koto","Kalimba","Bag pipe","Fiddle","Shanai","Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal","Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"];const setTrackInstrumentWithProgram=(trackId,instrumentId,programNumber,displayName)=>{updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,instrumentId,instrumentProgram:programNumber!==undefined?programNumber:undefined,instrumentName:displayName};}));setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSelectorTrackId(null);setSynthCategory(null);setSelectedSoundFontId(null);setSubTabs(prev=>prev.map(s=>{if(s.trackId!==trackId)return s;return{...s,instrumentProgram:programNumber!==undefined?programNumber:undefined,instrumentName:displayName,instrumentId};}));const playingSub=subTabs.find(s=>s.trackId===trackId&&s.type==='PIANO_ROLL'&&s.isPlaying);if(playingSub){stopAllPlayback();setTimeout(()=>{const context=getAudioContext();startOffsetTimeRef.current=playingSub.currentTime||0;startAudioTimeRef.current=context.currentTime;schedulePianoRollMidi(playingSub,playingSub.currentTime||0);startSubTabPlayback(playingSub,playingSub.currentTime||0);},50);}setTimeout(()=>lucide.createIcons(),50);};const setTrackInstrument=(trackId,instrumentId,displayName)=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);if(instrumentId&&instrumentId.startsWith('sf_')){// Set instrument on track immediately so Synth button shows the name
updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,instrumentId,instrumentProgram:undefined,instrumentName:displayName};}));setSelectedSoundFontId(instrumentId);setSynthCategory('soundfont');setInstrumentSelectorTrackId(trackId);setSfPresets(null);// Fetch actual presets from the SoundFont updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,instrumentId,instrumentProgram:undefined,instrumentName:displayName};}));setSelectedSoundFontId(instrumentId);setSynthCategory('soundfont');setInstrumentSelectorTrackId(trackId);setSfPresets(null);// Fetch actual presets from the SoundFont
const sfIdParam=instrumentId.replace('sf_','');window.SonicAPI.listSoundfontInstruments(sfIdParam).then(data=>setSfPresets(data.presets||[])).catch(e=>{console.error('listSoundfontInstruments failed:',e);setSfPresets([]);});}else{setTrackInstrumentWithProgram(trackId,instrumentId,undefined,displayName);}};const[activeTool,setActiveTool]=useState('select');// 'select' | 'grab' | 'razor' const sfIdParam=instrumentId.replace('sf_','');window.SonicAPI.listSoundfontInstruments(sfIdParam).then(data=>setSfPresets(data.presets||[])).catch(e=>{console.error('listSoundfontInstruments failed:',e);setSfPresets([]);});}else{setTrackInstrumentWithProgram(trackId,instrumentId,undefined,displayName);}};const[activeTool,setActiveTool]=useState('select');// 'select' | 'grab' | 'razor'
const[snapValue,setSnapValue]=useState('free');// 'free', '1', '1/2', '1/4', '1/8', '1/16', '1/32' const[snapValue,setSnapValue]=useState('free');// 'free', '1', '1/2', '1/4', '1/8', '1/16', '1/32'
+5
View File
@@ -220,3 +220,8 @@
- **Tóm tắt thay đổi:** (1) Xóa nút loop (repeat) khỏi MIDI tab toolbar — dùng global transport loop button thay thế. (2) Ctrl/Meta+Click trên ruler (tempo track lane) xóa loop range: set loopStartBeat/loopEndBeat = null, selectionStart/selectionEnd = null. - **Tóm tắt thay đổi:** (1) Xóa nút loop (repeat) khỏi MIDI tab toolbar — dùng global transport loop button thay thế. (2) Ctrl/Meta+Click trên ruler (tempo track lane) xóa loop range: set loopStartBeat/loopEndBeat = null, selectionStart/selectionEnd = null.
- **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass. - **Ghi chú/Test (nếu có):** `npm run build` pass.
### [2026-07-26 11:06] Task: Fix MIDI tab real-time instrument change - restart playback
- **Tóm tắt thay đổi:** `setTrackInstrumentWithProgram`: khi đổi instrument, kiểm tra nếu có PIANO_ROLL sub-tab với trackId đang playing → gọi `stopAllPlayback()` + `setTimeout` 50ms để `schedulePianoRollMidi` + `startSubTabPlayback` với instrument mới. Fix lỗi MIDI tab không cập nhật realtime khi thay đổi Synth.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.