ae6abc3ea5
_fluid_synth_noteoff chưa đủ để tắt âm với instrument loop. Thêm CC120 kill tất cả voices trên channel để note tắt ngay.
552 lines
26 KiB
JavaScript
552 lines
26 KiB
JavaScript
(function () {
|
|
const RENDER_BLOCK = 512;
|
|
const QUEUE_TARGET = 4;
|
|
let _audioCtx = null;
|
|
let _fluidModule = null;
|
|
let _synthPtr = null;
|
|
let _settingsPtr = null;
|
|
let _workletNode = null;
|
|
let _initialized = false;
|
|
let _initPromise = null;
|
|
let _currentSfId = null;
|
|
let _sfHandleMap = new Map();
|
|
let _channels = Array.from({ length: 16 }, () => ({ bank: 0, program: 0, isPercussion: false }));
|
|
let _nextMelodicChannel = 0;
|
|
let _sustainStates = new Array(16).fill(false);
|
|
let _leftBufPtr = null;
|
|
let _rightBufPtr = null;
|
|
let _renderTimer = null;
|
|
let _pendingNoteTimers = [];
|
|
let _loadedFonts = {};
|
|
let _activeOscillators = {};
|
|
let _gainNode = null;
|
|
let _scheduledNotes = [];
|
|
|
|
const getCtx = function () {
|
|
if (typeof getAudioContext === 'function') {
|
|
var ctx = getAudioContext();
|
|
if (!_gainNode) {
|
|
_gainNode = ctx.createGain();
|
|
_gainNode.gain.value = 0.3;
|
|
_gainNode.connect(ctx.destination);
|
|
}
|
|
return ctx;
|
|
}
|
|
if (!window.__sharedAudioCtx) {
|
|
window.__sharedAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
if (window.__sharedAudioCtx.state === 'suspended') {
|
|
window.__sharedAudioCtx.resume();
|
|
}
|
|
if (!_gainNode) {
|
|
_gainNode = window.__sharedAudioCtx.createGain();
|
|
_gainNode.gain.value = 0.3;
|
|
_gainNode.connect(window.__sharedAudioCtx.destination);
|
|
}
|
|
return window.__sharedAudioCtx;
|
|
};
|
|
|
|
const SonicSF = {
|
|
loadedFonts: _loadedFonts,
|
|
|
|
init: async function (audioContext) {
|
|
if (_initialized && _fluidModule) return;
|
|
if (_initPromise) return _initPromise;
|
|
_initPromise = (async () => {
|
|
if (!window.__FluidSynthModuleFactory) {
|
|
console.log("[SonicSF] Waiting for FluidSynth WASM module to load...");
|
|
await window.__FluidSynthReady;
|
|
if (!window.__FluidSynthModuleFactory) {
|
|
console.warn("[SonicSF] FluidSynth WASM still not available after waiting.");
|
|
_initPromise = null;
|
|
return;
|
|
}
|
|
}
|
|
try {
|
|
_audioCtx = audioContext;
|
|
if (_audioCtx.state === 'suspended') await _audioCtx.resume();
|
|
|
|
console.log("[SonicSF] AudioCtx state:", _audioCtx.state, "sampleRate:", _audioCtx.sampleRate);
|
|
|
|
var _useScriptNode = true;
|
|
try {
|
|
await _audioCtx.audioWorklet.addModule('/static/js/worklets/fluidsynth-bridge.js');
|
|
console.log("[SonicSF] Worklet registered OK");
|
|
} catch (e) {
|
|
console.warn("[SonicSF] Worklet reg failed:", e);
|
|
}
|
|
console.log("[SonicSF] Using ScriptProcessorNode (forced for debug)");
|
|
|
|
console.log("[SonicSF] Initializing FluidSynth WASM Engine...");
|
|
var TOTAL_MEMORY = 256 * 1024 * 1024;
|
|
_fluidModule = await window.__FluidSynthModuleFactory({
|
|
locateFile: function (path) {
|
|
if (path.endsWith('.wasm')) {
|
|
return window.__FluidSynthLocateWasm ? window.__FluidSynthLocateWasm() : path;
|
|
}
|
|
return path;
|
|
},
|
|
TOTAL_MEMORY: TOTAL_MEMORY,
|
|
printErr: function (msg) {
|
|
if (msg.indexOf('stderr') === -1) console.warn('[FluidSynth:err]', msg);
|
|
}
|
|
});
|
|
|
|
if (!_fluidModule || !_fluidModule._new_fluid_settings) {
|
|
throw new Error("FluidSynth WASM module loaded but API missing");
|
|
}
|
|
|
|
_settingsPtr = _fluidModule._new_fluid_settings();
|
|
_fluidModule._fluid_settings_setnum(_settingsPtr, "synth.sample-rate", _audioCtx.sampleRate || 44100);
|
|
_fluidModule._fluid_settings_setnum(_settingsPtr, "synth.gain", 2.0);
|
|
_fluidModule._fluid_settings_setnum(_settingsPtr, "synth.polyphony", 256);
|
|
_fluidModule._fluid_settings_setint(_settingsPtr, "synth.verbose", 1);
|
|
_fluidModule._fluid_settings_setint(_settingsPtr, "synth.ladspa.active", 0);
|
|
_fluidModule._fluid_settings_setstr(_settingsPtr, "player.timing-source", "audio");
|
|
console.log("[SonicSF] FluidSynth settings configured");
|
|
|
|
_synthPtr = _fluidModule._new_fluid_synth(_settingsPtr);
|
|
if (!_synthPtr) throw new Error("Failed to create FluidSynth synthesizer");
|
|
_fluidModule._fluid_synth_set_gain(_synthPtr, 1.0);
|
|
|
|
try { _fluidModule.FS.mkdir('/soundfonts'); } catch (e) {}
|
|
|
|
_leftBufPtr = _fluidModule._malloc(RENDER_BLOCK * 4);
|
|
_rightBufPtr = _fluidModule._malloc(RENDER_BLOCK * 4);
|
|
|
|
if (!_useScriptNode) {
|
|
try {
|
|
_workletNode = new AudioWorkletNode(_audioCtx, 'fluidsynth-bridge');
|
|
_workletNode.connect(_audioCtx.destination);
|
|
console.log("[SonicSF] AudioWorklet node connected");
|
|
_startRenderLoop();
|
|
} catch (e) {
|
|
console.warn("[SonicSF] AudioWorkletNode failed:", e);
|
|
_useScriptNode = true;
|
|
}
|
|
}
|
|
if (_useScriptNode) {
|
|
var spBufSz = 2048;
|
|
var spn = _audioCtx.createScriptProcessor(spBufSz, 0, 2);
|
|
var lp = _fluidModule._malloc(spBufSz * 4);
|
|
var rp = _fluidModule._malloc(spBufSz * 4);
|
|
spn.onaudioprocess = function (e) {
|
|
var left = e.outputBuffer.getChannelData(0);
|
|
var right = e.outputBuffer.getChannelData(1);
|
|
var sz = left.length;
|
|
try {
|
|
_fluidModule._fluid_synth_write_float(_synthPtr, sz, lp, 0, 1, rp, 0, 1);
|
|
var hf = _fluidModule.HEAPF32;
|
|
var lpb = lp >> 2, rpb = rp >> 2;
|
|
for (var si = 0; si < sz; si++) {
|
|
left[si] = hf[lpb + si];
|
|
right[si] = hf[rpb + si];
|
|
}
|
|
} catch (er) {}
|
|
};
|
|
spn.connect(_audioCtx.destination);
|
|
_workletNode = spn;
|
|
console.log("[SonicSF] ScriptProcessorNode fallback active (buf:", spBufSz, ")");
|
|
}
|
|
|
|
_initialized = true;
|
|
console.log("[SonicSF] FluidSynth WASM Engine initialized.");
|
|
} catch (e) {
|
|
console.error("[SonicSF] FluidSynth init failed:", e);
|
|
_initPromise = null;
|
|
_cleanupFluid();
|
|
}
|
|
})();
|
|
return _initPromise;
|
|
},
|
|
|
|
_allocCStr: function (str) {
|
|
var ptr = _fluidModule._malloc(str.length + 1);
|
|
for (var i = 0; i < str.length; i++) {
|
|
_fluidModule.HEAPU8[ptr + i] = str.charCodeAt(i);
|
|
}
|
|
_fluidModule.HEAPU8[ptr + str.length] = 0;
|
|
return ptr;
|
|
},
|
|
|
|
_tryLoadSFL: function (buf, ext) {
|
|
var fname = '/' + ext + '_' + Date.now();
|
|
try { _fluidModule.FS.unlink(fname); } catch (e) {}
|
|
_fluidModule.FS.writeFile(fname, new Uint8Array(buf));
|
|
var cPath = this._allocCStr(fname);
|
|
var handle = _fluidModule._fluid_synth_sfload(_synthPtr, cPath, 1);
|
|
_fluidModule._free(cPath);
|
|
try { _fluidModule.FS.unlink(fname); } catch (e) {}
|
|
return handle;
|
|
},
|
|
|
|
loadSoundFont: async function (sfId) {
|
|
if (!_initialized || !_fluidModule) return false;
|
|
if (_currentSfId === sfId) return true;
|
|
if (_sfHandleMap.has(sfId)) {
|
|
_currentSfId = sfId;
|
|
return true;
|
|
}
|
|
try {
|
|
var cache = window.SonicSFStorage;
|
|
var buf = cache ? await cache.getBuffer(sfId) : null;
|
|
if (!buf) {
|
|
var url = "/api/v1/plugins/soundfonts/download/" + encodeURIComponent(sfId) + "?t=" + Date.now();
|
|
var resp = await fetch(url);
|
|
if (!resp.ok) {
|
|
console.warn("[SonicSF] SoundFont not found:", sfId);
|
|
return false;
|
|
}
|
|
buf = await resp.arrayBuffer();
|
|
if (cache) await cache.saveBuffer(sfId, buf);
|
|
}
|
|
var sfHandle = this._tryLoadSFL(buf, '.sf3');
|
|
if (sfHandle === -1) {
|
|
console.warn("[SonicSF] sfload .sf3 failed, trying .sf2 for", sfId);
|
|
sfHandle = this._tryLoadSFL(buf, '.sf2');
|
|
}
|
|
if (sfHandle === -1) {
|
|
console.error("[SonicSF] FluidSynth failed to parse SoundFont:", sfId);
|
|
return false;
|
|
}
|
|
_sfHandleMap.set(sfId, sfHandle);
|
|
_currentSfId = sfId;
|
|
_loadedFonts[sfId] = true;
|
|
console.log("[SonicSF] SoundFont loaded:", sfId, "handle:", sfHandle);
|
|
return true;
|
|
} catch (e) {
|
|
console.warn("[SonicSF] loadSoundFont error:", e);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
selectInstrument: async function (channel, bank, program, sfId) {
|
|
if (!_initialized || !_fluidModule) {
|
|
var ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
await this.init(ctx);
|
|
}
|
|
if (sfId) {
|
|
var ok = await this.loadSoundFont(sfId);
|
|
if (!ok) return;
|
|
}
|
|
try {
|
|
_fluidModule._fluid_synth_bank_select(_synthPtr, channel, bank);
|
|
} catch (e) {}
|
|
try {
|
|
_fluidModule._fluid_synth_program_change(_synthPtr, channel, program);
|
|
} catch (e) {}
|
|
_channels[channel].bank = bank;
|
|
_channels[channel].program = program;
|
|
_channels[channel].isPercussion = (bank === 128);
|
|
},
|
|
|
|
controllerChange: function (channel, controller, value) {
|
|
if (channel < 0 || channel > 15) return;
|
|
if (_initialized && _fluidModule) {
|
|
try {
|
|
_fluidModule._fluid_synth_cc(_synthPtr, channel, controller, value);
|
|
} catch (e) {}
|
|
}
|
|
if (controller === 0) {
|
|
_channels[channel].bank = value;
|
|
_channels[channel].isPercussion = (value === 128);
|
|
}
|
|
if (controller === 64) {
|
|
_sustainStates[channel] = value >= 64;
|
|
}
|
|
},
|
|
|
|
programChange: function (channel, program) {
|
|
if (channel < 0 || channel > 15) return;
|
|
if (_initialized && _fluidModule) {
|
|
try {
|
|
_fluidModule._fluid_synth_program_change(_synthPtr, channel, program);
|
|
} catch (e) {}
|
|
}
|
|
_channels[channel].program = program;
|
|
},
|
|
|
|
allocateChannel: function (bank) {
|
|
if (bank === 128) return 9;
|
|
var ch = _nextMelodicChannel % 9;
|
|
_nextMelodicChannel = (_nextMelodicChannel + 1) % 9;
|
|
return ch;
|
|
},
|
|
|
|
applyAITrackInstrument: function (bank, program, synthEngine) {
|
|
if (synthEngine) {
|
|
bank = bank !== undefined ? bank : (synthEngine.soundfont_bank || 0);
|
|
program = program !== undefined ? program : (synthEngine.soundfont_program || 0);
|
|
}
|
|
var channel = this.allocateChannel(bank);
|
|
this.selectInstrument(channel, bank, program, synthEngine && synthEngine.soundfont_id);
|
|
return channel;
|
|
},
|
|
|
|
getChannelState: function (channel) {
|
|
if (channel < 0 || channel > 15) return null;
|
|
return { bank: _channels[channel].bank, program: _channels[channel].program, isPercussion: _channels[channel].isPercussion };
|
|
},
|
|
|
|
sustainActive: function (channel) {
|
|
if (channel < 0 || channel > 15) return false;
|
|
return _sustainStates[channel];
|
|
},
|
|
|
|
pitchBend: function (channel, value) {
|
|
if (channel < 0 || channel > 15) return;
|
|
if (_initialized && _fluidModule) {
|
|
try {
|
|
_fluidModule._fluid_synth_pitch_bend(_synthPtr, channel, value);
|
|
} catch (e) {}
|
|
}
|
|
},
|
|
|
|
stopNote: function (channel, pitch) {
|
|
if (channel < 0 || channel > 15) return;
|
|
if (_initialized && _fluidModule) {
|
|
try { _fluidModule._fluid_synth_noteoff(_synthPtr, channel, pitch); } catch (e) {}
|
|
try { _fluidModule._fluid_synth_cc(_synthPtr, channel, 120, 0); } catch (e) {}
|
|
}
|
|
},
|
|
|
|
playNote: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) {
|
|
if (!_initialized || !_fluidModule) {
|
|
this._lazyInit();
|
|
this._playNoteFallback(note, velocity, 2000, startTime, program, destinationNode, channel, synthEngine);
|
|
return;
|
|
}
|
|
this._playNoteFluid(note, velocity, durationMs, startTime, program, channel, synthEngine);
|
|
},
|
|
|
|
_lazyInit: async function () {
|
|
if (_initialized && _fluidModule) return;
|
|
if (_initPromise) return;
|
|
try {
|
|
var ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
if (ctx.state === 'suspended') await ctx.resume();
|
|
await this.init(ctx);
|
|
} catch (e) {}
|
|
},
|
|
|
|
_playNoteFluid: function (note, velocity, durationMs, startTime, program, channel, synthEngine) {
|
|
var midiPitch = Math.min(127, Math.max(0, parseInt(note) || 60));
|
|
var midiVel = Math.min(127, Math.max(1, Math.floor(
|
|
typeof velocity === 'number' ? (velocity > 1 ? velocity : velocity * 127) : 100
|
|
)));
|
|
var ch = channel;
|
|
var usedBank = 0, usedProg = 0;
|
|
if (synthEngine) {
|
|
if (ch === undefined) {
|
|
ch = synthEngine.soundfont_bank === 128 ? 9 : 0;
|
|
}
|
|
usedBank = synthEngine.soundfont_bank || 0;
|
|
usedProg = synthEngine.soundfont_program || 0;
|
|
try {
|
|
_fluidModule._fluid_synth_bank_select(_synthPtr, ch, usedBank);
|
|
} catch (e) {}
|
|
try {
|
|
_fluidModule._fluid_synth_program_change(_synthPtr, ch, usedProg);
|
|
} catch (e) {}
|
|
if (channel === undefined) channel = ch;
|
|
} else if (program !== undefined) {
|
|
usedProg = program;
|
|
try {
|
|
_fluidModule._fluid_synth_program_change(_synthPtr, ch || 0, usedProg);
|
|
} catch (e) {}
|
|
}
|
|
if (ch === undefined) ch = (usedBank === 128 ? 9 : 0);
|
|
var ctx = getCtx();
|
|
var now = ctx.currentTime;
|
|
var delay = (typeof startTime === 'number' && startTime > now) ? (startTime - now) : 0;
|
|
var durSec = (durationMs || 500) / 1000;
|
|
var scheduledNote = { on: null, off: null };
|
|
var self = this;
|
|
var doNote = function () {
|
|
try {
|
|
console.log("[SonicSF] noteOn ch:", ch, "pitch:", midiPitch, "vel:", midiVel);
|
|
_fluidModule._fluid_synth_noteon(_synthPtr, ch, midiPitch, midiVel);
|
|
if (delay > 0) {
|
|
scheduledNote.off = setTimeout(function () {
|
|
try {
|
|
_fluidModule._fluid_synth_noteoff(_synthPtr, ch, midiPitch);
|
|
} catch (e) {}
|
|
}, durSec * 1000);
|
|
}
|
|
} catch (e) {
|
|
console.warn("[SonicSF] FluidSynth noteOn error:", e);
|
|
self._playNoteFallback(note, velocity, durationMs, startTime, program, null, channel, synthEngine);
|
|
}
|
|
};
|
|
if (delay > 0) {
|
|
scheduledNote.on = setTimeout(doNote, delay * 1000);
|
|
_scheduledNotes.push(scheduledNote);
|
|
} else {
|
|
doNote();
|
|
}
|
|
},
|
|
|
|
_playNoteFallback: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) {
|
|
var ctx = getCtx();
|
|
var freq = 440 * Math.pow(2, (note - 69) / 12);
|
|
if (freq <= 0 || isNaN(freq)) return null;
|
|
if (synthEngine) {
|
|
var ch = channel !== undefined ? channel : (synthEngine.soundfont_bank === 128 ? 9 : 0);
|
|
this.controllerChange(ch, 0, synthEngine.soundfont_bank || 0);
|
|
this.programChange(ch, synthEngine.soundfont_program || 0);
|
|
if (channel === undefined) channel = ch;
|
|
if (program === undefined) program = synthEngine.soundfont_program;
|
|
}
|
|
var osc = ctx.createOscillator();
|
|
var noteGain = ctx.createGain();
|
|
var oscType = 'triangle';
|
|
var attackTime = 0.03, decayTime = 0.1, sustainLevel = 0.5, releaseTime = 0.2, volFactor = 0.25;
|
|
var prog = program !== undefined ? parseInt(program) : 0;
|
|
if (channel !== undefined && channel >= 0 && channel < 16) {
|
|
prog = _channels[channel].program || prog;
|
|
}
|
|
if (prog >= 0 && prog <= 7) { oscType = 'sine'; decayTime = 0.3; sustainLevel = 0.1; releaseTime = 0.2; }
|
|
else if (prog >= 8 && prog <= 15) { oscType = 'sine'; decayTime = 0.1; sustainLevel = 0.0; releaseTime = 0.1; }
|
|
else if (prog >= 16 && prog <= 23) { oscType = 'sine'; attackTime = 0.05; sustainLevel = 0.8; releaseTime = 0.1; }
|
|
else if (prog >= 24 && prog <= 31) { oscType = 'triangle'; decayTime = 0.4; sustainLevel = 0.2; releaseTime = 0.3; }
|
|
else if (prog >= 32 && prog <= 39) { oscType = 'triangle'; attackTime = 0.02; decayTime = 0.2; sustainLevel = 0.6; releaseTime = 0.2; }
|
|
else if (prog >= 40 && prog <= 47) { oscType = 'sawtooth'; attackTime = 0.15; sustainLevel = 0.8; releaseTime = 0.5; volFactor = 0.15; }
|
|
else if (prog >= 48 && prog <= 55) { oscType = 'sawtooth'; attackTime = 0.2; sustainLevel = 0.8; releaseTime = 0.6; volFactor = 0.12; }
|
|
else if (prog >= 56 && prog <= 63) { oscType = 'sawtooth'; attackTime = 0.08; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.15; }
|
|
else if (prog >= 64 && prog <= 71) { oscType = 'square'; attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.2; volFactor = 0.15; }
|
|
else if (prog >= 72 && prog <= 79) { oscType = 'sine'; attackTime = 0.1; sustainLevel = 0.7; releaseTime = 0.3; volFactor = 0.2; }
|
|
else if (prog >= 80 && prog <= 119) { oscType = 'sawtooth'; attackTime = 0.05; sustainLevel = 0.6; releaseTime = 0.4; volFactor = 0.15; }
|
|
osc.type = oscType;
|
|
osc.frequency.setValueAtTime(freq, 0);
|
|
var startAt = startTime !== undefined ? startTime : ctx.currentTime;
|
|
var durSec = durationMs / 1000;
|
|
var vel = typeof velocity === 'number' ? (velocity > 1 ? velocity / 127 : velocity) : 0.8;
|
|
var targetGain = vel * volFactor;
|
|
noteGain.gain.setValueAtTime(0, startAt);
|
|
noteGain.gain.linearRampToValueAtTime(targetGain, startAt + attackTime);
|
|
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, startAt + attackTime + decayTime);
|
|
var releaseStart = startAt + Math.max(attackTime + decayTime, durSec);
|
|
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, releaseStart);
|
|
noteGain.gain.linearRampToValueAtTime(0, releaseStart + releaseTime);
|
|
osc.connect(noteGain);
|
|
noteGain.connect(destinationNode || _gainNode || ctx.destination);
|
|
osc.start(startAt);
|
|
var stopAt = releaseStart + releaseTime + 0.02;
|
|
osc.stop(stopAt);
|
|
var oscId = note + '_' + Date.now() + '_' + Math.random();
|
|
_activeOscillators[oscId] = { osc: osc, gain: noteGain };
|
|
setTimeout(function () { delete _activeOscillators[oscId]; }, (stopAt - ctx.currentTime) * 1000 + 100);
|
|
return osc;
|
|
},
|
|
|
|
stopAll: function () {
|
|
if (_initialized && _fluidModule) {
|
|
for (var ch = 0; ch < 16; ch++) {
|
|
try { _fluidModule._fluid_synth_all_notes_off(_synthPtr, ch); } catch (e) {}
|
|
try { _fluidModule._fluid_synth_all_sounds_off(_synthPtr, ch); } catch (e) {}
|
|
}
|
|
}
|
|
while (_scheduledNotes.length > 0) {
|
|
var n = _scheduledNotes.pop();
|
|
if (n.on) { clearTimeout(n.on); n.on = null; }
|
|
if (n.off) { clearTimeout(n.off); n.off = null; }
|
|
}
|
|
var ctx = getCtx();
|
|
var now = ctx.currentTime;
|
|
Object.values(_activeOscillators).forEach(function (entry) {
|
|
try {
|
|
if (entry.gain) { entry.gain.gain.cancelScheduledValues(now); entry.gain.gain.setValueAtTime(0, now); }
|
|
if (entry.osc) try { entry.osc.stop(now); } catch (e) {}
|
|
} catch (e) {}
|
|
});
|
|
Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; });
|
|
},
|
|
|
|
saveToIndexedDB: async function (name, arrayBuffer) {
|
|
if (window.SonicStorage && window.SonicStorage.saveToIndexedDB) {
|
|
await window.SonicStorage.saveToIndexedDB('soundfont_' + name, arrayBuffer);
|
|
}
|
|
},
|
|
|
|
loadFromIndexedDB: async function (name) {
|
|
if (window.SonicStorage && window.SonicStorage.loadFromIndexedDB) {
|
|
return await window.SonicStorage.loadFromIndexedDB('soundfont_' + name);
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
function _startRenderLoop() {
|
|
if (_renderTimer) return;
|
|
var Module = _fluidModule;
|
|
var synth = _synthPtr;
|
|
var node = _workletNode;
|
|
var leftPtr = _leftBufPtr;
|
|
var rightPtr = _rightBufPtr;
|
|
var block = RENDER_BLOCK;
|
|
var queueDepth = 0;
|
|
var maxQueue = QUEUE_TARGET;
|
|
|
|
var _dbgPeak = 0;
|
|
function pushFrame() {
|
|
if (!Module || !synth || !node) return;
|
|
try {
|
|
var lpb = leftPtr >> 2;
|
|
var rpb = rightPtr >> 2;
|
|
Module.HEAPF32.fill(0, lpb, lpb + block);
|
|
Module.HEAPF32.fill(0, rpb, rpb + block);
|
|
Module._fluid_synth_write_float(synth, block, leftPtr, 0, 1, rightPtr, 0, 1);
|
|
var leftArr = new Float32Array(Module.HEAPF32.subarray(lpb, lpb + block));
|
|
var rightArr = new Float32Array(Module.HEAPF32.subarray(rpb, rpb + block));
|
|
var peak = 0;
|
|
var avg = 0;
|
|
for (var si = 0; si < leftArr.length; si++) {
|
|
var abs = leftArr[si] > 0 ? leftArr[si] : -leftArr[si];
|
|
if (abs > peak) peak = abs;
|
|
avg += abs;
|
|
}
|
|
avg /= leftArr.length;
|
|
if (!_dbgPeak) {
|
|
_dbgPeak = 1;
|
|
console.log("[SonicSF] FRAME peak:", peak.toFixed(6), "avg:", avg.toFixed(8), "gain check:", Module._fluid_synth_get_gain ? Module._fluid_synth_get_gain(synth) : 'N/A');
|
|
}
|
|
node.port.postMessage({ type: 'PCM', L: leftArr, R: rightArr }, [leftArr.buffer, rightArr.buffer]);
|
|
queueDepth++;
|
|
} catch (e) { console.warn("[SonicSF] pushFrame error:", e); }
|
|
}
|
|
|
|
function fillLoop() {
|
|
if (!Module || !synth || !node || !_initialized) {
|
|
_renderTimer = null;
|
|
return;
|
|
}
|
|
var needed = maxQueue - queueDepth;
|
|
for (var i = 0; i < needed; i++) {
|
|
pushFrame();
|
|
}
|
|
queueDepth = Math.max(0, queueDepth - 1);
|
|
}
|
|
|
|
_renderTimer = setInterval(fillLoop, Math.max(8, (block / _audioCtx.sampleRate) * 1000 * 0.75));
|
|
}
|
|
|
|
function _stopRenderLoop() {
|
|
if (_renderTimer) {
|
|
clearInterval(_renderTimer);
|
|
_renderTimer = null;
|
|
}
|
|
}
|
|
|
|
function _cleanupFluid() {
|
|
_stopRenderLoop();
|
|
if (_leftBufPtr && _fluidModule) { try { _fluidModule._free(_leftBufPtr); } catch (e) {} _leftBufPtr = null; }
|
|
if (_rightBufPtr && _fluidModule) { try { _fluidModule._free(_rightBufPtr); } catch (e) {} _rightBufPtr = null; }
|
|
if (_synthPtr && _fluidModule) { try { _fluidModule._delete_fluid_synth(_synthPtr); } catch (e) {} _synthPtr = null; }
|
|
if (_settingsPtr && _fluidModule) { try { _fluidModule._delete_fluid_settings(_settingsPtr); } catch (e) {} _settingsPtr = null; }
|
|
_fluidModule = null;
|
|
_initialized = false;
|
|
}
|
|
|
|
window.SonicSF = SonicSF;
|
|
})();
|