PageSourceSearch

https://www.hamienet.com/assets/MelodySearchPage-DWBpeuIZ.js

js hamienet.com collected 2026-10-02 04:59:46 UTC 30,173 bytes, 528 lines download raw bytes

1import { r as reactExports, j as jsxRuntimeExports, R as React } from "./vendor-react-Bay9Y7Lb.js";
2import { a as api } from "./index-w5lXls15.js";
3import { V as VideoCard } from "./main-aQ5jZgYp.js";
4import { u as useShouldShowAd, A as AdSlot } from "./AdSlot-BEBlpA_I.js";
5import { u as useTranslation } from "./vendor-i18n-BC7ZMXWQ.js";
6import { bg as Music, bS as CircleStop, bT as Mic, b_ as Pause, bV as Play, b5 as Trash2, cG as Keyboard, bW as RefreshCcw, bf as Search, ba as Info, bB as ListMusic } from "./vendor-D0P4XgNj.js";
7import "./vendor-react-three-BCww3P1x.js";
8import "./utils-CEVb7nN1.js";
9import "./synth-Fr-K52Ox.js";
10import "./wasm-synth-adapter-DpNGIIyM.js";
11import "./vendor-spessasynth-DH8n6o_V.js";
12import "./vendor-audio-D-wTggbx.js";
13import "./Waveform-BgnMyg5E.js";
14import "./vendor-framer-B-4FZk1Z.js";
15import "./labTools-HX5HYAPB.js";
16import "./UserAvatar-BQN4jlY-.js";
17const MelodySearchPage = ({ onSelectTrack, onToggleWatchLater, watchLaterSet, onAddToQueue, onAddToPlaylist }) => {
18  const { t } = useTranslation();
19  const shouldShowExploreAd = useShouldShowAd("exploreList");
20  const [notes, setNotes] = reactExports.useState([]);
21  const [isListening, setIsListening] = reactExports.useState(false);
22  const [isPlayingSequence, setIsPlayingSequence] = reactExports.useState(false);
23  const [clef, setClef] = reactExports.useState("treble");
24  const [searchMode, setSearchMode] = reactExports.useState("easy");
25  const [results, setResults] = reactExports.useState([]);
26  const [isSearching, setIsSearching] = reactExports.useState(false);
27  const [livePitch, setLivePitch] = reactExports.useState(null);
28  const [midiAccess, setMidiAccess] = reactExports.useState(null);
29  const [currentPlaybackIndex, setCurrentPlaybackIndex] = reactExports.useState(null);
30  const stabilityRef = reactExports.useRef({ note: -1, count: 0 });
31  const audioContextRef = reactExports.useRef(null);
32  const analyserRef = reactExports.useRef(null);
33  const micStreamRef = reactExports.useRef(null);
34  const requestRef = reactExports.useRef(null);
35  const c4KeyRef = reactExports.useRef(null);
36  const playbackTimeoutIdsRef = reactExports.useRef([]);
37  const [visibleResultsCount, setVisibleResultsCount] = reactExports.useState(12);
38  const sentinelRef = reactExports.useRef(null);
39  reactExports.useEffect(() => {
40    setVisibleResultsCount(12);
41  }, [results]);
42  reactExports.useEffect(() => {
43    const currentSentinel = sentinelRef.current;
44    if (!currentSentinel) return;
45    const observer = new IntersectionObserver((entries) => {
46      if (entries[0].isIntersecting) {
47        setVisibleResultsCount((prev) => Math.min(prev + 12, results.length));
48      }
49    }, { threshold: 0.1 });
50    observer.observe(currentSentinel);
51    return () => observer.unobserve(currentSentinel);
52  }, [results.length, visibleResultsCount]);
53  reactExports.useEffect(() => {
54    const timer = setTimeout(() => {
55      if (c4KeyRef.current) {
56        c4KeyRef.current.scrollIntoView({ behavior: "auto", block: "nearest", inline: "center" });
57      }
58    }, 150);
59    return () => clearTimeout(timer);
60  }, []);
61  reactExports.useEffect(() => {
62    if (navigator.requestMIDIAccess) {
63      navigator.requestMIDIAccess().then((access) => {
64        setMidiAccess(access);
65        const inputs = access.inputs.values();
66        for (let input = inputs.next(); input && !input.done; input = inputs.next()) {
67          input.value.onmidimessage = (message) => {
68            const [status, note, velocity] = message.data;
69            if (status === 144 && velocity > 0) {
70              handleNoteInput(note);
71            }
72          };
73        }
74      }).catch((err) => console.error("MIDI Access Denied", err));
75    }
76  }, []);
77  const detectPitch = reactExports.useCallback(() => {
78    if (!analyserRef.current || !audioContextRef.current) return;
79    const buffer = new Float32Array(analyserRef.current.fftSize);
80    analyserRef.current.getFloatTimeDomainData(buffer);
81    let bestOffset = -1;
82    let bestCorrelation = 0;
83    let rms = 0;
84    for (let i = 0; i < buffer.length; i++) {
85      rms += buffer[i] * buffer[i];
86    }
87    rms = Math.sqrt(rms / buffer.length);
88    if (rms > 0.05) {
89      for (let offset = 0; offset < buffer.length / 2; offset++) {
90        let correlation = 0;
91        for (let i = 0; i < buffer.length / 2; i++) {
92          correlation += Math.abs(buffer[i] - buffer[i + offset]);
93        }
94        correlation = 1 - correlation / (buffer.length / 2);
95        if (correlation > 0.9 && correlation > bestCorrelation) {
96          bestCorrelation = correlation;
97          bestOffset = offset;
98        }
99      }
100      if (bestOffset !== -1) {
101        const sampleRate = audioContextRef.current.sampleRate;
102        const frequency = sampleRate / bestOffset;
103        const midi = Math.round(69 + 12 * Math.log2(frequency / 440));
104        setLivePitch(midi);
105        if (stabilityRef.current.note === midi) {
106          stabilityRef.current.count++;
107        } else {
108          stabilityRef.current.note = midi;
109          stabilityRef.current.count = 1;
110        }
111        if (stabilityRef.current.count === 3) {
112          setNotes((prev) => {
113            const last = prev[prev.length - 1];
114            if (last !== midi) {
115              playNote(midi);
116              return [...prev, midi];
117            }
118            return prev;
119          });
120        }
121      }
122    } else {
123      setLivePitch(null);
124      stabilityRef.current = { note: -1, count: 0 };
125    }
126    requestRef.current = requestAnimationFrame(detectPitch);
127  }, []);
128  const startListening = async () => {
129    try {
130      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
131      micStreamRef.current = stream;
132      audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
133      const source = audioContextRef.current.createMediaStreamSource(stream);
134      analyserRef.current = audioContextRef.current.createAnalyser();
135      analyserRef.current.fftSize = 2048;
136      source.connect(analyserRef.current);
137      setIsListening(true);
138      detectPitch();
139    } catch (e) {
140      alert("Could not access microphone.");
141    }
142  };
143  const stopListening = () => {
144    setIsListening(false);
145    setLivePitch(null);
146    if (micStreamRef.current) {
147      micStreamRef.current.getTracks().forEach((track) => track.stop());
148    }
149    if (requestRef.current) cancelAnimationFrame(requestRef.current);
150  };
151  const handleNoteInput = (midi) => {
152    playNote(midi);
153    setNotes((prev) => [...prev, midi]);
154  };
155  const playNote = (midi) => {
156    const AudioContextClass = window.AudioContext || window.webkitAudioContext;
157    if (!AudioContextClass) return;
158    if (!audioContextRef.current) {
159      audioContextRef.current = new AudioContextClass();
160    }
161    const ctx = audioContextRef.current;
162    if (ctx.state === "suspended") {
163      ctx.resume().catch(() => {
164      });
165    }
166    const t2 = ctx.currentTime;
167    const freq = 440 * Math.pow(2, (midi - 69) / 12);
168    const masterGain = ctx.createGain();
169    masterGain.gain.setValueAtTime(0, t2);
170    masterGain.gain.linearRampToValueAtTime(0.3, t2 + 0.01);
171    masterGain.gain.exponentialRampToValueAtTime(0.01, t2 + 1.5);
172    masterGain.connect(ctx.destination);
173    const osc = ctx.createOscillator();
174    osc.type = "sine";
175    osc.frequency.value = freq;
176    osc.connect(masterGain);
177    osc.start(t2);
178    osc.stop(t2 + 1.5);
179  };
180  const stopSequence = reactExports.useCallback(() => {
181    playbackTimeoutIdsRef.current.forEach((id) => window.clearTimeout(id));
182    playbackTimeoutIdsRef.current = [];
183    setIsPlayingSequence(false);
184    setCurrentPlaybackIndex(null);
185  }, []);
186  const playSequence = reactExports.useCallback(() => {
187    if (notes.length === 0) return;
188    stopSequence();
189    setIsPlayingSequence(true);
190    const noteDuration = 400;
191    notes.forEach((note, index) => {
192      const id = window.setTimeout(() => {
193        setCurrentPlaybackIndex(index);
194        playNote(note);
195        if (index === notes.length - 1) {
196          const endId = window.setTimeout(() => {
197            setIsPlayingSequence(false);
198            setCurrentPlaybackIndex(null);
199          }, noteDuration);
200          playbackTimeoutIdsRef.current.push(endId);
201        }
202      }, index * noteDuration);
203      playbackTimeoutIdsRef.current.push(id);
204    });
205  }, [notes, stopSequence]);
206  reactExports.useEffect(() => {
207    return () => {
208      playbackTimeoutIdsRef.current.forEach((id) => window.clearTimeout(id));
209    };
210  }, []);
211  reactExports.useEffect(() => {
212    stopSequence();
213  }, [notes.length, stopSequence]);
214  const handleSearch = async () => {
215    if (notes.length < 2) return;
216    setIsSearching(true);
217    try {
218      const data = await api.searchMelody(notes, searchMode);
219      setResults(data);
220    } catch (e) {
221      console.error(e);
222    } finally {
223      setIsSearching(false);
224    }
225  };
226  const getDiatonicStep = (midi) => {
227    const pc = midi % 12;
228    const octave = Math.floor(midi / 12) - 1;
229    const stepMap = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];
230    return octave * 7 + stepMap[pc];
231  };
232  const getMidiFromDiatonicStep = (step) => {
233    const octave = Math.floor(step / 7) + 1;
234    const degree = step % 7;
235    const scaleMap = [0, 2, 4, 5, 7, 9, 11];
236    return octave * 12 + scaleMap[degree];
237  };
238  const renderStaff = () => {
239    const width = 800;
240    const height = 180;
241    const bottomLineY = 120;
242    const stepSpacing = 8;
243    let refStep = 30;
244    if (clef === "bass") refStep = 18;
245    if (clef === "alto") refStep = 24;
246    const getYForNote = (midi) => {
247      const step = getDiatonicStep(midi);
248      return bottomLineY - (step - refStep) * stepSpacing;
249    };
250    const clefSymbol = clef === "treble" ? "𝄞" : clef === "bass" ? "𝄢" : "𝄡";
251    const clefOffset = clef === "treble" ? 10 : clef === "bass" ? -10 : 0;
252    return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative bg-white dark:bg-zinc-950 rounded-xl border border-zinc-200 dark:border-zinc-800 shadow-inner overflow-hidden", children: [
253      /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-4 left-4 z-10 flex gap-1 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm rounded-lg p-1 border border-zinc-200 dark:border-zinc-800 shadow-sm", children: ["treble", "alto", "bass"].map((c) => /* @__PURE__ */ jsxRuntimeExports.jsx(
254        "button",
255        {
256          onClick: () => setClef(c),
257          className: `px-3 py-1 rounded-md text-xs font-bold transition-colors ${clef === c ? "bg-indigo-600 text-white" : "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"}`,
258          children: c.charAt(0).toUpperCase() + c.slice(1)
259        },
260        c
261      )) }),
262      /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "100%", height, viewBox: `0 0 ${width} ${height}`, className: "cursor-crosshair", onClick: (e) => {
263        const rect = e.currentTarget.getBoundingClientRect();
264        const clickY = (e.clientY - rect.top) * (height / rect.height);
265        const diffSteps = Math.round((bottomLineY - clickY) / stepSpacing);
266        const targetStep = refStep + diffSteps;
267        handleNoteInput(getMidiFromDiatonicStep(targetStep));
268      }, children: [
269        /* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "staffGrad", x1: "0%", y1: "0%", x2: "0%", y2: "100%", children: [
270          /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "transparent" }),
271          /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "50%", stopColor: "#f8fafc" }),
272          /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "transparent" })
273        ] }) }),
274        /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { width: "100%", height: "100%", fill: "url(#staffGrad)", opacity: "0.5" }),
275        [0, 2, 4, 6, 8].map((o) => /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "0", y1: bottomLineY - o * stepSpacing, x2: width, y2: bottomLineY - o * stepSpacing, stroke: "#cbd5e1", strokeWidth: "1" }, o)),
276        /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: "30", y: bottomLineY - 4 * stepSpacing + clefOffset, fontSize: "80", fontFamily: "serif", fill: "#1e293b", pointerEvents: "none", dominantBaseline: "middle", children: clefSymbol }),
277        livePitch && (() => {
278          const y = getYForNote(livePitch);
279          const step = getDiatonicStep(livePitch);
280          const ledgerLines = [];
281          if (step < refStep) {
282            for (let l = refStep - 2; l >= step; l -= 2) {
283              ledgerLines.push(bottomLineY - (l - refStep) * stepSpacing);
284            }
285          } else if (step > refStep + 8) {
286            for (let l = refStep + 10; l <= step; l += 2) {
287              ledgerLines.push(bottomLineY - (l - refStep) * stepSpacing);
288            }
289          }
290          return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "animate-pulse", children: [
291            ledgerLines.map((ly, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: 150 - 14, y1: ly, x2: 150 + 14, y2: ly, stroke: "#6366f1", strokeWidth: "1.5", opacity: "0.6" }, idx)),
292            /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "150", cy: y, r: "8", fill: "#6366f1", opacity: "0.6" }),
293            /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "150", y1: "0", x2: "150", y2: height, stroke: "#6366f1", strokeDasharray: "4 4", opacity: "0.3" })
294          ] });
295        })(),
296        notes.map((n, i) => {
297          const x = 120 + i * 40;
298          const y = getYForNote(n);
299          const isBlack = [1, 3, 6, 8, 10].includes(n % 12);
300          const step = getDiatonicStep(n);
301          const isCurrent = currentPlaybackIndex === i;
302          const ledgerLines = [];
303          if (step < refStep) {
304            for (let l = refStep - 2; l >= step; l -= 2) {
305              ledgerLines.push(bottomLineY - (l - refStep) * stepSpacing);
306            }
307          } else if (step > refStep + 8) {
308            for (let l = refStep + 10; l <= step; l += 2) {
309              ledgerLines.push(bottomLineY - (l - refStep) * stepSpacing);
310            }
311          }
312          return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "group cursor-pointer", onClick: (e) => {
313            e.stopPropagation();
314            setNotes((prev) => prev.filter((_, idx) => idx !== i));
315          }, children: [
316            ledgerLines.map((ly, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: x - 14, y1: ly, x2: x + 14, y2: ly, stroke: isCurrent ? "#6366f1" : "currentColor", strokeWidth: "1.5", className: isCurrent ? "" : "opacity-60 dark:opacity-80" }, idx)),
317            /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: x + (y > bottomLineY - 4 * stepSpacing ? 7 : -7), y1: y, x2: x + (y > bottomLineY - 4 * stepSpacing ? 7 : -7), y2: y > bottomLineY - 4 * stepSpacing ? y - 40 : y + 40, stroke: isCurrent ? "#6366f1" : "currentColor", strokeWidth: "2" }),
318            /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: x, cy: y, rx: "8", ry: "6", fill: isCurrent ? "#6366f1" : "currentColor", transform: `rotate(-20, ${x}, ${y})`, className: "group-hover:fill-red-500" }),
319            isBlack && /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: x - 20, y: y + 5, fontSize: "20", fontWeight: "bold", fill: isCurrent ? "#6366f1" : "currentColor", children: "♯" }),
320            /* @__PURE__ */ jsxRuntimeExports.jsx(
321              "text",
322              {
323                x,
324                y: y > bottomLineY - 4 * stepSpacing ? y + 18 : y - 18,
325                textAnchor: "middle",
326                fontSize: "10",
327                fontWeight: "bold",
328                fill: isCurrent ? "#6366f1" : "#4b5563",
329                className: "dark:fill-zinc-300 pointer-events-none select-none",
330                children: (() => {
331                  const pc = n % 12;
332                  const octave = Math.floor(n / 12) - 1;
333                  const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
334                  return `${names[pc]}${octave}`;
335                })()
336              }
337            )
338          ] }, i);
339        })
340      ] })
341    ] });
342  };
343  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full min-h-full bg-zinc-50 dark:bg-[#0f0f0f] p-4 md:p-6", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-6xl mx-auto flex flex-col gap-6", children: [
344    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-4 bg-white dark:bg-zinc-900 p-6 rounded-2xl shadow-sm border border-zinc-200 dark:border-zinc-800", children: [
345      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col md:flex-row md:items-center justify-between gap-4", children: [
346        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
347          /* @__PURE__ */ jsxRuntimeExports.jsxs("h1", { className: "text-2xl font-black text-zinc-900 dark:text-white flex items-center gap-2 flex-wrap", children: [
348            /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { className: "text-indigo-500" }),
349            /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("melodySearch.title", "Ultimate Melody Search") }),
350            /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-amber-500/10 text-amber-500 border border-amber-500/30", children: "beta" })
351          ] }),
352          /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500", children: t("melodySearch.subtitle", "Find tracks by humming, piano, or MIDI keyboard.") })
353        ] }),
354        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex bg-zinc-200 dark:bg-zinc-800 p-1 rounded-lg self-start md:self-auto", children: [
355          /* @__PURE__ */ jsxRuntimeExports.jsx(
356            "button",
357            {
358              onClick: () => setSearchMode("easy"),
359              className: `px-4 py-1.5 rounded-md text-sm font-bold transition-all ${searchMode === "easy" ? "bg-white dark:bg-zinc-700 text-indigo-600 shadow-sm" : "text-zinc-500"}`,
360              children: t("melodySearch.easy", "Easy (Parsons)")
361            }
362          ),
363          /* @__PURE__ */ jsxRuntimeExports.jsx(
364            "button",
365            {
366              onClick: () => setSearchMode("advanced"),
367              className: `px-4 py-1.5 rounded-md text-sm font-bold transition-all ${searchMode === "advanced" ? "bg-white dark:bg-zinc-700 text-indigo-600 shadow-sm" : "text-zinc-500"}`,
368              children: t("melodySearch.advanced", "Advanced (Exact)")
369            }
370          )
371        ] })
372      ] }),
373      renderStaff(),
374      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950 p-4 rounded-xl border border-zinc-200 dark:border-zinc-800", children: [
375        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4", children: [
376          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap gap-2", children: [
377            /* @__PURE__ */ jsxRuntimeExports.jsxs(
378              "button",
379              {
380                onClick: isListening ? stopListening : startListening,
381                className: `px-6 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-lg ${isListening ? "bg-red-500 text-white animate-pulse shadow-red-500/20" : "bg-indigo-600 text-white hover:bg-indigo-700 shadow-indigo-500/20"}`,
382                children: [
383                  isListening ? /* @__PURE__ */ jsxRuntimeExports.jsx(CircleStop, { size: 20 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 20 }),
384                  isListening ? t("melodySearch.recording", "Recording Pitch...") : t("melodySearch.startVoice", "Start Mic Input")
385                ]
386              }
387            ),
388            /* @__PURE__ */ jsxRuntimeExports.jsxs(
389              "button",
390              {
391                onClick: isPlayingSequence ? stopSequence : playSequence,
392                disabled: notes.length === 0,
393                className: `px-4 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all ${isPlayingSequence ? "bg-amber-500 text-white hover:bg-amber-600 shadow-lg shadow-amber-500/20" : "bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50"}`,
394                children: [
395                  isPlayingSequence ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 20 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 20 }),
396                  isPlayingSequence ? t("melodySearch.stopPlayback", "Stop") : t("melodySearch.playSequence", "Play")
397                ]
398              }
399            ),
400            /* @__PURE__ */ jsxRuntimeExports.jsx(
401              "button",
402              {
403                onClick: () => setNotes([]),
404                className: "px-4 py-2.5 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400 rounded-xl hover:bg-zinc-150 dark:hover:bg-zinc-700 transition-all",
405                children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 20 })
406              }
407            )
408          ] }),
409          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 text-sm font-medium text-zinc-500", children: [
410            /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [
411              /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 16 }),
412              midiAccess ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-emerald-500", children: "MIDI Connected" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "MIDI Support Ready" })
413            ] }),
414            /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-4 bg-zinc-300 dark:bg-zinc-700" }),
415            /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
416              notes.length,
417              " Notes Captured"
418            ] })
419          ] })
420        ] }),
421        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-start relative h-40 bg-zinc-100 dark:bg-zinc-800 rounded-xl overflow-x-auto overflow-y-hidden select-none border border-zinc-200 dark:border-zinc-800 custom-scrollbar p-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex relative h-full px-4", children: (() => {
422          const getNoteLabel = (midi) => {
423            const pc = midi % 12;
424            const octave = Math.floor(midi / 12) - 1;
425            const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
426            return `${names[pc]}${octave}`;
427          };
428          const startKey = 36;
429          const endKey = 83;
430          const allKeys = [];
431          for (let i = startKey; i <= endKey; i++) {
432            allKeys.push({ midi: i, isBlack: [1, 3, 6, 8, 10].includes(i % 12) });
433          }
434          const whiteKeys = allKeys.filter((k) => !k.isBlack);
435          return whiteKeys.map((k) => {
436            const isC4 = k.midi === 60;
437            const hasBlackNoteAfter = allKeys.find((bk) => bk.midi === k.midi + 1 && bk.isBlack);
438            const isWhiteActive = livePitch === k.midi;
439            const isBlackActive = livePitch === k.midi + 1;
440            return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative shrink-0", children: [
441              /* @__PURE__ */ jsxRuntimeExports.jsx(
442                "div",
443                {
444                  ref: isC4 ? c4KeyRef : null,
445                  onMouseDown: () => handleNoteInput(k.midi),
446                  className: `w-10 h-full border-r border-b border-zinc-300 dark:border-zinc-650 rounded-b-[4px] cursor-pointer shadow-sm flex flex-col justify-end pb-2 select-none transition-all ${isWhiteActive ? "bg-indigo-500 text-white" : isC4 ? "bg-indigo-50/80 dark:bg-indigo-900/20 hover:bg-indigo-100/80" : "bg-white hover:bg-zinc-50 dark:bg-zinc-900 dark:hover:bg-zinc-800"}`,
447                  children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[9px] text-center w-full pointer-events-none ${isWhiteActive ? "text-white" : isC4 ? "text-indigo-600 font-bold" : "text-zinc-400 dark:text-zinc-500"}`, children: getNoteLabel(k.midi) })
448                }
449              ),
450              hasBlackNoteAfter && /* @__PURE__ */ jsxRuntimeExports.jsx(
451                "div",
452                {
453                  onMouseDown: (e) => {
454                    e.stopPropagation();
455                    handleNoteInput(k.midi + 1);
456                  },
457                  className: `absolute z-10 w-6 h-24 rounded-b-[3px] top-0 -right-3 cursor-pointer shadow-md border-x border-b flex flex-col justify-end pb-1.5 select-none transition-all ${isBlackActive ? "bg-indigo-500 border-indigo-600" : "bg-black hover:bg-zinc-850 dark:hover:bg-zinc-900 border-zinc-800"}`,
458                  style: { boxShadow: "2px 2px 4px rgba(0,0,0,0.4)" },
459                  children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[7px] text-center w-full pointer-events-none font-medium ${isBlackActive ? "text-white" : "text-zinc-400"}`, children: getNoteLabel(k.midi + 1) })
460                }
461              )
462            ] }, k.midi);
463          });
464        })() }) })
465      ] }),
466      /* @__PURE__ */ jsxRuntimeExports.jsxs(
467        "button",
468        {
469          onClick: handleSearch,
470          disabled: notes.length < 2 || isSearching,
471          className: "w-full py-3.5 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-2xl font-black text-lg flex items-center justify-center gap-3 transition-all shadow-xl shadow-indigo-500/30",
472          children: [
473            isSearching ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCcw, { className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { size: 24 }),
474            t("melodySearch.searchNow", "Find Matching Melodies")
475          ]
476        }
477      ),
478      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-4 p-4 bg-amber-50 dark:bg-amber-900/10 border border-amber-100 dark:border-amber-900/30 rounded-xl", children: [
479        /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { className: "text-amber-600 shrink-0", size: 20 }),
480        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-amber-800 dark:text-amber-200", children: [
481          /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "How it works:" }),
482          " ",
483          searchMode === "easy" ? "Parsons Code matching looks for Up/Down/Same movement regardless of exact intervals. Good for humming!" : "Exact interval matching looks for the specific semitone gaps between your notes. Best for virtual piano or MIDI keyboards."
484        ] })
485      ] })
486    ] }),
487    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 p-6 rounded-2xl border border-zinc-200 dark:border-zinc-800 flex flex-col gap-4", children: [
488      /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-zinc-800 pb-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [
489        /* @__PURE__ */ jsxRuntimeExports.jsx(ListMusic, { className: "text-indigo-500" }),
490        " ",
491        t("melodySearch.results", "Search Results"),
492        results.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs font-normal text-zinc-500 ml-auto", children: [
493          results.length,
494          " found"
495        ] })
496      ] }) }),
497      results.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
498        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6", children: results.slice(0, visibleResultsCount).map((track, idx) => {
499          const showAd = (idx + 1) % 12 === 0;
500          return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
501            /* @__PURE__ */ jsxRuntimeExports.jsx(
502              VideoCard,
503              {
504                track,
505                onSelect: (t2) => onSelectTrack(t2, void 0, results),
506                onToggleWatchLater,
507                isWatchLater: watchLaterSet.has(track.id),
508                onAddToQueue,
509                onAddToPlaylist,
510                layout: "grid",
511                playlist: results
512              }
513            ),
514            showAd && shouldShowExploreAd && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "col-span-full", children: /* @__PURE__ */ jsxRuntimeExports.jsx(AdSlot, { type: "exploreList", className: "!my-2 min-h-[120px]" }) })
515          ] }, track.id);
516        }) }),
517        visibleResultsCount < results.length && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: sentinelRef, className: "h-16 flex items-center justify-center text-sm text-zinc-400 border-t border-zinc-100 dark:border-zinc-800 mt-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "animate-pulse", children: t("melodySearch.loadingMore", "Loading more results...") }) })
518      ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center text-center text-zinc-500 py-16", children: [
519        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-20 h-20 bg-zinc-100 dark:bg-zinc-800 rounded-full flex items-center justify-center mb-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { size: 40, className: "opacity-20" }) }),
520        /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-zinc-900 dark:text-white mb-2", children: t("melodySearch.noResultsYet", "Ready to Search") }),
521        /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm max-w-md", children: t("melodySearch.noResultsDesc", "Input some notes on the staff or hum a tune to find your favorite tracks.") })
522      ] })
523    ] })
524  ] }) });
525};
526export {
527  MelodySearchPage as default
528};

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.