fix: bật AudioWorklet hết deprecation ScriptProcessor + signature guard applyMasteringSettings hết BiquadFilterNode state is bad
This commit is contained in:
@@ -76,6 +76,14 @@ function makeDistortionCurve(k) {
|
|||||||
|
|
||||||
function applyMasteringSettings(s) {
|
function applyMasteringSettings(s) {
|
||||||
if (!masterBus || !audioCtx || !s) return;
|
if (!masterBus || !audioCtx || !s) return;
|
||||||
|
// getAudioContext() invokes this on EVERY call (stopAll, play, VU, …).
|
||||||
|
// Re-applying identical values in rapid bursts is "fast parameter automation"
|
||||||
|
// and makes Chromium flag the biquad EQ filters as unstable ("state is bad").
|
||||||
|
// Only touch the graph when a value actually changed.
|
||||||
|
const sig = [s.eqActive, s.eqLowGain, s.eqMid1Gain, s.eqMid2Gain, s.eqHighGain, s.imagerActive, s.w1, s.w2, s.w3, s.w4, s.maximizerActive, s.maxGain, s.maxSoftClip, s.maxUpward, s.ceiling].join('|');
|
||||||
|
if (sig === _lastMasteringSig) return;
|
||||||
|
_lastMasteringSig = sig;
|
||||||
|
|
||||||
const now = audioCtx.currentTime;
|
const now = audioCtx.currentTime;
|
||||||
// Clamp every parameter so a stale/incomplete settings object can never push
|
// Clamp every parameter so a stale/incomplete settings object can never push
|
||||||
// NaN or an extreme value into the biquad filters — that puts the master
|
// NaN or an extreme value into the biquad filters — that puts the master
|
||||||
@@ -329,6 +337,7 @@ function setMasterVolume(linear) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _lastMasteringActive = null;
|
let _lastMasteringActive = null;
|
||||||
|
let _lastMasteringSig = null;
|
||||||
function toggleMasteringOnMaster(activate, isBypassed) {
|
function toggleMasteringOnMaster(activate, isBypassed) {
|
||||||
if (!masterBus) return;
|
if (!masterBus) return;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ const trackMidiChannelsRef={current:{}};const ensureTrackMidiChannel=(track,trac
|
|||||||
(function handleSfsDeepLink(){try{const params=new URLSearchParams(window.location.search);const sfsParam=params.get('sfs');if(!sfsParam)return;const decoded=JSON.parse(decodeURIComponent(sfsParam));window.__pendingSfsProject=decoded;// consumed after auth in App
|
(function handleSfsDeepLink(){try{const params=new URLSearchParams(window.location.search);const sfsParam=params.get('sfs');if(!sfsParam)return;const decoded=JSON.parse(decodeURIComponent(sfsParam));window.__pendingSfsProject=decoded;// consumed after auth in App
|
||||||
if(window.history.replaceState){window.history.replaceState({},document.title,window.location.pathname);}}catch(e){window.__pendingSfsProject=null;}})();// Storage for server-side file IDs mapped to track IDs
|
if(window.history.replaceState){window.history.replaceState({},document.title,window.location.pathname);}}catch(e){window.__pendingSfsProject=null;}})();// Storage for server-side file IDs mapped to track IDs
|
||||||
let serverFileIdMap={};let audioCtx;let masterBus=null;// { input, compressor, analyser, output, masteringActive }
|
let serverFileIdMap={};let audioCtx;let masterBus=null;// { input, compressor, analyser, output, masteringActive }
|
||||||
function makeDistortionCurve(k){const n_samples=44100;const curve=new Float32Array(n_samples);for(let i=0;i<n_samples;++i){const x=i*2/n_samples-1;curve[i]=Math.atan(x*k)/(Math.atan(k)||1);}return curve;}function applyMasteringSettings(s){if(!masterBus||!audioCtx||!s)return;const now=audioCtx.currentTime;// Clamp every parameter so a stale/incomplete settings object can never push
|
function makeDistortionCurve(k){const n_samples=44100;const curve=new Float32Array(n_samples);for(let i=0;i<n_samples;++i){const x=i*2/n_samples-1;curve[i]=Math.atan(x*k)/(Math.atan(k)||1);}return curve;}function applyMasteringSettings(s){if(!masterBus||!audioCtx||!s)return;// getAudioContext() invokes this on EVERY call (stopAll, play, VU, …).
|
||||||
|
// Re-applying identical values in rapid bursts is "fast parameter automation"
|
||||||
|
// and makes Chromium flag the biquad EQ filters as unstable ("state is bad").
|
||||||
|
// Only touch the graph when a value actually changed.
|
||||||
|
const sig=[s.eqActive,s.eqLowGain,s.eqMid1Gain,s.eqMid2Gain,s.eqHighGain,s.imagerActive,s.w1,s.w2,s.w3,s.w4,s.maximizerActive,s.maxGain,s.maxSoftClip,s.maxUpward,s.ceiling].join('|');if(sig===_lastMasteringSig)return;_lastMasteringSig=sig;const now=audioCtx.currentTime;// Clamp every parameter so a stale/incomplete settings object can never push
|
||||||
// NaN or an extreme value into the biquad filters — that puts the master
|
// NaN or an extreme value into the biquad filters — that puts the master
|
||||||
// chain into a bad state and silences ALL audio (the "mất soundfont" symptom).
|
// chain into a bad state and silences ALL audio (the "mất soundfont" symptom).
|
||||||
const clamp=(v,lo,hi)=>{const n=Number(v);if(!isFinite(n))return 0;// missing/NaN → neutral, never a filter-breaking value
|
const clamp=(v,lo,hi)=>{const n=Number(v);if(!isFinite(n))return 0;// missing/NaN → neutral, never a filter-breaking value
|
||||||
@@ -38,7 +42,7 @@ eqLowFilter.connect(eqMid1Filter);eqMid1Filter.connect(eqMid2Filter);eqMid2Filte
|
|||||||
eqHighFilter.connect(imagerInput);// Connect Imager to Maximizer
|
eqHighFilter.connect(imagerInput);// Connect Imager to Maximizer
|
||||||
imagerOutput.connect(maximizerBoostGain);// Setup default non-mastered routing:
|
imagerOutput.connect(maximizerBoostGain);// Setup default non-mastered routing:
|
||||||
// input -> compressor -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination
|
// input -> compressor -> inputAnalyser -> outputAnalyser -> output -> analyser -> destination
|
||||||
masterBus.input.connect(masterBus.compressor);masterBus.compressor.connect(masterBus.inputAnalyser);masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.outputAnalyser.connect(masterBus.output);masterBus.output.connect(masterBus.analyser);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}let _lastMasteringActive=null;function toggleMasteringOnMaster(activate,isBypassed){if(!masterBus)return;const active=!!(activate&&!isBypassed);// Idempotent: don't disconnect/reconnect the mastering chain on every call
|
masterBus.input.connect(masterBus.compressor);masterBus.compressor.connect(masterBus.inputAnalyser);masterBus.inputAnalyser.connect(masterBus.outputAnalyser);masterBus.outputAnalyser.connect(masterBus.output);masterBus.output.connect(masterBus.analyser);masterBus.analyser.connect(ctx.destination);window.masterBus=masterBus;return masterBus;}function setMasterVolume(linear){if(masterBus)masterBus.output.gain.setValueAtTime(linear,audioCtx.currentTime);}let _lastMasteringActive=null;let _lastMasteringSig=null;function toggleMasteringOnMaster(activate,isBypassed){if(!masterBus)return;const active=!!(activate&&!isBypassed);// Idempotent: don't disconnect/reconnect the mastering chain on every call
|
||||||
// (getAudioContext invokes this constantly). Rapid re-connection while audio
|
// (getAudioContext invokes this constantly). Rapid re-connection while audio
|
||||||
// flows destabilizes the biquad filters → "BiquadFilterNode: state is bad".
|
// flows destabilizes the biquad filters → "BiquadFilterNode: state is bad".
|
||||||
if(_lastMasteringActive===active&&masterBus.masteringActive===active)return;_lastMasteringActive=active;// Disconnect the dynamic junction
|
if(_lastMasteringActive===active&&masterBus.masteringActive===active)return;_lastMasteringActive=active;// Disconnect the dynamic junction
|
||||||
|
|||||||
@@ -86,14 +86,15 @@
|
|||||||
|
|
||||||
console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate);
|
console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate);
|
||||||
|
|
||||||
// ScriptProcessor is the proven-stable renderer in this app.
|
// Use the AudioWorklet (fluidsynth-bridge) — ScriptProcessor
|
||||||
// The AudioWorklet path (fluidsynth-bridge) is implemented and
|
// is deprecated and logs a console warning. The worklet is
|
||||||
// mono-safe, but it caused device-specific audio failures, so
|
// now mono-safe (handles 1/2/N channel outputs) and the node
|
||||||
// keep it opt-in (flip to false to try it) — the deprecation
|
// is forced stereo, and the URL is cache-busted so the fixed
|
||||||
// console warning is cosmetic.
|
// module always loads. Falls back to ScriptProcessor if the
|
||||||
var _useScriptNode = true;
|
// worklet cannot be created (older browsers).
|
||||||
|
var _useScriptNode = false;
|
||||||
try {
|
try {
|
||||||
await _audioCtx.audioWorklet.addModule('/static/js/worklets/fluidsynth-bridge.js?v=202608031240');
|
await _audioCtx.audioWorklet.addModule('/static/js/worklets/fluidsynth-bridge.js?v=202608031315');
|
||||||
console.log("[SonicSF] Worklet registered OK");
|
console.log("[SonicSF] Worklet registered OK");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[SonicSF] Worklet reg failed:", e);
|
console.warn("[SonicSF] Worklet reg failed:", e);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||||
<script src="/static/js/services/storage.js?v=202607271016"></script>
|
<script src="/static/js/services/storage.js?v=202607271016"></script>
|
||||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||||
<script src="/static/js/services/soundfontPlayer.js?v=202608031315"></script>
|
<script src="/static/js/services/soundfontPlayer.js?v=202608031330"></script>
|
||||||
<script src="/static/js/services/aiGateway.js?v=202607271016"></script>
|
<script src="/static/js/services/aiGateway.js?v=202607271016"></script>
|
||||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></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/pianoRollTabService.js?v=202607272044"></script>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></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/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202608031315" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202608031330" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -1249,3 +1249,8 @@
|
|||||||
- **Tóm tắt thay đổi:** (1) **Master chain bị NaN**: `applyMasteringSettings` chạy mỗi lần `getAudioContext()`; nếu `window.currentMasteringSettings` bị thiếu field/NaN (vd project lưu cũ, slider kéo cực hạn) thì `eqLowFilter.gain.setTargetAtTime(undefined/NaN)` → biquad "state is bad" → master chain im lặng → mọi audio (kể cả MIDI/ARM) biến mất ("mất soundfont"). Thêm `clamp()` chặn mọi tham số EQ (±24dB), imager (±100), maximizer, ceiling — NaN/thiếu → 0 (trung tính), cực hạn → giới hạn an toàn. (2) `toggleMasteringOnMaster` trở nên idempotent (chỉ disconnect/reconnect khi trạng thái đổi) — trước đây gọi lại liên tục từ `getAudioContext()` gây reconnect nhanh → biquad mất ổn định. (3) **loadSoundFont không cache lỗi vĩnh viễn**: lỗi tải thoáng qua (mạng 503, áp lực bộ nhớ) trước đây bị giữ trong `_loadPromises` → instrument câm vĩnh viễn tới khi reload; giờ xóa cache lỗi để note kế tiếp thử lại và tự hồi phục.
|
- **Tóm tắt thay đổi:** (1) **Master chain bị NaN**: `applyMasteringSettings` chạy mỗi lần `getAudioContext()`; nếu `window.currentMasteringSettings` bị thiếu field/NaN (vd project lưu cũ, slider kéo cực hạn) thì `eqLowFilter.gain.setTargetAtTime(undefined/NaN)` → biquad "state is bad" → master chain im lặng → mọi audio (kể cả MIDI/ARM) biến mất ("mất soundfont"). Thêm `clamp()` chặn mọi tham số EQ (±24dB), imager (±100), maximizer, ceiling — NaN/thiếu → 0 (trung tính), cực hạn → giới hạn an toàn. (2) `toggleMasteringOnMaster` trở nên idempotent (chỉ disconnect/reconnect khi trạng thái đổi) — trước đây gọi lại liên tục từ `getAudioContext()` gây reconnect nhanh → biquad mất ổn định. (3) **loadSoundFont không cache lỗi vĩnh viễn**: lỗi tải thoáng qua (mạng 503, áp lực bộ nhớ) trước đây bị giữ trong `_loadPromises` → instrument câm vĩnh viễn tới khi reload; giờ xóa cache lỗi để note kế tiếp thử lại và tự hồi phục.
|
||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/static/js/services/soundfontPlayer.js`, `app/templates/index.html`
|
||||||
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK. Harness `node /tmp/kilo/test_retry.js`: clamp undefined/NaN → 0, cực hạn → giới hạn; load font lỗi 503 lần 1 → lần 2 retry thành công (2 fetches) → note phát được — ALL PASSED. 8 harness còn lại PASS. Deprecation ScriptProcessorNode vẫn còn (giữ ScriptProcessor vì ổn định, worklet từng gây lỗi thiết bị). Hard refresh.
|
- **Ghi chú/Test (nếu có):** `npm run build` OK, bundle syntax OK. Harness `node /tmp/kilo/test_retry.js`: clamp undefined/NaN → 0, cực hạn → giới hạn; load font lỗi 503 lần 1 → lần 2 retry thành công (2 fetches) → note phát được — ALL PASSED. 8 harness còn lại PASS. Deprecation ScriptProcessorNode vẫn còn (giữ ScriptProcessor vì ổn định, worklet từng gây lỗi thiết bị). Hard refresh.
|
||||||
|
|
||||||
|
### [2026-08-03 13:30] Task: Hết ScriptProcessor deprecation (bật AudioWorklet) + hết BiquadFilterNode state is bad (signature guard)
|
||||||
|
- **Tóm tắt thay đổi:** (1) **Deprecation**: bật lại AudioWorklet làm renderer mặc định (`_useScriptNode = false`) — worklet giờ đã mono-safe + ép stereo `outputChannelCount:[2]` + URL `addModule` có cache-busting `?v=` nên luôn nạp bản đã sửa; fallback ScriptProcessor khi tạo worklet thất bại. Trước đây "mất tiếng khi transport" là do worklet cũ (pre-mono-fix) bị cache. (2) **BiquadFilterNode: state is bad**: `applyMasteringSettings` được gọi mỗi lần `getAudioContext()` (stopAll/play/VU…), liên tục `setTargetAtTime` vào EQ biquad = "fast parameter automation" → Chromium báo state bad dù giá trị hợp lệ. Thêm **signature guard**: chỉ áp dụng khi giá trị thực sự đổi → bỏ chùm automation lặp, giữ clamp chống NaN. `toggleMasteringOnMaster` vẫn idempotent.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (build lại), `app/templates/index.html`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` OK. Harness `test_render.js` (worklet mặc định): workletCreated=1 scriptProcCreated=0, noteon=1, 26 PCM frames posted, peak 0.25 — ALL PASSED. `test_sonicsf_fix.js` cập nhật assertion worklet → PASS. 8 harness còn lại PASS. Hard refresh (Ctrl+F5) — lần đầu sẽ tải worklet mới. Nếu nghe crackle/mất tiếng khi phát, báo lại để quay lại ScriptProcessor.
|
||||||
|
|||||||
Reference in New Issue
Block a user