Merge main (V45.1-32 fixes: ReferenceError guard, crackle/solo/seek fixes) vao standalone-shm-bridge (giu FILL 96)
This commit is contained in:
@@ -62,3 +62,6 @@ native_bridge/debug/shm_selfcheck.exe
|
||||
install/daw_vst_bridge
|
||||
install/libsfizz.so.1
|
||||
build_juce
|
||||
|
||||
# build artifact (root, khong phai deploy dir)
|
||||
target/
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Any
|
||||
from app.config import settings
|
||||
from app.core.vst_engine import PluginManager, HAS_PYFLUIDSYNTH, render_soundfont_midi_to_audio, RENDER_LOCK, _render_locked
|
||||
from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_default_sf2
|
||||
from app.core.soundfont_inspector import SoundFontInspector
|
||||
from app.core.soundfont_converter import SoundFontConverter
|
||||
from app.core.soundfont_scanner import SoundFontAutoScanner
|
||||
@@ -593,6 +590,7 @@ async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_
|
||||
|
||||
@router.get("/soundfonts/download/{sf_id}")
|
||||
async def download_soundfont_asset(sf_id: str):
|
||||
from app.core.render_engine import _find_sf2_path, _find_default_sf2
|
||||
clean_id = sf_id.replace("sf_", "") if sf_id.startswith("sf_") else sf_id
|
||||
# Cũng tìm trong static/soundfonts (font bundled theo deployment) — trước
|
||||
# đây chỉ UPLOAD + SYSTEM → font bundled 404 → incognito (IndexedDB rỗng)
|
||||
@@ -986,6 +984,7 @@ class FxRenderRequest(BaseModel):
|
||||
@router.post("/fx-render")
|
||||
async def fx_render(req: FxRenderRequest, current_user: dict = Depends(get_current_user)):
|
||||
"""Áp fx_chain lên WAV đầu vào → WAV mới trong PROCESSED_DIR."""
|
||||
import soundfile as sf
|
||||
enforce_password_changed(current_user)
|
||||
src = (req.input_path or "").strip()
|
||||
if not src:
|
||||
@@ -1265,6 +1264,8 @@ async def soundfont_render(req: SoundfontRenderRequest, current_user: dict = Dep
|
||||
Khác FluidSynthWASM (docker): âm render server-side bằng pyfluidsynth, trả
|
||||
WAV để client play. Dùng cho: preview/keyboard, play MIDI item khi chạy
|
||||
transport ở môi trường standalone."""
|
||||
from app.core.render_engine import _find_sf2_path, _find_default_sf2
|
||||
import soundfile as sf
|
||||
enforce_password_changed(current_user)
|
||||
if not req.notes:
|
||||
raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để render")
|
||||
@@ -1320,6 +1321,9 @@ def render_project(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
enforce_password_changed(current_user)
|
||||
from app.core.render_engine import PythonRenderEngine
|
||||
import soundfile as sf
|
||||
import numpy as np
|
||||
engine = PythonRenderEngine()
|
||||
# Prevent path traversal: strip directory components; force .wav/.ogg.
|
||||
safe_name = os.path.basename((req.output_filename or "render_output.wav").replace("\\", "/"))
|
||||
|
||||
@@ -23,10 +23,7 @@ import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.config import settings
|
||||
from app.core.native_render import find_fx_bridge_exe, find_juce_fx_bridge_exe
|
||||
|
||||
FXRT_MAGIC = 0x46585254
|
||||
FXRT_BLOCK = 256
|
||||
@@ -91,6 +88,7 @@ class FxRealtimeSession:
|
||||
"""Một session realtime: SHM + subprocess fx_vst_bridge + I/O block."""
|
||||
|
||||
def __init__(self, fx_chain, sample_rate, block_size=FXRT_BLOCK):
|
||||
import numpy as np
|
||||
from multiprocessing import shared_memory
|
||||
self.fx_chain = fx_chain or []
|
||||
self.sample_rate = int(sample_rate)
|
||||
@@ -163,6 +161,7 @@ class FxRealtimeSession:
|
||||
return out
|
||||
|
||||
def _spawn(self):
|
||||
from app.core.native_render import find_fx_bridge_exe, find_juce_fx_bridge_exe
|
||||
juce_exe = os.getenv("SF_JUCE_FX_BRIDGE_PATH", "").strip()
|
||||
# G3: mặc định dùng JUCE bridge (latency graph thật + processed samples);
|
||||
# fallback bridge cũ khi không có exe JUCE (regression an toàn).
|
||||
@@ -265,6 +264,7 @@ class FxRealtimeSession:
|
||||
"""Gom FXRT_IN_SLOTS frame vào pending, đủ 4 → publish atomic vào ring
|
||||
(ghi data toàn bộ slot trước, sau đó in_write += n 1 lần). Ring đầy khi
|
||||
publish → drop batch cũ nhất (giữ latency thấp)."""
|
||||
import numpy as np
|
||||
arr = np.frombuffer(interleaved, dtype=np.float32)
|
||||
if arr.size != self.block_size * 2:
|
||||
return
|
||||
@@ -333,6 +333,7 @@ class FxRealtimeSession:
|
||||
self.stats['flush'] += 1
|
||||
|
||||
def read_output(self):
|
||||
import numpy as np
|
||||
"""Drain output ring → list bytes (mỗi block 2048 bytes interleaved)."""
|
||||
out = []
|
||||
with self._lock:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# SonicForge Studio VST / VSTi Engine Service
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import time as _time
|
||||
import numpy as np
|
||||
import functools
|
||||
import threading
|
||||
from ctypes import c_char_p
|
||||
@@ -52,6 +52,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int =
|
||||
Xử lí đúng note CHỒNG nhau + đuôi release (event-stream noteon/noteoff).
|
||||
Trả stereo float32 (2, N). Raise RuntimeError nếu pyfluidsynth thiếu / SF
|
||||
không load được."""
|
||||
import numpy as np
|
||||
import fluidsynth as _fs
|
||||
settings = _fs.new_fluid_settings()
|
||||
_fs.fluid_settings_setnum(settings, b'synth.sample-rate', float(sr))
|
||||
@@ -112,6 +113,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int =
|
||||
|
||||
@_render_locked
|
||||
def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray:
|
||||
import numpy as np
|
||||
beat_duration_sec = 60.0 / max(30.0, bpm)
|
||||
max_duration_sec = 2.0
|
||||
for event in midi_events:
|
||||
|
||||
+525
-138
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@
|
||||
var _ctx = null;
|
||||
var _initialized = false;
|
||||
var _useWorklet = false;
|
||||
var _workletStats = null; // stats tu worklet (overrun/underrun) — getDiag
|
||||
var _dest = null; // destination gain node da noi (chong connect trung)
|
||||
|
||||
function _getCtx() {
|
||||
@@ -31,7 +32,7 @@
|
||||
console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback');
|
||||
return false;
|
||||
}
|
||||
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608272301';
|
||||
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608311858';
|
||||
_ctx.audioWorklet.addModule(url).then(function () {
|
||||
if (!_initialized) return; // disconnect() happened while loading
|
||||
if (_useWorklet) return; // already set up (init retry)
|
||||
@@ -42,6 +43,9 @@
|
||||
outputChannelCount: [2]
|
||||
});
|
||||
_workletNode.connect(_gainNode);
|
||||
_workletNode.port.onmessage = function (ev) {
|
||||
if (ev.data && ev.data.type === 'stats') _workletStats = ev.data;
|
||||
};
|
||||
_useWorklet = true;
|
||||
// drain pre-buffer into worklet (order preserved)
|
||||
while (_preBuffer.length) {
|
||||
@@ -168,7 +172,8 @@
|
||||
hasWorkletNode: !!_workletNode,
|
||||
preBufferLen: _preBuffer.length,
|
||||
chunksLen: _chunks.length,
|
||||
gain: _gainNode ? _gainNode.gain.value : -1
|
||||
gain: _gainNode ? _gainNode.gain.value : -1,
|
||||
ws: _workletStats
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
// app/static/js/services/bridgeAudioWorklet.js
|
||||
// VSTi crackle fix: audio sink chay tren audio thread (khong jank main thread).
|
||||
// - Ring block 256 mau, toi da 8 block (~42ms @48k) — hap thu jitter pump
|
||||
// nhung gioi han backlog: 64 block (~371ms) tao tre ~0.3-0.4s khi play/
|
||||
// keyboard preview (audio moi phai cho ring cu chay het).
|
||||
// - Ring block 256 mau, toi da 32 block (~170ms @48k) — hap thu jitter pump
|
||||
// (V45.27: 16->32 de dung crackle VST2 loop/tremolo — render loop C++ bi
|
||||
// stall khi instrument xu li loop/tremolo nang CPU; 8->16 truoc do da dung
|
||||
// tremolo crackle, 16 van chua du cho stall >85ms). Flush o play start
|
||||
// (app.jsx) xoa ring nen do tre khong tang: ring thuong can o preview.
|
||||
// - Underrun: fade ve 0 thay vi click (hard silence).
|
||||
// - Resume sau silence: fade vao tu 0.
|
||||
// - Overrun: drop block cu nhat + crossfade 64 mau tai diem drop (het click).
|
||||
// - Stats: dem overrun/underrun, gui ve main thread moi ~0.5s — kiem tra
|
||||
// con crackle nao la do ring can (underrun) hay ring tran (overrun).
|
||||
class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this._ring = [];
|
||||
this._MAX = 8;
|
||||
this._MAX = 32;
|
||||
this._gain = 1.0;
|
||||
this._lastOutL = 0.0;
|
||||
this._lastOutR = 0.0;
|
||||
this._skipFade = 0; // >0: crossfade dang chay sau overrun drop
|
||||
this._skipFade = 0; // >0: con lai cua crossfade dang chay sau overrun drop
|
||||
this._fadeLen = 0; // tong chieu dai crossfade hien tai (cap 1024)
|
||||
this._skipFromL = 0.0;
|
||||
this._skipFromR = 0.0;
|
||||
this._wasUnderrun = false;
|
||||
this._statAccum = 0;
|
||||
this._stats = { overruns: 0, underruns: 0 };
|
||||
this.port.onmessage = (e) => {
|
||||
const m = e.data;
|
||||
if (!m) return;
|
||||
@@ -24,13 +32,19 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
|
||||
this._ring.push({ l: m.l, r: m.r });
|
||||
while (this._ring.length > this._MAX) {
|
||||
// overrun: bo block cu nhat; lan consume toi se crossfade ranh gioi
|
||||
// V45.32: crossfade tich luy — moi block drop cong 256 mau fade, cap
|
||||
// 1024 (~21ms) thay vi co dinh 64 mau (1.3ms qua ngan -> blip/wow khi
|
||||
// drop loat block o chord nhieu voice = crackle VST2 tremolo/loop)
|
||||
this._ring.shift();
|
||||
this._skipFade = 64;
|
||||
this._skipFade = Math.min(this._skipFade + 256, 1024);
|
||||
this._fadeLen = this._skipFade;
|
||||
this._stats.overruns++;
|
||||
}
|
||||
} else if (m.type === 'flush') {
|
||||
this._ring.length = 0;
|
||||
this._gain = 1.0;
|
||||
this._skipFade = 0;
|
||||
this._fadeLen = 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -43,12 +57,14 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
|
||||
let w = 0;
|
||||
while (w < n) {
|
||||
if (this._ring.length) {
|
||||
if (this._wasUnderrun) this._wasUnderrun = false;
|
||||
const blk = this._ring[0];
|
||||
const avail = blk.l.length;
|
||||
const take = Math.min(avail, n - w);
|
||||
if (this._skipFade > 0) {
|
||||
const done = this._fadeLen - this._skipFade;
|
||||
for (let i = 0; i < take; i++) {
|
||||
const t = i / 64;
|
||||
const t = (done + i) / this._fadeLen;
|
||||
L[w + i] = (this._lastOutL * (1 - t) + blk.l[i] * t) * this._gain;
|
||||
R[w + i] = (this._lastOutR * (1 - t) + blk.r[i] * t) * this._gain;
|
||||
}
|
||||
@@ -70,7 +86,8 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
|
||||
blk.r = blk.r.subarray(take);
|
||||
}
|
||||
} else {
|
||||
// underrun: fade ve 0 (khong click)
|
||||
// underrun: fade ve 0 (khong click) — dem 1 lan moi lan roi vao trang thai trong
|
||||
if (!this._wasUnderrun) { this._wasUnderrun = true; this._stats.underruns++; }
|
||||
for (let i = w; i < n; i++) {
|
||||
this._gain = Math.max(0, this._gain - 1.0 / 256.0);
|
||||
L[i] = this._lastOutL * this._gain;
|
||||
@@ -81,6 +98,20 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor {
|
||||
w = n;
|
||||
}
|
||||
}
|
||||
// Stats: gui ve main thread moi ~0.5s (24000 mau @48k) — log overrun/underrun
|
||||
this._statAccum += n;
|
||||
if (this._statAccum >= 24000) {
|
||||
this._statAccum = 0;
|
||||
try {
|
||||
this.port.postMessage({
|
||||
type: 'stats',
|
||||
overruns: this._stats.overruns,
|
||||
underruns: this._stats.underruns,
|
||||
ring: this._ring.length,
|
||||
max: this._MAX
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
|
||||
onStatusChange: function (cb) { this._statusCb = cb; },
|
||||
_notifyStatus: function (connected) {
|
||||
this.isBridgeConnected = !!connected;
|
||||
if (this._statusCb) this._statusCb({ connected: !!connected });
|
||||
connected = !!connected;
|
||||
// Loop guard (V45.7): poller queryStatus moi 400ms tung goi cb du connected
|
||||
// khong doi -> App reset map + reload toan bo track -> mark loading lai ->
|
||||
// poller lai thay loading -> LOAD skip vo han. Chi fire khi TRANG THAI DOI.
|
||||
if (this.isBridgeConnected === connected) return;
|
||||
this.isBridgeConnected = connected;
|
||||
if (this._statusCb) this._statusCb({ connected: connected });
|
||||
},
|
||||
|
||||
/** Fix C: re-establish bridge routing once audio frames arrive again
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview
|
||||
|
||||
var _lastTimelineDbgTs = 0;
|
||||
// V45.26: dem note live dang giu + lan live event gan nhat — quyet dinh co
|
||||
// clear master FX outQueue khong (chi idle->active; KHONG khi dang phat lien tuc).
|
||||
var _liveHeldCount = 0;
|
||||
var _lastLiveEventAt = 0;
|
||||
|
||||
var router = {
|
||||
bridgeConnected: false,
|
||||
@@ -59,7 +63,23 @@
|
||||
// V31: live note (ARM keyboard preview) — clear master FX outQueue backlog
|
||||
// (idle block ~341ms) truoc khi note moi render; timeline note (live=false)
|
||||
// KHONG clear — tranh drop audio dang phat.
|
||||
if (opts.live && opts.cmd === 'NOTE_ON') { try { if (window.__clearMasterFxQueue) window.__clearMasterFxQueue(); } catch (e) {} }
|
||||
// V45.26: chi clear khi THAT SU idle -> active (khong con note dang giu
|
||||
// VA im lang >500ms). Truoc day clear MOI note-on live -> rapid 1/8 lien
|
||||
// tuc: moi note wipe outQueue (dang chua tail note truoc + resume fade-in)
|
||||
// -> wobble/crackle (bug user). Note dang ngan / chuoi note nhanh -> giu
|
||||
// queue -> tail note truoc khong bi cat.
|
||||
if (opts.live) {
|
||||
try {
|
||||
var _nowEv = (window.performance && performance.now) ? performance.now() : Date.now();
|
||||
if (opts.cmd === 'NOTE_ON') {
|
||||
if (_liveHeldCount === 0 && (_nowEv - _lastLiveEventAt) > 500 && window.__clearMasterFxQueue) window.__clearMasterFxQueue();
|
||||
_liveHeldCount++;
|
||||
} else if (opts.cmd === 'NOTE_OFF') {
|
||||
if (_liveHeldCount > 0) _liveHeldCount--;
|
||||
}
|
||||
_lastLiveEventAt = _nowEv;
|
||||
} catch (e) {}
|
||||
}
|
||||
var cmd = opts.cmd, ch = opts.channel, pitch = opts.pitch;
|
||||
var vel = (opts.velocity === undefined ? 1.0 : opts.velocity);
|
||||
var sampleOffset = opts.sampleOffset || 0;
|
||||
@@ -73,7 +93,7 @@
|
||||
var _nowMs = (window.performance && performance.now) ? performance.now() : Date.now();
|
||||
if (window.__dbgLog && (opts.live || _nowMs - _lastTimelineDbgTs > 200)) {
|
||||
if (!opts.live) _lastTimelineDbgTs = _nowMs;
|
||||
window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' live=' + (opts.live ? 1 : 0) + ' vel=' + Math.round((vel || 0) * 100) + (window.__dbgBridgeGain ? ' ' + window.__dbgBridgeGain() : '') + ' q=' + (window.__fxRtQueueLen ? window.__fxRtQueueLen() : 'n/a'));
|
||||
window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' p=' + (pitch === undefined ? 'n/a' : pitch) + ' live=' + (opts.live ? 1 : 0) + ' vel=' + Math.round((vel || 0) * 100) + (window.__dbgBridgeGain ? ' ' + window.__dbgBridgeGain() : '') + ' q=' + (window.__fxRtQueueLen ? window.__fxRtQueueLen() : 'n/a'));
|
||||
}
|
||||
} catch (e) {}
|
||||
if (this.isBridgeActive()) {
|
||||
@@ -85,6 +105,7 @@
|
||||
|
||||
/** Stop/panic -> bridge transport panic (flush all notes) + fallback local stopAll. */
|
||||
panic: function () {
|
||||
_liveHeldCount = 0;
|
||||
if (this.isBridgeActive() && window.NativeBridgeService.transport) {
|
||||
window.NativeBridgeService.transport('panic');
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
this.lastOutR = 0.0;
|
||||
this.wrapFadeBlocks = -1; // V42: số block tail pass cũ còn lại sau clear_queue('loop') — đếm ngược để crossfade đúng seam tail→head
|
||||
this.latencySamples = 0; // G3: latency chain thật (engine qua WS) — align wrap fade theo processed tail
|
||||
this.wrapMode = false; // V45.3: dang trong wrap (clear_queue loop) — resume underrun bo flush + re-buffer nho
|
||||
this.WRAP_FADE = 256; // V42: crossfade 256 mẫu (~5.3ms @48k) tại seam wrap — che click jump nội dung end→start (FX path)
|
||||
this.fadeFromL = 0.0; // mẫu cố định khi fade-out underrun (fade tuyến tính tới 0)
|
||||
this.fadeFromR = 0.0;
|
||||
@@ -79,6 +80,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = this.RESUME_FILL;
|
||||
this.silenced = false;
|
||||
this.wrapMode = false;
|
||||
// V30: fade-out tu mau output CUOI (lastOut) — truoc day dung fadeFrom
|
||||
// cu (mac dinh 0) -> jump mau -> click/crackle ngay khi stop.
|
||||
this.fadeFromL = this.lastOutL;
|
||||
@@ -88,6 +90,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
return;
|
||||
}
|
||||
if (d && d.type === 'clear_queue') {
|
||||
this.clearMode = d.mode; // V45.19: nho mode de tag fx_seek_first tai dbgPlayFirst
|
||||
// V34: backlog guard o DAU handler (V33 dat sau mutation -> outQueue.length=0
|
||||
// chay truoc -> guard luon return, clear van drop queue + fade + re-buffer moi
|
||||
// lan = V31). Queue nho (< FILL 8 block) -> bo qua hoan toan (khong mutation,
|
||||
@@ -110,9 +113,51 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
// G3: align processed-tail — tail pass cũ còn in-flight trong bridge
|
||||
// (FX latency thật từ engine) chưa về queue; đếm đủ block tới seam
|
||||
// (block đầu pass mới lên front) → crossfade đúng chỗ jump end→start.
|
||||
// V45.2 (option B): cắt bớt tail cũ giữ KEEP block thay vì giữ nguyên
|
||||
// FILL 24 (~128ms) → downbeat pass mới không trễ ~110ms. skipFade
|
||||
// crossfade từ lastOut che jump cắt front; wrapFadeBlocks đếm lại từ
|
||||
// KEEP để seam crossfade vẫn đúng lúc.
|
||||
// V45.3: KEEP 4 -> 16 (~85ms): KEEP 4 (~21ms) engine (stall bridge
|
||||
// 68-138ms sau transport play) khong refill kip -> queue can -> underrun
|
||||
// -> flush + re-buffer FILL 24 = gap ~110-235ms moi wrap (WORSE hon
|
||||
// truoc V45.2: downbeat tre 106-128ms nhung khong gap). KEEP 16 che
|
||||
// stall dien hinh 68-82ms lien tuc (khong gap, downbeat ~85ms); stall
|
||||
// dai hon roi vao nhanh resume wrap (bo flush + resumeFill 8).
|
||||
const KEEP = 16;
|
||||
this.wrapMode = true;
|
||||
if (this.outQueue.length > KEEP) {
|
||||
const head = this.outQueue.length - KEEP;
|
||||
this.outQueue.splice(0, head);
|
||||
this.outPos = 0;
|
||||
if (this.playing) {
|
||||
this.skipFade = this.WRAP_FADE;
|
||||
this.skipFadeTotal = this.WRAP_FADE;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
}
|
||||
}
|
||||
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
|
||||
return;
|
||||
}
|
||||
if (d.mode === 'seek') {
|
||||
// V45.18: seek khi play master FX active — wipe queue cu (audio cu
|
||||
// choi tiep 0-50ms sau click) + resume nhanh RESUME_FILL 8 (~43ms)
|
||||
// thay vi FILL 24 (~128ms) — het "am tre 1 beat" (hit dau roi vao
|
||||
// vung cam 128ms). Giong resume wrap V45.4 (proven khong gap).
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.playing = false;
|
||||
this.wrapFadeBlocks = -1;
|
||||
this.wrapMode = false;
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = 8;
|
||||
this.silenced = false;
|
||||
this.fadeFromL = this.lastOutL;
|
||||
this.fadeFromR = this.lastOutR;
|
||||
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_seek_clear', t: Math.round(currentTime * 1000), q: 0 });
|
||||
return;
|
||||
}
|
||||
if (d.mode !== 'play' && this.outQueue.length < this.FILL) return;
|
||||
// V31: play/ARM live note — main thread bao xoa backlog block idle
|
||||
// (engine render lien tuc khi STOP -> outQueue day ~32 block ~170ms
|
||||
@@ -123,6 +168,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
this.outPos = 0;
|
||||
this.playing = false;
|
||||
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
|
||||
this.wrapMode = false; // V45.3: play/seek — het context wrap
|
||||
this.resumeFadeIn = true;
|
||||
// V34: mode phan biet — play re-buffer FILL 8 (~43ms, cushion du hap thu
|
||||
// engine ramp/jitter dau play); ARM live note giu RESUME_FILL 8 (~43ms) — preview
|
||||
@@ -300,21 +346,31 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
|
||||
// tại (giữa fade) — hết jump.
|
||||
const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
|
||||
if (wasInterrupted) {
|
||||
// V45.3: wrap (loop restart) — engine da transport('play') tu main
|
||||
// (D17): block moi = audio pass moi, KHONG phai backlog stale (boi
|
||||
// worklet flush gui 'flush' xuong engine ring = lam mat tac dung D17
|
||||
// skip-flush o stopAllPlayback -> gap giua seam). Bo flush (round-trip)
|
||||
// + re-buffer nho hon FILL 24 (~128ms) -> resume sau stall som hon
|
||||
// (~43ms) -> gap wrap giam tu ~110-235ms xuong con vai chuc ms.
|
||||
const _wrap = this.wrapMode;
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.flushPending = true;
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = this.FILL; // underrun-stall: giữ cushion FILL đầy đủ (V25 wobble)
|
||||
this.port.postMessage({ type: 'flush' });
|
||||
this.resumeFill = _wrap ? 8 : this.FILL;
|
||||
this.flushPending = _wrap ? false : true;
|
||||
if (!_wrap) this.port.postMessage({ type: 'flush' });
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
this.playing = true;
|
||||
this.outPos = 0;
|
||||
this.holdFadeLeft = 0;
|
||||
this.wrapMode = false; // V45.3: resume xong — ve che do thuong (underrun sau nay flush nhu cu)
|
||||
if (this.dbgPlayFirst) {
|
||||
this.dbgPlayFirst = false;
|
||||
this.port.postMessage({ type: 'dbg', tag: 'fx_play_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
|
||||
// V45.19 Bug 1b: seek -> tag fx_seek_first (main thread snap khong can
|
||||
// guard 50ms — seek resume nhanh ~43ms < 50ms). Wrap/play -> fx_play_first.
|
||||
this.port.postMessage({ type: 'dbg', tag: (this.clearMode === 'seek' ? 'fx_seek_first' : 'fx_play_first'), t: Math.round(currentTime * 1000), q: this.outQueue.length, m: this.clearMode || '' });
|
||||
}
|
||||
if (this.resumeFadeIn) {
|
||||
this.resumeFadeIn = false;
|
||||
|
||||
+21
-21
@@ -32,27 +32,27 @@
|
||||
<link rel="stylesheet" href="/static/vendor/fontawesome.min.css">
|
||||
<script src="/static/vendor/react.production.min.js"></script>
|
||||
<script src="/static/vendor/react-dom.production.min.js"></script>
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202608261629201545"></script>
|
||||
<script src="/static/js/services/runtime.js?v=202608101800"></script>
|
||||
<script src="/static/js/services/api.js?v=202608212400"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608038200"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
|
||||
<script src="/static/js/services/nativeBridgeService.js?v=202608250700"></script>
|
||||
<script src="/static/js/services/bridgeAudioNode.js?v=202608250700"></script>
|
||||
<script src="/static/js/services/unifiedMidiRouter.js?v=202608271630"></script>
|
||||
<script src="/static/js/services/audioRoutingEngine.js?v=202608250700"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
|
||||
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
|
||||
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></script>
|
||||
<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/services/chordTheory.js?v=202608211300"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608281631" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202608290945201545"></script>
|
||||
<script src="/static/js/services/runtime.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/api.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/storage.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/soundfontStorage.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/soundfontPlayer.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/nativeBridgeService.js?v=202608291510"></script>
|
||||
<script src="/static/js/services/bridgeAudioNode.js?v=202608311858"></script>
|
||||
<script src="/static/js/services/unifiedMidiRouter.js?v=202608311756"></script>
|
||||
<script src="/static/js/services/audioRoutingEngine.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/aiGateway.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/pianoRollTabService.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/ghostNoteExtractor.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/midiExtractor.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608311858" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
|
||||
<style>
|
||||
:root {
|
||||
--right-sidebar-width: 320px;
|
||||
|
||||
@@ -31,6 +31,15 @@ public:
|
||||
virtual void programChange(uint32_t channel, uint32_t program) = 0;
|
||||
virtual void pitchBend(uint32_t channel, uint32_t bend14) = 0;
|
||||
|
||||
// Flush every sounding note on the channel. Default: per-pitch
|
||||
// note-off sweep (direct engines - no shared queue). Sandbox VST2
|
||||
// overrides: a 128-note sweep would flood the child's 64-cap MIDI
|
||||
// queue and drop the tail (Qin keeps ringing after STOP) - it
|
||||
// sends one control event and the child panics locally instead.
|
||||
virtual void allNotesOff(uint32_t channel) {
|
||||
for (uint32_t n = 0; n < 128; ++n) noteOff(channel, n, 0);
|
||||
}
|
||||
|
||||
// Open/close Native GUI window
|
||||
virtual bool openGUI(void* parentWindowHandle) = 0;
|
||||
// Headless engines (VST2 without an editor, SF2/SFZ): the bridge must
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
@@ -72,6 +73,103 @@ bool jBool(const json_object_s* o, const char* key, bool def) {
|
||||
return def;
|
||||
}
|
||||
|
||||
int64_t jInt64(const json_object_s* o, const char* key, int64_t def) {
|
||||
const json_object_element_s* m = jMember(o, key);
|
||||
if (m && m->value && m->value->type == json_type_number) {
|
||||
const auto* n = static_cast<const json_number_s*>(m->value->payload);
|
||||
return (int64_t)std::strtoll(n->number, nullptr, 10);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
uint32_t jUInt32(const json_object_s* o, const char* key, uint32_t def) {
|
||||
const json_object_element_s* m = jMember(o, key);
|
||||
if (m && m->value && m->value->type == json_type_number) {
|
||||
const auto* n = static_cast<const json_number_s*>(m->value->payload);
|
||||
return (uint32_t)std::strtoul(n->number, nullptr, 10);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
std::string jsonEscape(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (char c : s) {
|
||||
if (c == '\\' || c == '"') { out += '\\'; out += c; }
|
||||
else out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- VST3 description cache -------------------------------------------------
|
||||
// findAllTypesForFile() slow path load DLL + quét factory + instantiate
|
||||
// component (hàng trăm ms/plugin mỗi session). createInstanceFromDescription
|
||||
// chỉ cần {fileOrIdentifier, name, uniqueId, deprecatedUid} —
|
||||
// findClassMatchingDescription so name+uid trên class info, KHÔNG instantiate.
|
||||
// Cache JSON theo path+mtime: hit → dựng desc trực tiếp, bỏ qua scan.
|
||||
struct DescCacheEntry {
|
||||
std::string path;
|
||||
int64_t mtime = 0;
|
||||
std::string name;
|
||||
uint32_t uniqueId = 0;
|
||||
uint32_t deprecatedUid = 0;
|
||||
};
|
||||
|
||||
std::string descCachePath() {
|
||||
return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
|
||||
.getChildFile("SonicForgeDAW")
|
||||
.getChildFile("juce_vst3_desc_cache.json")
|
||||
.getFullPathName()
|
||||
.toStdString();
|
||||
}
|
||||
|
||||
std::vector<DescCacheEntry> loadDescCache() {
|
||||
std::vector<DescCacheEntry> out;
|
||||
const juce::File f(descCachePath());
|
||||
if (!f.existsAsFile()) return out;
|
||||
const juce::String content = f.loadFileAsString();
|
||||
json_value_s* root = json_parse(content.toRawUTF8(), content.getNumBytesAsUTF8());
|
||||
if (root && root->type == json_type_array) {
|
||||
const auto* arr = static_cast<const json_array_s*>(root->payload);
|
||||
for (const json_array_element_s* el = arr->start; el; el = el->next) {
|
||||
const json_value_s* v = el->value;
|
||||
if (!v || v->type != json_type_object) continue;
|
||||
const auto* o = static_cast<const json_object_s*>(v->payload);
|
||||
DescCacheEntry e;
|
||||
e.path = jString(o, "path", "");
|
||||
e.mtime = jInt64(o, "mtime", 0);
|
||||
e.name = jString(o, "name", "");
|
||||
e.uniqueId = jUInt32(o, "unique_id", 0);
|
||||
e.deprecatedUid = jUInt32(o, "deprecated_uid", 0);
|
||||
if (!e.path.empty() && !e.name.empty())
|
||||
out.push_back(std::move(e));
|
||||
}
|
||||
}
|
||||
if (root) free(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
void saveDescCache(const std::vector<DescCacheEntry>& cache) {
|
||||
std::string json = "[\n";
|
||||
for (size_t i = 0; i < cache.size(); ++i) {
|
||||
const auto& e = cache[i];
|
||||
if (i) json += ",\n";
|
||||
json += " {\"path\":\"" + jsonEscape(e.path) +
|
||||
"\",\"mtime\":" + std::to_string(e.mtime) +
|
||||
",\"name\":\"" + jsonEscape(e.name) +
|
||||
"\",\"unique_id\":" + std::to_string(e.uniqueId) +
|
||||
",\"deprecated_uid\":" + std::to_string(e.deprecatedUid) + "}";
|
||||
}
|
||||
json += "\n]\n";
|
||||
try {
|
||||
const juce::File f(descCachePath());
|
||||
f.getParentDirectory().createDirectory();
|
||||
f.replaceWithText(juce::String(json));
|
||||
} catch (...) {
|
||||
// Cache lỗi không được làm hỏng prepare() — lần sau scan lại.
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct JuceFxEngine::Slot {
|
||||
@@ -155,6 +253,8 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) {
|
||||
// Slot bypass / type khác vst3 / load fail → passthrough (latency 0).
|
||||
impl_->pluginNodes.assign(impl_->slots.size(), nullptr);
|
||||
impl_->slotLatencies.assign(impl_->slots.size(), 0);
|
||||
static std::vector<DescCacheEntry> descCache = loadDescCache();
|
||||
bool cacheDirty = false;
|
||||
juce::AudioProcessorGraph::Node::Ptr prev = inNode;
|
||||
for (size_t i = 0; i < impl_->slots.size(); ++i) {
|
||||
const Slot& s = impl_->slots[i];
|
||||
@@ -162,7 +262,35 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) {
|
||||
continue;
|
||||
juce::VST3PluginFormat fmt;
|
||||
juce::OwnedArray<juce::PluginDescription> descs;
|
||||
fmt.findAllTypesForFile(descs, juce::File(s.path).getFullPathName());
|
||||
const juce::File pluginFile(s.path);
|
||||
const int64_t mtime = pluginFile.getLastModificationTime().toMilliseconds();
|
||||
const DescCacheEntry* hit = nullptr;
|
||||
for (const auto& e : descCache)
|
||||
if (e.path == s.path && e.mtime == mtime) { hit = &e; break; }
|
||||
if (hit) {
|
||||
// Cache hit: dựng desc (name+uid) — không scan factory, không
|
||||
// instantiate 2 lần như findAllTypesForFile slow path.
|
||||
auto* d = new juce::PluginDescription();
|
||||
d->fileOrIdentifier = juce::String(hit->path);
|
||||
d->name = juce::String(hit->name);
|
||||
d->uniqueId = hit->uniqueId;
|
||||
d->deprecatedUid = hit->deprecatedUid;
|
||||
d->pluginFormatName = "VST3";
|
||||
descs.add(d);
|
||||
} else {
|
||||
fmt.findAllTypesForFile(descs, pluginFile.getFullPathName());
|
||||
if (!descs.isEmpty()) {
|
||||
const auto* d = descs.getFirst();
|
||||
DescCacheEntry e;
|
||||
e.path = s.path;
|
||||
e.mtime = mtime;
|
||||
e.name = d->name.toStdString();
|
||||
e.uniqueId = d->uniqueId;
|
||||
e.deprecatedUid = d->deprecatedUid;
|
||||
descCache.push_back(std::move(e));
|
||||
cacheDirty = true;
|
||||
}
|
||||
}
|
||||
if (descs.isEmpty()) {
|
||||
std::cerr << "[JuceFxEngine] slot " << i << " no VST3 desc: "
|
||||
<< s.path << std::endl;
|
||||
@@ -195,6 +323,7 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) {
|
||||
}
|
||||
prev = node;
|
||||
}
|
||||
if (cacheDirty) saveDescCache(descCache);
|
||||
for (int ch = 0; ch < 2; ++ch) {
|
||||
NCh src; src.nodeID = prev->nodeID; src.channelIndex = ch;
|
||||
NCh dst; dst.nodeID = outNode->nodeID; dst.channelIndex = ch;
|
||||
|
||||
@@ -483,7 +483,7 @@ void InstrumentEngineManager::allNotesOff() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
for (auto& [ch, inst] : channels_) {
|
||||
if (channelQuiet(ch)) continue; // mid-rebuild / editor open: do not touch the instance
|
||||
for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0);
|
||||
inst->allNotesOff(ch);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,6 +195,7 @@ void SandboxVst2Host::pushControl(uint32_t type, uint32_t arg0, uint32_t arg1) {
|
||||
|
||||
void SandboxVst2Host::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
|
||||
if (!alive_.load() || !ipc_) return;
|
||||
std::cerr << "[Vst2Sandbox] ON ch=" << channel << " p=" << pitch << " v=" << (int)(velocity * 127.0f) << std::endl;
|
||||
(void)channel; // one instrument per child; Qin listens only on MIDI ch 0
|
||||
shm_write_midi(shm_, 0x9, 0, (uint8_t)pitch,
|
||||
(uint8_t)(velocity * 127.0f), sampleOffset);
|
||||
@@ -202,12 +203,14 @@ void SandboxVst2Host::noteOn(uint32_t channel, uint32_t pitch, float velocity, u
|
||||
|
||||
void SandboxVst2Host::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
|
||||
if (!alive_.load() || !ipc_) return;
|
||||
std::cerr << "[Vst2Sandbox] OFF ch=" << channel << " p=" << pitch << std::endl;
|
||||
(void)channel; // see noteOn
|
||||
shm_write_midi(shm_, 0x8, 0, (uint8_t)pitch, 0, sampleOffset);
|
||||
}
|
||||
|
||||
void SandboxVst2Host::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
|
||||
if (!alive_.load() || !ipc_) return;
|
||||
std::cerr << "[Vst2Sandbox] CC ch=" << channel << " cc=" << cc << " v=" << value << std::endl;
|
||||
(void)channel; // see noteOn
|
||||
shm_write_midi(shm_, 0xB, 0, (uint8_t)cc, 0, 0, (uint8_t)value, 0);
|
||||
}
|
||||
@@ -225,6 +228,12 @@ void SandboxVst2Host::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
(uint8_t)(bend14 & 0x7F), (uint8_t)((bend14 >> 7) & 0x7F));
|
||||
}
|
||||
|
||||
void SandboxVst2Host::allNotesOff(uint32_t /*channel*/) {
|
||||
if (!alive_.load() || !ipc_) return;
|
||||
std::cerr << "[Vst2Sandbox] ALL_NOTES_OFF ch=" << channel_ << std::endl;
|
||||
pushControl(7); // plugin_host panics locally - no 64-cap MIDI flood
|
||||
}
|
||||
|
||||
bool SandboxVst2Host::openGUI(void* parentWindowHandle) {
|
||||
(void)parentWindowHandle; // child owns its window
|
||||
if (!alive_.load() || !ipc_) return false;
|
||||
@@ -303,7 +312,13 @@ void SandboxVst2Host::loadSerializedState(const std::string& base64) {
|
||||
if (base64.empty()) return;
|
||||
lastPreset_ = base64; // keep for watchdog respawn re-queue
|
||||
if (!alive_.load() || !ipc_) return;
|
||||
queuePreset(base64, true); // wait for apply so early saves see the real chunk
|
||||
// V45.2: non-blocking. The old byte-identical echo wait (memcmp presetOut==input)
|
||||
// NEVER matched for Qin — the child republishes its own round-trip chunk (differs
|
||||
// at byte 144) so waitApply always burned the full 20s TIMEOUT. VST2 restore runs
|
||||
// on the audio thread (runOnMain), so every bridge startup stalled ALL audio 20s —
|
||||
// the user's first play pressed in that window = silence. Bridge now holds
|
||||
// reloading + defers saves until the child republishes (see main.cpp doRestore).
|
||||
queuePreset(base64, false);
|
||||
std::cerr << "[SandboxVst2Host] presetIn queued ch=" << channel_
|
||||
<< " (" << base64.size() << "B)" << std::endl;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ public:
|
||||
void controlChange(uint32_t channel, uint32_t cc, uint32_t value) override;
|
||||
void programChange(uint32_t channel, uint32_t program) override;
|
||||
void pitchBend(uint32_t channel, uint32_t bend14) override;
|
||||
// Control 7 (ALL_NOTES_OFF): the child runs the local panic
|
||||
// (CC64/CC123 + 128 note-offs) - a 128-note MIDI sweep would
|
||||
// overflow the child's 64-cap queue and Qin would keep ringing.
|
||||
void allNotesOff(uint32_t channel) override;
|
||||
// The child owns its GUI window (plugin_host opens it on OPEN_GUI control).
|
||||
bool openGUI(void* parentWindowHandle) override;
|
||||
bool canOpenGUI() const override { return true; }
|
||||
|
||||
@@ -1421,9 +1421,34 @@ int main(int argc, char* argv[]) {
|
||||
if (!inst2) {
|
||||
std::cerr << "[NativeBridge] restore lost inst ch=" << ch << std::endl;
|
||||
} else {
|
||||
if ((t == InstrumentType::VST3 || t == InstrumentType::VST2) && preset.size() >= 100)
|
||||
inst2->loadSerializedState(preset);
|
||||
else
|
||||
if ((t == InstrumentType::VST3 || t == InstrumentType::VST2) && preset.size() >= 100) {
|
||||
// V45.2: sandbox VST2 child applies presetIn ASYNCHRONOUSLY.
|
||||
// Queue non-blocking (see SandboxVst2Host) and hold reloading
|
||||
// until the child republishes a different chunk — otherwise a
|
||||
// saveStateJob in the ~2s republish window persists the seed
|
||||
// over the real preset (restore clobber). Never block the
|
||||
// audio thread for the echo (byte-identical never matches).
|
||||
if (t == InstrumentType::VST2) {
|
||||
std::string before = inst2->serializeState();
|
||||
instruments.setReloading(ch, true);
|
||||
inst2->loadSerializedState(preset);
|
||||
if (g_uiWorker) g_uiWorker->post([&instruments, ch, before]() {
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
while (std::chrono::steady_clock::now() - t0 < std::chrono::seconds(5)) {
|
||||
bool changed = false;
|
||||
instruments.withInstrument(ch, [&](INativeInstrument* i) {
|
||||
if (i && i->serializeState() != before) changed = true;
|
||||
return changed;
|
||||
});
|
||||
if (changed) break;
|
||||
Sleep(20);
|
||||
}
|
||||
instruments.setReloading(ch, false);
|
||||
});
|
||||
} else {
|
||||
inst2->loadSerializedState(preset);
|
||||
}
|
||||
} else
|
||||
inst2->selectProgram(ch, bank, program);
|
||||
// Re-calibrate AFTER the preset exists: assign()
|
||||
// calibrated the empty instance (VST2 like Qin
|
||||
|
||||
@@ -269,6 +269,15 @@ int main(int argc, char* argv[]) {
|
||||
if (g_hostHwnd) ShowWindow(g_hostHwnd, SW_HIDE);
|
||||
} else if (c.type == 6) {
|
||||
quit = true;
|
||||
} else if (c.type == 7) { // ALL_NOTES_OFF: runtime panic (STOP/solo)
|
||||
panicAfterPreset();
|
||||
// absorb the panic-mutated chunk so the ~2s periodic
|
||||
// republish never overwrites presetOut with it
|
||||
inst.captureState();
|
||||
std::string postPanic = inst.serializeState();
|
||||
if (!postPanic.empty()) lastOut = postPanic;
|
||||
printf("[plugin_host] ALL_NOTES_OFF ch=%u\n", channel);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
ipc->base.controlQueueCount = 0;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Q2 fix: wrap-aware resume trong worklet sf-fx-realtime.js
|
||||
# - KEEP 4 -> 16 (85ms tail che stall 68-82ms -> het gap wrap)
|
||||
# - wrap underrun: bo 'flush' (engine da restart, block moi khong stale) +
|
||||
# resumeFill 8 (~43ms) thay FILL 24 (~128ms)
|
||||
import io
|
||||
|
||||
P = r'app/static/js/sf-fx-realtime.js'
|
||||
s = io.open(P, 'r', encoding='utf-8').read()
|
||||
|
||||
# 1) constructor: wrapMode flag
|
||||
old = " this.latencySamples = 0; // G3: latency chain thật (engine qua WS) — align wrap fade theo processed tail"
|
||||
new = old + "\n this.wrapMode = false; // V45.3: dang trong wrap (clear_queue loop) — resume underrun bo flush + re-buffer nho"
|
||||
assert s.count(old) == 1, 'ctor latency line'
|
||||
s = s.replace(old, new)
|
||||
|
||||
# 2) clear_queue mode='loop': KEEP 4 -> 16 + set wrapMode
|
||||
old = """ // V45.2 (option B): cắt bớt tail cũ giữ KEEP block (~21ms cushion) thay
|
||||
// vì giữ nguyên FILL 24 (~128ms) → downbeat pass mới không trễ ~110ms.
|
||||
// skipFade crossfade từ lastOut che jump cắt front; wrapFadeBlocks đếm
|
||||
// lại từ KEEP để seam crossfade vẫn đúng lúc.
|
||||
const KEEP = 4;
|
||||
if (this.outQueue.length > KEEP) {
|
||||
const head = this.outQueue.length - KEEP;
|
||||
this.outQueue.splice(0, head);
|
||||
this.outPos = 0;
|
||||
if (this.playing) {
|
||||
this.skipFade = this.WRAP_FADE;
|
||||
this.skipFadeTotal = this.WRAP_FADE;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
}
|
||||
}
|
||||
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
|
||||
return;"""
|
||||
new = """ // V45.2 (option B): cắt bớt tail cũ giữ KEEP block thay vì giữ nguyên
|
||||
// FILL 24 (~128ms) → downbeat pass mới không trễ ~110ms. skipFade
|
||||
// crossfade từ lastOut che jump cắt front; wrapFadeBlocks đếm lại từ
|
||||
// KEEP để seam crossfade vẫn đúng lúc.
|
||||
// V45.3: KEEP 4 -> 16 (~85ms): KEEP 4 (~21ms) engine (stall bridge
|
||||
// 68-138ms sau transport play) khong refill kip -> queue can -> underrun
|
||||
// -> flush + re-buffer FILL 24 = gap ~110-235ms moi wrap (WORSE hon
|
||||
// truoc V45.2: downbeat tre 106-128ms nhung khong gap). KEEP 16 che
|
||||
// stall dien hinh 68-82ms lien tuc (khong gap, downbeat ~85ms); stall
|
||||
// dai hon roi vao nhanh resume wrap (bo flush + resumeFill 8).
|
||||
const KEEP = 16;
|
||||
this.wrapMode = true;
|
||||
if (this.outQueue.length > KEEP) {
|
||||
const head = this.outQueue.length - KEEP;
|
||||
this.outQueue.splice(0, head);
|
||||
this.outPos = 0;
|
||||
if (this.playing) {
|
||||
this.skipFade = this.WRAP_FADE;
|
||||
this.skipFadeTotal = this.WRAP_FADE;
|
||||
this.skipFromL = this.lastOutL;
|
||||
this.skipFromR = this.lastOutR;
|
||||
}
|
||||
}
|
||||
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
|
||||
return;"""
|
||||
assert s.count(old) == 1, 'loop clear block'
|
||||
s = s.replace(old, new)
|
||||
|
||||
# 3) flush_for_stop: reset wrapMode
|
||||
old = """ this.resumeFill = this.RESUME_FILL;
|
||||
this.silenced = false;"""
|
||||
new = """ this.resumeFill = this.RESUME_FILL;
|
||||
this.silenced = false;
|
||||
this.wrapMode = false;"""
|
||||
assert s.count(old) == 1, 'flush_for_stop'
|
||||
s = s.replace(old, new)
|
||||
|
||||
# 4) clear_queue wipe (play/ARM): reset wrapMode
|
||||
old = """ this.playing = false;
|
||||
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
|
||||
this.resumeFadeIn = true;"""
|
||||
new = """ this.playing = false;
|
||||
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
|
||||
this.wrapMode = false; // V45.3: play/seek — het context wrap
|
||||
this.resumeFadeIn = true;"""
|
||||
assert s.count(old) == 1, 'wipe branch'
|
||||
s = s.replace(old, new)
|
||||
|
||||
# 5) wasInterrupted: wrap-aware
|
||||
old = """ const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
|
||||
if (wasInterrupted) {
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.flushPending = true;
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = this.FILL; // underrun-stall: giữ cushion FILL đầy đủ (V25 wobble)
|
||||
this.port.postMessage({ type: 'flush' });
|
||||
i--;
|
||||
continue;
|
||||
}"""
|
||||
new = """ const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
|
||||
if (wasInterrupted) {
|
||||
// V45.3: wrap (loop restart) — engine da transport('play') tu main
|
||||
// (D17): block moi = audio pass moi, KHONG phai backlog stale (boi
|
||||
// worklet flush gui 'flush' xuong engine ring = lam mat tac dung D17
|
||||
// skip-flush o stopAllPlayback -> gap giua seam). Bo flush (round-trip)
|
||||
// + re-buffer nho hon FILL 24 (~128ms) -> resume sau stall som hon
|
||||
// (~43ms) -> gap wrap giam tu ~110-235ms xuong con vai chuc ms.
|
||||
const _wrap = this.wrapMode;
|
||||
this.outQueue.length = 0;
|
||||
this.outPos = 0;
|
||||
this.resumeFadeIn = true;
|
||||
this.resumeFill = _wrap ? 8 : this.FILL;
|
||||
this.flushPending = _wrap ? false : true;
|
||||
if (!_wrap) this.port.postMessage({ type: 'flush' });
|
||||
i--;
|
||||
continue;
|
||||
}"""
|
||||
assert s.count(old) == 1, 'wasInterrupted block'
|
||||
s = s.replace(old, new)
|
||||
|
||||
# 6) resume success: reset wrapMode
|
||||
old = """ this.playing = true;
|
||||
this.outPos = 0;
|
||||
this.holdFadeLeft = 0;"""
|
||||
new = """ this.playing = true;
|
||||
this.outPos = 0;
|
||||
this.holdFadeLeft = 0;
|
||||
this.wrapMode = false; // V45.3: resume xong — ve che do thuong (underrun sau nay flush nhu cu)"""
|
||||
assert s.count(old) == 1, 'resume success'
|
||||
s = s.replace(old, new)
|
||||
|
||||
io.open(P, 'w', encoding='utf-8', newline='\r\n').write(s)
|
||||
raw = open(P, 'rb').read()
|
||||
print('CRLF:', raw.count(b'\r\n'), 'LF-only:', raw.count(b'\n') - raw.count(b'\r\n'))
|
||||
print('patch ok')
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Q3 fix: activeTracksRef.current = effective ALWAYS (solo-out stale ref),
|
||||
# remove _prevActiveTracks restore.
|
||||
import io
|
||||
|
||||
P = r'app/static/js/app.jsx'
|
||||
s = io.open(P, 'r', encoding='utf-8').read() # universal newline -> \n
|
||||
|
||||
old_effective = " const effective = (trackId && patch) ? list.map(t => t.id === trackId ? { ...t, ...patch } : t) : list;"
|
||||
new_effective = old_effective + "\n // V45.3: LUON ghi ref voi state da patch (mute/solo). Truoc day chi ghi trong\n // nhanh becameAudible roi restore STALE -> solo-out khong co track became\n // audible -> D18b guard (25423) + scheduler (25498) + toggleTrackSoloEvaluate\n // (setTimeout 60ms restart) thay trang thai PRE-solo -> Qin note van fire\n // (tremolo). Render ke tiep tu dong dong bo lai ref.\n activeTracksRef.current = effective;"
|
||||
assert s.count(old_effective) == 1, 'effective line not unique/found'
|
||||
s = s.replace(old_effective, new_effective)
|
||||
|
||||
old_restore = """ const _prevActiveTracks = activeTracksRef.current;
|
||||
activeTracksRef.current = effective;
|
||||
try {
|
||||
startTrackPlayback(resumeAt);
|
||||
} finally {
|
||||
activeTracksRef.current = _prevActiveTracks;
|
||||
}"""
|
||||
new_restore = """ // V45.3: ref da = effective (dau ham) -> restart thay trang thai moi ngay.
|
||||
startTrackPlayback(resumeAt);"""
|
||||
assert s.count(old_restore) == 1, 'restore block not unique/found'
|
||||
s = s.replace(old_restore, new_restore)
|
||||
|
||||
out = io.open(P, 'w', encoding='utf-8', newline='\r\n')
|
||||
out.write(s)
|
||||
out.close()
|
||||
|
||||
# verify CRLF
|
||||
raw = open(P, 'rb').read()
|
||||
print('CRLF count:', raw.count(b'\r\n'), 'LF only:', raw.count(b'\n') - raw.count(b'\r\n'))
|
||||
print('patch ok')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user