PageSourceSearch

https://www.hamienet.com/assets/synth-Fr-K52Ox.js

js hamienet.com collected 2026-10-02 04:59:15 UTC 70,590 bytes, 1,804 lines download raw bytes

1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/vendor-audio-D-wTggbx.js","assets/vendor-react-Bay9Y7Lb.js","assets/vendor-D0P4XgNj.js","assets/vendor-i18n-BC7ZMXWQ.js","assets/vendor-DqKRdbjz.css"])))=>i.map(i=>d[i]);
2import { _ as __vitePreload } from "./vendor-react-three-BCww3P1x.js";
3import { e as isMobileDevice, f as isSlowMobile, l as GM_INSTRUMENTS } from "./utils-CEVb7nN1.js";
4import { W as WasmSynthAdapter } from "./wasm-synth-adapter-DpNGIIyM.js";
5import { a as Midi } from "./vendor-audio-D-wTggbx.js";
6import { ew as parseMidi, ex as writeMidi } from "./vendor-D0P4XgNj.js";
7class VocalDSP {
8  constructor(ctx) {
9    this.sourceNode = null;
10    this.stream = null;
11    this._autoVocalMode = false;
12    this._guideTrackMuted = false;
13    this._micThresholdDbfs = -28;
14    this._cooldownLatchTimeout = null;
15    this._animFrame = null;
16    this.ctx = ctx;
17    this.compressor = this.ctx.createDynamicsCompressor();
18    this.compressor.threshold.value = -30;
19    this.compressor.knee.value = 10;
20    this.compressor.ratio.value = 4;
21    this.compressor.attack.value = 0.05;
22    this.compressor.release.value = 0.25;
23    this.delay = this.ctx.createDelay(5);
24    this.delay.delayTime.value = 0;
25    this.delayGain = this.ctx.createGain();
26    this.delayGain.gain.value = 0;
27    const feedback = this.ctx.createGain();
28    feedback.gain.value = 0.3;
29    this.delay.connect(feedback);
30    feedback.connect(this.delay);
31    this.convolver = this.ctx.createConvolver();
32    this.convolver.buffer = this.generateSyntheticReverb(2);
33    this.convolverGain = this.ctx.createGain();
34    this.convolverGain.gain.value = 0;
35    this.dryGain = this.ctx.createGain();
36    this.dryGain.gain.value = 1;
37    this.masterGain = this.ctx.createGain();
38    this.masterGain.gain.value = 1;
39    this.analyser = this.ctx.createAnalyser();
40    this.analyser.fftSize = 2048;
41    this.setupRouting();
42  }
43  setupRouting() {
44    this.compressor.connect(this.dryGain);
45    this.compressor.connect(this.delay);
46    this.compressor.connect(this.convolver);
47    this.compressor.connect(this.analyser);
48    this.delay.connect(this.delayGain);
49    this.convolver.connect(this.convolverGain);
50    this.dryGain.connect(this.masterGain);
51    this.delayGain.connect(this.masterGain);
52    this.convolverGain.connect(this.masterGain);
53    this.masterGain.connect(this.ctx.destination);
54  }
55  generateSyntheticReverb(duration) {
56    const sampleRate = this.ctx.sampleRate;
57    const length = sampleRate * duration;
58    const impulse = this.ctx.createBuffer(2, length, sampleRate);
59    const left = impulse.getChannelData(0);
60    const right = impulse.getChannelData(1);
61    for (let i = 0; i < length; i++) {
62      const multiplier = Math.exp(-i / (sampleRate * (duration / 5)));
63      left[i] = (Math.random() * 2 - 1) * multiplier;
64      right[i] = (Math.random() * 2 - 1) * multiplier;
65    }
66    return impulse;
67  }
68  async startMic(deviceId) {
69    if (this.stream) {
70      this.stopMic();
71    }
72    try {
73      const constraints = {
74        audio: {
75          echoCancellation: false,
76          noiseSuppression: false,
77          autoGainControl: false,
78          ...deviceId ? { deviceId: { exact: deviceId } } : {}
79        }
80      };
81      this.stream = await navigator.mediaDevices.getUserMedia(constraints);
82      this.sourceNode = this.ctx.createMediaStreamSource(this.stream);
83      this.sourceNode.connect(this.compressor);
84      this.startAnalysisLoop();
85    } catch (e) {
86      console.error("Failed to access mic for VocalDSP:", e);
87    }
88  }
89  stopMic() {
90    if (this.stream) {
91      this.stream.getTracks().forEach((t) => t.stop());
92      this.stream = null;
93    }
94    if (this.sourceNode) {
95      this.sourceNode.disconnect();
96      this.sourceNode = null;
97    }
98    if (this._animFrame) {
99      cancelAnimationFrame(this._animFrame);
100      this._animFrame = null;
101    }
102  }
103  setEffects(reverbAmount, delayAmount, normalize) {
104    this.convolverGain.gain.value = reverbAmount;
105    this.delayGain.gain.value = delayAmount;
106    this.delay.delayTime.value = delayAmount > 0 ? 0.3 : 0;
107    this.dryGain.gain.value = 1 - Math.max(reverbAmount, delayAmount) * 0.5;
108    if (normalize) {
109      this.compressor.threshold.value = -30;
110      this.compressor.ratio.value = 4;
111    } else {
112      this.compressor.threshold.value = 0;
113      this.compressor.ratio.value = 1;
114    }
115  }
116  setAutoVocalMode(enabled) {
117    var _a;
118    this._autoVocalMode = enabled;
119    if (!enabled && this._guideTrackMuted) {
120      this._guideTrackMuted = false;
121      (_a = this.onMuteGuideTrack) == null ? void 0 : _a.call(this, false);
122      if (this._cooldownLatchTimeout) {
123        window.clearTimeout(this._cooldownLatchTimeout);
124        this._cooldownLatchTimeout = null;
125      }
126    }
127  }
128  startAnalysisLoop() {
129    const data = new Float32Array(this.analyser.fftSize);
130    const loop = () => {
131      if (!this._autoVocalMode || !this.onMuteGuideTrack) {
132        this._animFrame = requestAnimationFrame(loop);
133        return;
134      }
135      this.analyser.getFloatTimeDomainData(data);
136      let sum = 0;
137      for (let i = 0; i < data.length; i++) {
138        sum += data[i] * data[i];
139      }
140      const rms = Math.sqrt(sum / data.length);
141      const dbfs = 20 * Math.log10(rms || 1e-8);
142      if (dbfs > this._micThresholdDbfs) {
143        if (!this._guideTrackMuted) {
144          this._guideTrackMuted = true;
145          this.onMuteGuideTrack(true);
146        }
147        if (this._cooldownLatchTimeout) {
148          window.clearTimeout(this._cooldownLatchTimeout);
149        }
150        this._cooldownLatchTimeout = window.setTimeout(() => {
151          this._guideTrackMuted = false;
152          if (this.onMuteGuideTrack) this.onMuteGuideTrack(false);
153          this._cooldownLatchTimeout = null;
154        }, 1500);
155      }
156      this._animFrame = requestAnimationFrame(loop);
157    };
158    this._animFrame = requestAnimationFrame(loop);
159  }
160}
161class PitchTracker {
162  constructor(ctx) {
163    this.sourceNode = null;
164    this.stream = null;
165    this._animFrame = null;
166    this.ctx = ctx;
167    this.analyser = this.ctx.createAnalyser();
168    this.analyser.fftSize = 2048;
169    this.dataBuffer = new Float32Array(this.analyser.fftSize);
170  }
171  async start(deviceId) {
172    if (this.stream) return;
173    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
174      console.error("navigator.mediaDevices.getUserMedia is not supported or requires HTTPS");
175      return;
176    }
177    try {
178      const constraints = {
179        audio: {
180          echoCancellation: false,
181          noiseSuppression: false,
182          autoGainControl: false
183        }
184      };
185      if (deviceId) {
186        constraints.audio.deviceId = { exact: deviceId };
187      }
188      this.stream = await navigator.mediaDevices.getUserMedia(constraints);
189      this.sourceNode = this.ctx.createMediaStreamSource(this.stream);
190      this.sourceNode.connect(this.analyser);
191      this.startLoop();
192    } catch (e) {
193      console.error("Failed to access mic for PitchTracker:", e);
194    }
195  }
196  stop() {
197    if (this.stream) {
198      this.stream.getTracks().forEach((t) => t.stop());
199      this.stream = null;
200    }
201    if (this.sourceNode) {
202      this.sourceNode.disconnect();
203      this.sourceNode = null;
204    }
205    if (this._animFrame) {
206      cancelAnimationFrame(this._animFrame);
207      this._animFrame = null;
208    }
209  }
210  startLoop() {
211    const loop = () => {
212      this.analyser.getFloatTimeDomainData(this.dataBuffer);
213      let sumSquares = 0;
214      for (let i = 0; i < this.dataBuffer.length; i++) {
215        sumSquares += this.dataBuffer[i] * this.dataBuffer[i];
216      }
217      const rms = Math.sqrt(sumSquares / this.dataBuffer.length);
218      if (this.onVolumeUpdate) {
219        this.onVolumeUpdate(rms);
220      }
221      const pitch = this.yinPitch(this.dataBuffer, this.ctx.sampleRate);
222      if (pitch > 0 && this.onPitchDetected) {
223        this.onPitchDetected(pitch);
224      }
225      this._animFrame = requestAnimationFrame(loop);
226    };
227    this._animFrame = requestAnimationFrame(loop);
228  }
229  // YIN Auto-Correlation Algorithm
230  yinPitch(data, sampleRate) {
231    const threshold = 0.1;
232    const windowSize = data.length / 2;
233    const yinBuffer = new Float32Array(windowSize);
234    let j, tau;
235    for (tau = 0; tau < windowSize; tau++) {
236      yinBuffer[tau] = 0;
237      for (j = 0; j < windowSize; j++) {
238        const delta = data[j] - data[j + tau];
239        yinBuffer[tau] += delta * delta;
240      }
241    }
242    yinBuffer[0] = 1;
243    yinBuffer[1] = 1;
244    let runningSum = 0;
245    for (tau = 1; tau < windowSize; tau++) {
246      runningSum += yinBuffer[tau];
247      yinBuffer[tau] *= tau / runningSum;
248    }
249    let tauEstimate = -1;
250    for (tau = 2; tau < windowSize; tau++) {
251      if (yinBuffer[tau] < threshold) {
252        while (tau + 1 < windowSize && yinBuffer[tau + 1] < yinBuffer[tau]) {
253          tau++;
254        }
255        tauEstimate = tau;
256        break;
257      }
258    }
259    if (tauEstimate === -1) {
260      for (tau = 2; tau < windowSize; tau++) {
261        if (yinBuffer[tau] < yinBuffer[tauEstimate === -1 ? 0 : tauEstimate] && yinBuffer[tau] < 0.2) {
262          tauEstimate = tau;
263        }
264      }
265    }
266    if (tauEstimate !== -1) {
267      const x0 = tauEstimate < 1 ? tauEstimate : tauEstimate - 1;
268      const x2 = tauEstimate + 1 < windowSize ? tauEstimate + 1 : tauEstimate;
269      let betterTau = tauEstimate;
270      if (x0 !== tauEstimate && x2 !== tauEstimate) {
271        const s0 = yinBuffer[x0];
272        const s1 = yinBuffer[tauEstimate];
273        const s2 = yinBuffer[x2];
274        betterTau = tauEstimate + (s2 - s0) / (2 * (2 * s1 - s2 - s0));
275      }
276      return sampleRate / betterTau;
277    }
278    return -1;
279  }
280}
281class SoundTouchSource {
282  constructor(ctx, SoundTouch, SimpleFilter) {
283    this.tracks = [];
284    this.isPlaying = false;
285    this.onended = null;
286    this.currentPosition = 0;
287    this.ctx = ctx;
288    this.node = ctx.createScriptProcessor(4096, 0, 2);
289    this.samplesBuffer = new Float32Array(4096 * 2);
290    this.st = new SoundTouch(ctx.sampleRate);
291    this.st.tempo = 1;
292    const source = {
293      extract: (target, numFrames, position) => {
294        this.currentPosition = position;
295        target.fill(0);
296        if (this.tracks.length === 0) return 0;
297        let maxFramesRead = 0;
298        for (const track of this.tracks) {
299          if (!track.enabled || track.volume <= 1e-4) continue;
300          const buffer = track.buffer;
301          const l = buffer.getChannelData(0);
302          const r = buffer.numberOfChannels > 1 ? buffer.getChannelData(1) : l;
303          const vol = track.volume;
304          let framesRead = 0;
305          for (let i = 0; i < numFrames; i++) {
306            const idx = position + i;
307            if (idx < buffer.length) {
308              target[i * 2] += l[idx] * vol;
309              target[i * 2 + 1] += r[idx] * vol;
310              framesRead++;
311            }
312          }
313          if (framesRead > maxFramesRead) maxFramesRead = framesRead;
314        }
315        return maxFramesRead;
316      }
317    };
318    this.filter = new SimpleFilter(source, this.st);
319    this.node.onaudioprocess = this.process.bind(this);
320  }
321  static async create(ctx) {
322    const { SoundTouch, SimpleFilter } = await __vitePreload(async () => {
323      const { SoundTouch: SoundTouch2, SimpleFilter: SimpleFilter2 } = await import("./vendor-audio-D-wTggbx.js").then((n) => n.s);
324      return { SoundTouch: SoundTouch2, SimpleFilter: SimpleFilter2 };
325    }, true ? __vite__mapDeps([0,1,2,3,4]) : void 0);
326    return new SoundTouchSource(ctx, SoundTouch, SimpleFilter);
327  }
328  setTracks(tracks) {
329    this.tracks = tracks;
330  }
331  updateTrackVolume(index, volume, enabled) {
332    if (this.tracks[index]) {
333      this.tracks[index].volume = volume;
334      this.tracks[index].enabled = enabled;
335    }
336  }
337  setPlaybackRate(rate) {
338    if (this.st) {
339      this.st.tempo = rate;
340    }
341  }
342  setPitchSemitones(semitones) {
343    if (this.st) {
344      this.st.pitchSemitones = semitones;
345    }
346  }
347  connect(destination) {
348    this.node.connect(destination);
349  }
350  disconnect() {
351    this.node.disconnect();
352  }
353  start(offset = 0) {
354    if (!this.filter) return;
355    const sampleRate = this.tracks.length > 0 ? this.tracks[0].buffer.sampleRate : this.ctx.sampleRate;
356    this.filter.sourcePosition = Math.floor(offset * sampleRate);
357    this.isPlaying = true;
358    console.log(`[SoundTouchSource] Started at offset ${offset}s (pos: ${this.filter.sourcePosition})`);
359  }
360  stop() {
361    if (!this.isPlaying) return;
362    this.isPlaying = false;
363    console.log(`[SoundTouchSource] Stopped`);
364    if (this.onended) this.onended();
365  }
366  process(e) {
367    const outputBuffer = e.outputBuffer;
368    const numFrames = outputBuffer.length;
369    const outputL = outputBuffer.getChannelData(0);
370    const outputR = outputBuffer.getChannelData(1);
371    if (!this.isPlaying) {
372      outputL.fill(0);
373      outputR.fill(0);
374      return;
375    }
376    if (this.samplesBuffer.length < numFrames * 2) {
377      this.samplesBuffer = new Float32Array(numFrames * 2);
378    }
379    const framesExtracted = this.filter.extract(this.samplesBuffer, numFrames);
380    for (let i = 0; i < framesExtracted; i++) {
381      outputL[i] = this.samplesBuffer[i * 2];
382      outputR[i] = this.samplesBuffer[i * 2 + 1];
383    }
384    if (framesExtracted < numFrames) {
385      for (let i = framesExtracted; i < numFrames; i++) {
386        outputL[i] = 0;
387        outputR[i] = 0;
388      }
389      if (framesExtracted === 0 && this.isPlaying) {
390        this.stop();
391      }
392    }
393  }
394}
395function splitMidiType0ToType1Buffer(buffer) {
396  try {
397    const u8 = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
398    const parsed = parseMidi(u8);
399    if (parsed.header.format !== 0 || parsed.tracks.length === 0) {
400      return u8.buffer;
401    }
402    const rawTrack = parsed.tracks[0];
403    let absTick = 0;
404    const metaEvents = [];
405    const channelEvents = /* @__PURE__ */ new Map();
406    const channelPrograms = /* @__PURE__ */ new Map();
407    for (const ev of rawTrack) {
408      absTick += ev.deltaTime || 0;
409      if ("channel" in ev && ev.channel !== void 0) {
410        const ch = ev.channel;
411        if (!channelEvents.has(ch)) {
412          channelEvents.set(ch, []);
413        }
414        channelEvents.get(ch).push({ absTick, event: ev });
415        if (ev.type === "programChange") {
416          channelPrograms.set(ch, ev.programNumber);
417        }
418      } else {
419        metaEvents.push({ absTick, event: ev });
420      }
421    }
422    if (channelEvents.size === 0) {
423      return u8.buffer;
424    }
425    const newTracks = [];
426    const track0 = [];
427    let prevTick = 0;
428    for (const m of metaEvents) {
429      const dt = Math.max(0, m.absTick - prevTick);
430      track0.push({ ...m.event, deltaTime: dt });
431      prevTick = m.absTick;
432    }
433    if (track0.length === 0 || track0[track0.length - 1].type !== "endOfTrack") {
434      track0.push({ deltaTime: 0, type: "endOfTrack" });
435    }
436    newTracks.push(track0);
437    const sortedChannels = Array.from(channelEvents.keys()).sort((a, b) => a - b);
438    for (const ch of sortedChannels) {
439      const chTrack = [];
440      const prog = channelPrograms.get(ch) ?? 0;
441      const instrName = ch === 9 ? "Drums" : GM_INSTRUMENTS[prog] || `Instrument ${prog}`;
442      chTrack.push({ deltaTime: 0, type: "trackName", text: instrName });
443      let lastTick = 0;
444      for (const item of channelEvents.get(ch)) {
445        const dt = Math.max(0, item.absTick - lastTick);
446        chTrack.push({ ...item.event, deltaTime: dt });
447        lastTick = item.absTick;
448      }
449      if (chTrack.length === 0 || chTrack[chTrack.length - 1].type !== "endOfTrack") {
450        chTrack.push({ deltaTime: 0, type: "endOfTrack" });
451      }
452      newTracks.push(chTrack);
453    }
454    parsed.header.format = 1;
455    parsed.header.numTracks = newTracks.length;
456    parsed.tracks = newTracks;
457    const outBytes = writeMidi(parsed);
458    return new Uint8Array(outBytes).buffer;
459  } catch (e) {
460    console.warn("[splitMidiType0ToType1Buffer] Failed to convert Type 0 to Type 1:", e);
461    return buffer instanceof Uint8Array ? buffer.buffer : buffer;
462  }
463}
464function cleanMidiBuffer(buffer) {
465  const data = new Uint8Array(buffer);
466  const out = [];
467  let offset = 0;
468  while (offset < data.length) {
469    if (offset + 8 > data.length) break;
470    const id = String.fromCharCode(...data.slice(offset, offset + 4));
471    const length = data[offset + 4] << 24 | data[offset + 5] << 16 | data[offset + 6] << 8 | data[offset + 7];
472    if (id === "MThd" || id === "MTrk") {
473      out.push(data.slice(offset, offset + 8 + length));
474    }
475    offset += 8 + length;
476  }
477  const totalLen = out.reduce((sum, arr) => sum + arr.length, 0);
478  const cleaned = new Uint8Array(totalLen);
479  let curr = 0;
480  for (const arr of out) {
481    cleaned.set(arr, curr);
482    curr += arr.length;
483  }
484  return splitMidiType0ToType1Buffer(cleaned.buffer);
485}
486class HamieSynth {
487  constructor() {
488    this.reverbNode = null;
489    this.reverbWetGain = null;
490    this.currentReverbDecay = 1.8;
491    this.channels = [];
492    this.trackBlobs = /* @__PURE__ */ new Map();
493    this.abortController = null;
494    this.wasmSynth = null;
495    this.useSoundTouch = false;
496    this.isSoundfontNormalized = false;
497    this.limiter = null;
498    this.isPlaying = false;
499    this.startTime = 0;
500    this.startOffset = 0;
501    this.mutedChannels = /* @__PURE__ */ new Set();
502    this.hoveredChannels = /* @__PURE__ */ new Set();
503    this.soloChannel = null;
504    this.tickDiv = 480;
505    this.tempo = 120;
506    this.playbackRate = 1;
507    this.pitchSemitones = 0;
508    this.soundfont = "GeneralUser-GS";
509    this.defaultWasmSoundfont = "SoundBlasterOld.sf2";
510    this.defaultJsSoundfont = "MusyngKite";
511    this.isLiveMode = false;
512    this.eventsJson = null;
513    this.trackNotesMap = {};
514    this.channelNotesMap = {};
515    this.scheduledNotes = [];
516    this.audioMode = "wasmsynth";
517    this.midiAccess = null;
518    this.midiAccessStatus = "uninitialized";
519    this.activeMidiOutput = null;
520    this.midiOutputId = null;
521    this.schedulerTimer = null;
522    this.nextScheduledIndex = 0;
523    const isMobile = typeof window !== "undefined" && (/Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || /Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 0 || "ontouchstart" in window);
524    this.ctx = new (window.AudioContext || window.webkitAudioContext)(isMobile ? { latencyHint: "playback" } : void 0);
525    this.masterGain = this.ctx.createGain();
526    this.masterGain.gain.value = 1;
527    try {
528      this.limiter = this.ctx.createDynamicsCompressor();
529      this.limiter.threshold.setValueAtTime(-1, this.ctx.currentTime);
530      this.limiter.knee.setValueAtTime(0, this.ctx.currentTime);
531      this.limiter.ratio.setValueAtTime(20, this.ctx.currentTime);
532      this.limiter.attack.setValueAtTime(3e-3, this.ctx.currentTime);
533      this.limiter.release.setValueAtTime(0.1, this.ctx.currentTime);
534      this.masterGain.connect(this.limiter);
535      this.limiter.connect(this.ctx.destination);
536    } catch (e) {
537      console.warn("Failed to initialize master limiter:", e);
538      this.masterGain.connect(this.ctx.destination);
539    }
540    try {
541      this.reverbNode = this.ctx.createConvolver();
542      this.reverbWetGain = this.ctx.createGain();
543      this.reverbWetGain.gain.value = 0;
544      this.createReverbImpulseResponse(this.currentReverbDecay);
545      this.masterGain.connect(this.reverbNode);
546      this.reverbNode.connect(this.reverbWetGain);
547      if (this.limiter) {
548        this.reverbWetGain.connect(this.limiter);
549      } else {
550        this.reverbWetGain.connect(this.ctx.destination);
551      }
552    } catch (e) {
553      console.warn("Failed to initialize master reverb convolver:", e);
554    }
555    this.vocalDSP = new VocalDSP(this.ctx);
556    this.pitchTracker = new PitchTracker(this.ctx);
557  }
558  getMidiOutputs() {
559    if (!this.midiAccess) return [];
560    return Array.from(this.midiAccess.outputs.values());
561  }
562  setMidiOutputById(id) {
563    if (!this.midiAccess) return;
564    const outputs = this.getMidiOutputs();
565    const selected = outputs.find((o) => o.id === id);
566    if (selected) {
567      this.activeMidiOutput = selected;
568      this.midiOutputId = id;
569      console.log(`[HamieSynth] Selected MIDI output: ${selected.name}`);
570    }
571  }
572  getActiveMidiOutputId() {
573    return this.midiOutputId;
574  }
575  async initMidiOutput() {
576    if (typeof navigator === "undefined" || !navigator.requestMIDIAccess) {
577      console.warn("[HamieSynth] Web MIDI API is not supported in this browser.");
578      return;
579    }
580    if (this.midiAccess) return;
581    try {
582      this.midiAccess = await navigator.requestMIDIAccess();
583      this.midiAccessStatus = "granted";
584      const outputs = this.getMidiOutputs();
585      if (outputs.length > 0) {
586        if (this.midiOutputId) {
587          const selected = outputs.find((o) => o.id === this.midiOutputId);
588          if (selected) {
589            this.activeMidiOutput = selected;
590            return;
591          }
592        }
593        this.activeMidiOutput = outputs[0];
594        this.midiOutputId = outputs[0].id;
595        console.log(`[HamieSynth] Web MIDI initialized. Default output: ${outputs[0].name}`);
596      } else {
597        console.log("[HamieSynth] Web MIDI initialized, but no MIDI output ports found.");
598      }
599    } catch (e) {
600      console.error("[HamieSynth] Failed to request MIDI access:", e);
601      this.midiAccessStatus = "denied";
602    }
603  }
604  getLookaheadMs() {
605    return isMobileDevice() ? 100 : 50;
606  }
607  getScheduleAheadSec() {
608    return isMobileDevice() ? 0.8 : 0.25;
609  }
610  muteTrackByIndex(index, mute) {
611    if (this.channels[index] && this.channels[index].gainNode) {
612      this.channels[index].gainNode.gain.value = mute ? 0 : this.channels[index].userVolume;
613    }
614  }
615  setPan(index, pan) {
616    this.setTrackPan(index, pan);
617  }
618  async startVocalMic(deviceId) {
619    if (this.vocalDSP) {
620      await this.vocalDSP.startMic(deviceId);
621    }
622  }
623  setGuideTrackIndex(index) {
624    this.vocalDSP.onMuteGuideTrack = (mute) => {
625      this.muteTrackByIndex(index, mute);
626    };
627  }
628  getAudioContext() {
629    return this.ctx;
630  }
631  getMasterGain() {
632    return this.masterGain;
633  }
634  getWasmSynth() {
635    return this.wasmSynth;
636  }
637  getChannels() {
638    return this.channels.map((ch, idx) => ({
639      index: idx,
640      name: ch.track.name,
641      gainNode: ch.gainNode,
642      midiChannel: ch.track.midiChannel !== void 0 && ch.track.midiChannel >= 0 ? ch.track.midiChannel : idx % 16
643    }));
644  }
645  getChannelBuffers() {
646    return this.channels.map((ch) => ({
647      track: ch.track,
648      buffer: ch.buffer,
649      userVolume: ch.userVolume
650    }));
651  }
652  getTrackBlob(index) {
653    return this.trackBlobs.get(index) || null;
654  }
655  getTrackNotes() {
656    return this.trackNotesMap;
657  }
658  getChannelNotes() {
659    return this.channelNotesMap;
660  }
661  isTrackActiveAtTime(trackIdx, time) {
662    const notes = this.trackNotesMap[trackIdx];
663    if (!notes || notes.length === 0) return false;
664    return this.isNoteListActiveAtTime(notes, time);
665  }
666  isChannelActiveAtTime(channel, time) {
667    const notes = this.channelNotesMap[channel];
668    if (!notes || notes.length === 0) return false;
669    return this.isNoteListActiveAtTime(notes, time);
670  }
671  getTrackLevelAtTime(trackIdx, time) {
672    const notes = this.trackNotesMap[trackIdx];
673    if (!notes || notes.length === 0) return 0;
674    return this.getNoteListLevelAtTime(notes, time);
675  }
676  getChannelLevelAtTime(channel, time) {
677    const notes = this.channelNotesMap[channel];
678    if (!notes || notes.length === 0) return 0;
679    return this.getNoteListLevelAtTime(notes, time);
680  }
681  getNoteListLevelAtTime(list, time) {
682    let low = 0;
683    let high = list.length;
684    while (low < high) {
685      const mid = low + high >> 1;
686      if (list[mid].t > time) {
687        high = mid;
688      } else {
689        low = mid + 1;
690      }
691    }
692    let maxLevel = 0;
693    for (let i = low - 1; i >= 0; i--) {
694      const note = list[i];
695      if (note.t < time - 10) break;
696      if (time >= note.t && time <= note.e) {
697        const vel = note.v || 80;
698        const elapsed = time - note.t;
699        const decay = Math.max(0.65, 1 - elapsed * 0.7);
700        const level = Math.min(100, vel / 127 * 90 * decay);
701        if (level > maxLevel) maxLevel = level;
702      }
703    }
704    return maxLevel;
705  }
706  isNoteListActiveAtTime(list, time) {
707    let low = 0;
708    let high = list.length;
709    while (low < high) {
710      const mid = low + high >> 1;
711      if (list[mid].t > time) {
712        high = mid;
713      } else {
714        low = mid + 1;
715      }
716    }
717    for (let i = low - 1; i >= 0; i--) {
718      const note = list[i];
719      if (note.t < time - 30) {
720        break;
721      }
722      if (time >= note.t && time <= note.e) {
723        return true;
724      }
725    }
726    return false;
727  }
728  async loadTracks(instruments, tickDiv, tempo, tier, soundfont, eventsPath = null, onProgress, forceLive = false, defaultSoundfont = "MusyngKite", isAdmin = false) {
729    var _a, _b, _c;
730    const dbg = (...args) => {
731      if (isAdmin) console.log(...args);
732    };
733    if (this.abortController) this.abortController.abort();
734    this.abortController = new AbortController();
735    const signal = this.abortController.signal;
736    if (this.ctx.state === "suspended") {
737      try {
738        await this.ctx.resume();
739      } catch {
740      }
741    }
742    this.stop();
743    this.tickDiv = tickDiv;
744    this.tempo = tempo;
745    let finalSoundfont = soundfont;
746    const sfLower = soundfont.toLowerCase();
747    let isDirectSf2 = sfLower.endsWith(".sf2") || sfLower.endsWith(".sf3") || sfLower.endsWith(".hsf3");
748    let isHsf3Bank = sfLower.endsWith("_hsf3bank") || sfLower.includes("_hsf3bank/");
749    if (this.audioMode === "wasmsynth") {
750      const isExplicitSf = sfLower.endsWith(".sf2") || sfLower.endsWith(".sf3") || sfLower.endsWith(".hsf3") || sfLower.endsWith("_hsf3bank") || sfLower.includes("_hsf3bank/");
751      if (isExplicitSf) {
752        finalSoundfont = soundfont;
753      } else {
754        finalSoundfont = this.defaultWasmSoundfont || "SoundBlasterOld.sf2";
755      }
756      const fLower = finalSoundfont.toLowerCase();
757      isHsf3Bank = fLower.endsWith("_hsf3bank") || fLower.includes("_hsf3bank/");
758      if (!fLower.endsWith(".sf2") && !fLower.endsWith(".sf3") && !fLower.endsWith(".hsf3") && !isHsf3Bank) {
759        finalSoundfont += ".sf2";
760      }
761      isDirectSf2 = true;
762    } else if (this.audioMode === "chiptune" || this.audioMode === "fmsynth") {
763      isDirectSf2 = false;
764    }
765    if (isDirectSf2 && typeof AudioWorkletNode === "undefined") {
766      console.warn(`[HamieSynth] AudioWorkletNode is not supported in this environment (non-secure context). Falling back from direct SF2 to JS soundfont.`);
767      finalSoundfont = finalSoundfont.replace(/\.h?sf[23]$/i, "").replace(/\.hsf3$/i, "").replace(/_hsf3bank$/i, "");
768      isDirectSf2 = false;
769      isHsf3Bank = false;
770    }
771    this.soundfont = finalSoundfont;
772    this.isLiveMode = forceLive || this.audioMode === "chiptune" || this.audioMode === "fmsynth" || this.audioMode === "wasmsynth";
773    this.isSoundfontNormalized = false;
774    if (this.audioMode === "singleopus") {
775      console.log("[HamieSynth] Single Opus mode - skipping loading of stems/MIDI, marking all ready.");
776      this.isLiveMode = false;
777      this.channels = [];
778      if (onProgress) {
779        instruments.forEach((_, idx) => onProgress(idx, { percent: 100, ready: true }));
780      }
781      return;
782    }
783    this.channels = [];
784    this.trackBlobs.clear();
785    this.eventsJson = null;
786    this.trackNotesMap = {};
787    this.channelNotesMap = {};
788    if (this.wasmSynth) {
789      this.wasmSynth.destroy();
790      this.wasmSynth = null;
791    }
792    console.log(`[HamieSynth] Loading ${instruments.length} tracks (Tier: ${tier}, SF: ${finalSoundfont}, Live: ${this.isLiveMode}, Direct: ${isDirectSf2})...`);
793    dbg("%c[HamieSynth] loadTracks() called", "color:#60a5fa;font-weight:bold");
794    dbg("  instruments   :", instruments.map((i, n) => `[${n}] ${i.name} prog=${i.program} stemPath=${i.stemPath || "(none)"}`).join("\n                "));
795    dbg("  eventsPath    :", eventsPath || "(none)");
796    dbg("  tickDiv/tempo :", tickDiv, "/", tempo);
797    dbg("  tier/sf       :", tier, "/", finalSoundfont);
798    dbg("  forceLive     :", forceLive, "| isDirectSf2:", isDirectSf2);
799    let eventsJsonLoaded = false;
800    if (eventsPath) {
801      const pathsToTry = Array.isArray(eventsPath) ? eventsPath : [eventsPath];
802      for (const path of pathsToTry) {
803        dbg(`[HamieSynth] Attempting to load events from: ${path}`);
804        try {
805          const res = await fetch(path, { signal });
806          dbg(`  fetch status for ${path}:`, res.status, res.ok ? "OK" : "FAILED");
807          if (res.ok) {
808            const isMidi = path.toLowerCase().includes(".mid");
809            let rawEvents = [];
810            if (isMidi) {
811              dbg(`  [HamieSynth] Parsing binary MIDI file...`);
812              let arrayBuffer = await res.arrayBuffer();
813              arrayBuffer = cleanMidiBuffer(arrayBuffer);
814              const midi = new Midi.Midi(arrayBuffer);
815              dbg(`  [HamieSynth] MIDI parsed successfully. Tracks: ${midi.tracks.length}`);
816              midi.tracks.forEach((track, trackIdx) => {
817                const trackName = track.name || `Track ${trackIdx + 1}`;
818                const trackCh = track.channel ?? 0;
819                track.notes.forEach((note) => {
820                  const noteCh = note.channel ?? trackCh;
821                  rawEvents.push({
822                    t: note.time,
823                    d: note.duration,
824                    v: Math.round(note.velocity * 127),
825                    n: note.midi,
826                    i: trackName,
827                    trackIdx,
828                    // raw track index (kept for legacy JSON paths)
829                    midiCh: noteCh
830                    // actual MIDI channel (used for channel-keyed instruments)
831                  });
832                });
833              });
834            } else {
835              dbg(`  [HamieSynth] Parsing JSON events file...`);
836              rawEvents = await res.json();
837            }
838            if (!rawEvents || rawEvents.length === 0) {
839              throw new Error(`Events source ${path} yielded 0 notes, trying next source if available.`);
840            }
841            const musicalTrackIndices = [];
842            instruments.forEach((inst, idx) => {
843              if (inst.name !== "Informational Track") {
844                musicalTrackIndices.push(idx);
845              }
846            });
847            this.eventsJson = rawEvents.map((ev) => {
848              var _a2, _b2;
849              let mappedIdx = ev.trackIdx;
850              if (ev.midiCh !== void 0) {
851                const byChannel = instruments.findIndex((inst) => inst.midiChannel === ev.midiCh);
852                if (byChannel !== -1) return { ...ev, trackIdx: byChannel };
853              }
854              let targetIdx = -1;
855              const match = (_a2 = ev.i) == null ? void 0 : _a2.match(/^(.*) \((\d+)\)$/);
856              if (match) {
857                targetIdx = parseInt(match[2]) - 1;
858              } else {
859                const trackMatch = (_b2 = ev.i) == null ? void 0 : _b2.match(/^Track\s+(\d+)$/i);
860                if (trackMatch) {
861                  targetIdx = parseInt(trackMatch[1]) - 1;
862                } else if (!isNaN(parseInt(ev.i))) {
863                  targetIdx = parseInt(ev.i);
864                }
865              }
866              if (targetIdx >= 0) {
867                const foundByTrackId = instruments.findIndex((inst) => inst.trackId === targetIdx);
868                const foundInst = foundByTrackId !== -1 ? instruments[foundByTrackId] : null;
869                const nameMatches = foundInst && (!ev.i || ev.i.startsWith(foundInst.name || "") || foundInst.name && foundInst.name.startsWith(ev.i.split(" (")[0]));
870                if (foundByTrackId !== -1 && nameMatches) {
871                  mappedIdx = foundByTrackId;
872                } else if (targetIdx < musicalTrackIndices.length) {
873                  mappedIdx = musicalTrackIndices[targetIdx];
874                } else {
875                  mappedIdx = musicalTrackIndices[musicalTrackIndices.length - 1] || 0;
876                }
877              } else {
878                const instIdx = instruments.findIndex((inst) => inst.name === ev.i);
879                if (instIdx !== -1) {
880                  mappedIdx = instIdx;
881                } else if (ev.trackIdx < musicalTrackIndices.length) {
882                  mappedIdx = musicalTrackIndices[ev.trackIdx];
883                }
884              }
885              return {
886                ...ev,
887                trackIdx: mappedIdx
888              };
889            });
890            this.eventsJson.sort((a, b) => a.t - b.t);
891            console.log(`[HamieSynth] Successfully loaded, mapped and sorted ${this.eventsJson.length} live synthesis events from: ${path}`);
892            dbg("  eventsJson mapped. Sample[0]:", (_a = this.eventsJson) == null ? void 0 : _a[0]);
893            this.trackNotesMap = {};
894            this.channelNotesMap = {};
895            for (const ev of this.eventsJson) {
896              const minDur = Math.max(ev.d || 0, 0.1);
897              const entry = { t: ev.t, e: ev.t + minDur, v: ev.v || 80 };
898              if (ev.trackIdx !== void 0) {
899                if (!this.trackNotesMap[ev.trackIdx]) this.trackNotesMap[ev.trackIdx] = [];
900                this.trackNotesMap[ev.trackIdx].push(entry);
901              }
902              if (ev.midiCh !== void 0) {
903                if (!this.channelNotesMap[ev.midiCh]) this.channelNotesMap[ev.midiCh] = [];
904                this.channelNotesMap[ev.midiCh].push(entry);
905              } else if (ev.trackIdx !== void 0 && ((_b = instruments[ev.trackIdx]) == null ? void 0 : _b.midiChannel) !== void 0) {
906                const ch = instruments[ev.trackIdx].midiChannel;
907                if (ch !== void 0 && ch >= 0) {
908                  if (!this.channelNotesMap[ch]) this.channelNotesMap[ch] = [];
909                  this.channelNotesMap[ch].push(entry);
910                }
911              }
912            }
913            eventsJsonLoaded = true;
914            break;
915          }
916        } catch (e) {
917          console.error(`Failed to load/parse events from ${path}`, e);
918          dbg(`  [HamieSynth] Error fetching or parsing path ${path}:`, e);
919        }
920      }
921    }
922    if (this.isLiveMode && !eventsJsonLoaded) {
923      console.warn("[HamieSynth] Live mode requested but no events source could be loaded or parsed — no notes will play!");
924      dbg("%c[HamieSynth] ❌ Live mode active but no valid events loaded!", "color:red;font-weight:bold");
925      throw new Error("No valid events source could be loaded or parsed (possibly 404). Live synthesis aborted.");
926    }
927    const promises = instruments.map(async (inst, idx) => {
928      if (onProgress) onProgress(idx, { percent: 0, ready: false });
929      const gainNode = this.ctx.createGain();
930      const pannerNode = this.ctx.createStereoPanner();
931      gainNode.connect(pannerNode);
932      pannerNode.connect(this.masterGain);
933      const channel = {
934        track: inst,
935        buffer: null,
936        source: null,
937        stSource: null,
938        gainNode,
939        pannerNode,
940        userPan: 0,
941        userVolume: 1,
942        // Default volume
943        sfInstrument: null,
944        notes: null
945      };
946      this.channels.push(channel);
947      if (!inst.stemPath) {
948        if (!this.isLiveMode) {
949          dbg(`  [track ${idx}] No stemPath in stem mode — marking ready, skipping load.`);
950          if (onProgress) onProgress(idx, { percent: 100, ready: true });
951          return;
952        }
953        dbg(`  [track ${idx}] No stemPath, but live mode — continuing to load soundfont.`);
954      }
955      try {
956        if (this.isLiveMode) {
957          try {
958            if (this.audioMode === "chiptune" || this.audioMode === "fmsynth") {
959              dbg(`  [track ${idx}] ${this.audioMode} mode — skipping soundfont load, marking ready.`);
960              if (onProgress) onProgress(idx, { percent: 100, ready: true });
961            } else if (isDirectSf2) {
962              dbg(`  [track ${idx}] Direct SF2 mode — deferring to WASM load below.`);
963              if (onProgress) onProgress(idx, { percent: 10, ready: false });
964            } else {
965              const { loadSoundfontData, decodeSoundfontNotes, createBufferPlayer } = await __vitePreload(async () => {
966                const { loadSoundfontData: loadSoundfontData2, decodeSoundfontNotes: decodeSoundfontNotes2, createBufferPlayer: createBufferPlayer2 } = await import("./soundfont-utils-ieqHyFXd.js");
967                return { loadSoundfontData: loadSoundfontData2, decodeSoundfontNotes: decodeSoundfontNotes2, createBufferPlayer: createBufferPlayer2 };
968              }, true ? [] : void 0);
969              const program = inst.program || 0;
970              const baseUrl = `/storage/soundfonts/${this.soundfont}/`;
971              const sfUrl = `${baseUrl}${program}-opus.js`;
972              const fallbackUrl = `${baseUrl}0-opus.js`;
973              const defaultJsBaseUrl = `/storage/soundfonts/${this.defaultJsSoundfont}/`;
974              const defaultJsSfUrl = `${defaultJsBaseUrl}${program}-opus.js`;
975              const defaultJsFallbackUrl = `${defaultJsBaseUrl}0-opus.js`;
976              dbg(`  [track ${idx}] Loading JS soundfont program=${program} from: ${sfUrl}`);
977              try {
978                let noteData;
979                try {
980                  noteData = await loadSoundfontData(sfUrl);
981                  dbg(`  [track ${idx}] Soundfont loaded OK: ${sfUrl}`);
982                } catch (e1) {
983                  console.warn(`[HamieSynth] Failed to load program ${program} from ${this.soundfont}, trying 0-opus.js fallback...`);
984                  try {
985                    noteData = await loadSoundfontData(fallbackUrl);
986                  } catch (e2) {
987                    console.warn(`[HamieSynth] Failed to load 0-opus from ${this.soundfont}, trying global JS fallback: ${this.defaultJsSoundfont}...`);
988                    try {
989                      noteData = await loadSoundfontData(defaultJsSfUrl);
990                    } catch (e3) {
991                      console.warn(`[HamieSynth] Failed to load global JS fallback program ${program}, trying 0-opus.js...`);
992                      noteData = await loadSoundfontData(defaultJsFallbackUrl);
993                    }
994                  }
995                }
996                const buffers = await decodeSoundfontNotes(this.ctx, noteData);
997                if (noteData && noteData._normalized) {
998                  this.isSoundfontNormalized = true;
999                }
1000                channel.sfInstrument = createBufferPlayer(this.ctx, buffers, channel.gainNode);
1001                dbg(`  [track ${idx}] sfInstrument created. Keys available: ${Object.keys(buffers).length}`);
1002                if (onProgress) onProgress(idx, { percent: 100, ready: true });
1003              } catch (sfErr) {
1004                console.error(`[HamieSynth] Total failure to load soundfont ${this.soundfont}`, sfErr);
1005                dbg(`%c[HamieSynth] ❌ Soundfont load total failure for track ${idx}`, "color:red;font-weight:bold", sfErr);
1006                this.isLiveMode = false;
1007                if (onProgress) onProgress(idx, { percent: 0, ready: false, error: true });
1008              }
1009            }
1010          } catch (err) {
1011            console.error(`[HamieSynth] Live mode error for track ${idx}:`, err);
1012          }
1013        }
1014        if (!this.isLiveMode) {
1015          const stemUrl = inst.stemPathHigh || inst.stemPath;
1016          const mimeType = "audio/opus";
1017          if (!stemUrl) throw new Error(`No stem path found for track ${inst.name || idx}`);
1018          const timeoutSignal = AbortSignal.timeout ? AbortSignal.timeout(9e4) : null;
1019          const fetchSignal = timeoutSignal ? AbortSignal.any ? AbortSignal.any([signal, timeoutSignal]) : signal : signal;
1020          let response = await fetch(stemUrl, { signal: fetchSignal });
1021          if (!response.ok && inst.stemPath && stemUrl !== inst.stemPath) {
1022            console.warn(`[HamieSynth] Stem fetch failed for ${stemUrl} (Status: ${response.status}), trying fallback ${inst.stemPath}`);
1023            response = await fetch(inst.stemPath, { signal: fetchSignal });
1024          }
1025          if (!response.ok) throw new Error(`Failed to fetch stem: ${stemUrl} (Status: ${response.status})`);
1026          const contentLength = response.headers.get("content-length");
1027          const total = parseInt(contentLength || "0", 10);
1028          const chunks = [];
1029          let loaded = 0;
1030          if (response.body) {
1031            const reader = response.body.getReader();
1032            while (true) {
1033              const { done, value } = await reader.read();
1034              if (done) break;
1035              if (value) {
1036                chunks.push(value);
1037                loaded += value.length;
1038                if (onProgress && total > 0) {
1039                  const pct = Math.min(99, loaded / total * 100);
1040                  onProgress(idx, { percent: pct, ready: false });
1041                }
1042              }
1043            }
1044            this.trackBlobs.set(idx, new Blob(chunks, { type: mimeType }));
1045          } else {
1046            const blob = await response.blob();
1047            this.trackBlobs.set(idx, blob);
1048            chunks.push(new Uint8Array(await blob.arrayBuffer()));
1049            loaded = blob.size;
1050          }
1051          const combined = new Uint8Array(loaded);
1052          let offset = 0;
1053          for (const chunk of chunks) {
1054            combined.set(chunk, offset);
1055            offset += chunk.length;
1056          }
1057          const audioBuffer = await this.ctx.decodeAudioData(combined.buffer);
1058          channel.buffer = audioBuffer;
1059          channel.stSource = await SoundTouchSource.create(this.ctx);
1060          channel.stSource.setTracks([{
1061            buffer: audioBuffer,
1062            volume: 1,
1063            enabled: true
1064          }]);
1065          channel.stSource.connect(channel.gainNode);
1066          if (onProgress) onProgress(idx, { percent: 100, ready: true });
1067        }
1068      } catch (e) {
1069        if (e instanceof Error && e.name === "AbortError") {
1070          console.log(`[HamieSynth] Loading aborted for ${inst.name}`);
1071        } else {
1072          console.error(`[HamieSynth] Error loading track ${inst.name}:`, e);
1073          if (onProgress) onProgress(idx, { percent: 0, ready: false, error: true });
1074        }
1075      }
1076    });
1077    await Promise.all(promises);
1078    if (isDirectSf2 && this.isLiveMode) {
1079      try {
1080        this.wasmSynth = new WasmSynthAdapter(this.ctx, this.masterGain);
1081        let isBank = finalSoundfont.toLowerCase().endsWith("_hsf3bank") || finalSoundfont.toLowerCase().includes("_hsf3bank/");
1082        if (isBank) {
1083          const uniqueInsts = /* @__PURE__ */ new Map();
1084          this.channels.forEach((ch, idx) => {
1085            if (ch.track.program !== void 0) {
1086              const midiChannel = ch.track.midiChannel !== void 0 && ch.track.midiChannel >= 0 ? ch.track.midiChannel : idx % 16;
1087              const bank = midiChannel % 16 === 9 ? 128 : 0;
1088              const program = ch.track.program;
1089              uniqueInsts.set(`${bank}_${program}`, { bank, program });
1090            }
1091          });
1092          if (uniqueInsts.size === 0) {
1093            uniqueInsts.set("0_0", { bank: 0, program: 0 });
1094          }
1095          const sfProgress = (pct) => {
1096            this.channels.forEach((ch, idx) => {
1097              const mappedPct = 10 + pct * 85;
1098              if (onProgress) onProgress(idx, { percent: mappedPct, ready: false });
1099            });
1100          };
1101          await this.wasmSynth.loadHsf3Bank(finalSoundfont, Array.from(uniqueInsts.values()), sfProgress);
1102        } else {
1103          const sfProgress = (pct) => {
1104            this.channels.forEach((ch, idx) => {
1105              const mappedPct = 10 + pct * 85;
1106              if (onProgress) onProgress(idx, { percent: mappedPct, ready: false });
1107            });
1108          };
1109          await this.wasmSynth.load(`/storage/soundfonts/${finalSoundfont}`, sfProgress);
1110        }
1111        this.channels.forEach((ch, idx) => {
1112          if (this.wasmSynth && ch.track.program !== void 0) {
1113            const midiChannel = ch.track.midiChannel !== void 0 && ch.track.midiChannel >= 0 ? ch.track.midiChannel : idx % 16;
1114            const bank = midiChannel % 16 === 9 ? 128 : 0;
1115            this.wasmSynth.setProgram(midiChannel % 16, ch.track.program, bank);
1116          }
1117          if (onProgress) onProgress(idx, { percent: 100, ready: true });
1118        });
1119      } catch (err) {
1120        console.error(`[HamieSynth] Failed to load direct SF2:`, err);
1121        this.isLiveMode = false;
1122        this.channels.forEach((ch, idx) => {
1123          if (onProgress) onProgress(idx, { percent: 0, ready: false, error: true });
1124        });
1125      }
1126    }
1127    console.log(`[HamieSynth] Load finished. Mode: ${this.isLiveMode ? this.wasmSynth ? "Wasm-SF2" : "Live-JS" : "Stems"}`);
1128    dbg("%c[HamieSynth] Load complete", "color:#34d399;font-weight:bold");
1129    dbg("  final isLiveMode  :", this.isLiveMode);
1130    dbg("  eventsJson count  :", ((_c = this.eventsJson) == null ? void 0 : _c.length) ?? "null");
1131    dbg("  channels loaded   :", this.channels.length);
1132    dbg("  wasmSynth         :", !!this.wasmSynth);
1133    this.channels.forEach((ch, i) => {
1134      var _a2;
1135      dbg(`  [ch ${i}] ${ch.track.name} | buffer=${!!ch.buffer} | sfInstrument=${!!ch.sfInstrument} | notes=${((_a2 = ch.notes) == null ? void 0 : _a2.length) ?? "null"}`);
1136    });
1137    this.updateGains();
1138  }
1139  async play(offset = 0) {
1140    if (this.ctx.state === "suspended") {
1141      try {
1142        await this.ctx.resume();
1143      } catch {
1144      }
1145    }
1146    if (this.isPlaying) this.stop();
1147    this.isPlaying = true;
1148    this.startOffset = offset;
1149    this.startTime = this.ctx.currentTime;
1150    this.useSoundTouch = Math.abs(this.playbackRate - 1) > 0.01 || this.pitchSemitones !== 0;
1151    if (this.audioMode === "webmidi" && this.eventsJson) {
1152      console.log(`[HamieSynth] Playing Web MIDI (Offset: ${offset}s)`);
1153      this.scheduledNotes = [];
1154      if (this.schedulerTimer) {
1155        clearInterval(this.schedulerTimer);
1156        this.schedulerTimer = null;
1157      }
1158      this.nextScheduledIndex = 0;
1159      for (let i = 0; i < this.eventsJson.length; i++) {
1160        if (this.eventsJson[i].t >= offset) {
1161          this.nextScheduledIndex = i;
1162          break;
1163        }
1164      }
1165      this.initMidiOutput().then(() => {
1166        if (this.activeMidiOutput) {
1167          this.channels.forEach((ch, idx) => {
1168            const channel = ch.track.midiChannel !== void 0 ? ch.track.midiChannel : idx % 16;
1169            const program = ch.track.program || 0;
1170            try {
1171              this.activeMidiOutput.send([192 | channel, program]);
1172            } catch (e) {
1173              console.warn(`[HamieSynth] Failed to send MIDI Program Change for ch ${channel}`, e);
1174            }
1175          });
1176        }
1177      });
1178      this.scheduleNotes();
1179      this.schedulerTimer = setInterval(() => {
1180        this.scheduleNotes();
1181      }, this.getLookaheadMs());
1182    } else if (this.isLiveMode && this.eventsJson) {
1183      console.log(`[HamieSynth] Playing Live SF (Mode: ${this.wasmSynth ? "Wasm" : "JS"}, Offset: ${offset}s)`);
1184      if (this.wasmSynth) {
1185        this.wasmSynth.unmute();
1186      }
1187      this.scheduledNotes.forEach((item) => {
1188        const n = (item == null ? void 0 : item.node) || item;
1189        if (n && typeof n.stop === "function") {
1190          try {
1191            n.stop();
1192          } catch {
1193          }
1194        }
1195      });
1196      this.scheduledNotes = [];
1197      if (this.schedulerTimer) {
1198        clearInterval(this.schedulerTimer);
1199        this.schedulerTimer = null;
1200      }
1201      this.nextScheduledIndex = 0;
1202      for (let i = 0; i < this.eventsJson.length; i++) {
1203        if (this.eventsJson[i].t >= offset) {
1204          this.nextScheduledIndex = i;
1205          break;
1206        }
1207      }
1208      this.scheduleNotes();
1209      this.schedulerTimer = setInterval(() => {
1210        this.scheduleNotes();
1211      }, this.getLookaheadMs());
1212    } else if (this.useSoundTouch) {
1213      console.log(`[HamieSynth] Playing with SoundTouch (Rate: ${this.playbackRate}, Pitch: ${this.pitchSemitones})`);
1214      this.channels.forEach((ch) => {
1215        if (ch.stSource && ch.buffer) {
1216          ch.stSource.setPlaybackRate(this.playbackRate);
1217          ch.stSource.setPitchSemitones(this.pitchSemitones);
1218          ch.stSource.start(offset);
1219        }
1220      });
1221    } else {
1222      console.log(`[HamieSynth] Playing Native (Rate: ${this.playbackRate})`);
1223      this.channels.forEach((ch) => {
1224        if (ch.buffer) {
1225          if (ch.source) {
1226            try {
1227              ch.source.stop();
1228              ch.source.disconnect();
1229            } catch {
1230            }
1231          }
1232          const source = this.ctx.createBufferSource();
1233          source.buffer = ch.buffer;
1234          source.connect(ch.gainNode);
1235          if (offset < ch.buffer.duration) {
1236            source.playbackRate.value = this.playbackRate;
1237            source.start(0, offset);
1238          }
1239          ch.source = source;
1240        }
1241      });
1242    }
1243    this.updateGains();
1244    console.log(`[HamieSynth] Playing from ${offset.toFixed(2)}s`);
1245  }
1246  stop() {
1247    this.isPlaying = false;
1248    if (this.schedulerTimer) {
1249      clearInterval(this.schedulerTimer);
1250      this.schedulerTimer = null;
1251    }
1252    this.scheduledNotes.forEach((item) => {
1253      const n = (item == null ? void 0 : item.node) || item;
1254      if (n && typeof n.stop === "function") {
1255        try {
1256          n.stop();
1257        } catch {
1258        }
1259      }
1260    });
1261    this.scheduledNotes = [];
1262    if (this.activeMidiOutput) {
1263      for (let c = 0; c < 16; c++) {
1264        try {
1265          this.activeMidiOutput.send([176 | c, 123, 0]);
1266          this.activeMidiOutput.send([176 | c, 120, 0]);
1267        } catch (e) {
1268        }
1269      }
1270    }
1271    if (this.wasmSynth) {
1272      this.wasmSynth.stopAll();
1273    }
1274    this.channels.forEach((ch) => {
1275      if (ch.sfInstrument && typeof ch.sfInstrument.stop === "function") {
1276        try {
1277          ch.sfInstrument.stop(this.ctx.currentTime);
1278        } catch {
1279        }
1280      }
1281    });
1282    this.channels.forEach((ch) => {
1283      if (ch.source) {
1284        try {
1285          ch.source.stop();
1286          ch.source.disconnect();
1287        } catch {
1288        }
1289        ch.source = null;
1290      }
1291      if (ch.stSource) {
1292        ch.stSource.stop();
1293      }
1294    });
1295  }
1296  seek(time) {
1297    if (this.isPlaying) {
1298      this.play(time);
1299    } else {
1300      this.startOffset = time;
1301    }
1302  }
1303  scheduleNotes() {
1304    if (!this.isPlaying || !this.eventsJson) return;
1305    const now = this.ctx.currentTime;
1306    const currentSongTime = this.getCurrentTime();
1307    const scheduleLimitTime = currentSongTime + this.getScheduleAheadSec() * this.playbackRate;
1308    this.scheduledNotes = this.scheduledNotes.filter((item) => {
1309      if (item && item.endTime && item.endTime < now - 1) {
1310        return false;
1311      }
1312      return true;
1313    });
1314    const isLowPerf = typeof window !== "undefined" && localStorage.getItem("low_performance") === "true";
1315    const isMobile = isMobileDevice();
1316    const isSlow = isSlowMobile();
1317    const polyphonyLimit = isSlow || isLowPerf ? 16 : isMobile ? 24 : 96;
1318    while (this.nextScheduledIndex < this.eventsJson.length) {
1319      const ev = this.eventsJson[this.nextScheduledIndex];
1320      if (ev.t > scheduleLimitTime) {
1321        break;
1322      }
1323      if (ev.t >= this.startOffset) {
1324        const ch = this.channels[ev.trackIdx];
1325        if (ch) {
1326          const activeVoices = this.scheduledNotes.filter((item) => item.endTime > now).length;
1327          const isDrum = ch.track.midiChannel !== void 0 && ch.track.midiChannel % 16 === 9 || ev.midiCh !== void 0 && ev.midiCh % 16 === 9;
1328          if (activeVoices < polyphonyLimit || isDrum) {
1329            const playTime = this.startTime + (ev.t - this.startOffset) / this.playbackRate;
1330            const duration = ev.d / this.playbackRate;
1331            const velocity = Math.floor(ev.v * ch.userVolume);
1332            if (this.audioMode === "webmidi" && this.activeMidiOutput) {
1333              const performanceTime = performance.now() + (playTime - now) * 1e3;
1334              const channel = ch.track.midiChannel !== void 0 ? ch.track.midiChannel : ev.trackIdx % 16;
1335              try {
1336                this.activeMidiOutput.send([144 | channel, ev.n, velocity], performanceTime);
1337                this.activeMidiOutput.send([128 | channel, ev.n, 0], performanceTime + duration * 1e3);
1338              } catch (e) {
1339                console.warn("[HamieSynth] Web MIDI send error", e);
1340              }
1341              this.scheduledNotes.push({ node: null, endTime: playTime + duration });
1342            } else if (this.audioMode === "chiptune") {
1343              try {
1344                const osc = this.ctx.createOscillator();
1345                const gainNode = this.ctx.createGain();
1346                osc.connect(gainNode);
1347                gainNode.connect(this.masterGain);
1348                const channel = ch.track.midiChannel !== void 0 ? ch.track.midiChannel : ev.trackIdx % 16;
1349                if (channel === 9) {
1350                  osc.type = "triangle";
1351                  const freq2 = 120;
1352                  osc.frequency.setValueAtTime(freq2, playTime);
1353                  osc.frequency.exponentialRampToValueAtTime(30, playTime + 0.08);
1354                } else if (channel === 0 || channel === 1 || channel === 2) {
1355                  osc.type = "square";
1356                } else if (channel === 3 || channel === 4) {
1357                  osc.type = "sawtooth";
1358                } else {
1359                  osc.type = ev.n < 48 ? "triangle" : "sine";
1360                }
1361                const freq = 440 * Math.pow(2, (ev.n - 69) / 12);
1362                osc.frequency.setValueAtTime(freq, playTime);
1363                let waveVolume = 0.28;
1364                if (osc.type === "square" || osc.type === "sawtooth") waveVolume = 0.12;
1365                if (osc.type === "sine") waveVolume = 0.38;
1366                const gainVal = waveVolume * (velocity / 127) * ch.userVolume;
1367                gainNode.gain.setValueAtTime(gainVal, playTime);
1368                gainNode.gain.exponentialRampToValueAtTime(1e-4, playTime + duration);
1369                osc.start(playTime);
1370                osc.stop(playTime + duration);
1371                this.scheduledNotes.push({
1372                  node: {
1373                    stop: () => {
1374                      try {
1375                        osc.stop();
1376                      } catch {
1377                      }
1378                    }
1379                  },
1380                  endTime: playTime + duration
1381                });
1382              } catch (e) {
1383                console.warn("[HamieSynth] Failed to play chiptune oscillator", e);
1384              }
1385            } else if (this.audioMode === "fmsynth") {
1386              try {
1387                const carrier = this.ctx.createOscillator();
1388                const modulator = this.ctx.createOscillator();
1389                const modGain = this.ctx.createGain();
1390                const carrierGain = this.ctx.createGain();
1391                carrier.connect(carrierGain);
1392                carrierGain.connect(this.masterGain);
1393                modulator.connect(modGain);
1394                modGain.connect(carrier.frequency);
1395                const channelNum = ch.track.midiChannel !== void 0 ? ch.track.midiChannel : ev.trackIdx % 16;
1396                const freq = 440 * Math.pow(2, (ev.n - 69) / 12);
1397                let ratio = 1;
1398                let modIndex = 2;
1399                let modDecay = duration;
1400                let carrierType = "sine";
1401                let modulatorType = "sine";
1402                let voiceVolume = 0.55;
1403                if (channelNum === 9) {
1404                  carrierType = "triangle";
1405                  modulatorType = "sawtooth";
1406                  ratio = 5.25;
1407                  modIndex = 30;
1408                  modDecay = Math.min(0.08, duration);
1409                  voiceVolume = 0.33;
1410                } else if (channelNum === 0 || channelNum === 1 || channelNum === 2) {
1411                  carrierType = "sawtooth";
1412                  modulatorType = "sine";
1413                  ratio = 0.5;
1414                  modIndex = 4;
1415                  modDecay = duration * 0.5;
1416                  voiceVolume = 0.18;
1417                } else if (channelNum === 3 || channelNum === 4) {
1418                  carrierType = "sine";
1419                  modulatorType = "sine";
1420                  ratio = 1;
1421                  modIndex = 2.5;
1422                  modDecay = duration;
1423                  voiceVolume = 0.44;
1424                } else {
1425                  carrierType = "sine";
1426                  modulatorType = "sine";
1427                  if (ev.n > 72) {
1428                    ratio = 3.5;
1429                    modIndex = 8;
1430                    modDecay = duration * 0.3;
1431                    voiceVolume = 0.4;
1432                  } else {
1433                    ratio = 1;
1434                    modIndex = 3;
1435                    modDecay = duration * 0.4;
1436                    voiceVolume = 0.48;
1437                  }
1438                }
1439                carrier.type = carrierType;
1440                carrier.frequency.setValueAtTime(freq, playTime);
1441                modulator.type = modulatorType;
1442                modulator.frequency.setValueAtTime(freq * ratio, playTime);
1443                const startMod = freq * modIndex;
1444                modGain.gain.setValueAtTime(startMod, playTime);
1445                modGain.gain.exponentialRampToValueAtTime(1e-4, playTime + modDecay);
1446                const gainVal = voiceVolume * (velocity / 127) * ch.userVolume;
1447                carrierGain.gain.setValueAtTime(gainVal, playTime);
1448                carrierGain.gain.exponentialRampToValueAtTime(1e-4, playTime + duration);
1449                modulator.start(playTime);
1450                modulator.stop(playTime + duration);
1451                carrier.start(playTime);
1452                carrier.stop(playTime + duration);
1453                this.scheduledNotes.push({
1454                  node: {
1455                    stop: () => {
1456                      try {
1457                        carrier.stop();
1458                        modulator.stop();
1459                      } catch {
1460                      }
1461                    }
1462                  },
1463                  endTime: playTime + duration
1464                });
1465              } catch (e) {
1466                console.warn("[HamieSynth] Failed to play FM oscillator", e);
1467              }
1468            } else if (this.wasmSynth) {
1469              const midiChannel = ev.midiCh !== void 0 ? ev.midiCh : ev.trackIdx % 16;
1470              this.wasmSynth.noteOn(midiChannel % 16, ev.n, velocity, playTime);
1471              this.wasmSynth.noteOff(midiChannel % 16, ev.n, playTime + duration);
1472              this.scheduledNotes.push({ node: null, endTime: playTime + duration });
1473            } else if (ch.sfInstrument) {
1474              const node = ch.sfInstrument.play(ev.n, playTime, {
1475                duration,
1476                gain: ev.v / 127
1477              });
1478              this.scheduledNotes.push({ node, endTime: playTime + duration });
1479            }
1480          }
1481        }
1482      }
1483      this.nextScheduledIndex++;
1484    }
1485  }
1486  getCurrentTime() {
1487    if (!this.isPlaying) return this.startOffset;
1488    return this.startOffset + (this.ctx.currentTime - this.startTime) * this.playbackRate;
1489  }
1490  /** Returns the max duration (in seconds) across all loaded stem AudioBuffers. */
1491  getDuration() {
1492    let maxDuration = 0;
1493    for (const ch of this.channels) {
1494      if (ch.buffer && ch.buffer.duration > maxDuration) {
1495        maxDuration = ch.buffer.duration;
1496      }
1497    }
1498    return maxDuration;
1499  }
1500  // --- Control Methods ---
1501  setPitchSemitones(semitones) {
1502    if (this.pitchSemitones === semitones) return;
1503    if (this.isPlaying) {
1504      const elapsed = this.ctx.currentTime - this.startTime;
1505      this.startOffset += elapsed * this.playbackRate;
1506      this.startTime = this.ctx.currentTime;
1507    }
1508    this.pitchSemitones = semitones;
1509    if (!this.isPlaying) return;
1510    const wasSoundTouch = this.useSoundTouch;
1511    const needsSoundTouch = Math.abs(this.playbackRate - 1) > 0.01 || semitones !== 0;
1512    this.useSoundTouch = needsSoundTouch;
1513    if (wasSoundTouch !== needsSoundTouch) {
1514      this.stop();
1515      this.play(this.startOffset);
1516    } else {
1517      if (this.useSoundTouch) {
1518        this.channels.forEach((ch) => {
1519          if (ch.stSource) ch.stSource.setPitchSemitones(semitones);
1520        });
1521      }
1522    }
1523  }
1524  setPlaybackRate(rate) {
1525    const oldRate = this.playbackRate;
1526    if (Math.abs(oldRate - rate) < 1e-3) return;
1527    if (this.isPlaying) {
1528      const elapsed = this.ctx.currentTime - this.startTime;
1529      this.startOffset += elapsed * oldRate;
1530      this.startTime = this.ctx.currentTime;
1531    }
1532    this.playbackRate = rate;
1533    if (!this.isPlaying) return;
1534    const wasSoundTouch = this.useSoundTouch;
1535    const needsSoundTouch = Math.abs(rate - 1) > 0.01 || this.pitchSemitones !== 0;
1536    this.useSoundTouch = needsSoundTouch;
1537    if (wasSoundTouch !== needsSoundTouch) {
1538      this.stop();
1539      this.play(this.startOffset);
1540    } else {
1541      if (this.useSoundTouch) {
1542        this.channels.forEach((ch) => {
1543          if (ch.stSource) ch.stSource.setPlaybackRate(rate);
1544        });
1545      } else {
1546        this.channels.forEach((ch) => {
1547          if (ch.source) {
1548            try {
1549              ch.source.playbackRate.value = rate;
1550            } catch {
1551            }
1552          }
1553        });
1554      }
1555    }
1556  }
1557  setMasterVolume(volume) {
1558    let multiplier = 6;
1559    try {
1560      if (typeof window !== "undefined") {
1561        const saved = localStorage.getItem("hamie_settings");
1562        if (saved) {
1563          const parsed = JSON.parse(saved);
1564          if (typeof parsed.soundfontVolume === "number") {
1565            multiplier = parsed.soundfontVolume === 4 ? 6 : parsed.soundfontVolume;
1566          }
1567        }
1568      }
1569    } catch (e) {
1570      console.error("Failed to read soundfontVolume from local storage", e);
1571    }
1572    const needsBoost = this.isLiveMode && this.audioMode === "synth" && !this.wasmSynth && !this.isSoundfontNormalized;
1573    const finalMultiplier = needsBoost ? multiplier : 1;
1574    const boostedVolume = volume * finalMultiplier;
1575    this.masterGain.gain.setTargetAtTime(boostedVolume, this.ctx.currentTime, 0.05);
1576  }
1577  setMute(channelIndex, isMuted) {
1578    if (isMuted) this.mutedChannels.add(channelIndex);
1579    else this.mutedChannels.delete(channelIndex);
1580    this.updateGains();
1581  }
1582  isChannelMuted(channelIndex) {
1583    if (this.soloChannel !== null) {
1584      return channelIndex !== this.soloChannel;
1585    }
1586    return this.mutedChannels.has(channelIndex);
1587  }
1588  setTrackInstrument(channelIndex, program) {
1589    if (channelIndex < 0 || channelIndex >= this.channels.length) return;
1590    const ch = this.channels[channelIndex];
1591    ch.track.program = program;
1592    const midiChannel = ch.track.midiChannel !== void 0 && ch.track.midiChannel >= 0 ? ch.track.midiChannel : channelIndex % 16;
1593    const bank = midiChannel % 16 === 9 ? 128 : 0;
1594    if (this.wasmSynth) {
1595      try {
1596        this.wasmSynth.setProgram(midiChannel % 16, program, bank);
1597      } catch (e) {
1598        console.warn(`[HamieSynth] Failed to change SpessaSynth program to ${program} for channel ${midiChannel}`, e);
1599      }
1600    }
1601    if (this.activeMidiOutput) {
1602      try {
1603        this.activeMidiOutput.send([192 | midiChannel, program]);
1604      } catch (e) {
1605        console.warn(`[HamieSynth] Failed to send MIDI Program Change to ${program} for channel ${midiChannel}`, e);
1606      }
1607    }
1608  }
1609  setHoverBoost(channelIndex, isBoosted) {
1610    if (isBoosted) this.hoveredChannels.add(channelIndex);
1611    else this.hoveredChannels.delete(channelIndex);
1612    this.updateGains();
1613  }
1614  setSolo(channelIndex) {
1615    this.soloChannel = channelIndex;
1616    this.updateGains();
1617  }
1618  setTrackVolume(channelIndex, volume) {
1619    if (channelIndex >= 0 && channelIndex < this.channels.length) {
1620      const ch = this.channels[channelIndex];
1621      ch.userVolume = volume;
1622      this.updateGains();
1623    }
1624  }
1625  setTrackPan(channelIndex, pan) {
1626    if (channelIndex >= 0 && channelIndex < this.channels.length) {
1627      const ch = this.channels[channelIndex];
1628      ch.userPan = Math.max(-1, Math.min(1, pan));
1629      if (ch.pannerNode) {
1630        ch.pannerNode.pan.setValueAtTime(ch.userPan, this.ctx.currentTime);
1631      }
1632      if (this.wasmSynth) {
1633        const midiChannel = ch.track.midiChannel !== void 0 && ch.track.midiChannel >= 0 ? ch.track.midiChannel : channelIndex % 16;
1634        this.wasmSynth.setChannelPan(midiChannel % 16, ch.userPan);
1635      }
1636    }
1637  }
1638  resetMixer() {
1639    this.mutedChannels.clear();
1640    this.soloChannel = null;
1641    this.channels.forEach((ch) => {
1642      ch.userVolume = 1;
1643      ch.userPan = 0;
1644      if (ch.pannerNode) {
1645        ch.pannerNode.pan.setValueAtTime(0, this.ctx.currentTime);
1646      }
1647    });
1648    if (this.wasmSynth) {
1649      this.channels.forEach((_, idx) => {
1650        this.wasmSynth.setChannelPan(idx % 16, 0);
1651      });
1652    }
1653    this.updateGains();
1654  }
1655  updateGains() {
1656    this.channels.forEach((ch, idx) => {
1657      let gain = ch.userVolume;
1658      let isMuted = false;
1659      if (this.soloChannel !== null) {
1660        if (idx !== this.soloChannel) {
1661          gain = 0;
1662          isMuted = true;
1663        }
1664      } else {
1665        if (this.mutedChannels.has(idx)) {
1666          gain = 0;
1667          isMuted = true;
1668        }
1669      }
1670      if (gain > 0 && this.hoveredChannels.has(idx)) {
1671        gain *= 1.5;
1672      }
1673      ch.gainNode.gain.cancelScheduledValues(this.ctx.currentTime);
1674      ch.gainNode.gain.setValueAtTime(gain, this.ctx.currentTime);
1675      if (this.wasmSynth) {
1676        const midiChannel = ch.track.midiChannel !== void 0 && ch.track.midiChannel >= 0 ? ch.track.midiChannel : idx % 16;
1677        this.wasmSynth.muteChannel(midiChannel % 16, isMuted);
1678        this.wasmSynth.setChannelVolume(midiChannel % 16, gain);
1679      }
1680    });
1681  }
1682  createReverbImpulseResponse(decayTime) {
1683    if (!this.reverbNode) return;
1684    try {
1685      const rate = this.ctx.sampleRate;
1686      const length = Math.max(1, rate * decayTime);
1687      const impulse = this.ctx.createBuffer(2, length, rate);
1688      const left = impulse.getChannelData(0);
1689      const right = impulse.getChannelData(1);
1690      for (let i = 0; i < length; i++) {
1691        const percent = i / length;
1692        const decay = Math.exp(-percent * 6);
1693        left[i] = (Math.random() * 2 - 1) * decay;
1694        right[i] = (Math.random() * 2 - 1) * decay;
1695      }
1696      this.reverbNode.buffer = impulse;
1697    } catch (e) {
1698      console.warn("Failed to create reverb impulse response:", e);
1699    }
1700  }
1701  setMasterReverbLevel(level) {
1702    if (this.reverbWetGain) {
1703      this.reverbWetGain.gain.setTargetAtTime(level, this.ctx.currentTime, 0.05);
1704    }
1705  }
1706  setMasterReverbRoomSize(size) {
1707    let decay = 1.8;
1708    if (size === "Small Room") decay = 0.8;
1709    else if (size === "Medium Hall") decay = 1.8;
1710    else if (size === "Large Hall") decay = 3;
1711    else if (size === "Cathedral") decay = 6;
1712    else if (size === "Plate") decay = 2.2;
1713    this.currentReverbDecay = decay;
1714    this.createReverbImpulseResponse(decay);
1715  }
1716}
1717const synth = new HamieSynth();
1718const getAudioContext = () => synth.getAudioContext();
1719async function parseMidiInstruments(urls) {
1720  for (const url of urls) {
1721    try {
1722      const res = await fetch(url);
1723      if (!res.ok) continue;
1724      let buf = await res.arrayBuffer();
1725      if (url.toLowerCase().includes(".mid")) {
1726        buf = cleanMidiBuffer(buf);
1727      }
1728      const midi = new Midi.Midi(buf);
1729      const tickDiv = midi.header.ppq || 480;
1730      const tempo = midi.header.tempos.length > 0 ? midi.header.tempos[0].bpm : 120;
1731      const channelMap = /* @__PURE__ */ new Map();
1732      midi.tracks.forEach((track) => {
1733        var _a, _b;
1734        if (track.notes.length === 0) return;
1735        const ch = track.channel ?? 0;
1736        const isDrum = ch === 9;
1737        let name = track.name || ((_a = track.instrument) == null ? void 0 : _a.name);
1738        if (isDrum && (!name || name === "undefined" || name.toLowerCase().startsWith("channel"))) {
1739          name = "Drums";
1740        } else if (!name || name === "undefined") {
1741          name = `Channel ${ch + 1}`;
1742        }
1743        if (!channelMap.has(ch)) {
1744          const program = ((_b = track.instrument) == null ? void 0 : _b.number) ?? 0;
1745          channelMap.set(ch, { program, name, notesCount: track.notes.length });
1746        } else {
1747          const existing = channelMap.get(ch);
1748          existing.notesCount += track.notes.length;
1749          if (existing.name.startsWith("Channel ") && !name.startsWith("Channel ")) {
1750            existing.name = name;
1751          }
1752        }
1753      });
1754      if (channelMap.size === 0) {
1755        return { tracks: [{ name: "Master", program: 0, midiChannel: 0, notesCount: 1, trackId: 0 }], duration: midi.duration, tempo, tickDiv, notes: [] };
1756      }
1757      const sorted = Array.from(channelMap.entries()).sort(([a], [b]) => a - b);
1758      const channelToTrackIdx = /* @__PURE__ */ new Map();
1759      sorted.forEach(([ch], idx) => {
1760        channelToTrackIdx.set(ch, idx);
1761      });
1762      const parsedNotes = [];
1763      midi.tracks.forEach((track) => {
1764        track.notes.forEach((note) => {
1765          var _a, _b;
1766          const noteCh = note.channel ?? track.channel ?? 0;
1767          const trackIdx = channelToTrackIdx.get(noteCh) ?? 0;
1768          const trackName = ((_b = (_a = sorted[trackIdx]) == null ? void 0 : _a[1]) == null ? void 0 : _b.name) || track.name || `Track ${trackIdx + 1}`;
1769          parsedNotes.push({
1770            t: Math.round(note.time * 1e3) / 1e3,
1771            d: Math.max(0.01, Math.round(note.duration * 1e3) / 1e3),
1772            n: note.midi,
1773            v: Math.round(note.velocity * 127),
1774            i: `${trackName} (${trackIdx + 1})`,
1775            trackIdx
1776          });
1777        });
1778      });
1779      parsedNotes.sort((a, b) => a.t - b.t);
1780      return {
1781        tracks: sorted.map(([ch, info], idx) => ({
1782          name: info.name,
1783          program: info.program,
1784          midiChannel: ch,
1785          notesCount: info.notesCount || 1,
1786          trackId: idx,
1787          stemPath: void 0
1788        })),
1789        duration: midi.duration,
1790        tempo,
1791        tickDiv,
1792        notes: parsedNotes
1793      };
1794    } catch {
1795    }
1796  }
1797  console.warn("[parseMidiInstruments] Could not parse any MIDI URL — using single default track.");
1798  return { tracks: [{ name: "Master", program: 0, midiChannel: 0, notesCount: 1, trackId: 0 }], duration: 0, tempo: 120, tickDiv: 480, notes: [] };
1799}
1800export {
1801  getAudioContext as g,
1802  parseMidiInstruments as p,
1803  synth as s
1804};

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.