fix: normalize peak audio render offline chống clip polyphony; TCP wheel điều chỉnh volume/pan
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user