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:
+10
-1
@@ -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 bằng 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 lại theo preset mới (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
@@ -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) }),
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user