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.