Merge main (V45.1-32 fixes: ReferenceError guard, crackle/solo/seek fixes) vao standalone-shm-bridge (giu FILL 96)

This commit is contained in:
2026-09-01 16:03:28 +07:00
25 changed files with 1291 additions and 287 deletions
+3
View File
@@ -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/
+7 -3
View File
@@ -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("\\", "/"))
+4 -3
View File
@@ -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:
+3 -1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+7 -2
View File
@@ -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
};
}
};
+39 -8
View File
@@ -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
+23 -2
View File
@@ -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');
}
+60 -4
View File
@@ -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
View File
@@ -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
+130 -1
View File
@@ -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;
+1 -1
View File
@@ -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);
}
}
+16 -1
View File
@@ -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;
}
+4
View File
@@ -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; }
+28 -3
View File
@@ -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
+9
View File
@@ -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;
+131
View File
@@ -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')
+33
View File
@@ -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.