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.