PageSourceSearch

https://www.hamienet.com/assets/SharedKeyboard-B3r3Jaho.js

js hamienet.com collected 2026-09-24 19:00:06 UTC 24,622 bytes, 714 lines download raw bytes

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.