From fa2f96dbe3397dc4dce76161c6ad1cc3eaf7ab3b Mon Sep 17 00:00:00 2001 From: locphamtran Date: Tue, 11 Aug 2026 15:36:10 +0700 Subject: [PATCH] T3: Autosample UX - fail reason + toast + pre-warm feedback vstiAutosample: fail map thanh object {ts, reason} (cooldown check object-aware), recordFail o API-missing/response loi/catch, export failReasonFor. soundfontPlayer: toast ly do khi ensure null (throttle 10s) roi fallback oscillator. app.jsx: pre-warm VSTi hien thi toast khi autosample fail. --- app/static/js/app.jsx | 8 +++++- app/static/js/app.precompiled.js | 2 +- app/static/js/services/soundfontPlayer.js | 9 ++++++- app/static/js/services/vstiAutosample.js | 31 +++++++++++++++++++---- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 09c4564..11587ed 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -15496,7 +15496,13 @@ const App = () => { // VSTi live playback: pre-warm autosample (SF2 backend) — note đầu không bị // delay (SonicSF cũng ensure lazily trong _playNoteFluid nếu chưa có). if (window.SonicVstiAutosample && instrumentId && !isSfInstrument) { - window.SonicVstiAutosample.ensure({ plugin_id: instrumentId, type: 'vst3' }); + const _warmEngine = { plugin_id: instrumentId, type: 'vst3' }; + window.SonicVstiAutosample.ensure(_warmEngine).then(_sfId => { + if (!_sfId && window.SonicVstiAutosample.failReasonFor) { + const _reason = window.SonicVstiAutosample.failReasonFor(_warmEngine); + if (_reason && window.showToast) window.showToast('Autosample VSTi that bai: ' + _reason + ' - dung am default', 'warning'); + } + }); } // Trigger FluidSynth load + program change when soundfont instrument selected if (window.SonicSF && window.SonicSF.selectInstrument && instrumentId && isSfInstrument) { diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 7b1ab59..aee429a 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -786,7 +786,7 @@ const[instrumentDropdownTrackId,setInstrumentDropdownTrackId]=useState(null);con // play qua Carla bridge (âm sai instrument + âm kẹt không dừng được). try{const _hasInst=!!instrumentId;const _wasVst=curTrk&&curTrk.synth_engine&&String(curTrk.synth_engine.type||'').indexOf('vst')!==-1;const _nowVst=_hasInst&&!isSfInstrument;if(_wasVst&&!_nowVst&&window.SonicCarlaMidi&&window.SonicCarlaMidi.stopBridge){window.SonicCarlaMidi.stopBridge();try{if(window.SonicSF&&window.SonicSF.stopAll)window.SonicSF.stopAll();}catch(e){}stopAllNativeSfNotes();console.log('[Instrument] Carla bridge unloaded — track',trackId,'switched from VSTi to non-VST');}}catch(e){console.warn('[Instrument] carla-stop on instrument switch error:',e);}var mch=curTrk?assignTrackMidiChannel(curTrk,mt):sfBank===128?9:0;updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const hasInstrument=!!instrumentId;const instrType=isSfInstrument?'soundfont':hasInstrument?'vst3':'default';const synthEngine=hasInstrument?{type:instrType,plugin_id:instrumentId,soundfont_bank:sfBank!==undefined?sfBank:0,soundfont_program:sfProg!==undefined?sfProg:0,soundfont_id:isSfInstrument?instrumentId.replace('sf_',''):''}:undefined;return{...t,midiChannel:mch,instrumentId,instrumentProgram:sfProg,instrumentName:displayName,soundfont_bank:sfBank,soundfont_program:sfProg,synth_engine:synthEngine,type:hasInstrument?'MIDI':t.type==='MIDI'?'audio':t.type};}));setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setSynthCategory(null);// VSTi live playback: pre-warm autosample (SF2 backend) — note đầu không bị // delay (SonicSF cũng ensure lazily trong _playNoteFluid nếu chưa có). -if(window.SonicVstiAutosample&&instrumentId&&!isSfInstrument){window.SonicVstiAutosample.ensure({plugin_id:instrumentId,type:'vst3'});}// Trigger FluidSynth load + program change when soundfont instrument selected +if(window.SonicVstiAutosample&&instrumentId&&!isSfInstrument){const _warmEngine={plugin_id:instrumentId,type:'vst3'};window.SonicVstiAutosample.ensure(_warmEngine).then(_sfId=>{if(!_sfId&&window.SonicVstiAutosample.failReasonFor){const _reason=window.SonicVstiAutosample.failReasonFor(_warmEngine);if(_reason&&window.showToast)window.showToast('Autosample VSTi that bai: '+_reason+' - dung am default','warning');}});}// Trigger FluidSynth load + program change when soundfont instrument selected if(window.SonicSF&&window.SonicSF.selectInstrument&&instrumentId&&isSfInstrument){const sfId=instrumentId.replace('sf_','');window.SonicSF.selectInstrument(mch,sfBank||0,sfProg||0,sfId);}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){const pid=playingSub.id;const ctx=getAudioContext();const tNode=activeTrackNodesRef.current[trackId];if(tNode&&tNode.gainNode){tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value||1,ctx.currentTime);tNode.gainNode.gain.linearRampToValueAtTime(0.001,ctx.currentTime+0.04);}setTimeout(()=>{stopAllPlayback();if(tNode&&tNode.gainNode){const trackData=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===trackId):null;const volDb=trackData?trackData.volumeDb??0:0;const volLinear=volDb<=-50?0:Math.pow(10,volDb/20);tNode.gainNode.gain.setValueAtTime(0.001,ctx.currentTime);tNode.gainNode.gain.linearRampToValueAtTime(volLinear||0.8,ctx.currentTime+0.015);}const context=getAudioContext();const offset=playingSub.currentTime||0;startOffsetTimeRef.current=offset;startAudioTimeRef.current=context.currentTime;startBufferOffsetRef.current=offset*(playingSub.speed||1.0);schedulePianoRollMidi(playingSub,offset);startSubTabPlayback(playingSub,offset);setSubTabs(prev=>prev.map(s=>s.id===pid?{...s,isPlaying:true}:s));if(subTabsRef.current){subTabsRef.current=subTabsRef.current.map(s=>s.id===pid?{...s,isPlaying:true}:s);}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);},60);}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 var mt2=activeTracksRef.current||tracks;var qTrk=null;for(var qi=0;qiprev.map(t=>{if(t.id!==trackId)return t;const sfClean=instrumentId.replace('sf_','');const synthEngine={type:'soundfont',plugin_id:instrumentId,soundfont_bank:0,soundfont_program:0,soundfont_id:sfClean};return{...t,midiChannel:qch,instrumentId,instrumentProgram:undefined,instrumentName:displayName,synth_engine:synthEngine};}));setSelectedSoundFontId(instrumentId);setSynthCategory('soundfont');setInstrumentSelectorTrackId(trackId);setSfPresets(null);setSfPresetSearchQuery('');const sfIdParam=instrumentId.replace('sf_','');// Use cached presets from instrumentSelectorData 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' diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index 39b44f8..44494df 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -609,7 +609,14 @@ if (!sfId) { // Không autosample được (404/501) → fallback oscillator để note // KHÔNG câm; cooldown trong ensure chặn re-request. - try { self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine); } catch (e) {} + try { + var _reason = window.SonicVstiAutosample.failReasonFor ? window.SonicVstiAutosample.failReasonFor(synthEngine) : null; + if (_reason && window.showToast && (!self._vstiToastAt || Date.now() - self._vstiToastAt > 10000)) { + self._vstiToastAt = Date.now(); + window.showToast('Autosample VSTi that bai: ' + _reason + ' - dung am default', 'warning'); + } + self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine); + } catch (e) {} return; } synthEngine.autosampled_sf_id = sfId; diff --git a/app/static/js/services/vstiAutosample.js b/app/static/js/services/vstiAutosample.js index bccef7f..61bb097 100644 --- a/app/static/js/services/vstiAutosample.js +++ b/app/static/js/services/vstiAutosample.js @@ -19,6 +19,13 @@ window.SonicVstiAutosample = window.SonicVstiAutosample || {}; function readFails() { try { return JSON.parse(localStorage.getItem(LS_FAIL_KEY) || '{}') || {}; } catch (e) { return {}; } } + function recordFail(k, reason) { + try { + var fails2 = readFails(); + fails2[k] = { ts: Date.now(), reason: reason || 'Autosample that bai' }; + localStorage.setItem(LS_FAIL_KEY, JSON.stringify(fails2)); + } catch (e) {} + } // Key = plugin + preset → autosample lại khi đổi preset (âm preset mới). function keyFor(synthEngine) { @@ -51,10 +58,13 @@ window.SonicVstiAutosample = window.SonicVstiAutosample || {}; if (cached) return Promise.resolve(cached); if (_inFlight[k]) return _inFlight[k]; var fails = readFails(); - if (fails[k] && (Date.now() - fails[k]) < FAIL_COOLDOWN_MS) { + var _f = fails[k]; + var _lastTs = (_f && typeof _f === 'object') ? _f.ts : _f; + if (_lastTs && (Date.now() - _lastTs) < FAIL_COOLDOWN_MS) { return Promise.resolve(null); } if (!window.SonicAPI || !window.SonicAPI.autosampleVsti) { + recordFail(k, 'API autosample khong kha dung'); return Promise.resolve(null); } _inFlight[k] = new Promise(function (resolve) { @@ -72,19 +82,29 @@ window.SonicVstiAutosample = window.SonicVstiAutosample || {}; writeMap(map); resolve(res.sf_id); } else { + recordFail(k, (res && res.error) ? String(res.error) : 'Server khong tra sf_id'); resolve(null); } - }).catch(function () { + }).catch(function (err) { delete _inFlight[k]; - var fails2 = readFails(); - fails2[k] = Date.now(); - try { localStorage.setItem(LS_FAIL_KEY, JSON.stringify(fails2)); } catch (e) {} + recordFail(k, (err && err.message) ? String(err.message) : 'Loi mang / server autosample'); resolve(null); }); }); return _inFlight[k]; } + // Lý do autosample thất bại gần nhất (fail map) — null nếu chưa từng fail + // (hoặc entry cũ định dạng number). Dùng cho UI toast khi ensure() trả null. + function failReasonFor(synthEngine) { + var k = keyFor(synthEngine); + if (!k) return null; + var fails = readFails(); + var f = fails[k]; + if (!f || typeof f !== 'object') return null; + return f.reason || null; + } + // Xoá cache + cooldown — cho phép re-sample (đổi preset/plugin hoặc debug). function clearCache() { try { localStorage.removeItem(LS_MAP_KEY); } catch (e) {} @@ -96,6 +116,7 @@ window.SonicVstiAutosample = window.SonicVstiAutosample || {}; sfIdFor: sfIdFor, entryFor: entryFor, ensure: ensure, + failReasonFor: failReasonFor, clearCache: clearCache }; })();