fix: normalize peak audio render offline chống clip polyphony; TCP wheel điều chỉnh volume/pan

This commit is contained in:
2026-08-17 20:04:34 +07:00
parent dc331f39d8
commit bbde74f1a7
5 changed files with 344 additions and 210 deletions
+2
View File
@@ -1048,6 +1048,8 @@ async def soundfont_render(req: SoundfontRenderRequest, current_user: dict = Dep
bank=req.bank, program=req.program,
sr=req.sample_rate, bpm=req.bpm,
)
from app.core import native_render
audio = native_render.normalize_audio_peak(audio)
sf.write(out_path, audio.T, req.sample_rate)
return {
"success": True,
+30
View File
@@ -39,6 +39,35 @@ def find_bridge_exe() -> str:
return ""
def normalize_audio_peak(audio, target_db=-3.0):
"""Scale audio (numpy array, peak) về target_db nếu vượt; giữ nguyên nếu dưới.
Chống clip do note MIDI chồng (polyphony) — spec midi_polyphony_loudness §II:
render offline nhiều note đồng thời → tổng biên độ vượt 0dBFS → WAV vỡ đỉnh
(hard clip, méo harmonic) trước khi tới client; limiter master không cứu được.
"""
try:
import numpy as np
peak = float(np.max(np.abs(audio))) if audio.size else 0.0
limit = 10 ** (target_db / 20)
if peak > limit and peak > 1e-9:
return audio * (limit / peak)
except Exception:
pass
return audio
def normalize_peak_file(path, target_db=-3.0):
"""Post-render WAV: scan peak, vượt target_db → scale toàn file (giữ format)."""
try:
info = sf.info(path)
data, sr = sf.read(path, dtype="float32", always_2d=True)
data = normalize_audio_peak(data, target_db)
sf.write(path, data, sr, subtype=info.subtype)
except Exception:
pass
def classify_instrument(path: str) -> int:
"""InstrumentType enum của bridge: 0=VST3, 1=VST2, 2=SF2/SF3, 3=SFZ."""
low = path.lower()
@@ -139,6 +168,7 @@ def render_offline(instrument_id, notes, bpm, sample_rate, preset_id=None,
detail=f"Bridge không load được plugin {instrument_id}. {log[-500:]}")
raise HTTPException(status_code=500,
detail=f"Render thất bại (rc={r.returncode}). {log[-500:]}")
normalize_peak_file(out_path)
return out_path, sf.info(out_path).frames / float(sample_rate)
except HTTPException:
raise
+258 -175
View File
@@ -243,57 +243,227 @@ const stopAllNativeSfNotes = () => {
try { Object.keys(_nativeSfPreviews).forEach(function (k) { stopNativeSfNote(k); }); } catch (e) {}
};
// ── Native soundfont item render (standalone, transport) ──────────────────
// Render TOÀN BỘ MIDI item bằng native FluidSynth → decode AudioBuffer →
// schedule nguồn audio đúng vị trí item (giống audio clip). opts:
// ── Native track render (standalone, transport) ──────────────────────────────────────────────────────
// Render TOÀN BỘ MIDI items của track bằng 1 request (native FluidSynth cho
// soundfont, native_bridge --render cho VSTi) → decode → 1 BufferSource duy
// nhất cho cả track (như audio clip). Thay thế render per-item: RENDER_LOCK
// server serialize từng item → project nhiều item = delay vài chục giây.
// opts:
// baseOffsetSec: offset thêm (section: secStart) | limitSec: chặn tại secEnd
// isActive(): guard stop giữa chừng | sources: mảng nguồn để stop.
const scheduleNativeSfItem = (track, item, offsetTime, context, destNode, bpm, opts) => {
// isActive(): guard stop giữa chừng | sources: mảng nguồn để stop
// playheadNow(): playhead THẬT tại thời điểm resolve — render trễ vài chục
// giây → item vẫn schedule đúng vị trí playhead hiện tại (không chồng
// âm/wall-of-sound như dùng offsetTime cũ).
// notesOverride: dùng thay track.midiItems (piano roll tab).
// Epoch guard: stop/rebuild tăng _nativePlayEpoch → mọi render pending của
// lần play cũ resolve nhưng bị skip (không duplicate schedule).
let _nativePlayEpoch = 0;
const _nativeTrackUrls = {};
const _nativeTrackPending = {};
// ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─
// Dedup: note cùng pitch + cùng start_beat → gộp 1, giữ velocity cao hơn
// (tránh synth instantiate nhiều voice cùng pitch — spec §II.5).
// Legato cleanup: note trước cùng pitch kéo dài qua start_beat note sau → cắt
// 0.01 beats trước Note-On mới (tránh duplicate voice allocation).
const _cleanNativeNotes = (notes) => {
const out = [];
notes.forEach(function (n) {
const pitch = n.pitch || 60;
const start = n.start_beat || 0;
const dup = out.find(function (o) { return o.pitch === pitch && o.start_beat === start; });
if (dup) {
dup.velocity = Math.max(dup.velocity, n.velocity != null ? n.velocity : 0.8);
dup.duration_beats = Math.max(dup.duration_beats, n.duration_beats || 1);
return;
}
out.push({ pitch: pitch, start_beat: start, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8 });
});
out.forEach(function (n) {
let nextStart = Infinity;
out.forEach(function (m) {
if (m === n || m.pitch !== n.pitch || m.start_beat <= n.start_beat) return;
if (m.start_beat < nextStart) nextStart = m.start_beat;
});
if (nextStart < n.start_beat + n.duration_beats) {
n.duration_beats = Math.max(0.01, nextStart - n.start_beat - 0.01);
}
});
return out;
};
// Polyphony gain compensation (spec §II.1): scale velocity theo số voice đồng
// thời tại thời điểm note bắt đầu — Equal Power G = 1/sqrt(N). Solo N=1 → giữ
// nguyên; chord 8 note → -9dB mỗi voice, tổng peak không vượt trần.
const _scaleNativePolyphony = (notes) => {
notes.forEach(function (n) {
const nStart = n.start_beat || 0;
let active = 0;
notes.forEach(function (m) {
const mEnd = (m.start_beat || 0) + (m.duration_beats || 1);
if ((m.start_beat || 0) <= nStart && nStart < mEnd) active++;
});
if (active > 1) {
const g = 1 / Math.sqrt(active);
n.velocity = (n.velocity != null ? n.velocity : 0.8) * g;
}
});
return notes;
};
const _buildNativeTrackNotes = (track, bpm, baseOffsetSec, notesOverride) => {
const secPerBeat = 60.0 / (parseInt(bpm) || 120);
const items = notesOverride ? [{ startTime: 0, notes: notesOverride }] : ((track && track.midiItems) || []);
let trackStartAbs = Infinity;
const absNotes = [];
items.forEach(function (item) {
const itemStartAbs = (baseOffsetSec || 0) + (item.startTime || 0);
if (itemStartAbs < trackStartAbs) trackStartAbs = itemStartAbs;
(item.notes || []).forEach(function (n) {
absNotes.push({
pitch: n.pitch || 60,
start_beat: itemStartAbs / secPerBeat + (n.start_beat || 0),
duration_beats: n.duration_beats || 1,
velocity: n.velocity != null ? n.velocity : 0.8,
});
});
});
return { absNotes: _scaleNativePolyphony(_cleanNativeNotes(absNotes)), trackStartAbs: trackStartAbs };
};
const _nativeTrackKey = (track, bpm, baseOffsetSec, notesOverride) => {
try {
const eng = track && track.synth_engine;
const sfId = eng && eng.soundfont_id;
if (!sfId) return;
const notes = (item && item.notes) || [];
if (!notes.length) return;
const secPerBeat = 60.0 / (parseInt(bpm) || 120);
const baseOffsetSec = (opts && opts.baseOffsetSec) || 0;
const itemStartAbs = baseOffsetSec + (item.startTime || 0);
let itemEndAbs = itemStartAbs + 0.05;
notes.forEach(function (n) {
const end = itemStartAbs + ((n.start_beat || 0) + (n.duration_beats || 1)) * secPerBeat;
if (end > itemEndAbs) itemEndAbs = end;
});
window.SonicAPI.soundfontRender({
soundfont_id: sfId,
bank: (eng && eng.soundfont_bank) || 0,
program: (eng && eng.soundfont_program) || 0,
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId) || '';
const sfId = (eng && eng.soundfont_id) || '';
const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride);
const nk = data.absNotes.map(function (n) {
return n.pitch + ':' + n.start_beat + ':' + n.duration_beats + ':' + n.velocity;
}).join(',');
return [track && track.id ? track.id : '', instrumentId, sfId, (eng && eng.preset_id) || '', (eng && eng.preset_path) || '', parseFloat(bpm) || 120, baseOffsetSec || 0, nk].join('|');
} catch (e) { return ''; }
};
const _renderNativeTrack = (track, bpm, baseOffsetSec, notesOverride) => {
try {
const eng = track && track.synth_engine;
const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride);
if (!data.absNotes.length) return Promise.resolve(null);
if (isSfTrackEngine(eng)) {
const sfId = eng && eng.soundfont_id;
if (!sfId) return Promise.resolve(null);
return window.SonicAPI.soundfontRender({
soundfont_id: sfId,
bank: (eng && eng.soundfont_bank) || 0,
program: (eng && eng.soundfont_program) || 0,
bpm: parseFloat(bpm) || 120,
notes: data.absNotes,
}).then(function (res) {
if (!res || !res.success || !res.url) return null;
return API_BASE_URL + res.url;
}).catch(function () { return null; });
}
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId);
if (!instrumentId || !isVstTrackEngine(eng)) return Promise.resolve(null);
return window.SonicAPI.midiRender({
instrument_id: instrumentId,
notes: data.absNotes,
bpm: parseFloat(bpm) || 120,
notes: notes.map(function (n) { return { pitch: n.pitch || 60, start_beat: n.start_beat || 0, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8 }; }),
preset_id: (eng && eng.preset_id) || undefined,
preset_path: (eng && eng.preset_path) || undefined,
preset_data: (eng && eng.preset_data) || undefined,
}).then(function (res) {
if (!res || !res.success || !res.url) return;
fetch(API_BASE_URL + res.url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) {
context.decodeAudioData(buf, function (audioBuf) {
if (!res || !res.success || !res.url) return null;
return API_BASE_URL + res.url;
}).catch(function () { return null; });
} catch (e) { return Promise.resolve(null); }
};
// Dedupe in-flight render + cache WAV: play lặp/re-schedule trong lúc render
// đang chạy → chờ cùng promise, không bắn duplicate request (tránh queue sau
// RENDER_LOCK + render 2 lần cùng nội dung).
const ensureRenderNativeTrack = (track, bpm, baseOffsetSec, notesOverride) => {
const key = _nativeTrackKey(track, bpm, baseOffsetSec, notesOverride);
if (!key) return _renderNativeTrack(track, bpm, baseOffsetSec, notesOverride);
if (_nativeTrackUrls[key]) return Promise.resolve(_nativeTrackUrls[key]);
if (_nativeTrackPending[key]) return _nativeTrackPending[key];
const p = _renderNativeTrack(track, bpm, baseOffsetSec, notesOverride).then(function (url) {
if (url) _nativeTrackUrls[key] = url;
return url;
}).then(function (url) {
delete _nativeTrackPending[key];
return url;
});
_nativeTrackPending[key] = p;
return p;
};
// Giới hạn decodeAudioData song song (max 3) — nhiều WAV fetch về cùng lúc →
// main thread nghẽn → UI đơ, Stop không phản hồi.
let _nativeDecodeActive = 0;
const _nativeDecodeQueue = [];
const _pumpNativeDecodes = () => {
while (_nativeDecodeActive < 3 && _nativeDecodeQueue.length) {
const job = _nativeDecodeQueue.shift();
_nativeDecodeActive++;
try {
job.ctx.decodeAudioData(job.buf, function (ab) {
_nativeDecodeActive--;
job.cb(ab);
_pumpNativeDecodes();
}, function () {
_nativeDecodeActive--;
job.cb(null);
_pumpNativeDecodes();
});
} catch (e) {
_nativeDecodeActive--;
job.cb(null);
_pumpNativeDecodes();
}
}
};
const _decodeNativeQueue = (ctx, buf, cb) => {
_nativeDecodeQueue.push({ ctx: ctx, buf: buf, cb: cb });
_pumpNativeDecodes();
};
const scheduleNativeTrack = (track, offsetTime, context, destNode, bpm, opts) => {
try {
const eng = track && track.synth_engine;
if (!isSfTrackEngine(eng) && !isVstTrackEngine(eng)) return;
const baseOffsetSec = (opts && opts.baseOffsetSec) || 0;
const notesOverride = (opts && opts.notesOverride) || null;
const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride);
if (!data.absNotes.length) return;
const key = _nativeTrackKey(track, bpm, baseOffsetSec, notesOverride);
const epoch = _nativePlayEpoch;
const scheduleUrl = function (url) {
if (!url) return;
fetch(url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) {
_decodeNativeQueue(context, buf, function (audioBuf) {
try {
if (!audioBuf) return;
if (epoch !== _nativePlayEpoch) return; // stale play (stop/rebuild)
if (opts && typeof opts.isActive === 'function' && !opts.isActive()) return;
const ph = (opts && typeof opts.playheadNow === 'function') ? opts.playheadNow() : offsetTime;
const src = context.createBufferSource();
src.buffer = audioBuf;
src.connect(destNode);
if (offsetTime < itemStartAbs) {
src.start(context.currentTime + (itemStartAbs - offsetTime), 0);
} else if (offsetTime < itemEndAbs) {
src.start(context.currentTime, Math.min(offsetTime - itemStartAbs, Math.max(0, audioBuf.duration - 0.05)));
const trackEndAbs = data.trackStartAbs + audioBuf.duration;
if (ph < data.trackStartAbs) {
src.start(context.currentTime + (data.trackStartAbs - ph), 0);
} else if (ph < trackEndAbs) {
src.start(context.currentTime, Math.min(ph - data.trackStartAbs, Math.max(0, audioBuf.duration - 0.05)));
} else {
return;
}
if (opts && opts.limitSec && opts.limitSec < itemStartAbs + audioBuf.duration) {
src.stop(context.currentTime + Math.max(0.02, opts.limitSec - Math.max(offsetTime, itemStartAbs)));
if (opts && opts.limitSec && opts.limitSec < trackEndAbs) {
src.stop(context.currentTime + Math.max(0.02, opts.limitSec - Math.max(ph, data.trackStartAbs)));
}
if (opts && opts.sources && opts.sources.push) opts.sources.push(src);
} catch (e) { console.warn('[NativeSF] schedule decode error:', e); }
}, function () {});
} catch (e) { console.warn('[NativeTrack] schedule decode error:', e); }
});
}).catch(function () {});
}).catch(function () {});
} catch (e) { console.warn('[NativeSF] scheduleNativeSfItem error:', e); }
};
if (key && _nativeTrackUrls[key]) { scheduleUrl(_nativeTrackUrls[key]); return; }
ensureRenderNativeTrack(track, bpm, baseOffsetSec, notesOverride).then(function (url) {
if (!url) return;
scheduleUrl(url);
});
} catch (e) { console.warn('[NativeTrack] scheduleNativeTrack error:', e); }
};
// ── Native VSTi preview (standalone, bridge inactive) ─────────────────────
@@ -344,131 +514,29 @@ const stopNativeVstNote = (key) => {
const stopAllNativeVstNotes = () => {
try { Object.keys(_nativeVstPreviews).forEach(function (k) { stopNativeVstNote(k); }); } catch (e) {}
};
// URL cache cho midi-render: key = fingerprint (item+instrument+preset+notes)
// → re-play cùng item dùng lại WAV đã render, không chờ round-trip.
const _nativeVstItemUrls = {};
const _nativeVstItemKey = (track, item, bpm) => {
try {
const eng = track && track.synth_engine;
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId) || '';
const notes = (item && item.notes) || [];
const nk = notes.map(function (n) {
return (n.pitch || 60) + ':' + (n.start_beat || 0) + ':' + (n.duration_beats || 1) + ':' + (n.velocity != null ? n.velocity : 0.8);
}).join(',');
return [item && item.id ? item.id : (instrumentId + ':' + nk), instrumentId, (eng && eng.preset_id) || '', (eng && eng.preset_path) || '', bpm].join('|') + '|' + nk;
} catch (e) { return ''; }
};
const _renderNativeVstItem = (track, item, bpm) => {
try {
const eng = track && track.synth_engine;
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId);
if (!instrumentId || !isVstTrackEngine(eng)) return Promise.resolve(null);
const notes = (item && item.notes) || [];
if (!notes.length) return Promise.resolve(null);
return window.SonicAPI.midiRender({
instrument_id: instrumentId,
notes: notes.map(function (n) { return { pitch: n.pitch || 60, start_beat: n.start_beat || 0, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8 }; }),
bpm: parseFloat(bpm) || 120,
preset_id: (eng && eng.preset_id) || undefined,
preset_path: (eng && eng.preset_path) || undefined,
preset_data: (eng && eng.preset_data) || undefined,
}).then(function (res) {
if (!res || !res.success || !res.url) return null;
return API_BASE_URL + res.url;
}).catch(function () { return null; });
} catch (e) { return Promise.resolve(null); }
};
// Dedupe in-flight render: play trong lúc pre-render đang chạy → chờ cùng promise,
// không bắn duplicate request (tránh queue sau RENDER_LOCK + render 2 lần).
const _nativeVstItemPending = {};
const ensureRenderNativeVstItem = (track, item, bpm) => {
const key = _nativeVstItemKey(track, item, bpm);
if (!key) return _renderNativeVstItem(track, item, bpm);
if (_nativeVstItemUrls[key]) return Promise.resolve(_nativeVstItemUrls[key]);
if (_nativeVstItemPending[key]) return _nativeVstItemPending[key];
const p = _renderNativeVstItem(track, item, bpm).then(function (url) {
if (url) _nativeVstItemUrls[key] = url;
return url;
}).then(function (url) {
delete _nativeVstItemPending[key];
return url;
});
_nativeVstItemPending[key] = p;
return p;
};
let _prerenderNativeVstTimer = null;
const prerenderNativeVstItems = (tracks, bpm) => {
// ── Native track pre-render (standalone) ──────────────────────────────────────────────────────────────
// Render CẢ TRACK 1 request khi project load → play lần đầu không chờ render.
let _prerenderNativeTracksTimer = null;
const prerenderNativeTracks = (tracks, bpm) => {
try {
if (!isStandaloneSf()) return;
if (_prerenderNativeVstTimer) { clearTimeout(_prerenderNativeVstTimer); _prerenderNativeVstTimer = null; }
_prerenderNativeVstTimer = setTimeout(function () {
_prerenderNativeVstTimer = null;
if (_prerenderNativeTracksTimer) { clearTimeout(_prerenderNativeTracksTimer); _prerenderNativeTracksTimer = null; }
_prerenderNativeTracksTimer = setTimeout(function () {
_prerenderNativeTracksTimer = null;
const seen = {};
(tracks || []).forEach(function (track) {
const nativeVst = isStandaloneSf() && isVstTrackEngine(track && track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (!nativeVst) return;
(track.midiItems || []).forEach(function (item) {
const key = _nativeVstItemKey(track, item, bpm);
if (!key || seen[key] || _nativeVstItemUrls[key]) return;
seen[key] = true;
ensureRenderNativeVstItem(track, item, bpm).then(function () {});
});
const eng = track && track.synth_engine;
const native = isStandaloneSf() && (isVstTrackEngine(eng) || isSfTrackEngine(eng)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (!native) return;
const key = _nativeTrackKey(track, bpm, 0, null);
if (!key || seen[key] || _nativeTrackUrls[key]) return;
seen[key] = true;
ensureRenderNativeTrack(track, bpm, 0, null).then(function () {});
});
}, 800);
} catch (e) {}
};
// ── Native VSTi item render (standalone, transport) ────────────────────────
// Render TOÀN BỘ MIDI item bằng pedalboard (VST3) → decode AudioBuffer →
// schedule giống scheduleNativeSfItem. opts giống hệt nativeSf.
const scheduleNativeVstItem = (track, item, offsetTime, context, destNode, bpm, opts) => {
try {
const eng = track && track.synth_engine;
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId);
if (!instrumentId || !isVstTrackEngine(eng)) return;
const notes = (item && item.notes) || [];
if (!notes.length) return;
const secPerBeat = 60.0 / (parseInt(bpm) || 120);
const baseOffsetSec = (opts && opts.baseOffsetSec) || 0;
const itemStartAbs = baseOffsetSec + (item.startTime || 0);
let itemEndAbs = itemStartAbs + 0.05;
notes.forEach(function (n) {
const end = itemStartAbs + ((n.start_beat || 0) + (n.duration_beats || 1)) * secPerBeat;
if (end > itemEndAbs) itemEndAbs = end;
});
const key = _nativeVstItemKey(track, item, bpm);
const scheduleUrl = function (url) {
if (!url) return;
fetch(url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) {
context.decodeAudioData(buf, function (audioBuf) {
try {
if (opts && typeof opts.isActive === 'function' && !opts.isActive()) return;
const src = context.createBufferSource();
src.buffer = audioBuf;
src.connect(destNode);
if (offsetTime < itemStartAbs) {
src.start(context.currentTime + (itemStartAbs - offsetTime), 0);
} else if (offsetTime < itemEndAbs) {
src.start(context.currentTime, Math.min(offsetTime - itemStartAbs, Math.max(0, audioBuf.duration - 0.05)));
} else {
return;
}
if (opts && opts.limitSec && opts.limitSec < itemStartAbs + audioBuf.duration) {
src.stop(context.currentTime + Math.max(0.02, opts.limitSec - Math.max(offsetTime, itemStartAbs)));
}
if (opts && opts.sources && opts.sources.push) opts.sources.push(src);
} catch (e) { console.warn('[NativeVST] schedule decode error:', e); }
}, function () {});
}).catch(function () {});
};
if (key && _nativeVstItemUrls[key]) { scheduleUrl(_nativeVstItemUrls[key]); return; }
ensureRenderNativeVstItem(track, item, bpm).then(function (url) {
if (!url) return;
scheduleUrl(url);
});
} catch (e) { console.warn('[NativeVST] scheduleNativeVstItem error:', e); }
};
// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink() {
try {
@@ -17589,7 +17657,7 @@ const App = () => {
loadAudioBuffersForTracks(restoredTracks).catch(function(err) { console.warn('loadAudioBuffersForTracks error:', err); });
trackMidiChannelsRef.current = {};
preloadTrackInstruments(restoredTracks).catch(function(err) { console.warn('preloadTrackInstruments error:', err); });
prerenderNativeVstItems(restoredTracks, restoredBpm);
prerenderNativeTracks(restoredTracks, restoredBpm);
setBpm(restoredBpm.toString());
setSelectedTrackId(restoredTracks[0]?.id || '1');
setProjectName(lastName);
@@ -17728,7 +17796,7 @@ const App = () => {
if (restoredTracks.length > 0) {
setTracks(restoredTracks);
setBpm(restoredBpm.toString());
prerenderNativeVstItems(restoredTracks, restoredBpm);
prerenderNativeTracks(restoredTracks, restoredBpm);
if (restoredSessionTabs.length > 0) {
setSessionTabs(restoredSessionTabs);
}
@@ -22281,10 +22349,8 @@ const App = () => {
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const nativeVst = isStandaloneSf() && !routeCarla && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf || nativeVst)) {
if (nativeSf) {
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
} else if (nativeVst) {
midiItems.forEach(item => scheduleNativeVstItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
if (nativeSf || nativeVst) {
scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
} else {
// Preview cache: capture the soundfont (pre track-FX) for fast offline export
ensureMidiCapture(track, activeTrackNodesRef.current[track.id]);
@@ -22447,12 +22513,8 @@ const App = () => {
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const subNativeVst = isStandaloneSf() && !subRouteCarla && isVstTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf || subNativeVst)) {
if (subNativeSf) {
subMidiItems.forEach(item => scheduleNativeSfItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
return; // native render → audio buffer (không qua WASM/Carla per-note)
}
if (subNativeVst) {
subMidiItems.forEach(item => scheduleNativeVstItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
if (subNativeSf || subNativeVst) {
scheduleNativeTrack(subTrack, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
return; // native render → audio buffer (không qua WASM/Carla per-note)
}
subMidiItems.forEach(item => {
@@ -22585,12 +22647,8 @@ const App = () => {
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
const nativeVst = isStandaloneSf() && !routeCarla && isVstTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf || nativeVst)) {
if (nativeSf) {
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
return; // native render → audio buffer (không qua WASM/Carla per-note)
}
if (nativeVst) {
midiItems.forEach(item => scheduleNativeVstItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
if (nativeSf || nativeVst) {
scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
return; // native render → audio buffer (không qua WASM/Carla per-note)
}
var lcCh = assignTrackMidiChannel(track, tracks);
@@ -22686,12 +22744,8 @@ const App = () => {
// future (silence when pressing play). Ghost notes are already relative to
// the item window, so no absolute-session offset is applied anywhere here.
const routeCarla = shouldRouteCarla(synthEngine);
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
scheduleNativeSfItem(track, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
return; // native render cả tab → audio buffer (không per-note WASM/Carla)
}
if (isStandaloneSf() && !routeCarla && isVstTrackEngine(synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
scheduleNativeVstItem(track, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
if (isStandaloneSf() && !routeCarla && (isSfTrackEngine(synthEngine) || isVstTrackEngine(synthEngine)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
scheduleNativeTrack(track, offsetSeconds, context, destNode, bpm, { notesOverride: midiNotes, playheadNow: () => startOffsetTimeRef.current + (getAudioContext().currentTime - startAudioTimeRef.current), sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
return; // native render cả tab → audio buffer (không per-note WASM/Carla)
}
midiNotes.forEach(note => {
@@ -22857,6 +22911,7 @@ const App = () => {
const stopAllPlayback = () => {
try {
isPlayingRef.current = false;
_nativePlayEpoch++; // hủy mọi render/decode pending của lần play cũ
// V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer
// cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref).
bridgeNoteEpochRef.current++;
@@ -30124,6 +30179,13 @@ STRICT CONSTRAINTS:
step: "0.5",
value: track.volumeDb ?? 0,
onChange: e => updateTrackVolumeDb(track.id, parseFloat(e.target.value)),
onWheel: e => {
e.preventDefault();
const step = e.deltaY > 0 ? -0.5 : 0.5;
const nv = Math.max(-50, Math.min(7, (track.volumeDb ?? 0) + step));
updateTrackVolumeDb(track.id, nv);
},
onDoubleClick: e => { e.stopPropagation(); updateTrackVolumeDb(track.id, 0); },
className: "flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-cyan-500",
style: {
height: '4px'
@@ -30141,6 +30203,13 @@ STRICT CONSTRAINTS:
step: "1",
value: track.pan ?? 0,
onChange: e => updateTrackPan(track.id, parseInt(e.target.value)),
onWheel: e => {
e.preventDefault();
const step = e.deltaY > 0 ? -5 : 5;
const np = Math.max(-100, Math.min(100, (track.pan ?? 0) + step));
updateTrackPan(track.id, np);
},
onDoubleClick: e => { e.stopPropagation(); updateTrackPan(track.id, 0); },
className: "flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-purple-500",
style: {
height: '4px'
@@ -30665,6 +30734,13 @@ STRICT CONSTRAINTS:
id: `tcp-vol-${st.id}`,
value: vTrack.volumeDb ?? 0,
onChange: e => updateTrackVolumeDb(vTrack.id, parseFloat(e.target.value)),
onWheel: e => {
e.preventDefault();
const step = e.deltaY > 0 ? -0.5 : 0.5;
const nv = Math.max(-50, Math.min(7, (vTrack.volumeDb ?? 0) + step));
updateTrackVolumeDb(vTrack.id, nv);
},
onDoubleClick: e => { e.stopPropagation(); updateTrackVolumeDb(vTrack.id, 0); },
className: "flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-cyan-500",
style: {
height: '4px'
@@ -30684,6 +30760,13 @@ STRICT CONSTRAINTS:
id: `tcp-pan-${st.id}`,
value: vTrack.pan ?? 0,
onChange: e => updateTrackPan(vTrack.id, parseInt(e.target.value)),
onWheel: e => {
e.preventDefault();
const step = e.deltaY > 0 ? -5 : 5;
const np = Math.max(-100, Math.min(100, (vTrack.pan ?? 0) + step));
updateTrackPan(vTrack.id, np);
},
onDoubleClick: e => { e.stopPropagation(); updateTrackPan(vTrack.id, 0); },
className: "flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-purple-500",
style: {
height: '4px'
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -50,7 +50,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608152100" defer></script>
<script src="/static/js/app.precompiled.js?v=20260817185530" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {