1import { _ as __vitePreload } from "./vendor-react-three-MDLA5jog.js"; 2import { r as reactExports } from "./vendor-react-Bay9Y7Lb.js"; 3import { u as useMidiKeyboard } from "./useMidiKeyboard-DlyEIN8T.js"; 4import { W as WasmSynthAdapter } from "./wasm-synth-adapter-BrqgBX1y.js"; 5const CONFIG = { 6 keyRangeStart: 21, 7 // A0 8 keyRangeEnd: 108, 9 // C8 10 lookAheadTime: 2.5, 11 // Seconds 12 visualOffset: 0.1 13 // Seconds 14}; 15const usePcKeyboard = ({ 16 playNote, 17 stopNote, 18 baseOctaveRef, 19 activeKeysRef, 20 enabled = true, 21 onOctaveChange 22}) => { 23 const playNoteRef = reactExports.useRef(playNote); 24 const stopNoteRef = reactExports.useRef(stopNote); 25 const onOctaveChangeRef = reactExports.useRef(onOctaveChange); 26 reactExports.useEffect(() => { 27 playNoteRef.current = playNote; 28 stopNoteRef.current = stopNote; 29 onOctaveChangeRef.current = onOctaveChange; 30 }); 31 reactExports.useEffect(() => { 32 if (!enabled) return; 33 const keyMapping = { 34 // Row 1 (lower octave) 35 "KeyZ": 0, 36 "KeyS": 1, 37 "KeyX": 2, 38 "KeyD": 3, 39 "KeyC": 4, 40 "KeyV": 5, 41 "KeyG": 6, 42 "KeyB": 7, 43 "KeyH": 8, 44 "KeyN": 9, 45 "KeyJ": 10, 46 "KeyM": 11, 47 // Row 2 (upper octave) 48 "KeyQ": 12, 49 "Digit2": 13, 50 "KeyW": 14, 51 "Digit3": 15, 52 "KeyE": 16, 53 "KeyR": 17, 54 "Digit5": 18, 55 "KeyT": 19, 56 "Digit6": 20, 57 "KeyY": 21, 58 "Digit7": 22, 59 "KeyU": 23, 60 // Row 3 (extended) 61 "KeyI": 24, 62 "Digit9": 25, 63 "KeyO": 26, 64 "Digit0": 27, 65 "KeyP": 28 66 }; 67 const handleKeyDown = (e) => { 68 if (e.target instanceof HTMLTextAreaElement) return; 69 if (e.target instanceof HTMLInputElement) { 70 const inputType = e.target.type; 71 if (inputType !== "range" && inputType !== "checkbox" && inputType !== "radio" && inputType !== "button") return; 72 } 73 if (e.target instanceof HTMLSelectElement) { 74 e.target.blur(); 75 } 76 if (e.repeat) return; 77 if (e.code === "ShiftLeft") { 78 const newOctave = Math.max(0, baseOctaveRef.current - 1); 79 baseOctaveRef.current = newOctave; 80 if (onOctaveChangeRef.current) onOctaveChangeRef.current(newOctave); 81 return; 82 } 83 if (e.code === "ShiftRight") { 84 const newOctave = Math.min(8, baseOctaveRef.current + 1); 85 baseOctaveRef.current = newOctave; 86 if (onOctaveChangeRef.current) onOctaveChangeRef.current(newOctave); 87 return; 88 } 89 const offset = keyMapping[e.code]; 90 if (offset !== void 0) { 91 const keyId = e.code; 92 const midi = baseOctaveRef.current * 12 + offset; 93 if (midi >= CONFIG.keyRangeStart && midi <= CONFIG.keyRangeEnd) { 94 activeKeysRef.current.set(keyId, midi); 95 if (playNoteRef.current) playNoteRef.current(midi); 96 } 97 } 98 }; 99 const handleKeyUp = (e) => { 100 const keyId = e.code; 101 let midi = activeKeysRef.current.get(keyId); 102 if (midi !== void 0) { 103 activeKeysRef.current.delete(keyId); 104 if (stopNoteRef.current) stopNoteRef.current(midi); 105 } 106 }; 107 window.addEventListener("keydown", handleKeyDown); 108 window.addEventListener("keyup", handleKeyUp); 109 const currentActiveKeys = activeKeysRef.current; 110 return () => { 111 window.removeEventListener("keydown", handleKeyDown); 112 window.removeEventListener("keyup", handleKeyUp); 113 currentActiveKeys.forEach((midi) => { 114 if (stopNoteRef.current) stopNoteRef.current(midi); 115 }); 116 currentActiveKeys.clear(); 117 }; 118 }, [enabled, baseOctaveRef, activeKeysRef]); 119}; 120const useKeyboardInput = (props) => { 121 usePcKeyboard(props); 122 const midi = useMidiKeyboard({ 123 playNote: props.playNote, 124 stopNote: props.stopNote, 125 enabled: props.enabled 126 }); 127 return midi; 128}; 129const isBlackKey = (midi) => { 130 const n = midi % 12; 131 return n === 1 || n === 3 || n === 6 || n === 8 || n === 10; 132}; 133const WHITE_KEY_INDEX_TABLE = new Int32Array(128); 134let whiteKeyCounter = 0; 135for (let i = 0; i < 128; i++) { 136 WHITE_KEY_INDEX_TABLE[i] = whiteKeyCounter; 137 const n = i % 12; 138 const isBlack = n === 1 || n === 3 || n === 6 || n === 8 || n === 10; 139 if (!isBlack) { 140 whiteKeyCounter++; 141 } 142} 143const getKeyGeometry = (midi, whiteKeyWidth, blackKeyWidth) => { 144 const keyIndexStart = WHITE_KEY_INDEX_TABLE[CONFIG.keyRangeStart]; 145 const whiteKeyIndex = WHITE_KEY_INDEX_TABLE[midi] - keyIndexStart; 146 if (isBlackKey(midi)) { 147 return { 148 x: whiteKeyIndex * whiteKeyWidth - blackKeyWidth / 2, 149 w: blackKeyWidth, 150 isBlack: true 151 }; 152 } else { 153 return { 154 x: whiteKeyIndex * whiteKeyWidth, 155 w: whiteKeyWidth, 156 isBlack: false 157 }; 158 } 159}; 160const getSoundfontName = (program) => { 161 const names = [ 162 "acoustic_grand_piano", 163 "bright_acoustic_piano", 164 "electric_grand_piano", 165 "honkytonk_piano", 166 "electric_piano_1", 167 "electric_piano_2", 168 "harpsichord", 169 "clavinet", 170 "celesta", 171 "glockenspiel", 172 "music_box", 173 "vibraphone", 174 "marimba", 175 "xylophone", 176 "tubular_bells", 177 "dulcimer", 178 "drawbar_organ", 179 "percussive_organ", 180 "rock_organ", 181 "church_organ", 182 "reed_organ", 183 "accordion", 184 "harmonica", 185 "tango_accordion", 186 "acoustic_guitar_nylon", 187 "acoustic_guitar_steel", 188 "electric_guitar_jazz", 189 "electric_guitar_clean", 190 "electric_guitar_muted", 191 "overdriven_guitar", 192 "distortion_guitar", 193 "guitar_harmonics", 194 "acoustic_bass", 195 "electric_bass_finger", 196 "electric_bass_pick", 197 "fretless_bass", 198 "slap_bass_1", 199 "slap_bass_2", 200 "synth_bass_1", 201 "synth_bass_2", 202 "violin", 203 "viola", 204 "cello", 205 "contrabass", 206 "tremolo_strings", 207 "pizzicato_strings", 208 "orchestral_harp", 209 "timpani", 210 "string_ensemble_1", 211 "string_ensemble_2", 212 "synthstrings_1", 213 "synthstrings_2", 214 "choir_aahs", 215 "voice_oohs", 216 "synth_voice", 217 "orchestra_hit", 218 "trumpet", 219 "trombone", 220 "tuba", 221 "muted_trumpet", 222 "french_horn", 223 "brass_section", 224 "synthbrass_1", 225 "synthbrass_2",
226 "soprano_sax", 227 "alto_sax", 228 "tenor_sax", 229 "baritone_sax", 230 "oboe", 231 "english_horn", 232 "bassoon", 233 "clarinet", 234 "piccolo", 235 "flute", 236 "recorder", 237 "pan_flute", 238 "blown_bottle", 239 "shakuhachi", 240 "whistle", 241 "ocarina", 242 "lead_1_square", 243 "lead_2_sawtooth", 244 "lead_3_calliope", 245 "lead_4_chiff", 246 "lead_5_charang", 247 "lead_6_voice", 248 "lead_7_fifths", 249 "lead_8_bass__lead", 250 "pad_1_new_age", 251 "pad_2_warm", 252 "pad_3_polysynth", 253 "pad_4_choir", 254 "pad_5_bowed", 255 "pad_6_metallic", 256 "pad_7_halo", 257 "pad_8_sweep", 258 "fx_1_rain", 259 "fx_2_soundtrack", 260 "fx_3_crystal", 261 "fx_4_atmosphere", 262 "fx_5_brightness", 263 "fx_6_goblins", 264 "fx_7_echoes", 265 "fx_8_sci_fi", 266 "sitar", 267 "banjo", 268 "shamisen", 269 "koto", 270 "kalimba", 271 "bagpipe", 272 "fiddle", 273 "shanai", 274 "tinkle_bell", 275 "agogo", 276 "steel_drums", 277 "woodblock", 278 "taiko_drum", 279 "melodic_tom", 280 "synth_drum", 281 "reverse_cymbal", 282 "guitar_fret_noise", 283 "breath_noise", 284 "seashore", 285 "bird_tweet", 286 "telephone_ring", 287 "helicopter", 288 "applause", 289 "gunshot" 290 ]; 291 return names[program % 128] || "acoustic_grand_piano"; 292}; 293let globalAudioContext = null; 294const useSoundfontKeyboard = (selectedTrackIndex, processedInstruments, currentMelodyTrackIdx, soundfont = "MusyngKite", gain) => { 295 const audioContextRef = reactExports.useRef(null); 296 const wasmSynthRef = reactExports.useRef(null); 297 const instrumentRef = reactExports.useRef(null); 298 const destinationNodeRef = reactExports.useRef(null); 299 const [isSf2Mode, setIsSf2Mode] = reactExports.useState(false); 300 const [isLoading, setIsLoading] = reactExports.useState(false); 301 const [loadingProgress, setLoadingProgress] = reactExports.useState(0); 302 const activeNodesRef = reactExports.useRef(/* @__PURE__ */ new Map()); 303 const previewActiveNotesRef = reactExports.useRef(/* @__PURE__ */ new Set()); 304 const loadedSfRef = reactExports.useRef(""); 305 const loadedProgramRef = reactExports.useRef(-1); 306 const gainRef = reactExports.useRef(gain); 307 reactExports.useEffect(() => { 308 const loadInstrument = async () => { 309 setIsLoading(true); 310 setLoadingProgress(0); 311 if (!globalAudioContext) { 312 const AudioContextClass = window.AudioContext || window.webkitAudioContext; 313 const isMobile = typeof window !== "undefined" && (/Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || /Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 0 || "ontouchstart" in window); 314 globalAudioContext = new AudioContextClass(isMobile ? { latencyHint: "balanced" } : { latencyHint: "interactive" }); 315 } 316 audioContextRef.current = globalAudioContext; 317 let resolvedSoundfont = soundfont; 318 if (soundfont === "pre-rendered" || !soundfont) { 319 try { 320 const resp = await fetch("/api/settings/DEFAULT_LIVE_SOUNDFONT"); 321 if (resp.ok) { 322 const data = await resp.json(); 323 resolvedSoundfont = data.value || "MusyngKite"; 324 } else { 325 resolvedSoundfont = "MusyngKite"; 326 } 327 } catch { 328 resolvedSoundfont = "MusyngKite"; 329 } 330 } 331 resolvedSoundfont = resolvedSoundfont.replace(/"/g, ""); 332 let activeIdx = selectedTrackIndex; 333 if (currentMelodyTrackIdx !== void 0 && currentMelodyTrackIdx !== -1) { 334 activeIdx = currentMelodyTrackIdx; 335 } 336 let isPercussion = false; 337 if (activeIdx !== void 0 && activeIdx !== "master" && processedInstruments && processedInstruments[activeIdx]) { 338 const inst = processedInstruments[activeIdx]; 339 isPercussion = inst.midiChannel === 9; 340 } 341 let program = 0; 342 if (activeIdx !== void 0 && activeIdx !== "master" && processedInstruments && processedInstruments[activeIdx]) { 343 const inst = processedInstruments[activeIdx]; 344 program = isPercussion ? 128 : inst.program || 0; 345 } 346 const isHsf3Bank = resolvedSoundfont.toLowerCase().endsWith("_hsf3bank") || resolvedSoundfont.toLowerCase().endsWith("_sf3bank"); 347 let isSF2 = resolvedSoundfont.toLowerCase().endsWith(".sf2") || resolvedSoundfont.toLowerCase().endsWith(".sf3") || isHsf3Bank; 348 if (isSF2 && wasmSynthRef.current && loadedSfRef.current === resolvedSoundfont) { 349 if (!isHsf3Bank || loadedProgramRef.current === program) { 350 const targetChannel = isPercussion ? 9 : 0; 351 wasmSynthRef.current.setProgram(targetChannel, program, isPercussion ? 128 : 0); 352 loadedProgramRef.current = program; 353 setIsLoading(false);
354 console.log("[SharedKeyboard] loadInstrument DONE (setIsLoading=false)"); 355 return; 356 } 357 } 358 if (isSF2 && typeof AudioWorkletNode === "undefined") { 359 console.warn(`[SharedKeyboard] AudioWorkletNode is undefined (non-secure context). Falling back from direct SF2 to JS soundfont.`); 360 resolvedSoundfont = resolvedSoundfont.replace(/\.h?sf[23]$/i, ""); 361 isSF2 = false; 362 } 363 setIsSf2Mode(isSF2); 364 if (wasmSynthRef.current) { 365 wasmSynthRef.current.destroy(); 366 wasmSynthRef.current = null; 367 } 368 instrumentRef.current = null; 369 if (destinationNodeRef.current) { 370 destinationNodeRef.current.disconnect(); 371 } 372 const destination = audioContextRef.current.createGain(); 373 destination.gain.value = gainRef.current ?? 1; 374 destination.connect(audioContextRef.current.destination); 375 destinationNodeRef.current = destination; 376 if (isSF2) { 377 try { 378 const adapter = new WasmSynthAdapter(audioContextRef.current, destination); 379 let sf2Url = `/storage/soundfonts/${resolvedSoundfont}`; 380 if (isHsf3Bank) { 381 const cleanLib = resolvedSoundfont.replace(/\/.*$/, ""); 382 const bank = isPercussion ? 128 : 0; 383 const progStr = program.toString().padStart(3, "0"); 384 sf2Url = `/storage/soundfonts/${cleanLib}/${bank}_${progStr}.hsf3`; 385 } 386 await adapter.load(sf2Url, setLoadingProgress); 387 const targetChannel = isPercussion ? 9 : 0; 388 adapter.setProgram(targetChannel, program, isPercussion ? 128 : 0); 389 adapter.setGain(gainRef.current ?? 1); 390 setLoadingProgress(1); 391 wasmSynthRef.current = adapter; 392 loadedSfRef.current = resolvedSoundfont; 393 loadedProgramRef.current = program; 394 } catch (err) { 395 console.warn("[SharedKeyboard] Failed to load WasmSynth for SF2, falling back to JS folder mode:", err); 396 resolvedSoundfont = resolvedSoundfont.replace(/\.h?sf[23]$/i, ""); 397 isSF2 = false; 398 setIsSf2Mode(false); 399 } 400 } 401 if (!isSF2) { 402 try { 403 const { loadSoundfontData, decodeSoundfontNotes, createBufferPlayer } = await __vitePreload(async () => { 404 const { loadSoundfontData: loadSoundfontData2, decodeSoundfontNotes: decodeSoundfontNotes2, createBufferPlayer: createBufferPlayer2 } = await import("./soundfont-utils-ieqHyFXd.js"); 405 return { loadSoundfontData: loadSoundfontData2, decodeSoundfontNotes: decodeSoundfontNotes2, createBufferPlayer: createBufferPlayer2 }; 406 }, true ? [] : void 0); 407 const baseUrl = `/storage/soundfonts/${resolvedSoundfont}/`; 408 const sfUrl = `${baseUrl}${program}-opus.js`; 409 const fallbackUrl = `${baseUrl}0-opus.js`; 410 let noteData; 411 try { 412 noteData = await loadSoundfontData(sfUrl); 413 } catch { 414 console.log(`[SharedKeyboard] Primary load failed for program ${program}, falling back to 0-opus`); 415 noteData = await loadSoundfontData(fallbackUrl); 416 } 417 const buffers = await decodeSoundfontNotes(audioContextRef.current, noteData); 418 instrumentRef.current = createBufferPlayer(audioContextRef.current, buffers, destination); 419 loadedSfRef.current = resolvedSoundfont; 420 loadedProgramRef.current = program; 421 } catch (err) { 422 console.error(`[SharedKeyboard] Total failure to load soundfont for program ${program}`, err); 423 } 424 } 425 setIsLoading(false); 426 }; 427 loadInstrument(); 428 }, [selectedTrackIndex, processedInstruments, currentMelodyTrackIdx, soundfont]); 429 reactExports.useEffect(() => { 430 gainRef.current = gain; 431 if (destinationNodeRef.current) { 432 destinationNodeRef.current.gain.value = gain ?? 1; 433 } 434 if (wasmSynthRef.current) { 435 wasmSynthRef.current.setGain(gain ?? 1); 436 } 437 }, [gain]); 438 const playNote = reactExports.useCallback((midi, overrideChannel, velocity = 100) => { 439 if (audioContextRef.current && audioContextRef.current.state === "suspended") { 440 audioContextRef.current.resume().catch(() => { 441 }); 442 } 443 previewActiveNotesRef.current.add(midi); 444 const channel = overrideChannel !== void 0 ? overrideChannel : 0; 445 if (wasmSynthRef.current) { 446 wasmSynthRef.current.noteOn(channel, midi, velocity, audioContextRef.current.currentTime); 447 } else if (channel === 9) {
448 try { 449 const ctx = audioContextRef.current; 450 if (ctx) { 451 const carrier = ctx.createOscillator(); 452 const modulator = ctx.createOscillator(); 453 const modGain = ctx.createGain(); 454 const carrierGain = ctx.createGain(); 455 carrier.connect(carrierGain); 456 carrierGain.connect(ctx.destination); 457 modulator.connect(modGain); 458 modGain.connect(carrier.frequency); 459 const freq = 440 * Math.pow(2, (midi - 69) / 12); 460 carrier.frequency.setValueAtTime(freq, ctx.currentTime); 461 modulator.frequency.setValueAtTime(freq * 5.25, ctx.currentTime); 462 modGain.gain.setValueAtTime(30 * freq * 5.25, ctx.currentTime); 463 modGain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08); 464 carrierGain.gain.setValueAtTime(0.33, ctx.currentTime); 465 carrierGain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15); 466 carrier.type = "triangle"; 467 modulator.type = "sawtooth"; 468 carrier.start(ctx.currentTime); 469 modulator.start(ctx.currentTime); 470 carrier.stop(ctx.currentTime + 0.2); 471 modulator.stop(ctx.currentTime + 0.2); 472 } 473 } catch (err) { 474 console.error("Failed to play fallback percussion sound:", err); 475 } 476 } else if (instrumentRef.current) { 477 const node = instrumentRef.current.play(String(midi), audioContextRef.current.currentTime, { gain: velocity / 127 }); 478 if (node) { 479 activeNodesRef.current.set(midi, node); 480 } 481 } 482 }, [selectedTrackIndex, currentMelodyTrackIdx]); 483 const stopNote = reactExports.useCallback((midi, overrideChannel) => { 484 previewActiveNotesRef.current.delete(midi); 485 const channel = overrideChannel !== void 0 ? overrideChannel : 0; 486 if (wasmSynthRef.current) { 487 wasmSynthRef.current.noteOff(channel, midi, audioContextRef.current.currentTime); 488 } else { 489 const node = activeNodesRef.current.get(midi); 490 if (node && node.stop) node.stop(); 491 activeNodesRef.current.delete(midi); 492 } 493 }, []); 494 const stopAllNotes = reactExports.useCallback(() => { 495 previewActiveNotesRef.current.clear(); 496 if (wasmSynthRef.current) { 497 wasmSynthRef.current.stopAll(); 498 } else { 499 activeNodesRef.current.forEach((node) => { 500 if (node && node.stop) node.stop(); 501 }); 502 activeNodesRef.current.clear(); 503 } 504 }, []); 505 return { 506 audioContextRef, 507 playNote, 508 stopNote, 509 stopAllNotes, 510 previewActiveNotesRef, 511 isLoading 512 }; 513}; 514const getMidiFromKeyboardClick = (x, y, width, height) => { 515 const whiteKeyCount = 52; 516 const keyWidth = width / whiteKeyCount; 517 const blackKeyWidth = keyWidth * 0.65; 518 const blackKeyHeight = height * 0.65; 519 let whiteKeyIdx = 0; 520 for (let i = CONFIG.keyRangeStart; i <= CONFIG.keyRangeEnd; i++) { 521 if (!isBlackKey(i)) { 522 whiteKeyIdx++; 523 continue; 524 } 525 const bx = whiteKeyIdx * keyWidth - blackKeyWidth / 2; 526 if (x >= bx && x <= bx + blackKeyWidth && y <= blackKeyHeight) { 527 return i; 528 } 529 } 530 whiteKeyIdx = 0; 531 for (let i = CONFIG.keyRangeStart; i <= CONFIG.keyRangeEnd; i++) { 532 if (!isBlackKey(i)) { 533 const wx = whiteKeyIdx * keyWidth; 534 if (x >= wx && x <= wx + keyWidth) { 535 return i; 536 } 537 whiteKeyIdx++; 538 } 539 } 540 return null; 541}; 542const getPcKeyLabel = (midi, baseOctave = 4) => { 543 const upperStart = baseOctave * 12; 544 const offset = midi - upperStart; 545 const offsetLabels = { 546 0: "Z", 547 1: "S", 548 2: "X", 549 3: "D", 550 4: "C", 551 5: "V", 552 6: "G", 553 7: "B", 554 8: "H", 555 9: "N", 556 10: "J", 557 11: "M", 558 12: "Q", 559 13: "2", 560 14: "W", 561 15: "3", 562 16: "E", 563 17: "R", 564 18: "5", 565 19: "T", 566 20: "6", 567 21: "Y", 568 22: "7", 569 23: "U", 570 24: "I", 571 25: "9", 572 26: "O", 573 27: "0", 574 28: "P" 575 }; 576 return offsetLabels[offset] || null; 577}; 578const renderSharedKeyboard = (ctx, width, height, keyHeight, activeNotes, labelMode = "off", baseOctave = 4, targetNotes, micNotes) => { 579 const whiteKeyCount = 52; 580 const keyWidth = width / whiteKeyCount; 581 const blackKeyWidth = keyWidth * 0.65; 582 const blackKeyHeight = keyHeight * 0.65; 583 const keyTop = height - keyHeight; 584 const getLabel = (midi) => { 585 if (labelMode === "off") return null; 586 if (labelMode === "notes") { 587 const notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 588 const noteName = notes[midi % 12]; 589 const octave = Math.floor(midi / 12) - 1; 590 return `${noteName}${octave}`; 591 } 592 return getPcKeyLabel(midi, baseOctave); 593 }; 594 const whiteActiveGrad = ctx.createLinearGradient(0, keyTop, 0, height); 595 whiteActiveGrad.addColorStop(0, "#3b82f6"); 596 whiteActiveGrad.addColorStop(1, "#ffffff"); 597 const whiteInactiveGrad = ctx.createLinearGradient(0, keyTop, 0, height); 598 whiteInactiveGrad.addColorStop(0, "#111"); 599 whiteInactiveGrad.addColorStop(1, "#eee"); 600 const whiteTargetGrad = ctx.createLinearGradient(0, keyTop, 0, height); 601 whiteTargetGrad.addColorStop(0, "#f59e0b"); 602 whiteTargetGrad.addColorStop(1, "#fef3c7"); 603 const whiteMicGrad = ctx.createLinearGradient(0, keyTop, 0, height); 604 whiteMicGrad.addColorStop(0, "#10b981"); 605 whiteMicGrad.addColorStop(1, "#a7f3d0"); 606 const blackActiveGrad = ctx.createLinearGradient(0, keyTop, 0, keyTop + blackKeyHeight); 607 blackActiveGrad.addColorStop(0, "#3b82f6"); 608 blackActiveGrad.addColorStop(1, "#fff"); 609 const blackInactiveGrad = ctx.createLinearGradient(0, keyTop, 0, keyTop + blackKeyHeight); 610 blackInactiveGrad.addColorStop(0, "#000"); 611 blackInactiveGrad.addColorStop(1, "#111"); 612 const blackTargetGrad = ctx.createLinearGradient(0, keyTop, 0, keyTop + blackKeyHeight); 613 blackTargetGrad.addColorStop(0, "#d97706"); 614 blackTargetGrad.addColorStop(1, "#f59e0b"); 615 const blackMicGrad = ctx.createLinearGradient(0, keyTop, 0, keyTop + blackKeyHeight); 616 blackMicGrad.addColorStop(0, "#059669"); 617 blackMicGrad.addColorStop(1, "#10b981"); 618 let whiteKeyIdx = 0; 619 for (let i = CONFIG.keyRangeStart; i <= CONFIG.keyRangeEnd; i++) { 620 if (!isBlackKey(i)) { 621 const x = whiteKeyIdx * keyWidth; 622 const isActive = activeNotes.has(i); 623 const isTarget = targetNotes == null ? void 0 : targetNotes.has(i); 624 const isMic = micNotes == null ? void 0 : micNotes.has(i); 625 ctx.fillStyle = isMic ? whiteMicGrad : isActive ? whiteActiveGrad : isTarget ? whiteTargetGrad : whiteInactiveGrad;
626 ctx.fillRect(x + 1, keyTop, keyWidth - 2, keyHeight); 627 if (labelMode !== "off") { 628 const label = getLabel(i); 629 const inRange = label !== null; 630 ctx.fillStyle = inRange ? "#000" : "#aaa"; 631 ctx.font = "bold 10px monospace"; 632 ctx.textAlign = "center"; 633 ctx.textBaseline = "middle"; 634 const blackKeyBottom = keyTop + blackKeyHeight; 635 const whiteKeyBottom = height; 636 const midY = (blackKeyBottom + whiteKeyBottom) / 2; 637 if (label) { 638 ctx.fillText(label, x + keyWidth / 2, midY); 639 } 640 } 641 whiteKeyIdx++; 642 } 643 } 644 whiteKeyIdx = 0; 645 for (let i = CONFIG.keyRangeStart; i <= CONFIG.keyRangeEnd; i++) { 646 if (!isBlackKey(i)) { 647 whiteKeyIdx++; 648 continue; 649 } 650 const x = whiteKeyIdx * keyWidth - blackKeyWidth / 2; 651 const isActive = activeNotes.has(i); 652 const isTarget = targetNotes == null ? void 0 : targetNotes.has(i); 653 const isMic = micNotes == null ? void 0 : micNotes.has(i); 654 ctx.fillStyle = isMic ? blackMicGrad : isActive ? blackActiveGrad : isTarget ? blackTargetGrad : blackInactiveGrad; 655 ctx.fillRect(x, keyTop, blackKeyWidth, blackKeyHeight); 656 if (labelMode !== "off") { 657 const label = getLabel(i); 658 const inRange = label !== null; 659 ctx.fillStyle = inRange ? "#fff" : "#555"; 660 ctx.font = "bold 10px monospace"; 661 ctx.textAlign = "center"; 662 if (label) { 663 ctx.lineWidth = 2; 664 ctx.lineJoin = "round"; 665 ctx.miterLimit = 2; 666 ctx.strokeStyle = "#000"; 667 ctx.strokeText(label, x + blackKeyWidth / 2, keyTop + blackKeyHeight - 10); 668 ctx.fillText(label, x + blackKeyWidth / 2, keyTop + blackKeyHeight - 10); 669 } 670 } 671 } 672 ctx.strokeStyle = "#3b82f6"; 673 ctx.lineWidth = 2; 674 ctx.beginPath(); 675 ctx.moveTo(0, keyTop); 676 ctx.lineTo(width, keyTop); 677 ctx.stroke(); 678 if (labelMode !== "off") { 679 ctx.save(); 680 ctx.fillStyle = "rgba(15, 23, 42, 0.85)"; 681 ctx.strokeStyle = "rgba(59, 130, 246, 0.4)"; 682 ctx.lineWidth = 1; 683 const labelText = `â LShift [${baseOctave}] RShift â`; 684 ctx.font = "bold 10px monospace"; 685 const textWidth = ctx.measureText(labelText).width; 686 const pillWidth = textWidth + 16; 687 const pillHeight = 18; 688 const pillX = 10; 689 const pillY = keyTop + 5; 690 ctx.beginPath(); 691 if (ctx.roundRect) { 692 ctx.roundRect(pillX, pillY, pillWidth, pillHeight, 4); 693 } else { 694 ctx.rect(pillX, pillY, pillWidth, pillHeight); 695 } 696 ctx.fill(); 697 ctx.stroke(); 698 ctx.fillStyle = "#f8fafc"; 699 ctx.textAlign = "left"; 700 ctx.textBaseline = "middle"; 701 ctx.fillText(labelText, pillX + 8, pillY + pillHeight / 2); 702 ctx.restore(); 703 } 704}; 705export { 706 CONFIG as C, 707 useKeyboardInput as a, 708 getMidiFromKeyboardClick as b, 709 getPcKeyLabel as c, 710 getSoundfontName as d, 711 getKeyGeometry as g, 712 renderSharedKeyboard as r, 713 useSoundfontKeyboard as u 714};
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.