feat: VSTi live playback 100% client-side (không Carla) — autosample SF2 backend (pedalboard) → FluidSynth WASM qua masterBus → mastering → main out; SonicVstiAutosample ensure+dedup+cooldown; runtime gating __enableCarlaLivePlayback; âm bắt qua ensureMidiCapture → clientSideExport

This commit is contained in:
2026-08-11 14:28:42 +07:00
parent 7293d7ac7e
commit cadb5402a3
12 changed files with 676 additions and 44 deletions
+10 -1
View File
@@ -198,7 +198,7 @@ const isSfTrackEngine = (se) => {
};
const isVstTrackEngine = (se) => !!se && String(se.type || '').indexOf('vst') !== -1;
// Track dùng VSTi + Carla local route Carla (native GUI, realtime).
const shouldRouteCarla = (se) => !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(se));
const shouldRouteCarla = () => false; // live playback 100% client (SF2 autosampled WASM) không route Carla
// Native soundfont preview (standalone)
// Render 1 note bng backend native FluidSynth (pyfluidsynth) play WAV.
@@ -15492,6 +15492,11 @@ const App = () => {
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.SonicSF && window.SonicSF.selectInstrument && instrumentId && isSfInstrument) {
const sfId = instrumentId.replace('sf_', '');
@@ -16398,6 +16403,10 @@ const App = () => {
return;
}
updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, synth_engine: { ...(t.synth_engine || {}), preset_id: presetId } } : t));
// Autosample li theo preset mi (key khác) live playback dùng âm preset
if (window.SonicVstiAutosample && se.plugin_id) {
window.SonicVstiAutosample.ensure({ plugin_id: se.plugin_id, preset_id: presetId, type: 'vst3' });
}
setInstrumentDropdownTrackId(null);
setInstrumentDropdownBtnRect(null);
var p = ((window.SonicRuntime && window.SonicRuntime.presets) || []).find(function (x) { return x.id === presetId; });
File diff suppressed because one or more lines are too long
+2
View File
@@ -91,6 +91,8 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
// để UI preview / gán clip vào track / download
midiRender: (payload) => apiRequest('/api/v1/plugins/midi-render', { method: 'POST', body: JSON.stringify(payload) }),
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
// Autosample VSTi → SF2 (backend pedalboard) — live playback client-side qua WASM
autosampleVsti: (payload) => apiRequest('/api/v1/plugins/autosample', { method: 'POST', body: JSON.stringify(payload) }),
// Phát dãy MIDI notes realtime qua Carla bridge (OSC) — preview khi
// pedalboard không render được plugin (VD VST2)
carlaPlayNotes: (payload) => apiRequest('/api/v1/plugins/carla-play-notes', { method: 'POST', body: JSON.stringify(payload) }),
+6
View File
@@ -74,6 +74,10 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
window.SonicCarlaMidi = window.SonicCarlaMidi || {
shouldRoute: function (synthEngine, isArmed) {
try {
// Live playback qua Carla TẮT mặc định: VSTi phát client-side qua SF2
// autosampled (FluidSynth WASM) → masterBus → mastering → main out.
// Bật lại = window.__enableCarlaLivePlayback = true (debug/legacy).
if (window.__enableCarlaLivePlayback !== true) return false;
var c = window.SonicRuntime && window.SonicRuntime.capabilities;
if (!c || !c.features || !c.features.carla_local) return false;
if (!isArmed) return false;
@@ -85,6 +89,8 @@ window.SonicCarlaMidi = window.SonicCarlaMidi || {
// user đã chủ động bấm Play trên item đó).
shouldRoutePlayback: function (synthEngine) {
try {
// (xem shouldRoute) — live playback qua Carla tắt mặc định
if (window.__enableCarlaLivePlayback !== true) return false;
var c = window.SonicRuntime && window.SonicRuntime.capabilities;
if (!c || !c.features || !c.features.carla_local) return false;
var se = synthEngine || {};
+31 -2
View File
@@ -464,7 +464,7 @@
bank = bank !== undefined ? bank : (synthEngine.soundfont_bank || 0);
program = program !== undefined ? program : (synthEngine.soundfont_program || 0);
}
var sfId = synthEngine && synthEngine.soundfont_id;
var sfId = synthEngine && (synthEngine.soundfont_id || synthEngine.autosampled_sf_id);
var engKey = (sfId || '') + ':' + bank + ':' + program;
if (!_engineChMap[engKey]) {
var channel = this.allocateChannel(bank);
@@ -587,7 +587,36 @@
if (isNaN(finalBank) || !isFinite(finalBank)) finalBank = 0;
var finalProg = parseInt(usedProg);
if (isNaN(finalProg) || !isFinite(finalProg)) finalProg = 0;
var finalSfId = synthEngine ? synthEngine.soundfont_id : undefined;
var finalSfId = synthEngine ? (synthEngine.soundfont_id || synthEngine.autosampled_sf_id) : undefined;
// VSTi live playback (không Carla): chưa có SF2 autosampled → chờ
// SonicVstiAutosample.ensure (1 request backend / plugin+preset) rồi
// play qua FluidSynth WASM như soundfont thường — âm đi qua masterBus
// → mastering → main out. ensure dedup in-flight + cooldown lỗi 60s.
if (!finalSfId && window.SonicVstiAutosample && synthEngine && !synthEngine.soundfont_id
&& String(synthEngine.type || '').indexOf('vst') !== -1 && !!synthEngine.plugin_id) {
var _pendKey = ch + ':' + midiPitch;
var _gen = _noteGeneration;
_pendingNoteOns[_pendKey] = (_pendingNoteOns[_pendKey] || 0) + 1;
window.SonicVstiAutosample.ensure(synthEngine).then(function (sfId) {
if (_gen !== _noteGeneration) return;
var _pend = _pendingNoteOns[_pendKey];
if (_pend) {
_pendingNoteOns[_pendKey] = _pend - 1;
if (_pendingNoteOns[_pendKey] <= 0) delete _pendingNoteOns[_pendKey];
} else {
return;
}
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) {}
return;
}
synthEngine.autosampled_sf_id = sfId;
doNote();
});
return;
}
var cachedCh = _channels[ch];
// The note's own synth engine (track instrument) is
// authoritative. Channel state is only a cache: it must never
+101
View File
@@ -0,0 +1,101 @@
// SonicForge VSTi Autosample Service
// Live playback 100% client-side (không Carla): VSTi note → SF2 autosampled
// (backend pedalboard, render 1 lần / plugin+preset) → FluidSynth WASM qua
// masterBus → mastering → main out. ensure() dedup in-flight request + cooldown
// lỗi (404/501/no pedalboard) để không spam server.
window.SonicVstiAutosample = window.SonicVstiAutosample || {};
(function () {
var LS_MAP_KEY = 'sf_vsti_autosample_map';
var LS_FAIL_KEY = 'sf_vsti_autosample_fail';
var FAIL_COOLDOWN_MS = 60000;
function readMap() {
try { return JSON.parse(localStorage.getItem(LS_MAP_KEY) || '{}') || {}; } catch (e) { return {}; }
}
function writeMap(map) {
try { localStorage.setItem(LS_MAP_KEY, JSON.stringify(map)); } catch (e) {}
}
function readFails() {
try { return JSON.parse(localStorage.getItem(LS_FAIL_KEY) || '{}') || {}; } catch (e) { return {}; }
}
// Key = plugin + preset → autosample lại khi đổi preset (âm preset mới).
function keyFor(synthEngine) {
if (!synthEngine) return '';
return String(synthEngine.plugin_id || '') + '|' + String(synthEngine.preset_id || synthEngine.presetId || '');
}
// SF id đã autosample cho engine (cache localStorage) — đồng bộ, không request.
function sfIdFor(synthEngine) {
var k = keyFor(synthEngine);
if (!k) return null;
var map = readMap();
return (map[k] && map[k].sf_id) ? map[k].sf_id : null;
}
function entryFor(synthEngine) {
var k = keyFor(synthEngine);
if (!k) return null;
return readMap()[k] || null;
}
var _inFlight = {}; // key → Promise<sf_id|null> — nhiều note cùng lúc chỉ 1 request
// Đảm bảo SF2 autosampled tồn tại. Trả Promise<sf_id|null>; null =
// không autosample được → caller fallback (oscillator / không phát).
function ensure(synthEngine) {
var k = keyFor(synthEngine);
if (!k) return Promise.resolve(null);
var cached = sfIdFor(synthEngine);
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) {
return Promise.resolve(null);
}
if (!window.SonicAPI || !window.SonicAPI.autosampleVsti) {
return Promise.resolve(null);
}
_inFlight[k] = new Promise(function (resolve) {
var payload = {
instrument_id: synthEngine.plugin_id,
preset_id: synthEngine.preset_id || synthEngine.presetId || null,
preset_path: synthEngine.preset_path || null,
preset_data: synthEngine.preset_data || null
};
window.SonicAPI.autosampleVsti(payload).then(function (res) {
delete _inFlight[k];
if (res && res.success && res.sf_id) {
var map = readMap();
map[k] = { sf_id: res.sf_id, size_bytes: res.size_bytes || 0, note_count: res.note_count || 0, plugin_id: synthEngine.plugin_id, preset_id: synthEngine.preset_id || synthEngine.presetId || '', ts: Date.now() };
writeMap(map);
resolve(res.sf_id);
} else {
resolve(null);
}
}).catch(function () {
delete _inFlight[k];
var fails2 = readFails();
fails2[k] = Date.now();
try { localStorage.setItem(LS_FAIL_KEY, JSON.stringify(fails2)); } catch (e) {}
resolve(null);
});
});
return _inFlight[k];
}
// Xoá cache + cooldown — cho phép re-sample (đổi preset/plugin hoặc debug).
function clearCache() {
try { localStorage.removeItem(LS_MAP_KEY); } catch (e) {}
try { localStorage.removeItem(LS_FAIL_KEY); } catch (e) {}
}
window.SonicVstiAutosample = {
keyFor: keyFor,
sfIdFor: sfIdFor,
entryFor: entryFor,
ensure: ensure,
clearCache: clearCache
};
})();