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.ctx = ctx; 586 this.destination = destination; 587 } 588 async load(sf2Url, onProgress) { 589 console.log(`[WasmSynth] Loading SoundFont: ${sf2Url}`); 590 const response = await fetch(sf2Url); 591 if (!response.ok) throw new Error(`Failed to fetch SF2: ${response.statusText}`); 592 let rawBuffer; 593 if (!response.body) { 594 rawBuffer = await response.arrayBuffer(); 595 } else { 596 const contentLength = response.headers.get("content-length"); 597 const total = contentLength ? parseInt(contentLength, 10) : 0; 598 let loaded = 0; 599 const reader = response.body.getReader(); 600 const chunks = []; 601 while (true) { 602 const { done, value } = await reader.read(); 603 if (done) break; 604 if (value) { 605 chunks.push(value); 606 loaded += value.length; 607 if (total && onProgress) { 608 onProgress(loaded / total * 0.95); 609 } 610 } 611 } 612 const arrayBuffer2 = new Uint8Array(loaded); 613 let offset = 0; 614 for (const chunk of chunks) { 615 arrayBuffer2.set(chunk, offset); 616 offset += chunk.length; 617 } 618 rawBuffer = arrayBuffer2.buffer; 619 } 620 if (sf2Url.endsWith(".hsf3")) { 621 rawBuffer = deobfuscateHsf3Buffer(rawBuffer.slice(0)); 622 } 623 const arrayBuffer = await decodeSf3ToSf2(rawBuffer, this.ctx); 624 if (typeof AudioWorkletNode === "undefined") { 625 throw new Error("AudioWorkletNode is not defined. Wasm Synth (SF2/SF3) requires a secure context (HTTPS or localhost)."); 626 } 627 try { 628 await this.ctx.audioWorklet.addModule(spessasynthWorkletUrl); 629 } catch (e) { 630 console.error("[WasmSynth] Failed to load AudioWorklet module:", e); 631 throw new Error("Failed to load the AudioWorklet processor module."); 632 }
633 this.synth = new WorkletSynthesizer(this.ctx); 634 console.log("[WasmSynth] WorkletSynthesizer created:", this.synth); 635 this.synth.setMasterParameter("masterGain", _WasmSynthAdapter.BASE_MASTER_GAIN); 636 this.synth.connect(this.destination); 637 if (this.synth.worklet) { 638 this.synth.worklet.onprocessorerror = (e) => { 639 console.error("[WasmSynth] WORKLET FATAL ERROR:", e); 640 }; 641 } 642 try { 643 await Promise.race([ 644 this.synth.soundBankManager.addSoundBank(arrayBuffer, "main_sf2"), 645 new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout adding SoundBank")), 5e3)) 646 ]); 647 } catch (e) { 648 console.error("[WasmSynth] Error adding SoundBank:", e); 649 throw new Error(`Failed to load SoundBank: ${e.message}`); 650 } 651 this.isReady = true; 652 console.log(`[WasmSynth] SoundFont loaded and ready.`); 653 } 654 /** 655 * Mutates the SF2 ArrayBuffer in-place to change the bank and program of all presets 656 * EXCEPT the target to 255/255. This resolves metadata overlap in zero-padded banks.
657 */ 658 isolatePresetInSf2(arrayBuffer, targetBank, targetProgram) { 659 const view = new DataView(arrayBuffer); 660 let pos = 12; 661 while (pos + 8 <= arrayBuffer.byteLength) { 662 const chunkId = String.fromCharCode(view.getUint8(pos), view.getUint8(pos + 1), view.getUint8(pos + 2), view.getUint8(pos + 3)); 663 const chunkSize = view.getUint32(pos + 4, true); 664 if (chunkId === "LIST") { 665 const listType = String.fromCharCode(view.getUint8(pos + 8), view.getUint8(pos + 9), view.getUint8(pos + 10), view.getUint8(pos + 11)); 666 if (listType === "pdta") { 667 let subPos = pos + 12; 668 while (subPos + 8 <= pos + 8 + chunkSize) { 669 const subChunkId = String.fromCharCode(view.getUint8(subPos), view.getUint8(subPos + 1), view.getUint8(subPos + 2), view.getUint8(subPos + 3)); 670 const subSize = view.getUint32(subPos + 4, true); 671 if (subChunkId === "phdr") { 672 const phdrOffset = subPos + 8; 673 const numPresets = Math.floor(subSize / 38); 674 let bestPresetIndex = -1; 675 for (let p = 0; p < numPresets - 1; p++) { 676 const presetOffset = phdrOffset + p * 38; 677 const presetNum = view.getUint16(presetOffset + 20, true); 678 const bankNum = view.getUint16(presetOffset + 22, true); 679 if (presetNum === targetProgram && bankNum === targetBank) { 680 bestPresetIndex = p; 681 break; 682 } 683 } 684 if (bestPresetIndex === -1) { 685 let bestBank = 999; 686 for (let p = 0; p < numPresets - 1; p++) { 687 const presetOffset = phdrOffset + p * 38; 688 const presetNum = view.getUint16(presetOffset + 20, true); 689 const bankNum = view.getUint16(presetOffset + 22, true); 690 if (presetNum === targetProgram && bankNum < bestBank) { 691 bestBank = bankNum; 692 bestPresetIndex = p; 693 } 694 } 695 } 696 if (bestPresetIndex === -1) { 697 for (let p = 0; p < numPresets - 1; p++) { 698 const presetOffset = phdrOffset + p * 38; 699 const presetNum = view.getUint16(presetOffset + 20, true); 700 const bankNum = view.getUint16(presetOffset + 22, true); 701 if (presetNum === 0 && bankNum === 0) { 702 bestPresetIndex = p; 703 break; 704 } 705 } 706 } 707 if (bestPresetIndex === -1 && numPresets > 1) { 708 bestPresetIndex = 0; 709 } 710 if (bestPresetIndex !== -1) { 711 for (let p = 0; p < numPresets - 1; p++) { 712 if (p !== bestPresetIndex) { 713 const presetOffset = phdrOffset + p * 38; 714 view.setUint16(presetOffset + 22, 125, true); 715 } 716 } 717 } 718 return arrayBuffer; 719 } 720 subPos += 8 + subSize + subSize % 2; 721 } 722 break; 723 } 724 } 725 pos += 8 + chunkSize + chunkSize % 2; 726 } 727 return arrayBuffer; 728 } 729 async loadHsf3Bank(bankName, programs, onProgress) { 730 console.log(`[WasmSynth] Loading HSF3 Bank: ${bankName} with programs:`, programs); 731 if (typeof AudioWorkletNode === "undefined") { 732 throw new Error("AudioWorkletNode is not defined. Wasm Synth (SF2/SF3) requires a secure context (HTTPS or localhost)."); 733 } 734 try { 735 await this.ctx.audioWorklet.addModule(spessasynthWorkletUrl); 736 } catch (e) { 737 console.error("[WasmSynth] Failed to load AudioWorklet module:", e); 738 throw new Error("Failed to load the AudioWorklet processor module."); 739 } 740 this.synth = new WorkletSynthesizer(this.ctx); 741 console.log("[WasmSynth] WorkletSynthesizer created:", this.synth); 742 this.synth.setMasterParameter("masterGain", _WasmSynthAdapter.BASE_MASTER_GAIN); 743 this.synth.connect(this.destination); 744 if (this.synth.worklet) { 745 this.synth.worklet.onprocessorerror = (e) => { 746 console.error("[WasmSynth] WORKLET FATAL ERROR:", e); 747 }; 748 } 749 let loadedCount = 0; 750 const totalCount = programs.length; 751 const cleanLib = bankName.replace(/\/.*$/, ""); 752 const loadPromises = programs.map(async (inst) => { 753 const { bank, program } = inst; 754 const progStr = program.toString().padStart(3, "0"); 755 const sf2Url = `/storage/soundfonts/${cleanLib}/${bank}_${progStr}.hsf3`;
756 try { 757 const response = await fetch(sf2Url); 758 if (!response.ok) throw new Error(`Failed to fetch ${sf2Url}: ${response.statusText}`); 759 let rawBuffer = await response.arrayBuffer(); 760 rawBuffer = deobfuscateHsf3Buffer(rawBuffer.slice(0)); 761 const arrayBuffer = await decodeSf3ToSf2(rawBuffer, this.ctx); 762 let finalBuffer = arrayBuffer; 763 finalBuffer = this.isolatePresetInSf2(finalBuffer, bank, program); 764 await Promise.race([ 765 this.synth.soundBankManager.addSoundBank(finalBuffer, `inst_${bank}_${program}`), 766 new Promise((_, reject) => setTimeout(() => reject(new Error("Add sound bank timeout")), 1e4)) 767 ]); 768 loadedCount++; 769 if (onProgress) { 770 onProgress(loadedCount / totalCount); 771 } 772 } catch (err) { 773 console.warn(`[WasmSynth] Failed to load program ${program} (bank ${bank}) from HSF3 Bank ${bankName}:`, err); 774 } 775 }); 776 await Promise.all(loadPromises); 777 this.isReady = true; 778 console.log(`[WasmSynth] HSF3 Bank ${bankName} loaded and ready.`); 779 } 780 /** 781 * Applies a gain multiplier to the synth via SpessaSynth's masterGain parameter. 782 * This is layered on top of the base gain (1.2) for reliable volume control. 783 */ 784 setGain(gain) { 785 if (!this.synth) return; 786 this.synth.setMasterParameter("masterGain", _WasmSynthAdapter.BASE_MASTER_GAIN * gain); 787 } 788 setProgram(channel, program, bank = 0) { 789 if (!this.synth) return; 790 this.synth.controllerChange(channel, 0, bank); 791 this.synth.programChange(channel, program); 792 } 793 muteChannel(channel, isMuted) { 794 if (!this.synth) { 795 console.warn(`[WasmSynthAdapter] muteChannel(${channel}, ${isMuted}) failed: this.synth is null`); 796 return; 797 } 798 try { 799 this.synth.muteChannel(channel, isMuted); 800 } catch (err) { 801 console.error(`[WasmSynthAdapter] Exception in synth.muteChannel:`, err); 802 } 803 } 804 setChannelVolume(channel, volume) { 805 if (!this.synth) { 806 return; 807 } 808 const val = Math.max(0, Math.min(127, Math.round(volume * 127))); 809 try { 810 this.synth.controllerChange(channel, 7, val); 811 } catch (err) { 812 console.error(`[WasmSynthAdapter] Exception in synth.controllerChange:`, err); 813 } 814 } 815 setChannelPan(channel, pan) { 816 if (!this.synth) { 817 return; 818 } 819 const panValue = Math.max(-1, Math.min(1, pan)); 820 const val = Math.round((panValue + 1) / 2 * 127); 821 try { 822 this.synth.controllerChange(channel, 10, val); 823 } catch (err) { 824 console.error(`[WasmSynthAdapter] Exception in synth.controllerChange (pan):`, err); 825 } 826 } 827 noteOn(channel, note, velocity, time) { 828 if (!this.synth) return; 829 this.synth.noteOn(channel, note, velocity, { time }); 830 } 831 noteOff(channel, note, time) { 832 if (!this.synth) return; 833 this.synth.noteOff(channel, note, { time }); 834 } 835 stopAll() { 836 if (!this.synth) return; 837 this.synth.stopAll(true); 838 } 839 destroy() { 840 if (this.synth) { 841 this.synth.disconnect(); 842 this.synth = null; 843 } 844 this.isReady = false; 845 } 846}; 847_WasmSynthAdapter.BASE_MASTER_GAIN = 1.2; 848let WasmSynthAdapter = _WasmSynthAdapter; 849export { 850 AudioEngineManager as A, 851 WasmSynthAdapter as W 852};
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.