Files
SonicForgeStudio/plans/patch_fx_crossfade.py
T
admin 10997a65b9 fix(fx-rt): het crackle master FX - limiter attack smooth + crossfade khi bat/tat chain
- NativeInstrumentEngine: kAttack 0.9->0.1 (het zipper khi sum nhieu track)
- app.jsx: crossfade masterFxDryGain/masterFxGain thay hard-rewire graph luc
  activation/deactivation master chain FX; worklet bao 'started' -> ramp dung
  luc; PDC tre sau khi dry silent (tranh gap 80ms)
- sf-fx-realtime.js: fade-in 512 mau luc FILL xong / resume sau underrun
- index.html + addModule: cache-buster v=202608241200
- rebuild app.precompiled.js, copy bundle vao dist/ + src-tauri/resources/
- keo dai fix cac round truoc: FILL 16, FXRT_BURST_MS, REBUFFER, pacing,
  VST GUI click/focus, solo crackle
- them harness + plans/patch scripts
2026-08-24 07:46:09 +07:00

91 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# Patch: crossfade activation/deactivation master chain FX (het crackle click)
# Applies to app/static/js/app.jsx + app/static/js/sf-fx-realtime.js (CRLF).
import io, sys
def patch(path, repls, expect_all_multi=False):
with io.open(path, 'r', encoding='utf-8', newline='') as f:
src = f.read()
for old, new, count in repls:
n = src.count(old)
if count is None:
if n < 1:
print('MISS (expected >=1):', old[:80]); sys.exit(1)
src = src.replace(old, new)
else:
if n != count:
print('BAD COUNT %d (want %d): %s' % (n, count, old[:80])); sys.exit(1)
src = src.replace(old, new)
with io.open(path, 'w', encoding='utf-8', newline='') as f:
f.write(src)
print('PATCHED', path)
# ── app.jsx ─────────────────────────────────────────────────────────────────
appjsx = 'app/static/js/app.jsx'
A = [
# 1. fxRt state: them fadedIn flag
("const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, lastInSent: 0, chainKey: null, latencyTotal: 0 };",
"const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, lastInSent: 0, chainKey: null, latencyTotal: 0, fadedIn: false };",
1),
# 2a. initMasterBus: tao 2 gain crossfade
(" // Global fader / output\n const output = ctx.createGain();\n output.gain.value = 1.0;",
" // Global fader / output\n const output = ctx.createGain();\n output.gain.value = 1.0;\n\n // Crossfade master-FX activation (het click khi bat/tat master chain FX):\n // path mac dinh outputAnalyser -> masterFxDryGain(1) -> pdcDelay -> output;\n // nhanh FX noi vao masterFxGain(0) -> output. fxRtStart/fxRtStop ramp 2 gain\n // nay thay vi hard-rewire graph (dry path luon noi san — no click, no gap\n // trong luc worklet FILL ~86ms).\n const masterFxDryGain = ctx.createGain();\n masterFxDryGain.gain.value = 1.0;\n const masterFxGain = ctx.createGain();\n masterFxGain.gain.value = 0.0;",
1),
# 2b. masterBus object literal
(" analyser,\n output,\n masteringActive: false,",
" analyser,\n output,\n masterFxDryGain,\n masterFxGain,\n masteringActive: false,",
1),
# 2c. wire graph: dry path qua masterFxDryGain -> pdcDelay -> output
(" masterBus.input.connect(masterBus.inputAnalyser);\n masterBus.inputAnalyser.connect(masterBus.outputAnalyser);\n masterBus.outputAnalyser.connect(masterBus.output);",
" masterBus.input.connect(masterBus.inputAnalyser);\n masterBus.inputAnalyser.connect(masterBus.outputAnalyser);\n masterBus.outputAnalyser.connect(masterBus.masterFxDryGain);\n masterBus.masterFxDryGain.connect(masterBus.pdcDelay);\n masterBus.outputAnalyser.connect(masterBus.masterFxGain);\n masterBus.masterFxGain.connect(masterBus.output);",
1),
# 3. latency: chi ap PDC sau khi crossfade xong (dry silent) — ap som = gap 80ms
(" if (d && d.cmd === 'latency' && typeof d.total === 'number') {\n fxRt.latencyTotal = d.total;\n fxRtApplyPdc();\n }",
" if (d && d.cmd === 'latency' && typeof d.total === 'number') {\n fxRt.latencyTotal = d.total;\n // Chi delay dry path khi crossfade da xong (dry silent) — ap som\n // trong luc dry con la path nghe duoc se tao gap ~80ms.\n if (fxRt.fadedIn) fxRtApplyPdc();\n }",
1),
# 4. guard start khi masterBus moi thieu gains (recovery)
(" if (!ctx || !ctx.audioWorklet || !masterBus || !masterBus.output || !masterBus.analyser) return;",
" if (!ctx || !ctx.audioWorklet || !masterBus || !masterBus.output || !masterBus.analyser || !masterBus.masterFxGain || !masterBus.masterFxDryGain) return;",
1),
# 5. onmessage: xu ly 'started' -> crossfade(true) + PDC sau khi dry silent
(" fxRt.node.port.onmessage = (ev) => {\n const d = ev.data;\n if (!d || !fxRt.ws) return;\n if (d.type === 'set_param') {\n try { fxRt.ws.send(JSON.stringify({ cmd: 'set_param', slot: d.slot, key: d.key, value: d.value })); } catch (e) {}\n return;\n }\n if (d.type !== 'in') return;",
" fxRt.node.port.onmessage = (ev) => {\n const d = ev.data;\n if (!d || !fxRt.ws) return;\n if (d.type === 'started') {\n // Worklet bat dau phat (sau FILL) -> crossfade dry->fx muot. Token guard\n // chong race: neu stop/restart xay ra trong luc cho, bo qua.\n if (fxRt.fadedIn) return;\n setTimeout(() => {\n if (token !== fxRt.startToken) return;\n fxRt.fadedIn = true;\n fxRtCrossfade(true, token);\n // Dry gan silent (tc 0.025) roi moi tre pdcDelay — tranh gap khi delay\n // dang lon dan.\n setTimeout(() => fxRtApplyPdc(), 250);\n }, 30);\n return;\n }\n if (d.type === 'set_param') {\n try { fxRt.ws.send(JSON.stringify({ cmd: 'set_param', slot: d.slot, key: d.key, value: d.value })); } catch (e) {}\n return;\n }\n if (d.type !== 'in') return;",
1),
# 6. rewire start: noi fx vao masterFxGain (khong disconnect dry)
(" try { masterBus.outputAnalyser.disconnect(masterBus.output); } catch (e) {}\n masterBus.outputAnalyser.connect(fxRt.node);\n fxRt.node.connect(masterBus.output);\n fxRt.chainKey = ck;",
" // Crossfade activation: KHONG disconnect dry path — outputAnalyser van noi\n // masterFxDryGain -> pdcDelay -> output (gain 1); nhanh fx noi masterFxGain\n // (gain 0) -> output. Tin hieu 'started' tu worklet -> ramp 2 gain -> switch\n // muot, het click hard-rewire.\n try { masterBus.outputAnalyser.disconnect(masterBus.output); } catch (e) {}\n masterBus.outputAnalyser.connect(fxRt.node);\n fxRt.node.connect(masterBus.masterFxGain);\n fxRt.chainKey = ck;",
1),
# 7. them ham fxRtCrossfade sau fxRtApplyPdc
(" try { bus.pdcDelay.delayTime.setTargetAtTime(l / (ctx.sampleRate || 44100), ctx.currentTime, 0.05); } catch (e) {}\n}",
" try { bus.pdcDelay.delayTime.setTargetAtTime(l / (ctx.sampleRate || 44100), ctx.currentTime, 0.05); } catch (e) {}\n}\n\n// Crossfade master-FX switch: ramp masterFxGain/masterFxDryGain thay vi\n// hard-rewire (click). Hai gain nay duoc tao trong initMasterBus.\nfunction fxRtCrossfade(toFx, token) {\n const ctx = getAudioContext();\n const bus = masterBus;\n if (!ctx || !bus || !bus.masterFxGain || !bus.masterFxDryGain) return;\n if (token !== undefined && token !== fxRt.startToken) return;\n const tc = 0.025;\n try {\n bus.masterFxGain.gain.setTargetAtTime(toFx ? 1 : 0, ctx.currentTime, tc);\n bus.masterFxDryGain.gain.setTargetAtTime(toFx ? 0 : 1, ctx.currentTime, tc);\n } catch (e) {}\n}",
1),
# 8. fxRtStop: crossfade deactivation thay hard-switch; disconnect tre 180ms
(" if (!fxRt.active && !fxRt.ws && !fxRt.node && !fxRt.sessionId) return;\n try {\n if (fxRt.node) {\n fxRt.node.disconnect();\n if (masterBus && masterBus.outputAnalyser && masterBus.output) {\n try { masterBus.outputAnalyser.disconnect(masterBus.output); } catch (e) {}\n try { masterBus.outputAnalyser.connect(masterBus.output); } catch (e) {}\n }\n fxRt.node.port.onmessage = null;\n fxRt.node = null;\n }\n } catch (e) {}",
" if (!fxRt.active && !fxRt.ws && !fxRt.node && !fxRt.sessionId) return;\n let _fxStopNode = null;\n try {\n if (fxRt.node) {\n _fxStopNode = fxRt.node;\n if (masterBus && masterBus.masterFxGain && masterBus.masterFxDryGain) {\n // Crossfade deactivation: ramp dry 0->1 / fx 1->0 roi MOI disconnect\n // (tranh click hard-switch nguoc). Dry path da noi san qua\n // masterFxDryGain — khong can reconnect outputAnalyser->output. PDC giam\n // ve 0 sau khi dry chiem cho (fx silent).\n try { fxRtCrossfade(false); } catch (e) {}\n setTimeout(() => {\n try { _fxStopNode.disconnect(); } catch (e) {}\n _fxStopNode.port.onmessage = null;\n if (fxRt.node === _fxStopNode) fxRt.node = null;\n fxRt.fadedIn = false;\n fxRtApplyPdc();\n }, 180);\n } else {\n // Fallback graph cu (khong co crossfade gains) — hard switch nhu cu.\n try { _fxStopNode.disconnect(); } catch (e) {}\n if (masterBus && masterBus.outputAnalyser && masterBus.output) {\n try { masterBus.outputAnalyser.connect(masterBus.output); } catch (e) {}\n }\n _fxStopNode.port.onmessage = null;\n if (fxRt.node === _fxStopNode) fxRt.node = null;\n }\n }\n } catch (e) {}",
1),
# 9. fxRtStop tail: PDC da xu ly trong setTimeout crossfade (hoac ngay neu khong co node)
(" fxRt.chainKey = null;\n fxRt.latencyTotal = 0;\n fxRtApplyPdc();\n if (sid) {",
" fxRt.chainKey = null;\n fxRt.latencyTotal = 0;\n // PDC giam ve 0 sau khi dry chiem cho (trong setTimeout cua nhanh crossfade)\n // hoac ngay neu khong co node — khong goi tai day de tranh gap 80ms.\n if (!_fxStopNode) fxRtApplyPdc();\n if (sid) {",
1),
# 10. bump cache-buster (2 noi: master + per-track)
("sf-fx-realtime.js?v=202608241030", "sf-fx-realtime.js?v=202608241200", None),
]
patch(appjsx, A)
# ── sf-fx-realtime.js ───────────────────────────────────────────────────────
wl = 'app/static/js/sf-fx-realtime.js'
W = [
(" this.hasStarted = false; // đã phát block đầu chưa — phân biệt FILL khởi đầu vs REBUFFER sau underrun",
" this.hasStarted = false; // đã phát block đầu chưa — phân biệt FILL khởi đầu vs REBUFFER sau underrun\n this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click\n this.outGain = 0; // gain phát hiện tại (0→1 trong fade)\n this.fadeLeft = 0; // số mẫu còn lại của fade",
1),
(" if (this.outQueue.length >= need) {\n this.playing = true;\n this.hasStarted = true;\n } else {",
" if (this.outQueue.length >= need) {\n this.playing = true;\n this.hasStarted = true;\n // Fade-in mượt (hết click lúc FILL xong / resume sau underrun); báo\n // main thread để crossfade dry→fx đúng thời điểm.\n this.outGain = 0;\n this.fadeLeft = this.FADE;\n this.port.postMessage({ type: 'started' });\n } else {",
1),
(" const blk = this.outQueue[0];\n outL[i] = blk.l[this.outPos];\n if (outR) outR[i] = blk.r[this.outPos];",
" const blk = this.outQueue[0];\n if (this.fadeLeft > 0) {\n this.outGain = Math.min(1, this.outGain + 1 / this.FADE);\n this.fadeLeft--;\n }\n outL[i] = blk.l[this.outPos] * this.outGain;\n if (outR) outR[i] = blk.r[this.outPos] * this.outGain;",
1),
]
patch(wl, W)
print('ALL OK')