1import { s as spessasynthWorkletUrl, W as WorkletSynthesizer } from "./vendor-spessasynth-DH8n6o_V.js"; 2async function decodeSf3ToSf2(sf3Buffer, ctx) { 3 const u8 = new Uint8Array(sf3Buffer); 4 let firstOgg = -1; 5 for (let i = 0; i < Math.min(u8.length - 4, 15e3); i++) { 6 if (u8[i] === 79 && u8[i + 1] === 103 && u8[i + 2] === 103 && u8[i + 3] === 83) { 7 firstOgg = i; 8 break; 9 } 10 } 11 if (firstOgg === -1) { 12 return sf3Buffer; 13 } 14 console.log(`[SF3Decoder] SF3 detected (${(sf3Buffer.byteLength / 1024 / 1024).toFixed(2)} MB). Decoding Ogg Vorbis stream in memory...`); 15 const view = new DataView(sf3Buffer); 16 const smplSize = view.getUint32(firstOgg - 4, true); 17 let pdtaStart = -1; 18 for (let i = firstOgg + smplSize; i < u8.length - 12; i++) { 19 if (u8[i] === 76 && u8[i + 1] === 73 && u8[i + 2] === 83 && u8[i + 3] === 84) { 20 if (u8[i + 8] === 112 && u8[i + 9] === 100 && u8[i + 10] === 116 && u8[i + 11] === 97) { 21 pdtaStart = i; 22 break; 23 } 24 } 25 } 26 const oggData = sf3Buffer.slice(firstOgg, firstOgg + smplSize); 27 let sampleRate = 44100; 28 let channels = 1; 29 for (let i = firstOgg; i < firstOgg + 1e3; i++) { 30 if (u8[i] === 1 && u8[i + 1] === 118 && u8[i + 2] === 111 && u8[i + 3] === 114 && u8[i + 4] === 98 && u8[i + 5] === 105 && u8[i + 6] === 115) { 31 channels = u8[i + 11]; 32 sampleRate = u8[i + 12] | u8[i + 13] << 8 | u8[i + 14] << 16 | u8[i + 15] << 24; 33 break; 34 } 35 } 36 console.log(`[SF3Decoder] Vorbis Sample Rate: ${sampleRate}Hz, Channels: ${channels}. Decoding without resampling...`); 37 const OfflineCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext; 38 const offlineCtx = new OfflineCtx(channels, 1, sampleRate); 39 const audioBuffer = await Promise.race([ 40 offlineCtx.decodeAudioData(oggData), 41 new Promise((_, reject) => setTimeout(() => reject(new Error("decodeAudioData timeout")), 1e4)) 42 ]); 43 const floatSamples = audioBuffer.getChannelData(0); 44 const pcmInt16 = new Int16Array(floatSamples.length); 45 for (let i = 0; i < floatSamples.length; i++) { 46 const s = Math.max(-1, Math.min(1, floatSamples[i])); 47 pcmInt16[i] = s < 0 ? s * 32768 : s * 32767; 48 } 49 const pcmBytes = new Uint8Array(pcmInt16.buffer); 50 const smplChunkSize = pcmBytes.byteLength; 51 const infoStart = 12; 52 const infoSize = view.getUint32(infoStart + 4, true); 53 const infoChunk = u8.slice(infoStart, infoStart + 8 + infoSize + infoSize % 2); 54 let pdtaChunk = pdtaStart > 0 ? u8.slice(pdtaStart) : null; 55 if (pdtaChunk) { 56 let shdrStart = -1; 57 let shdrSize = 0; 58 let offset = 12; 59 const pdtaView = new DataView(pdtaChunk.buffer, pdtaChunk.byteOffset, pdtaChunk.byteLength); 60 while (offset + 8 <= pdtaChunk.byteLength) { 61 const cid = String.fromCharCode(pdtaChunk[offset], pdtaChunk[offset + 1], pdtaChunk[offset + 2], pdtaChunk[offset + 3]); 62 const csize = pdtaView.getUint32(offset + 4, true); 63 if (cid === "shdr") { 64 shdrStart = offset + 8; 65 shdrSize = csize; 66 break; 67 } 68 offset += 8 + csize + csize % 2; 69 } 70 if (shdrStart !== -1) { 71 const totalSamples = pcmInt16.length; 72 pdtaChunk = new Uint8Array(pdtaChunk); 73 const shdrView = new DataView(pdtaChunk.buffer, pdtaChunk.byteOffset, pdtaChunk.byteLength); 74 for (let p = shdrStart; p + 46 <= shdrStart + shdrSize; p += 46) { 75 let start = shdrView.getUint32(p + 20, true); 76 let end = shdrView.getUint32(p + 24, true); 77 let startLoop = shdrView.getUint32(p + 28, true); 78 let endLoop = shdrView.getUint32(p + 32, true); 79 if (start >= totalSamples) start = Math.max(0, totalSamples - 1); 80 if (end >= totalSamples) end = Math.max(0, totalSamples - 1); 81 if (startLoop >= totalSamples) startLoop = Math.max(0, totalSamples - 1); 82 if (endLoop >= totalSamples) endLoop = Math.max(0, totalSamples - 1); 83 if (start > end) start = end; 84 if (startLoop > endLoop) startLoop = endLoop; 85 if (startLoop < start) startLoop = start; 86 if (endLoop > end) endLoop = end; 87 shdrView.setUint32(p + 20, start, true); 88 shdrView.setUint32(p + 24, end, true); 89 shdrView.setUint32(p + 28, startLoop, true); 90 shdrView.setUint32(p + 32, endLoop, true); 91 } 92 } 93 } 94 const sdtaTotalSize = 12 + 8 + smplChunkSize; 95 const newRiffSize = 4 + infoChunk.byteLength + sdtaTotalSize + (pdtaChunk ? pdtaChunk.byteLength : 0); 96 const outBuffer = new ArrayBuffer(8 + newRiffSize); 97 const outU8 = new Uint8Array(outBuffer); 98 const outView = new DataView(outBuffer); 99 outU8.set([82, 73, 70, 70], 0); 100 outView.setUint32(4, newRiffSize, true); 101 outU8.set([115, 102, 98, 107], 8); 102 let writePos = 12; 103 outU8.set(infoChunk, writePos);
104 writePos += infoChunk.byteLength; 105 outU8.set([76, 73, 83, 84], writePos); 106 outView.setUint32(writePos + 4, 4 + 8 + smplChunkSize, true); 107 outU8.set([115, 100, 116, 97], writePos + 8); 108 outU8.set([115, 109, 112, 108], writePos + 12); 109 outView.setUint32(writePos + 16, smplChunkSize, true); 110 outU8.set(pcmBytes, writePos + 20); 111 writePos += sdtaTotalSize; 112 if (pdtaChunk) { 113 outU8.set(pdtaChunk, writePos); 114 } 115 console.log(`[SF3Decoder] Decoded SF3 -> SF2 in-memory (${(sf3Buffer.byteLength / 1024 / 1024).toFixed(2)} MB -> ${(outBuffer.byteLength / 1024 / 1024).toFixed(2)} MB PCM)`); 116 return outBuffer; 117} 118class LaneAudioChain { 119 constructor(ctx, synthOutput, destination) { 120 this.currentChain = null; 121 this.reverbIrBuffer = null; 122 this.ctx = ctx; 123 this.synthOutput = synthOutput; 124 this.destination = destination; 125 this.generateReverbIR(); 126 } 127 async generateReverbIR() { 128 const sampleRate = this.ctx.sampleRate; 129 const length = sampleRate * 2; 130 const impulse = this.ctx.createBuffer(2, length, sampleRate); 131 for (let channel = 0; channel < 2; channel++) { 132 const channelData = impulse.getChannelData(channel); 133 for (let i = 0; i < length; i++) { 134 const noise = Math.random() * 2 - 1; 135 channelData[i] = noise * Math.exp(-i / (sampleRate * 0.3)); 136 } 137 } 138 this.reverbIrBuffer = impulse; 139 } 140 destroy() { 141 try { 142 this.synthOutput.disconnect(); 143 } catch (e) { 144 } 145 if (this.currentChain) { 146 try { 147 this.currentChain.output.disconnect(); 148 } catch (e) { 149 } 150 this.currentChain = null; 151 } 152 } 153 rebuildChain(modules) { 154 this.synthOutput.disconnect(); 155 if (this.currentChain) { 156 this.currentChain.output.disconnect(); 157 } 158 const activeModules = modules.filter((m) => m.enabled); 159 if (activeModules.length === 0) { 160 this.synthOutput.connect(this.destination); 161 this.currentChain = null; 162 return; 163 } 164 let previousOutputNode = this.synthOutput; 165 const nodesRecord = {}; 166 for (const module of activeModules) { 167 const { input, output, nodes } = this.createModuleNodes(module); 168 previousOutputNode.connect(input); 169 previousOutputNode = output; 170 nodesRecord[module.id] = nodes; 171 } 172 previousOutputNode.connect(this.destination); 173 this.currentChain = { 174 input: this.synthOutput, 175 // The start of the chain (not really a single node) 176 output: previousOutputNode, 177 nodes: nodesRecord 178 }; 179 } 180 updateModuleParams(moduleId, params) { 181 if (!this.currentChain || !this.currentChain.nodes[moduleId]) return; 182 const nodes = this.currentChain.nodes[moduleId]; 183 if (nodes instanceof GainNode && nodes.__isMasterOutput) { 184 this.updateMasterOutputParams(nodes, nodes.__panner, params); 185 } else if (Array.isArray(nodes) && nodes.length === 3 && nodes[0] instanceof BiquadFilterNode) { 186 this.updateEq3Params(nodes, params); 187 } else if (nodes instanceof DynamicsCompressorNode) { 188 this.updateCompressorParams(nodes, params); 189 } else if (nodes instanceof DelayNode) ; 190 else if (Array.isArray(nodes) && nodes.__isDelay) { 191 this.updateDelayParams(nodes, params); 192 } else if (Array.isArray(nodes) && nodes.__isReverb) { 193 this.updateReverbParams(nodes, params); 194 } 195 } 196 createModuleNodes(module) { 197 switch (module.type) { 198 case "master-output": { 199 const gainNode = this.ctx.createGain(); 200 const pannerNode = this.ctx.createStereoPanner(); 201 gainNode.connect(pannerNode); 202 gainNode.__isMasterOutput = true; 203 gainNode.__panner = pannerNode; 204 this.updateMasterOutputParams(gainNode, pannerNode, module.params); 205 return { input: gainNode, output: pannerNode, nodes: gainNode }; 206 } 207 case "eq3": { 208 const low = this.ctx.createBiquadFilter(); 209 low.type = "lowshelf"; 210 low.frequency.value = 250; 211 const mid = this.ctx.createBiquadFilter(); 212 mid.type = "peaking"; 213 mid.frequency.value = 1e3; 214 mid.Q.value = 1; 215 const high = this.ctx.createBiquadFilter();
216 high.type = "highshelf"; 217 high.frequency.value = 4e3; 218 low.connect(mid); 219 mid.connect(high); 220 const nodes = [low, mid, high]; 221 this.updateEq3Params(nodes, module.params); 222 return { input: low, output: high, nodes }; 223 } 224 case "compressor": { 225 const comp = this.ctx.createDynamicsCompressor(); 226 const makeup = this.ctx.createGain(); 227 comp.connect(makeup); 228 comp.__makeup = makeup; 229 this.updateCompressorParams(comp, module.params); 230 return { input: comp, output: makeup, nodes: comp }; 231 } 232 case "delay": { 233 const inputGain = this.ctx.createGain(); 234 const delay = this.ctx.createDelay(5); 235 const feedback = this.ctx.createGain(); 236 const wetGain = this.ctx.createGain(); 237 const dryGain = this.ctx.createGain(); 238 const output = this.ctx.createGain(); 239 inputGain.connect(dryGain); 240 dryGain.connect(output); 241 inputGain.connect(delay); 242 delay.connect(wetGain); 243 wetGain.connect(output); 244 delay.connect(feedback); 245 feedback.connect(delay); 246 const nodes = [inputGain, delay, feedback, wetGain, dryGain, output]; 247 nodes.__isDelay = true; 248 this.updateDelayParams(nodes, module.params); 249 return { input: inputGain, output, nodes }; 250 } 251 case "reverb": { 252 const inputGain = this.ctx.createGain(); 253 const convolver = this.ctx.createConvolver(); 254 if (this.reverbIrBuffer) { 255 convolver.buffer = this.reverbIrBuffer; 256 } 257 const wetGain = this.ctx.createGain(); 258 const dryGain = this.ctx.createGain(); 259 const output = this.ctx.createGain(); 260 inputGain.connect(dryGain); 261 dryGain.connect(output); 262 inputGain.connect(convolver); 263 convolver.connect(wetGain); 264 wetGain.connect(output); 265 const nodes = [inputGain, convolver, wetGain, dryGain, output]; 266 nodes.__isReverb = true; 267 this.updateReverbParams(nodes, module.params); 268 return { input: inputGain, output, nodes }; 269 } 270 default: { 271 const bypass = this.ctx.createGain(); 272 return { input: bypass, output: bypass, nodes: bypass }; 273 } 274 } 275 } 276 updateMasterOutputParams(gain, panner, params) { 277 const vol = params.mute ? 0 : (params.volume ?? 80) / 100; 278 gain.gain.setTargetAtTime(vol, this.ctx.currentTime, 0.05); 279 const pan = -((params.pan ?? 0) / 100); 280 panner.pan.setTargetAtTime(pan, this.ctx.currentTime, 0.05); 281 } 282 updateEq3Params(nodes, params) { 283 nodes[0].gain.setTargetAtTime(params.low ?? 0, this.ctx.currentTime, 0.05); 284 nodes[1].gain.setTargetAtTime(params.mid ?? 0, this.ctx.currentTime, 0.05); 285 nodes[2].gain.setTargetAtTime(params.high ?? 0, this.ctx.currentTime, 0.05); 286 } 287 updateCompressorParams(comp, params) { 288 const amount = (params.amount ?? 40) / 100; 289 const sustain = (params.sustain ?? 50) / 100; 290 comp.threshold.value = -10 - amount * 40; 291 comp.ratio.value = 1 + amount * 19; 292 comp.release.value = 0.1 + sustain * 0.9; 293 const makeupGain = comp.__makeup; 294 const makeupDb = params.makeup ?? 0; 295 const linearGain = Math.pow(10, makeupDb / 20); 296 makeupGain.gain.setTargetAtTime(linearGain, this.ctx.currentTime, 0.05); 297 } 298 updateDelayParams(nodes, params) { 299 const delay = nodes[1]; 300 const feedback = nodes[2]; 301 const wet = nodes[3]; 302 const dry = nodes[4]; 303 const time = (params.time ?? 350) / 1e3; 304 delay.delayTime.setTargetAtTime(time, this.ctx.currentTime, 0.05); 305 const fb = (params.feedback ?? 40) / 100; 306 feedback.gain.setTargetAtTime(fb, this.ctx.currentTime, 0.05); 307 const mix = (params.mix ?? 25) / 100; 308 wet.gain.setTargetAtTime(mix, this.ctx.currentTime, 0.05); 309 dry.gain.setTargetAtTime(1 - mix, this.ctx.currentTime, 0.05); 310 } 311 updateReverbParams(nodes, params) { 312 const wet = nodes[2]; 313 const dry = nodes[3]; 314 const mix = (params.wetDry ?? 30) / 100; 315 wet.gain.setTargetAtTime(mix, this.ctx.currentTime, 0.05); 316 dry.gain.setTargetAtTime(1 - mix, this.ctx.currentTime, 0.05); 317 } 318} 319const HSF3_KEY = new TextEncoder().encode("HamieNET_HSF3_Secret_2026_Key!"); 320function deobfuscateHsf3Buffer(buffer) { 321 const bytes = new Uint8Array(buffer); 322 const obfLen = Math.min(bytes.length, 4096); 323 const keyLen = HSF3_KEY.length; 324 for (let i = 0; i < obfLen; i++) { 325 bytes[i] ^= HSF3_KEY[i % keyLen]; 326 } 327 return buffer; 328} 329class AudioEngineManagerImpl {
330 constructor() { 331 this.ctx = null; 332 this.workletLoaded = false; 333 this.synths = /* @__PURE__ */ new Map(); 334 this.chains = /* @__PURE__ */ new Map(); 335 this.sf2Cache = /* @__PURE__ */ new Map(); 336 this.laneBanks = /* @__PURE__ */ new Map(); 337 this.masterAnalyser = null; 338 this.masterGain = null; 339 this.laneLoadTokens = /* @__PURE__ */ new Map(); 340 this.userGestureBound = false; 341 this.activeNotesCount = 0; 342 } 343 bindUserGestureToResume() { 344 if (this.userGestureBound || typeof window === "undefined") return; 345 this.userGestureBound = true; 346 const resume = () => { 347 if (this.ctx && this.ctx.state === "suspended") { 348 this.ctx.resume().catch(() => { 349 }); 350 } 351 }; 352 ["click", "keydown", "pointerdown", "touchstart"].forEach((evt) => { 353 window.addEventListener(evt, resume, { passive: true }); 354 }); 355 } 356 async getContext() { 357 if (!this.ctx) { 358 this.ctx = new (window.AudioContext || window.webkitAudioContext)(); 359 this.masterGain = this.ctx.createGain(); 360 this.masterAnalyser = this.ctx.createAnalyser(); 361 this.masterAnalyser.fftSize = 2048; 362 this.masterGain.connect(this.masterAnalyser); 363 this.masterAnalyser.connect(this.ctx.destination); 364 } 365 this.bindUserGestureToResume(); 366 if (!this.workletLoaded) { 367 try { 368 await this.ctx.audioWorklet.addModule(spessasynthWorkletUrl); 369 this.workletLoaded = true; 370 } catch (e) { 371 console.error("[AudioEngine] Failed to load audio worklet", e); 372 throw e; 373 } 374 } 375 return this.ctx; 376 } 377 resumeContext() { 378 if (this.ctx && this.ctx.state === "suspended") { 379 this.ctx.resume().catch(() => { 380 }); 381 } 382 } 383 setMasterVolume(volPercent) { 384 if (this.masterGain) { 385 this.masterGain.gain.value = volPercent / 100; 386 } 387 } 388 fetchSf2WithProgress(sf2Url, laneId) { 389 window.dispatchEvent(new CustomEvent("sf_loading", { detail: { laneId, sf2Url, status: "loading", progress: 0 } })); 390 return (async () => { 391 try { 392 const response = await fetch(sf2Url); 393 if (!response.ok) throw new Error(`Failed to fetch SF2: ${response.statusText}`); 394 const contentLength = response.headers.get("content-length"); 395 const total = contentLength ? parseInt(contentLength, 10) : 0; 396 if (!response.body || total === 0) { 397 const buffer = await response.arrayBuffer(); 398 window.dispatchEvent(new CustomEvent("sf_loading", { detail: { laneId, sf2Url, status: "loading", progress: 100 } })); 399 return buffer; 400 } 401 const reader = response.body.getReader(); 402 let loaded = 0; 403 const chunks = []; 404 while (true) { 405 const { done, value } = await reader.read(); 406 if (done) break; 407 if (value) { 408 chunks.push(value); 409 loaded += value.length; 410 const progress = Math.min(99, Math.round(loaded / total * 100)); 411 window.dispatchEvent(new CustomEvent("sf_loading", { detail: { laneId, sf2Url, status: "loading", progress } })); 412 } 413 } 414 const chunksAll = new Uint8Array(loaded); 415 let position = 0; 416 for (const chunk of chunks) { 417 chunksAll.set(chunk, position); 418 position += chunk.length; 419 } 420 window.dispatchEvent(new CustomEvent("sf_loading", { detail: { laneId, sf2Url, status: "loading", progress: 100 } })); 421 return chunksAll.buffer; 422 } catch (err) { 423 this.sf2Cache.delete(sf2Url); 424 throw err; 425 } 426 })(); 427 } 428 async loadLane(lane) { 429 const currentToken = (this.laneLoadTokens.get(lane.id) || 0) + 1; 430 this.laneLoadTokens.set(lane.id, currentToken); 431 if (lane.soundSource.type === "js-soundfont") { 432 console.warn(`[AudioEngine] Lane ${lane.id} uses js-soundfont type which is not supported by this engine. Skipping load.`); 433 window.dispatchEvent(new CustomEvent("sf_loading", { detail: { laneId: lane.id, status: "done" } })); 434 return; 435 } 436 try { 437 let library = lane.soundSource.library || "GeneralUser-GS.sf3"; 438 let program = lane.soundSource.program ?? 0; 439 let bank = lane.soundSource.bank ?? 0; 440 let sf2Url = ""; 441 if (library.startsWith("blob:")) { 442 sf2Url = library; 443 } else if (lane.soundSource.type === "hsf3-bank" || library.endsWith("_HSF3Bank") || library.includes("_HSF3Bank/")) { 444 const cleanLib = library.replace(/\/.*$/, ""); 445 const progStr = program.toString().padStart(3, "0"); 446 sf2Url = `/storage/soundfonts/${cleanLib}/${bank}_${progStr}.hsf3`; 447 } else if (library.endsWith(".sf3") || library.endsWith(".sf2") || library.endsWith(".hsf3")) { 448 sf2Url = `/storage/soundfonts/${library}`; 449 } else { 450 sf2Url = `/storage/soundfonts/${library}.sf3`; 451 } 452 let bufferPromise = this.sf2Cache.get(sf2Url); 453 if (!bufferPromise) { 454 console.log(`[AudioEngine] Fetching SoundFont: ${sf2Url}`); 455 bufferPromise = this.fetchSf2WithProgress(sf2Url, lane.id); 456 this.sf2Cache.set(sf2Url, bufferPromise); 457 } 458 const ctx = await this.getContext(); 459 this.resumeContext(); 460 let rawBuffer = await bufferPromise; 461 if (sf2Url.endsWith(".hsf3")) { 462 rawBuffer = deobfuscateHsf3Buffer(rawBuffer.slice(0)); 463 } 464 if (this.laneLoadTokens.get(lane.id) !== currentToken) { 465 console.log(`[AudioEngine] Stale load for lane ${lane.id} cancelled`); 466 return; 467 } 468 const buffer = await decodeSf3ToSf2(rawBuffer, ctx); 469 if (this.laneLoadTokens.get(lane.id) !== currentToken) { 470 console.log(`[AudioEngine] Post-decode load for lane ${lane.id} cancelled`); 471 return; 472 } 473 const existingChain = this.chains.get(lane.id); 474 if (existingChain) { 475 existingChain.destroy(); 476 this.chains.delete(lane.id); 477 } 478 const existingSynth = this.synths.get(lane.id); 479 if (existingSynth) { 480 try { 481 existingSynth.disconnect(); 482 } catch (e) { 483 } 484 this.synths.delete(lane.id); 485 }
486 let synth = new WorkletSynthesizer(ctx); 487 synth.setMasterParameter("masterGain", 0.6); 488 const synthGain = ctx.createGain(); 489 synth.connect(synthGain); 490 const chain = new LaneAudioChain(ctx, synthGain, this.masterGain); 491 chain.rebuildChain(lane.modules); 492 this.synths.set(lane.id, synth); 493 this.chains.set(lane.id, chain); 494 await synth.soundBankManager.addSoundBank(buffer.slice(0), "lane_sf2"); 495 if (this.laneLoadTokens.get(lane.id) !== currentToken) { 496 console.log(`[AudioEngine] Post-addSoundBank load for lane ${lane.id} cancelled`); 497 synth.disconnect(); 498 return; 499 } 500 this.laneBanks.set(lane.id, lane.soundSource.bank); 501 const channel = lane.soundSource.bank === 128 ? 9 : 0; 502 synth.programChange(channel, lane.soundSource.program); 503 this.setLaneVolume(lane.id, lane.outputVolume, lane.outputMute); 504 console.log(`[AudioEngine] Loaded SF2 for lane ${lane.id}: ${sf2Url} (Program ${lane.soundSource.program})`); 505 window.dispatchEvent(new CustomEvent("sf_loading", { detail: { laneId: lane.id, sf2Url, status: "done" } })); 506 } catch (e) { 507 console.error(`[AudioEngine] Error loading SF2 for lane ${lane.id}:`, e); 508 window.dispatchEvent(new CustomEvent("sf_loading", { detail: { laneId: lane.id, status: "error" } })); 509 } 510 } 511 noteOn(laneId, note, velocity) { 512 this.activeNotesCount++; 513 this.resumeContext(); 514 const synth = this.synths.get(laneId); 515 if (synth) { 516 const channel = this.laneBanks.get(laneId) === 128 ? 9 : 0; 517 synth.noteOn(channel, note, velocity); 518 } 519 } 520 noteOff(laneId, note) { 521 this.activeNotesCount = Math.max(0, this.activeNotesCount - 1); 522 const synth = this.synths.get(laneId); 523 if (synth) { 524 const channel = this.laneBanks.get(laneId) === 128 ? 9 : 0; 525 synth.noteOff(channel, note); 526 } 527 } 528 setLaneVolume(laneId, volume, isMuted) { 529 const synth = this.synths.get(laneId); 530 if (synth) { 531 const vol = isMuted ? 0 : volume / 100; 532 const ccVal = Math.max(0, Math.min(127, Math.round(vol * 127))); 533 const channel = this.laneBanks.get(laneId) === 128 ? 9 : 0; 534 synth.controllerChange(channel, 7, ccVal); 535 } 536 } 537 setProgram(laneId, program, bank) { 538 const synth = this.synths.get(laneId); 539 if (synth) { 540 this.laneBanks.set(laneId, bank); 541 const channel = bank === 128 ? 9 : 0; 542 synth.controllerChange(channel, 0, bank); 543 synth.programChange(channel, program); 544 } 545 } 546 setPitchBend(laneId, value) { 547 const synth = this.synths.get(laneId); 548 if (synth) { 549 const channel = this.laneBanks.get(laneId) === 128 ? 9 : 0; 550 synth.pitchWheel(channel, value); 551 } 552 } 553 setModWheel(laneId, value) { 554 const synth = this.synths.get(laneId); 555 if (synth) { 556 const channel = this.laneBanks.get(laneId) === 128 ? 9 : 0; 557 synth.controllerChange(channel, 1, value); 558 } 559 } 560 setSustain(laneId, value) { 561 const synth = this.synths.get(laneId); 562 if (synth) { 563 const channel = this.laneBanks.get(laneId) === 128 ? 9 : 0; 564 synth.controllerChange(channel, 64, value); 565 } 566 } 567 rebuildLaneChain(laneId, modules) { 568 const chain = this.chains.get(laneId); 569 if (chain) { 570 chain.rebuildChain(modules); 571 } 572 } 573 updateModuleParams(laneId, moduleId, params) { 574 const chain = this.chains.get(laneId); 575 if (chain) { 576 chain.updateModuleParams(moduleId, params); 577 } 578 } 579} 580const AudioEngineManager = new AudioEngineManagerImpl(); 581const _WasmSynthAdapter = class _WasmSynthAdapter { 582 constructor(ctx, destination) { 583 this.synth = null; 584 this.isReady = false; 585 this.isMuted = false; 586 this.ctx = ctx; 587 this.destination = destination; 588 this.outputGain = this.ctx.createGain(); 589 this.outputGain.gain.setValueAtTime(1, this.ctx.currentTime); 590 this.limiter = this.ctx.createDynamicsCompressor(); 591 this.limiter.threshold.setValueAtTime(-1, this.ctx.currentTime); 592 this.limiter.knee.setValueAtTime(0, this.ctx.currentTime); 593 this.limiter.ratio.setValueAtTime(20, this.ctx.currentTime); 594 this.limiter.attack.setValueAtTime(2e-3, this.ctx.currentTime); 595 this.limiter.release.setValueAtTime(0.05, this.ctx.currentTime); 596 this.outputGain.connect(this.limiter); 597 this.limiter.connect(this.destination); 598 } 599 /** 600 * Connects an individual SpessaSynth channel output (1-16) to an external node (e.g. AnalyserNode for VU meters). 601 * Output 0 is master/reverb/chorus, while outputs 1-16 are dry channel outputs. 602 */ 603 connectChannel(channelNumber, targetNode) { 604 if (this.synth && this.synth.connectChannel) {
605 try { 606 return this.synth.connectChannel(targetNode, channelNumber); 607 } catch (err) { 608 console.warn("[WasmSynth] connectChannel error:", err); 609 } 610 } 611 } 612 /** 613 * Disconnects an individual SpessaSynth channel output from an external node. 614 */ 615 disconnectChannel(channelNumber, targetNode) { 616 if (this.synth && this.synth.disconnectChannel) { 617 try { 618 this.synth.disconnectChannel(targetNode, channelNumber); 619 } catch (err) { 620 console.warn("[WasmSynth] disconnectChannel error:", err); 621 } 622 } 623 } 624 async load(sf2Url, onProgress) { 625 console.log(`[WasmSynth] Loading SoundFont: ${sf2Url}`); 626 const response = await fetch(sf2Url); 627 if (!response.ok) throw new Error(`Failed to fetch SF2: ${response.statusText}`); 628 let rawBuffer; 629 if (!response.body) { 630 rawBuffer = await response.arrayBuffer(); 631 } else { 632 const contentLength = response.headers.get("content-length"); 633 const total = contentLength ? parseInt(contentLength, 10) : 0; 634 let loaded = 0; 635 const reader = response.body.getReader(); 636 const chunks = []; 637 while (true) { 638 const { done, value } = await reader.read(); 639 if (done) break; 640 if (value) { 641 chunks.push(value); 642 loaded += value.length; 643 if (total && onProgress) { 644 onProgress(loaded / total * 0.95); 645 } 646 } 647 } 648 const arrayBuffer2 = new Uint8Array(loaded); 649 let offset = 0; 650 for (const chunk of chunks) { 651 arrayBuffer2.set(chunk, offset); 652 offset += chunk.length; 653 } 654 rawBuffer = arrayBuffer2.buffer; 655 } 656 if (sf2Url.endsWith(".hsf3")) { 657 rawBuffer = deobfuscateHsf3Buffer(rawBuffer.slice(0)); 658 } 659 const arrayBuffer = await decodeSf3ToSf2(rawBuffer, this.ctx); 660 if (typeof AudioWorkletNode === "undefined") { 661 throw new Error("AudioWorkletNode is not defined. Wasm Synth (SF2/SF3) requires a secure context (HTTPS or localhost)."); 662 } 663 try { 664 await this.ctx.audioWorklet.addModule(spessasynthWorkletUrl); 665 } catch (e) { 666 console.error("[WasmSynth] Failed to load AudioWorklet module:", e); 667 throw new Error("Failed to load the AudioWorklet processor module."); 668 } 669 this.synth = new WorkletSynthesizer(this.ctx); 670 console.log("[WasmSynth] WorkletSynthesizer created:", this.synth); 671 this.synth.setMasterParameter("masterGain", _WasmSynthAdapter.BASE_MASTER_GAIN); 672 this.synth.connect(this.outputGain); 673 if (this.synth.worklet) { 674 this.synth.worklet.onprocessorerror = (e) => { 675 console.error("[WasmSynth] WORKLET FATAL ERROR:", e); 676 }; 677 } 678 try { 679 await Promise.race([ 680 this.synth.soundBankManager.addSoundBank(arrayBuffer, "main_sf2"), 681 new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout adding SoundBank")), 5e3)) 682 ]); 683 } catch (e) { 684 console.error("[WasmSynth] Error adding SoundBank:", e); 685 throw new Error(`Failed to load SoundBank: ${e.message}`); 686 } 687 this.isReady = true; 688 console.log(`[WasmSynth] SoundFont loaded and ready.`); 689 } 690 /** 691 * Mutates the SF2 ArrayBuffer in-place to change the bank and program of all presets 692 * EXCEPT the target to 255/255. This resolves metadata overlap in zero-padded banks.
693 */ 694 isolatePresetInSf2(arrayBuffer, targetBank, targetProgram) { 695 const view = new DataView(arrayBuffer); 696 let pos = 12; 697 while (pos + 8 <= arrayBuffer.byteLength) { 698 const chunkId = String.fromCharCode(view.getUint8(pos), view.getUint8(pos + 1), view.getUint8(pos + 2), view.getUint8(pos + 3)); 699 const chunkSize = view.getUint32(pos + 4, true); 700 if (chunkId === "LIST") { 701 const listType = String.fromCharCode(view.getUint8(pos + 8), view.getUint8(pos + 9), view.getUint8(pos + 10), view.getUint8(pos + 11)); 702 if (listType === "pdta") { 703 let subPos = pos + 12; 704 while (subPos + 8 <= pos + 8 + chunkSize) { 705 const subChunkId = String.fromCharCode(view.getUint8(subPos), view.getUint8(subPos + 1), view.getUint8(subPos + 2), view.getUint8(subPos + 3)); 706 const subSize = view.getUint32(subPos + 4, true); 707 if (subChunkId === "phdr") { 708 const phdrOffset = subPos + 8; 709 const numPresets = Math.floor(subSize / 38); 710 let bestPresetIndex = -1; 711 for (let p = 0; p < numPresets - 1; p++) { 712 const presetOffset = phdrOffset + p * 38; 713 const presetNum = view.getUint16(presetOffset + 20, true); 714 const bankNum = view.getUint16(presetOffset + 22, true); 715 if (presetNum === targetProgram && bankNum === targetBank) { 716 bestPresetIndex = p; 717 break; 718 } 719 } 720 if (bestPresetIndex === -1) { 721 let bestBank = 999; 722 for (let p = 0; p < numPresets - 1; p++) { 723 const presetOffset = phdrOffset + p * 38; 724 const presetNum = view.getUint16(presetOffset + 20, true); 725 const bankNum = view.getUint16(presetOffset + 22, true); 726 if (presetNum === targetProgram && bankNum < bestBank) { 727 bestBank = bankNum; 728 bestPresetIndex = p; 729 } 730 } 731 } 732 if (bestPresetIndex === -1) { 733 for (let p = 0; p < numPresets - 1; p++) { 734 const presetOffset = phdrOffset + p * 38; 735 const presetNum = view.getUint16(presetOffset + 20, true); 736 const bankNum = view.getUint16(presetOffset + 22, true); 737 if (presetNum === 0 && bankNum === 0) { 738 bestPresetIndex = p; 739 break; 740 } 741 } 742 } 743 if (bestPresetIndex === -1 && numPresets > 1) { 744 bestPresetIndex = 0; 745 } 746 if (bestPresetIndex !== -1) { 747 for (let p = 0; p < numPresets - 1; p++) { 748 if (p !== bestPresetIndex) { 749 const presetOffset = phdrOffset + p * 38; 750 view.setUint16(presetOffset + 22, 125, true); 751 } 752 } 753 } 754 return arrayBuffer; 755 } 756 subPos += 8 + subSize + subSize % 2; 757 } 758 break; 759 } 760 } 761 pos += 8 + chunkSize + chunkSize % 2; 762 } 763 return arrayBuffer; 764 } 765 async loadHsf3Bank(bankName, programs, onProgress) { 766 console.log(`[WasmSynth] Loading HSF3 Bank: ${bankName} with programs:`, programs); 767 if (typeof AudioWorkletNode === "undefined") { 768 throw new Error("AudioWorkletNode is not defined. Wasm Synth (SF2/SF3) requires a secure context (HTTPS or localhost)."); 769 } 770 try { 771 await this.ctx.audioWorklet.addModule(spessasynthWorkletUrl); 772 } catch (e) { 773 console.error("[WasmSynth] Failed to load AudioWorklet module:", e); 774 throw new Error("Failed to load the AudioWorklet processor module."); 775 } 776 this.synth = new WorkletSynthesizer(this.ctx); 777 console.log("[WasmSynth] WorkletSynthesizer created:", this.synth); 778 this.synth.setMasterParameter("masterGain", _WasmSynthAdapter.BASE_MASTER_GAIN); 779 this.synth.connect(this.outputGain); 780 if (this.synth.worklet) { 781 this.synth.worklet.onprocessorerror = (e) => { 782 console.error("[WasmSynth] WORKLET FATAL ERROR:", e); 783 }; 784 } 785 let loadedCount = 0; 786 const totalCount = programs.length; 787 const cleanLib = bankName.replace(/\/.*$/, ""); 788 const loadPromises = programs.map(async (inst) => { 789 const { bank, program } = inst; 790 const progStr = program.toString().padStart(3, "0"); 791 const sf2Url = `/storage/soundfonts/${cleanLib}/${bank}_${progStr}.hsf3`;
792 try { 793 const response = await fetch(sf2Url); 794 if (!response.ok) throw new Error(`Failed to fetch ${sf2Url}: ${response.statusText}`); 795 let rawBuffer = await response.arrayBuffer(); 796 rawBuffer = deobfuscateHsf3Buffer(rawBuffer.slice(0)); 797 const arrayBuffer = await decodeSf3ToSf2(rawBuffer, this.ctx); 798 let finalBuffer = arrayBuffer; 799 finalBuffer = this.isolatePresetInSf2(finalBuffer, bank, program); 800 await Promise.race([ 801 this.synth.soundBankManager.addSoundBank(finalBuffer, `inst_${bank}_${program}`), 802 new Promise((_, reject) => setTimeout(() => reject(new Error("Add sound bank timeout")), 1e4)) 803 ]); 804 loadedCount++; 805 if (onProgress) { 806 onProgress(loadedCount / totalCount); 807 } 808 } catch (err) { 809 console.warn(`[WasmSynth] Failed to load program ${program} (bank ${bank}) from HSF3 Bank ${bankName}:`, err); 810 } 811 }); 812 await Promise.all(loadPromises); 813 this.isReady = true; 814 console.log(`[WasmSynth] HSF3 Bank ${bankName} loaded and ready.`); 815 } 816 /** 817 * Applies a gain multiplier to the synth via SpessaSynth's masterGain parameter. 818 * This is layered on top of the base gain (1.2) for reliable volume control. 819 */ 820 setGain(gain) { 821 if (!this.synth) return; 822 this.synth.setMasterParameter("masterGain", _WasmSynthAdapter.BASE_MASTER_GAIN * gain); 823 } 824 setProgram(channel, program, bank = 0) { 825 if (!this.synth) return; 826 this.synth.controllerChange(channel, 0, bank); 827 this.synth.programChange(channel, program); 828 } 829 muteChannel(channel, isMuted) { 830 if (!this.synth) { 831 console.warn(`[WasmSynthAdapter] muteChannel(${channel}, ${isMuted}) failed: this.synth is null`); 832 return; 833 } 834 try { 835 this.synth.muteChannel(channel, isMuted); 836 } catch (err) { 837 console.error(`[WasmSynthAdapter] Exception in synth.muteChannel:`, err); 838 } 839 } 840 setChannelVolume(channel, volume) { 841 if (!this.synth) { 842 return; 843 } 844 const val = Math.max(0, Math.min(127, Math.round(volume * 127))); 845 try { 846 this.synth.controllerChange(channel, 7, val); 847 } catch (err) { 848 console.error(`[WasmSynthAdapter] Exception in synth.controllerChange:`, err); 849 } 850 } 851 setChannelPan(channel, pan) { 852 if (!this.synth) { 853 return; 854 } 855 const panValue = Math.max(-1, Math.min(1, pan)); 856 const val = Math.round((panValue + 1) / 2 * 127); 857 try { 858 this.synth.controllerChange(channel, 10, val); 859 } catch (err) { 860 console.error(`[WasmSynthAdapter] Exception in synth.controllerChange (pan):`, err); 861 } 862 } 863 unmute() { 864 if (!this.outputGain) return; 865 this.isMuted = false; 866 const now = this.ctx.currentTime; 867 this.outputGain.gain.cancelScheduledValues(now); 868 this.outputGain.gain.setValueAtTime(this.outputGain.gain.value, now); 869 this.outputGain.gain.linearRampToValueAtTime(1, now + 5e-3); 870 } 871 noteOn(channel, note, velocity, time) { 872 if (!this.synth) return; 873 if (this.isMuted) { 874 this.unmute(); 875 } 876 this.synth.noteOn(channel, note, velocity, { time }); 877 } 878 noteOff(channel, note, time) { 879 if (!this.synth) return; 880 this.synth.noteOff(channel, note, { time }); 881 } 882 stopAll() { 883 this.isMuted = true; 884 if (this.outputGain) { 885 const now = this.ctx.currentTime; 886 this.outputGain.gain.cancelScheduledValues(now); 887 this.outputGain.gain.setValueAtTime(this.outputGain.gain.value, now); 888 this.outputGain.gain.linearRampToValueAtTime(0, now + 5e-3); 889 } 890 if (!this.synth) return; 891 try { 892 this.synth.stopAll(true); 893 } catch (err) { 894 console.error("[WasmSynthAdapter] Exception in synth.stopAll:", err); 895 } 896 for (let ch = 0; ch < 16; ch++) { 897 try { 898 this.synth.controllerChange(ch, 64, 0); 899 this.synth.controllerChange(ch, 120, 0); 900 this.synth.controllerChange(ch, 123, 0); 901 this.synth.controllerChange(ch, 121, 0); 902 } catch { 903 } 904 } 905 } 906 destroy() { 907 if (this.limiter) { 908 try { 909 this.limiter.disconnect(); 910 } catch { 911 } 912 } 913 if (this.outputGain) {
914 try { 915 this.outputGain.disconnect(); 916 } catch { 917 } 918 } 919 if (this.synth) { 920 this.synth.disconnect(); 921 this.synth = null; 922 } 923 this.isReady = false; 924 } 925}; 926_WasmSynthAdapter.BASE_MASTER_GAIN = 0.45; 927let WasmSynthAdapter = _WasmSynthAdapter; 928export { 929 AudioEngineManager as A, 930 WasmSynthAdapter as W 931};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.