T15: drop WebAudio master for tracks - native master gain + track gain/pan IPC (SF/VST2/VST3 bridges), NativeAudioService + /api/v1/native router, native-first TrackInstrument, test matrix 8 passed
This commit is contained in:
@@ -1980,6 +1980,8 @@ const MasterStripConsole = ({ masterVolume, setMasterVolume, showMasteringModal,
|
||||
|
||||
const handleFaderChange = (val) => {
|
||||
setMasterVolume(val);
|
||||
// T15: master fader track instrument → native engine (NativeMixer)
|
||||
if (window.SonicNativeAudio) window.SonicNativeAudio.setMasterGain(val);
|
||||
ensureAudio();
|
||||
if (masterBus && masterBus.output) {
|
||||
const linear = val <= -50 ? 0 : Math.pow(10, val / 20);
|
||||
@@ -24241,6 +24243,12 @@ const App = () => {
|
||||
if (sn && sn.gainNode) sn.gainNode.gain.setValueAtTime(volLinear, ctx.currentTime);
|
||||
}
|
||||
});
|
||||
// T15: track gain → native engine (NativeMixer)
|
||||
if (window.SonicNativeAudio) {
|
||||
const trk = (tracks || []).find(t => t.id === trackId);
|
||||
const curPan = trk && trk.pan != null ? trk.pan : 0;
|
||||
window.SonicNativeAudio.setTrackGainPan(trackId, val, curPan);
|
||||
}
|
||||
};
|
||||
const updateTrackPan = (trackId, val) => {
|
||||
const beforeSnap = captureTrackSnapshot(trackId);
|
||||
@@ -24264,6 +24272,12 @@ const App = () => {
|
||||
if (nodes) {
|
||||
nodes.pannerNode.pan.setValueAtTime(val / 100, getAudioContext().currentTime);
|
||||
}
|
||||
// T15: track pan → native engine (NativeMixer; pan -100..100 → -1..1)
|
||||
if (window.SonicNativeAudio) {
|
||||
const trk = (tracks || []).find(t => t.id === trackId);
|
||||
const curVol = trk && trk.volumeDb != null ? trk.volumeDb : 0;
|
||||
window.SonicNativeAudio.setTrackGainPan(trackId, curVol, val / 100);
|
||||
}
|
||||
};
|
||||
const updateTrackName = (trackId, name) => {
|
||||
const beforeSnap = captureTrackSnapshot(trackId);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
|
||||
// SonicForge Native Audio Client (T15) — IPC JS → native engine.
|
||||
// Track instrument KHÔNG còn đi masterBus WebAudio: master fader + track
|
||||
// gain/pan áp native qua NativeMixer (SF host / VST2 / VST3 bridge DLL).
|
||||
// WebAudio chỉ giữ cho UI/aux. Mọi call fire-and-forget — native engine
|
||||
// tự âm thanh; không block UI.
|
||||
window.SonicNativeAudio = window.SonicNativeAudio || {};
|
||||
|
||||
(function () {
|
||||
var BASE = window.API_BASE_URL || window.location.origin;
|
||||
|
||||
function headers() {
|
||||
var h = { 'Content-Type': 'application/json' };
|
||||
var token = localStorage.getItem('sonic_token') || '';
|
||||
if (token) h['Authorization'] = 'Bearer ' + token;
|
||||
return h;
|
||||
}
|
||||
|
||||
async function post(path, body) {
|
||||
var resp = await fetch(BASE + '/api/v1/native' + path, {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify(body || {})
|
||||
});
|
||||
var data = await resp.json().catch(function () { return {}; });
|
||||
if (!resp.ok) throw new Error(data.detail || 'native API lỗi ' + resp.status);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function get(path) {
|
||||
var resp = await fetch(BASE + '/api/v1/native' + path, { headers: headers() });
|
||||
var data = await resp.json().catch(function () { return {}; });
|
||||
if (!resp.ok) throw new Error(data.detail || 'native API lỗi ' + resp.status);
|
||||
return data;
|
||||
}
|
||||
|
||||
var _masterTimer = null;
|
||||
|
||||
window.SonicNativeAudio = {
|
||||
status: function () {
|
||||
return get('/status');
|
||||
},
|
||||
// Master fader: debounce 40ms — fader kéo liên tục, chỉ gửi giá trị cuối.
|
||||
setMasterGain: function (gainDb) {
|
||||
if (_masterTimer) clearTimeout(_masterTimer);
|
||||
_masterTimer = setTimeout(function () {
|
||||
post('/set_master_gain', { gain_db: gainDb }).catch(function (e) {
|
||||
console.warn('[NativeAudio] set_master_gain:', e.message);
|
||||
});
|
||||
}, 40);
|
||||
},
|
||||
setTrackGainPan: function (trackId, gainDb, pan) {
|
||||
return post('/track_gain_pan', { track_id: trackId, gain_db: gainDb, pan: pan }).catch(function (e) {
|
||||
console.warn('[NativeAudio] track_gain_pan:', e.message);
|
||||
});
|
||||
},
|
||||
ensureSf: function (trackId, sfId, bank, program) {
|
||||
return post('/sf/ensure', { track_id: trackId, sf_id: sfId || null, bank: bank || 0, program: program || 0, live: true }).catch(function (e) {
|
||||
console.warn('[NativeAudio] sf/ensure:', e.message);
|
||||
});
|
||||
},
|
||||
noteOn: function (kind, trackId, channel, pitch, velocity) {
|
||||
var path = kind === 'vst2' ? '/vst2/note_on' : '/sf/note_on';
|
||||
return post(path, { track_id: trackId, channel: channel || 0, pitch: pitch, velocity: velocity != null ? velocity : 100 }).catch(function (e) {
|
||||
console.warn('[NativeAudio] note_on:', e.message);
|
||||
});
|
||||
},
|
||||
noteOff: function (kind, trackId, channel, pitch) {
|
||||
var path = kind === 'vst2' ? '/vst2/note_off' : '/sf/note_off';
|
||||
return post(path, { track_id: trackId, channel: channel || 0, pitch: pitch }).catch(function (e) {
|
||||
console.warn('[NativeAudio] note_off:', e.message);
|
||||
});
|
||||
},
|
||||
sfAudioStop: function (trackId) {
|
||||
return post('/sf/audio_stop', { track_id: trackId, pitch: 0 }).catch(function () {});
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -1,7 +1,9 @@
|
||||
// SonicForge TrackInstrument Service
|
||||
// Bọc FluidSynth channel per-track: playNote/noteOff theo TrackInstrumentCtx
|
||||
// Bọc engine per-track: playNote/noteOff theo TrackInstrumentCtx
|
||||
// (ch/program/synthEngine/sfId/bank/prog/dest) — dùng cho preview mọi nguồn
|
||||
// (keybed, piano roll, draw, hardware MIDI, click) qua UnifiedMidiRouter.
|
||||
// T15: native-first — SF track đi thẳng vào engine native (NativeAudioService
|
||||
// + SF host bridge), SonicSF WASM chỉ là fallback khi native không sẵn sàng.
|
||||
// Tạo mới mỗi note-on (overwrite registry) — voice cũ giữ engine cũ trong
|
||||
// tracker nên note-off luôn stop đúng channel.
|
||||
window.TrackInstrument = window.TrackInstrument || {};
|
||||
@@ -18,6 +20,11 @@ window.TrackInstrument = window.TrackInstrument || {};
|
||||
this.dest = ctx ? (ctx.dest || null) : null;
|
||||
}
|
||||
|
||||
// Native engine sẵn sàng cho track SF? (chỉ khi có sfId — đường native)
|
||||
TrackInstrument.prototype._nativeReady = function () {
|
||||
try { return !!(window.SonicNativeAudio && this.sfId); } catch (e) { return false; }
|
||||
};
|
||||
|
||||
// Đảm bảo channel đã select đúng instrument trước khi play (fire-and-forget:
|
||||
// playNote tự load + retry nếu SF chưa load xong — dedup trong loadSoundFont).
|
||||
TrackInstrument.prototype._ensure = function () {
|
||||
@@ -33,18 +40,40 @@ window.TrackInstrument = window.TrackInstrument || {};
|
||||
}
|
||||
};
|
||||
|
||||
// velocity: router đã normalize int 1-127 (unifiedMidiRouter.normalizeVelocity).
|
||||
TrackInstrument.prototype.playNote = function (pitch, velocity, durationMs, startTime) {
|
||||
var vel = velocity != null ? velocity : 100;
|
||||
if (this._nativeReady()) {
|
||||
try {
|
||||
var self = this;
|
||||
var dur = (durationMs != null ? durationMs : 500) || 500;
|
||||
// ensure native SF engine cho track (server dedup theo track_id)
|
||||
window.SonicNativeAudio.ensureSf(this.trackId, this.sfId, this.bank, this.prog)
|
||||
.catch(function () {});
|
||||
window.SonicNativeAudio.noteOn('sf', this.trackId, this.ch, pitch, vel);
|
||||
// ponytail: TrackInstrument không biết audioCtx → bỏ startTime
|
||||
// offset (delay = duration); thêm scheduling chính xác khi cần
|
||||
setTimeout(function () { self.noteOff(pitch); }, dur + 40);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn('[TrackInstrument] native playNote error:', e);
|
||||
}
|
||||
}
|
||||
// Fallback SonicSF (WASM)
|
||||
try {
|
||||
if (!window.SonicSF || !window.SonicSF.playNote) return;
|
||||
this._ensure();
|
||||
var dur = (durationMs != null ? durationMs : 500) || 500;
|
||||
window.SonicSF.playNote(pitch, velocity != null ? velocity : 0.8, dur, startTime, this.program, this.dest, this.ch, this.synthEngine);
|
||||
var durF = (durationMs != null ? durationMs : 500) || 500;
|
||||
window.SonicSF.playNote(pitch, velocity != null ? velocity : 0.8, durF, startTime, this.program, this.dest, this.ch, this.synthEngine);
|
||||
} catch (e) {
|
||||
console.warn('[TrackInstrument] playNote error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
TrackInstrument.prototype.noteOff = function (pitch) {
|
||||
if (this._nativeReady()) {
|
||||
try { window.SonicNativeAudio.noteOff('sf', this.trackId, this.ch, pitch); return; } catch (e) {}
|
||||
}
|
||||
try {
|
||||
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(this.ch, pitch);
|
||||
} catch (e) {}
|
||||
|
||||
Reference in New Issue
Block a user