PageSourceSearch

https://www.hamienet.com/assets/wasm-synth-adapter-BrqgBX1y.js

js hamienet.com collected 2026-09-24 18:58:59 UTC 33,769 bytes, 852 lines download raw bytes

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.