1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { a as api } from "./index-D0Bmh-UX.js"; 3import { u as useSoundfontKeyboard, a as useKeyboardInput, r as renderSharedKeyboard, b as getMidiFromKeyboardClick } from "./SharedKeyboard-B3r3Jaho.js"; 4import { h as formatFileSize } from "./utils-BYCVoSTN.js"; 5import { cB as ChevronDown, bE as ChevronRight, df as SlidersVertical, ci as Cpu, cL as RefreshCw, bi as Sparkles, ce as Plus, cc as Save, bs as Zap, cj as Layers, b5 as Trash2, b1 as CircleAlert, cY as ChevronLeft } from "./vendor-dVNaLJx_.js"; 6import "./vendor-i18n-R-bMoYp_.js"; 7import "./vendor-react-three-MDLA5jog.js"; 8import "./useMidiKeyboard-DlyEIN8T.js"; 9import "./wasm-synth-adapter-BrqgBX1y.js"; 10import "./vendor-spessasynth-DH8n6o_V.js"; 11const CompactTestKeyboard = ({ 12 previewSf, 13 notesString, 14 skipStep, 15 program, 16 isSelected = false, 17 fileSizes = {} 18}) => { 19 const playableNotes = reactExports.useRef(/* @__PURE__ */ new Set()); 20 const directlySampledNotes = reactExports.useRef(/* @__PURE__ */ new Set()); 21 const [octavesWithNotes, setOctavesWithNotes] = reactExports.useState([]); 22 reactExports.useEffect(() => { 23 const parsed = /* @__PURE__ */ new Set(); 24 const noteMap = { 25 "C": 0, 26 "C#": 1, 27 "DB": 1, 28 "D": 2, 29 "D#": 3, 30 "EB": 3, 31 "E": 4, 32 "F": 5, 33 "F#": 6, 34 "GB": 6, 35 "G": 7, 36 "G#": 8, 37 "AB": 8, 38 "A": 9, 39 "A#": 10, 40 "BB": 10, 41 "B": 11 42 }; 43 const parseSingle = (str) => { 44 str = str.trim().toUpperCase(); 45 if (/^\d+$/.test(str)) return parseInt(str, 10); 46 const m = str.match(/^([A-G][B#]?)(-?\d+)$/); 47 if (m) { 48 const name = m[1]; 49 const octave = parseInt(m[2], 10); 50 return (octave + 1) * 12 + (noteMap[name] ?? 0); 51 } 52 return null; 53 }; 54 if (notesString) { 55 notesString.split(",").forEach((part) => { 56 part = part.trim(); 57 if (part.includes("-")) { 58 const rangeParts = part.split("-"); 59 if (rangeParts.length === 2) { 60 const start = parseSingle(rangeParts[0]); 61 const end = parseSingle(rangeParts[1]); 62 if (start !== null && end !== null) { 63 const minVal = Math.min(start, end); 64 const maxVal = Math.max(start, end); 65 for (let i = minVal; i <= maxVal; i++) { 66 parsed.add(i); 67 } 68 } 69 } 70 } else { 71 const val = parseSingle(part); 72 if (val !== null) parsed.add(val); 73 } 74 }); 75 } else { 76 for (let i = 0; i < 128; i += skipStep) { 77 parsed.add(i); 78 } 79 } 80 playableNotes.current = parsed; 81 const octSet = /* @__PURE__ */ new Set(); 82 parsed.forEach((m) => { 83 const oct = Math.floor(m / 12) - 1; 84 octSet.add(oct); 85 }); 86 setOctavesWithNotes(Array.from(octSet).sort((a, b) => a - b)); 87 const sortedList = Array.from(parsed).sort((a, b) => a - b); 88 const directlySampled = /* @__PURE__ */ new Set(); 89 sortedList.forEach((midi, idx) => { 90 if (!notesString) { 91 if (midi % skipStep === 0) { 92 directlySampled.add(midi); 93 } 94 } else { 95 if (idx % skipStep === 0) { 96 directlySampled.add(midi); 97 } 98 } 99 }); 100 directlySampledNotes.current = directlySampled; 101 }, [notesString, skipStep]); 102 const { playNote, stopNote, audioContextRef } = useSoundfontKeyboard(0, [{ program }], void 0, previewSf); 103 const canvasRef = reactExports.useRef(null); 104 const renderActiveNotesRef = reactExports.useRef(/* @__PURE__ */ new Set()); 105 const [baseOctave, setBaseOctave] = reactExports.useState(4); 106 const baseOctaveRef = reactExports.useRef(4); 107 const activeKeysRef = reactExports.useRef(/* @__PURE__ */ new Map()); 108 const mouseActiveKeyRef = reactExports.useRef(null); 109 const changeOctave = reactExports.useCallback((newOctave) => { 110 const bounded = Math.max(0, Math.min(8, newOctave)); 111 setBaseOctave(bounded); 112 baseOctaveRef.current = bounded; 113 }, []); 114 const handleKeyOn = reactExports.useCallback((midi) => { 115 if (audioContextRef.current && audioContextRef.current.state === "suspended") { 116 audioContextRef.current.resume().catch(() => { 117 }); 118 } 119 const isEnabled = !notesString ? true : playableNotes.current.has(midi); 120 if (!isEnabled) return; 121 playNote(midi); 122 renderActiveNotesRef.current.add(midi); 123 }, [notesString, playNote, audioContextRef]); 124 const handleKeyOff = reactExports.useCallback((midi) => { 125 stopNote(midi); 126 renderActiveNotesRef.current.delete(midi); 127 }, [stopNote]); 128 useKeyboardInput({ 129 playNote: handleKeyOn, 130 stopNote: handleKeyOff, 131 baseOctaveRef, 132 activeKeysRef, 133 enabled: isSelected, 134 onOctaveChange: setBaseOctave 135 }); 136 reactExports.useEffect(() => { 137 let animationFrameId; 138 const render = () => { 139 if (canvasRef.current) { 140 const canvas = canvasRef.current; 141 const ctx = canvas.getContext("2d"); 142 if (ctx) { 143 const width = canvas.width; 144 const height = canvas.height; 145 ctx.clearRect(0, 0, width, height); 146 renderSharedKeyboard( 147 ctx, 148 width, 149 height, 150 height, 151 // keyHeight 152 renderActiveNotesRef.current, 153 "pc", 154 baseOctaveRef.current, 155 directlySampledNotes.current 156 // targetNotes highlights directly sampled keys in amber 157 ); 158 } 159 } 160 animationFrameId = requestAnimationFrame(render); 161 }; 162 render(); 163 return () => cancelAnimationFrame(animationFrameId); 164 }, []); 165 const handlePointerDown = (e) => { 166 if (!isSelected) return; 167 e.preventDefault(); 168 const canvas = canvasRef.current; 169 if (!canvas) return; 170 const rect = canvas.getBoundingClientRect(); 171 const x = e.clientX - rect.left; 172 const y = e.clientY - rect.top; 173 const midi = getMidiFromKeyboardClick(x, y, rect.width, rect.height); 174 if (midi !== null) { 175 mouseActiveKeyRef.current = midi; 176 handleKeyOn(midi); 177 } 178 }; 179 const handlePointerMove = (e) => { 180 if (!isSelected) return; 181 e.preventDefault(); 182 if (e.buttons !== 1) return; 183 const canvas = canvasRef.current; 184 if (!canvas) return; 185 const rect = canvas.getBoundingClientRect(); 186 const x = e.clientX - rect.left; 187 const y = e.clientY - rect.top; 188 const midi = getMidiFromKeyboardClick(x, y, rect.width, rect.height); 189 if (midi !== null && midi !== mouseActiveKeyRef.current) { 190 if (mouseActiveKeyRef.current !== null) { 191 handleKeyOff(mouseActiveKeyRef.current); 192 } 193 mouseActiveKeyRef.current = midi; 194 handleKeyOn(midi); 195 } 196 }; 197 const handlePointerUp = (e) => { 198 if (!isSelected) return; 199 e.preventDefault(); 200 if (mouseActiveKeyRef.current !== null) { 201 handleKeyOff(mouseActiveKeyRef.current); 202 mouseActiveKeyRef.current = null; 203 } 204 }; 205 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
206 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-1 text-xs bg-zinc-200/60 dark:bg-zinc-900/80 p-1.5 rounded-md border border-zinc-300/60 dark:border-zinc-800", children: [ 207 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 208 /* @__PURE__ */ jsxRuntimeExports.jsx( 209 "button", 210 { 211 onClick: (e) => { 212 e.stopPropagation(); 213 changeOctave(baseOctave - 1); 214 }, 215 className: "p-1 hover:bg-zinc-300 dark:hover:bg-zinc-800 rounded text-zinc-700 dark:text-zinc-300 transition-colors disabled:opacity-30", 216 disabled: baseOctave <= 0, 217 title: "Previous Octave (Key Z or Left Arrow)", 218 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronLeft, { className: "w-3.5 h-3.5" }) 219 } 220 ), 221 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono font-semibold text-[11px] text-zinc-800 dark:text-zinc-200 px-1", children: [ 222 "Octave ", 223 baseOctave, 224 " (C", 225 baseOctave, 226 "-B", 227 baseOctave, 228 ") ",
229 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-500 text-[10px]", children: [ 230 "MIDI ", 231 (baseOctave + 1) * 12, 232 "â", 233 (baseOctave + 1) * 12 + 11 234 ] }) 235 ] }), 236 /* @__PURE__ */ jsxRuntimeExports.jsx( 237 "button", 238 { 239 onClick: (e) => { 240 e.stopPropagation(); 241 changeOctave(baseOctave + 1); 242 }, 243 className: "p-1 hover:bg-zinc-300 dark:hover:bg-zinc-800 rounded text-zinc-700 dark:text-zinc-300 transition-colors disabled:opacity-30", 244 disabled: baseOctave >= 8, 245 title: "Next Octave (Key X or Right Arrow)", 246 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "w-3.5 h-3.5" }) 247 } 248 ) 249 ] }), 250 octavesWithNotes.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 overflow-x-auto select-none", children: [ 251 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-bold text-zinc-500 uppercase mr-0.5", children: "Active Octaves:" }), 252 octavesWithNotes.map((oct) => { 253 const count = Array.from(playableNotes.current).filter((m) => Math.floor(m / 12) - 1 === oct).length; 254 const isCurrent = oct === baseOctave; 255 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 256 "button", 257 { 258 onClick: (e) => { 259 e.stopPropagation(); 260 changeOctave(oct); 261 }, 262 className: `px-1.5 py-0.5 rounded text-[9px] font-mono font-bold transition-all ${isCurrent ? "bg-indigo-600 text-white shadow-sm ring-1 ring-indigo-400" : "bg-zinc-300/80 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-indigo-500/20 hover:text-indigo-600"}`, 263 title: `Jump to Octave ${oct} (${count} note${count > 1 ? "s" : ""})`, 264 children: [ 265 "Oct ", 266 oct, 267 " (", 268 count, 269 ")" 270 ] 271 }, 272 oct 273 ); 274 }) 275 ] }) 276 ] }), 277 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `relative h-28 w-full border border-zinc-300 dark:border-zinc-800 bg-zinc-100 rounded-lg overflow-hidden flex select-none shadow-inner touch-none ${!isSelected && "opacity-70 pointer-events-none grayscale-[0.3]"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx( 278 "canvas", 279 { 280 ref: canvasRef, 281 width: 800, 282 height: 112, 283 className: "w-full h-full cursor-pointer", 284 onPointerDown: handlePointerDown, 285 onPointerMove: handlePointerMove, 286 onPointerUp: handlePointerUp, 287 onPointerLeave: handlePointerUp 288 } 289 ) }), 290 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-[10px] text-zinc-500 px-1", children: [
291 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 292 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1", children: [ 293 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-2 h-2 rounded-full bg-amber-500 inline-block shadow-sm" }), 294 " Sampled (HQ)" 295 ] }), 296 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1", children: [ 297 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-2 h-2 rounded-full bg-zinc-300 inline-block shadow-sm" }), 298 " Interpolated (Pitch Shifted)" 299 ] }) 300 ] }), 301 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "italic", children: isSelected ? "Play chords with PC Keys (A S D F...) or click/drag. Z/X or buttons change octave." : "Click card to play on PC keyboard" }) 302 ] }) 303 ] }); 304}; 305const SoundfontQualityLab = ({ sf2Files, onRefreshProfiles }) => { 306 const [isOpen, setIsOpen] = reactExports.useState(false); 307 const [selectedSf2, setSelectedSf2] = reactExports.useState(""); 308 const [program, setProgram] = reactExports.useState(0); 309 const [skipStep, setSkipStep] = reactExports.useState(1); 310 const [noteDuration, setNoteDuration] = reactExports.useState(2); 311 const [opusBitrate, setOpusBitrate] = reactExports.useState("64k"); 312 const [gain, setGain] = reactExports.useState(1.8); 313 const [fadeOutDuration, setFadeOutDuration] = reactExports.useState(0.3); 314 const [reverbLevel, setReverbLevel] = reactExports.useState(0.5); 315 const [chorusLevel, setChorusLevel] = reactExports.useState(0.3); 316 const [notesToSample, setNotesToSample] = reactExports.useState("60-72"); 317 const [isAutoLeveling, setIsAutoLeveling] = reactExports.useState(false); 318 const [autoLevelTargetDb, setAutoLevelTargetDb] = reactExports.useState(-18); 319 const parseNotesString = (notesStr) => { 320 const noteMap = { 321 "C": 0, 322 "C#": 1, 323 "DB": 1, 324 "D": 2, 325 "D#": 3, 326 "EB": 3, 327 "E": 4, 328 "F": 5, 329 "F#": 6, 330 "GB": 6, 331 "G": 7, 332 "G#": 8, 333 "AB": 8, 334 "A": 9, 335 "A#": 10, 336 "BB": 10, 337 "B": 11 338 }; 339 const parseSingle = (str) => { 340 str = str.trim().toUpperCase(); 341 if (/^\d+$/.test(str)) return parseInt(str, 10); 342 const m = str.match(/^([A-G][B#]?)(-?\d+)$/); 343 if (m) { 344 const name = m[1]; 345 const octave = parseInt(m[2], 10); 346 return (octave + 1) * 12 + (noteMap[name] ?? 0); 347 } 348 return null; 349 }; 350 const parsed = /* @__PURE__ */ new Set(); 351 if (!notesStr) return []; 352 notesStr.split(",").forEach((part) => { 353 part = part.trim(); 354 if (part.includes("-")) { 355 const rangeParts = part.split("-"); 356 if (rangeParts.length === 2) { 357 const start = parseSingle(rangeParts[0]); 358 const end = parseSingle(rangeParts[1]); 359 if (start !== null && end !== null) { 360 const minVal = Math.min(start, end); 361 const maxVal = Math.max(start, end); 362 for (let i = minVal; i <= maxVal; i++) { 363 parsed.add(i); 364 } 365 } 366 } 367 } else { 368 const val = parseSingle(part); 369 if (val !== null) parsed.add(val); 370 } 371 }); 372 return Array.from(parsed).sort((a, b) => a - b); 373 }; 374 const resolvedNotes = parseNotesString(notesToSample); 375 const resolvedNotesPreview = resolvedNotes.length > 0 ? `â MIDI ${resolvedNotes.slice(0, 8).join(", ")}${resolvedNotes.length > 8 ? "..." : ""} (${resolvedNotes.length} notes)` : "C3, C4, C5-E5 notation supported"; 376 const [testSlots, setTestSlots] = reactExports.useState([]); 377 const [profiles, setProfiles] = reactExports.useState({}); 378 const [isGenerating, setIsGenerating] = reactExports.useState(false); 379 const [selectedSlotId, setSelectedSlotId] = reactExports.useState(null); 380 reactExports.useEffect(() => { 381 if (testSlots.length > 0 && !selectedSlotId) { 382 const firstReady = testSlots.find((s) => s.status === "completed"); 383 if (firstReady) { 384 setSelectedSlotId(firstReady.jobId); 385 } 386 } 387 }, [testSlots, selectedSlotId]); 388 const loadProfilesAndJobs = async () => { 389 try { 390 const [profileSet, jobsData] = await Promise.all([ 391 api.getSystemSetting("SOUNDFONT_QUALITY_PROFILES"), 392 api.getSoundfontJobs() 393 ]); 394 if (profileSet && profileSet.value) { 395 setProfiles(typeof profileSet.value === "string" ? JSON.parse(profileSet.value) : profileSet.value); 396 } 397 const activeTests = jobsData.filter((j) => j.jobType === "SF2_TEST").map((job) => { 398 let params = { 399 skipStep: 1, 400 noteDuration: 2, 401 opusBitrate: "64k", 402 gain: 1.8, 403 fadeOutDuration: 0.3, 404 reverbLevel: 0.5, 405 chorusLevel: 0.3, 406 notes: "60-72", 407 program: 0 408 }; 409 let displayDetails = job.details; 410 if (job.details) { 411 try { 412 const parsed = JSON.parse(job.details); 413 if (parsed && typeof parsed === "object") { 414 params = { ...params, ...parsed }; 415 displayDetails = parsed.error || parsed.message || job.details; 416 } 417 } catch { 418 } 419 } 420 return { 421 jobId: job.id, 422 params, 423 status: job.status, 424 progress: job.progress, 425 details: displayDetails, 426 outputSize: job.outputSize, 427 fileSizes: job.fileSizes, 428 sf2Path: job.inputPath, 429 origin_node_id: job.origin_node_id 430 }; 431 }); 432 setTestSlots((prev) => { 433 const nextSlots = [...prev];
434 activeTests.forEach((test) => { 435 const matchIdx = nextSlots.findIndex((s) => s.jobId === test.jobId); 436 if (matchIdx !== -1) { 437 nextSlots[matchIdx] = { ...nextSlots[matchIdx], ...test }; 438 } else if (nextSlots.length < 8) { 439 nextSlots.push(test); 440 } 441 }); 442 return nextSlots; 443 }); 444 } catch (e) { 445 console.error("Failed to load soundfont lab data", e); 446 } 447 }; 448 reactExports.useEffect(() => { 449 if (sf2Files.length > 0 && !selectedSf2) { 450 setSelectedSf2(sf2Files[0].path); 451 } 452 }, [sf2Files]); 453 reactExports.useEffect(() => { 454 loadProfilesAndJobs(); 455 const interval = setInterval(loadProfilesAndJobs, 3e3); 456 return () => clearInterval(interval); 457 }, []); 458 const applyPreset = (preset) => { 459 if (preset === "low") { 460 setSkipStep(3); 461 setNoteDuration(1); 462 setOpusBitrate("24k"); 463 setGain(1.8); 464 setFadeOutDuration(0.15); 465 setReverbLevel(0.3); 466 setChorusLevel(0.2); 467 } else if (preset === "medium") { 468 setSkipStep(2); 469 setNoteDuration(1.5); 470 setOpusBitrate("48k"); 471 setGain(1.8); 472 setFadeOutDuration(0.25); 473 setReverbLevel(0.4); 474 setChorusLevel(0.3); 475 } else if (preset === "high") { 476 setSkipStep(1); 477 setNoteDuration(2.5); 478 setOpusBitrate("64k"); 479 setGain(1.8); 480 setFadeOutDuration(0.3); 481 setReverbLevel(0.5); 482 setChorusLevel(0.3); 483 } else if (preset === "ultra") { 484 setSkipStep(1); 485 setNoteDuration(4); 486 setOpusBitrate("128k"); 487 setGain(2); 488 setFadeOutDuration(0.5); 489 setReverbLevel(0.6); 490 setChorusLevel(0.4); 491 } 492 }; 493 const handleAutoLevel = async () => { 494 if (!selectedSf2) return; 495 setIsAutoLeveling(true); 496 try { 497 const result = await api.autoLevelSoundfont(selectedSf2, program, autoLevelTargetDb); 498 if (result.recommendedGain) { 499 const newGain = Math.min(10, Math.max(0.1, result.recommendedGain)); 500 setGain(parseFloat(newGain.toFixed(2))); 501 } 502 } catch (e) { 503 alert(`Auto level failed: ${e.message || "Unknown error"}`); 504 } finally { 505 setIsAutoLeveling(false); 506 } 507 }; 508 const handleStartTest = async () => { 509 if (!selectedSf2) return; 510 if (testSlots.length >= 8) { 511 alert("Maximum limit of 8 test slots reached. Please delete an old test slot first."); 512 return; 513 } 514 setIsGenerating(true); 515 try { 516 const params = { 517 skipStep, 518 noteDuration, 519 opusBitrate, 520 gain, 521 fadeOutDuration, 522 reverbLevel, 523 chorusLevel, 524 notes: notesToSample, 525 program 526 }; 527 const job = await api.startSoundfontTest(selectedSf2, params); 528 const newSlot = { 529 jobId: job.id, 530 params, 531 status: "pending", 532 progress: 0, 533 sf2Path: selectedSf2 534 }; 535 setTestSlots((prev) => [...prev, newSlot]); 536 } catch (e) { 537 alert("Failed to start soundfont quality test generation"); 538 } finally { 539 setIsGenerating(false); 540 } 541 }; 542 const handleSampleAllQualities = async () => { 543 if (!selectedSf2) return; 544 if (testSlots.length + 4 > 8) { 545 const clearConfirm = confirm(`Maximum limit of 8 test slots would be exceeded. Do you want to delete all existing test slots first?`); 546 if (clearConfirm) { 547 await handleDeleteAllSlots(); 548 } else { 549 return; 550 } 551 } 552 const presetsToSample = [ 553 { 554 name: "low", 555 params: { skipStep: 3, noteDuration: 1, opusBitrate: "24k", gain: 1.8, fadeOutDuration: 0.15, reverbLevel: 0.3, chorusLevel: 0.2, notes: notesToSample, program } 556 }, 557 { 558 name: "medium", 559 params: { skipStep: 2, noteDuration: 1.5, opusBitrate: "48k", gain: 1.8, fadeOutDuration: 0.25, reverbLevel: 0.4, chorusLevel: 0.3, notes: notesToSample, program } 560 }, 561 { 562 name: "high", 563 params: { skipStep: 1, noteDuration: 2.5, opusBitrate: "64k", gain: 1.8, fadeOutDuration: 0.3, reverbLevel: 0.5, chorusLevel: 0.3, notes: notesToSample, program } 564 }, 565 { 566 name: "ultra", 567 params: { skipStep: 1, noteDuration: 4, opusBitrate: "128k", gain: 2, fadeOutDuration: 0.5, reverbLevel: 0.6, chorusLevel: 0.4, notes: notesToSample, program } 568 } 569 ]; 570 setIsGenerating(true); 571 try { 572 const pendingSlots = []; 573 for (const preset of presetsToSample) { 574 const job = await api.startSoundfontTest(selectedSf2, preset.params); 575 pendingSlots.push({ 576 jobId: job.id, 577 params: preset.params, 578 status: "pending", 579 progress: 0, 580 sf2Path: selectedSf2 581 }); 582 } 583 setTestSlots((prev) => [...prev, ...pendingSlots]); 584 } catch (e) { 585 alert("Failed to start some soundfont quality test generations"); 586 } finally { 587 setIsGenerating(false); 588 } 589 }; 590 const handleDeleteSlot = async (slotId) => { 591 try { 592 await api.deleteSoundfontJob(slotId); 593 await api.deleteSoundfont(`_test_${slotId}`); 594 setTestSlots((prev) => prev.filter((s) => s.jobId !== slotId)); 595 } catch (e) { 596 alert("Failed to delete test slot"); 597 } 598 }; 599 const handleDeleteAllSlots = async () => { 600 if (testSlots.length === 0) return; 601 try { 602 await Promise.all(testSlots.map(async (slot) => { 603 try { 604 await api.deleteSoundfontJob(slot.jobId); 605 await api.deleteSoundfont(`_test_${slot.jobId}`); 606 } catch { 607 } 608 })); 609 setTestSlots([]); 610 setSelectedSlotId(null); 611 } catch (e) { 612 alert("Failed to delete all test slots"); 613 } 614 }; 615 const handleSaveProfile = async () => { 616 if (!selectedSf2) return; 617 const libName = selectedSf2.replace(/\.sf2$/i, ""); 618 const nextProfiles = { 619 ...profiles, 620 [libName]: { 621 skipStep, 622 noteDuration, 623 opusBitrate, 624 gain, 625 fadeOutDuration, 626 reverbLevel, 627 chorusLevel, 628 notes: notesToSample, 629 program, 630 savedAt: (/* @__PURE__ */ new Date()).toISOString() 631 } 632 }; 633 try { 634 await api.saveSystemSetting("SOUNDFONT_QUALITY_PROFILES", nextProfiles); 635 setProfiles(nextProfiles); 636 if (onRefreshProfiles) onRefreshProfiles(); 637 alert(`Quality profile saved successfully for "${libName}". All future JS conversions for this soundfont will use these settings!`); 638 } catch (e) { 639 alert("Failed to save global quality profile"); 640 } 641 }; 642 const handleSaveProfileAndConvert = async () => { 643 if (!selectedSf2) return; 644 const libName = selectedSf2.replace(/\.sf2$/i, ""); 645 const nextProfiles = { 646 ...profiles, 647 [libName]: { 648 skipStep, 649 noteDuration, 650 opusBitrate, 651 gain, 652 fadeOutDuration, 653 reverbLevel, 654 chorusLevel, 655 notes: notesToSample, 656 program, 657 savedAt: (/* @__PURE__ */ new Date()).toISOString() 658 } 659 }; 660 try { 661 await api.saveSystemSetting("SOUNDFONT_QUALITY_PROFILES", nextProfiles); 662 setProfiles(nextProfiles); 663 if (onRefreshProfiles) onRefreshProfiles(); 664 await api.startSoundfontJob(`Sample SF2 to JS: ${selectedSf2.split("/").pop()}`, selectedSf2, "SF2_TO_JS"); 665 alert(`Quality profile saved & full conversion queued for "${libName}". You can monitor progress in the Active Jobs section.`); 666 } catch (e) { 667 alert("Failed to save profile or start conversion"); 668 } 669 }; 670 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden shadow-sm", children: [
671 /* @__PURE__ */ jsxRuntimeExports.jsxs( 672 "div", 673 { 674 onClick: () => setIsOpen(!isOpen), 675 className: `p-4 ${isOpen ? "border-b border-zinc-200 dark:border-zinc-800" : ""} bg-zinc-50/50 dark:bg-zinc-800/50 flex items-center justify-between cursor-pointer select-none`, 676 children: [ 677 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 678 isOpen ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "w-4 h-4 text-indigo-500 shrink-0" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "w-4 h-4 text-indigo-500 shrink-0" }), 679 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 680 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-sm uppercase tracking-wider text-zinc-700 dark:text-zinc-300 flex items-center gap-2", children: [ 681 /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersVertical, { className: "w-4 h-4 text-indigo-500" }), 682 "JS Soundfont Quality Laboratory" 683 ] }),
684 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-zinc-500 mt-0.5", children: "Test quality settings, render custom ranges instantly, and listen on the keyboard before full library conversion." }) 685 ] }) 686 ] }), 687 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1.5", onClick: (e) => e.stopPropagation(), children: [ 688 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => applyPreset("low"), className: "px-2 py-0.5 text-[9px] font-bold bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 rounded border border-zinc-200 dark:border-zinc-700", children: "Low" }), 689 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => applyPreset("medium"), className: "px-2 py-0.5 text-[9px] font-bold bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 rounded border border-zinc-200 dark:border-zinc-700", children: "Medium" }), 690 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => applyPreset("high"), className: "px-2 py-0.5 text-[9px] font-bold bg-indigo-50 text-indigo-600 hover:bg-indigo-100 dark:bg-indigo-950/40 dark:text-indigo-400 dark:hover:bg-indigo-950/60 rounded border border-indigo-200 dark:border-indigo-900/40", children: "HQ" }),
691 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => applyPreset("ultra"), className: "px-2 py-0.5 text-[9px] font-bold bg-amber-50 text-amber-600 hover:bg-amber-100 dark:bg-amber-950/40 dark:text-amber-400 dark:hover:bg-amber-950/60 rounded border border-amber-200 dark:border-amber-900/40", children: "Ultra" }) 692 ] }) 693 ] 694 } 695 ), 696 isOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-5 grid grid-cols-1 lg:grid-cols-3 gap-6", children: [ 697 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lg:col-span-1 space-y-4", children: [ 698 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "font-bold text-xs uppercase tracking-tight text-zinc-500 flex items-center gap-1.5", children: [ 699 /* @__PURE__ */ jsxRuntimeExports.jsx(Cpu, { className: "w-3.5 h-3.5 text-zinc-400" }), 700 " Configure Sampling Settings" 701 ] }), 702 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 703 /* @__P
703URE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] font-bold uppercase text-zinc-500", children: "Target Soundfont (.sf2)" }), 704 /* @__PURE__ */ jsxRuntimeExports.jsx( 705 "select", 706 { 707 value: selectedSf2, 708 onChange: (e) => setSelectedSf2(e.target.value), 709 className: "w-full bg-zinc-50 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 border border-zinc-200 dark:border-zinc-800 rounded-lg px-2.5 py-1.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500 font-semibold", 710 children: sf2Files.map((file) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: file.path, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-800", children: file.name || file.path.split("/").pop() }, file.path)) 711 } 712 ) 713 ] }), 714 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [ 715 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [
716 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] font-bold uppercase text-zinc-500", children: "MIDI Program (Inst)" }), 717 /* @__PURE__ */ jsxRuntimeExports.jsx( 718 "input", 719 { 720 type: "number", 721 min: 0, 722 max: 127, 723 value: program, 724 onChange: (e) => setProgram(parseInt(e.target.value)), 725 className: "w-full bg-zinc-50 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 border border-zinc-200 dark:border-zinc-800 rounded-lg px-2.5 py-1.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500 font-semibold" 726 } 727 ) 728 ] }), 729 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 730 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] font-bold uppercase text-zinc-500", children: "Notes to Sample" }), 731 /* @__PURE__ */ jsxRuntimeExports.jsx( 732 "input", 733 { 734 type: "text", 735 value: notesToSample, 736 onChange: (e) => setNotesToSample(e.target.value), 737 placeholder: "60-72, C4-C5", 738 className: "w-full bg-zinc-50 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 border border-zinc-200 dark:border-zinc-800 rounded-lg px-2.5 py-1.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500 font-semibold" 739 } 740 ),
741 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 dark:text-zinc-400 font-semibold truncate mt-1", title: resolvedNotesPreview, children: resolvedNotesPreview }) 742 ] }) 743 ] }), 744 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3.5 pt-2 border-t border-zinc-200 dark:border-zinc-800", children: [ 745 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 746 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-[10px]", children: [ 747 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold uppercase text-zinc-500", children: "Skip Step (Midi Spacing)" }), 748 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold text-indigo-500", children: skipStep === 1 ? "Every Note" : `Every ${skipStep} Notes` }) 749 ] }), 750 /* @__PURE__ */ jsxRuntimeExports.jsx( 751 "input", 752 { 753 type: "range", 754 min: 1, 755 max: 6, 756 step: 1, 757 value: skipStep, 758 onChange: (e) => setSkipStep(parseInt(e.target.value)), 759 className: "w-full h-1 bg-zinc-200 dark:bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" 760 } 761 ) 762 ] }),
763 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 764 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-[10px]", children: [ 765 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold uppercase text-zinc-500", children: "Sustain Duration" }), 766 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono font-bold text-indigo-500", children: [ 767 noteDuration.toFixed(1), 768 " seconds" 769 ] }) 770 ] }), 771 /* @__PURE__ */ jsxRuntimeExports.jsx( 772 "input", 773 { 774 type: "range", 775 min: 0.5, 776 max: 5, 777 step: 0.5, 778 value: noteDuration, 779 onChange: (e) => setNoteDuration(parseFloat(e.target.value)), 780 className: "w-full h-1 bg-zinc-200 dark:bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" 781 } 782 ) 783 ] }),
784 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 785 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] font-bold uppercase text-zinc-500 block", children: "Opus Encoder Bitrate" }), 786 /* @__PURE__ */ jsxRuntimeExports.jsxs( 787 "select", 788 { 789 value: opusBitrate, 790 onChange: (e) => setOpusBitrate(e.target.value), 791 className: "w-full bg-zinc-50 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 border border-zinc-200 dark:border-zinc-800 rounded-lg px-2.5 py-1.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500 font-semibold", 792 children: [ 793 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "16k", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-800", children: "16 kbps (Ultra compressed)" }), 794 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "24k", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-800", children: "24 kbps (Standard default)" }),
795 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "32k", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-800", children: "32 kbps (Medium quality)" }), 796 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "48k", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-800", children: "48 kbps (Good fidelity)" }), 797 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "64k", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-800", children: "64 kbps (High fidelity)" }), 798 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "96k", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-800", children: "96 kbps (Premium stereo-like)" }), 799 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "128k", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-800", children: "128 kbps (Studio lossless-like)" }) 800 ] 801 } 802 ) 803 ] }),
804 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 805 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-[10px]", children: [ 806 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold uppercase text-zinc-500", children: "FluidSynth Gain (Volume)" }), 807 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 808 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center bg-zinc-100 dark:bg-zinc-800 rounded px-1.5 py-0.5 border border-zinc-200 dark:border-zinc-700", title: "Target dB for Auto Level", children: [ 809 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 mr-1", children: "Target dB" }), 810 /* @__PURE__ */ jsxRuntimeExports.jsx( 811 "input", 812 { 813 type: "number", 814 value: autoLevelTargetDb, 815 onChange: (e) => setAutoLevelTargetDb(parseFloat(e.target.value) || -18), 816 className: "bg-transparent w-10 text-right outline-none text-zinc-700 dark:text-zinc-300 font-mono font-bold", 817 step: 1 818 } 819 ) 820 ] }),
821 /* @__PURE__ */ jsxRuntimeExports.jsxs( 822 "button", 823 { 824 onClick: handleAutoLevel, 825 disabled: isAutoLeveling, 826 className: "flex items-center gap-1 bg-amber-50 text-amber-600 hover:bg-amber-100 dark:bg-amber-950/40 dark:text-amber-400 dark:hover:bg-amber-950/60 border border-amber-200 dark:border-amber-900/40 px-2 py-0.5 rounded font-bold transition-colors disabled:opacity-50 disabled:cursor-not-allowed", 827 title: "Automatically sample and adjust gain multiplier", 828 children: [ 829 isAutoLeveling ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "w-3 h-3" }), 830 "Auto Level" 831 ] 832 } 833 ), 834 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono font-bold text-indigo-500", children: [ 835 gain.toFixed(2), 836 "x" 837 ] }) 838 ] }) 839 ] }),
840 /* @__PURE__ */ jsxRuntimeExports.jsx( 841 "input", 842 { 843 type: "range", 844 min: 0.1, 845 max: 10, 846 step: 0.1, 847 value: gain, 848 onChange: (e) => setGain(parseFloat(e.target.value)), 849 className: "w-full h-1 bg-zinc-200 dark:bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" 850 } 851 ) 852 ] }), 853 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 854 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-[10px]", children: [ 855 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold uppercase text-zinc-500", children: "Fade-Out Duration" }), 856 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono font-bold text-indigo-500", children: [ 857 fadeOutDuration.toFixed(2), 858 "s" 859 ] }) 860 ] }),
861 /* @__PURE__ */ jsxRuntimeExports.jsx( 862 "input", 863 { 864 type: "range", 865 min: 0.05, 866 max: 1, 867 step: 0.05, 868 value: fadeOutDuration, 869 onChange: (e) => setFadeOutDuration(parseFloat(e.target.value)), 870 className: "w-full h-1 bg-zinc-200 dark:bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" 871 } 872 ) 873 ] }), 874 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3 pt-1", children: [ 875 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 876 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-[9px]", children: [ 877 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold uppercase text-zinc-500", children: "Reverb Mix" }), 878 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold text-zinc-600", children: reverbLevel.toFixed(1) }) 879 ] }),
880 /* @__PURE__ */ jsxRuntimeExports.jsx( 881 "input", 882 { 883 type: "range", 884 min: 0, 885 max: 1, 886 step: 0.1, 887 value: reverbLevel, 888 onChange: (e) => setReverbLevel(parseFloat(e.target.value)), 889 className: "w-full h-1 bg-zinc-200 dark:bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" 890 } 891 ) 892 ] }), 893 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1", children: [ 894 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-[9px]", children: [ 895 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold uppercase text-zinc-500", children: "Chorus Mix" }), 896 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold text-zinc-600", children: chorusLevel.toFixed(1) }) 897 ] }),
898 /* @__PURE__ */ jsxRuntimeExports.jsx( 899 "input", 900 { 901 type: "range", 902 min: 0, 903 max: 1, 904 step: 0.1, 905 value: chorusLevel, 906 onChange: (e) => setChorusLevel(parseFloat(e.target.value)), 907 className: "w-full h-1 bg-zinc-200 dark:bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" 908 } 909 ) 910 ] }) 911 ] }) 912 ] }), 913 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2 pt-4", children: [ 914 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 915 /* @__PURE__ */ jsxRuntimeExports.jsxs( 916 "button", 917 { 918 onClick: handleStartTest, 919 disabled: isGenerating || !selectedSf2, 920 className: "flex-1 bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 shadow-sm disabled:opacity-50", 921 children: [
922 isGenerating ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3.5 h-3.5 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "w-3.5 h-3.5" }), 923 "Generate Test Slot" 924 ] 925 } 926 ), 927 /* @__PURE__ */ jsxRuntimeExports.jsxs( 928 "button", 929 { 930 onClick: handleSampleAllQualities, 931 disabled: isGenerating || !selectedSf2, 932 className: "flex-1 bg-amber-500 hover:bg-amber-600 text-white text-xs font-bold py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 shadow-sm disabled:opacity-50", 933 title: "Queue 4 test slots at different quality sets automatically", 934 children: [ 935 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "w-3.5 h-3.5" }), 936 "Sample All Qualities" 937 ] 938 } 939 ) 940 ] }), 941 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
942 /* @__PURE__ */ jsxRuntimeExports.jsxs( 943 "button", 944 { 945 onClick: handleSaveProfile, 946 disabled: !selectedSf2, 947 className: "flex-1 bg-zinc-800 hover:bg-zinc-900 text-white text-xs font-bold py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 shadow-sm disabled:opacity-50", 948 title: "Save these parameters as default JS quality profile for this SoundFont", 949 children: [ 950 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { className: "w-3.5 h-3.5" }), 951 "Save Profile" 952 ] 953 } 954 ), 955 /* @__PURE__ */ jsxRuntimeExports.jsxs( 956 "button", 957 { 958 onClick: handleSaveProfileAndConvert, 959 disabled: !selectedSf2, 960 className: "flex-1 bg-gradient-to-r from-emerald-650 to-indigo-600 hover:from-emerald-700 hover:to-indigo-700 text-white text-xs font-bold py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 shadow-sm disabled:opacity-50", 961 title: "Save profile and immediately convert the SF2 soundfont to JS", 962 children: [
963 /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { className: "w-3.5 h-3.5 text-yellow-300" }), 964 "Save & Convert Now" 965 ] 966 } 967 ) 968 ] }) 969 ] }) 970 ] }), 971 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "lg:col-span-2 space-y-4", children: [ 972 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "font-bold text-xs uppercase tracking-tight text-zinc-500 flex items-center justify-between", children: [ 973 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5", children: [ 974 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { className: "w-3.5 h-3.5 text-zinc-400" }), 975 " Active Quality Test Slots (", 976 testSlots.length, 977 "/8)" 978 ] }), 979 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 980 testSlots.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs( 981 "button", 982 { 983 onClick: () => { 984 if (confirm("Are you sure you want to delete all active test slots? This will delete all DB entries and cached test audio folders on disk.")) { 985 handleDeleteAllSlots(); 986 } 987 }, 988 className: "text-red-500 hover:text-red-650 dark:text-red-400 dark:hover:text-red-300 transition-colors text-[10px] font-bold uppercase flex items-center gap-1 bg-red-500/5 hover:bg-red-500/10 px-2 py-0.5 rounded border border-red-500/10", 989 children: [
990 /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-3 h-3" }), 991 "Delete All" 992 ] 993 } 994 ), 995 testSlots.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] text-zinc-400 italic", children: "Select slot to play below" }) 996 ] }) 997 ] }), 998 testSlots.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-2 border-dashed border-zinc-200 dark:border-zinc-800 rounded-xl p-12 text-center text-zinc-400 dark:text-zinc-500", children: [ 999 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "w-10 h-10 mx-auto text-zinc-300 dark:text-zinc-700 mb-3 animate-pulse" }), 1000 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm font-semibold", children: "No active test slots." }), 1001 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-400 mt-1 max-w-sm mx-auto", children: "Choose a SoundFont, configure parameters, and generate a test slot to listen on the interactive keyboard." }) 1002 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-4", children: testSlots.map((slot) => { 1003 var _a, _b; 1004 const isReady = slot.status === "completed"; 1005 const isFailed = slot.status === "error"; 1006 const isPending = slot.status === "pending" || slot.status === "processing"; 1007 const isSelected = selectedSlotId === slot.jobId; 1008 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1009 "div", 1010 { 1011 onClick: () => isReady && setSelectedSlotId(slot.jobId), 1012 className: `border rounded-xl p-4 bg-zinc-50/50 dark:bg-zinc-950/20 relative group hover:border-zinc-300 dark:hover:border-zinc-700 transition-all shadow-sm ${isReady ? "cursor-pointer" : ""} ${isSelected ? "ring-2 ring-indigo-500 border-transparent shadow-indigo-100/10" : "border-zinc-200 dark:border-zinc-800"}`, 1013 children: [
1014 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-3", children: [ 1015 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1016 isPending && /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3.5 h-3.5 text-indigo-500 animate-spin" }), 1017 isReady && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-2 py-0.5 bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 rounded text-[9px] font-bold uppercase tracking-wider", children: "Ready" }), 1018 isFailed && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-2 py-0.5 bg-red-500/10 text-red-500 border border-red-500/20 rounded text-[9px] font-bold uppercase tracking-wider", children: "Error" }), 1019 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs font-mono font-bold text-zinc-700 dark:text-zinc-400", children: [ 1020 "Slot #", 1021 slot.jobId.substring(0, 6) 1022 ] }) 1023 ] }),
1024 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 1025 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-zinc-400 font-mono", children: [ 1026 slot.params.skipStep === 1 ? "Density: HQ" : `Skip: ${slot.params.skipStep}`, 1027 " | ", 1028 slot.params.noteDuration, 1029 "s | ", 1030 slot.params.opusBitrate 1031 ] }), 1032 /* @__PURE__ */ jsxRuntimeExports.jsx( 1033 "button", 1034 { 1035 onClick: (e) => { 1036 e.stopPropagation(); 1037 handleDeleteSlot(slot.jobId); 1038 }, 1039 className: "text-zinc-400 hover:text-red-500 transition-colors p-1", 1040 title: "Delete test files", 1041 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-3.5 h-3.5" }) 1042 } 1043 ) 1044 ] }) 1045 ] }), 1046 isPending && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-6 flex flex-col items-center justify-center space-y-2", children: [
1047 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full bg-zinc-200 dark:bg-zinc-800 h-1.5 rounded-full max-w-xs overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-indigo-500 h-full transition-all duration-300", style: { width: `${slot.progress || 5}%` } }) }), 1048 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-zinc-500 font-bold", children: [ 1049 "Generating JS test files: ", 1050 slot.progress, 1051 "%..." 1052 ] }) 1053 ] }), 1054 isFailed && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-4 flex items-center gap-2 bg-red-500/5 border border-red-500/10 rounded-lg p-3 text-red-500 text-xs", children: [ 1055 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 16, className: "shrink-0" }), 1056 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-semibold", children: slot.details || "Unknown rendering exception inside worker." }) 1057 ] }),
1058 isReady && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [ 1059 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-100 dark:bg-zinc-900 border border-zinc-205 dark:border-zinc-800 rounded-lg p-2.5 text-[10px] grid grid-cols-2 sm:grid-cols-4 gap-2 text-zinc-650 dark:text-zinc-400", children: [ 1060 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1061 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "SF2:" }), 1062 " ", 1063 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200 truncate block", title: (_a = slot.sf2Path) == null ? void 0 : _a.split("/").pop(), children: (_b = slot.sf2Path) == null ? void 0 : _b.split("/").pop() }) 1064 ] }), 1065 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1066 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Program:" }), 1067 " ", 1068 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-semibold text-zinc-850 dark:text-zinc-200", children: slot.params.program }) 1069 ] }), 1070 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1071 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Skip Step:" }), 1072 " ", 1073 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200", children: slot.params.skipStep }) 1074 ] }), 1075 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1076 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Sustain:" }), 1077 " ",
1078 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200", children: [ 1079 slot.params.noteDuration, 1080 "s" 1081 ] }) 1082 ] }), 1083 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1084 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Bitrate:" }), 1085 " ", 1086 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200", children: slot.params.opusBitrate }) 1087 ] }), 1088 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1089 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Gain:" }), 1090 " ", 1091 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200", children: [ 1092 slot.params.gain, 1093 "x" 1094 ] }) 1095 ] }),
1096 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1097 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Fade-Out:" }), 1098 " ", 1099 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200", children: [ 1100 slot.params.fadeOutDuration, 1101 "s" 1102 ] }) 1103 ] }), 1104 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1105 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Reverb/Chorus:" }), 1106 " ", 1107 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200", children: [ 1108 slot.params.reverbLevel, 1109 "/", 1110 slot.params.chorusLevel 1111 ] }) 1112 ] }),
1113 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2", children: [ 1114 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Notes Range:" }), 1115 " ", 1116 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200 truncate block", title: slot.params.notes, children: slot.params.notes }) 1117 ] }), 1118 slot.outputSize !== void 0 && (() => { 1119 const noteMap = { 1120 "C": 0, 1121 "C#": 1, 1122 "DB": 1, 1123 "D": 2, 1124 "D#": 3, 1125 "EB": 3, 1126 "E": 4, 1127 "F": 5, 1128 "F#": 6, 1129 "GB": 6, 1130 "G": 7, 1131 "G#": 8, 1132 "AB": 8, 1133 "A": 9, 1134 "A#": 10, 1135 "BB": 10, 1136 "B": 11 1137 }; 1138 const parseSingle = (str) => { 1139 str = str.trim().toUpperCase(); 1140 if (/^\d+$/.test(str)) return parseInt(str, 10); 1141 const m = str.match(/^([A-G][B#]?)(-?\d+)$/); 1142 if (m) { 1143 const name = m[1]; 1144 const octave = parseInt(m[2], 10); 1145 return (octave + 1) * 12 + (noteMap[name] ?? 0); 1146 } 1147 return null; 1148 }; 1149 const parsed = /* @__PURE__ */ new Set(); 1150 if (slot.params.notes) { 1151 slot.params.notes.split(",").forEach((part) => { 1152 part = part.trim(); 1153 if (part.includes("-")) { 1154 const rangeParts = part.split("-"); 1155 if (rangeParts.length === 2) { 1156 const start = parseSingle(rangeParts[0]); 1157 const end = parseSingle(rangeParts[1]); 1158 if (start !== null && end !== null) { 1159 const minVal = Math.min(start, end); 1160 const maxVal = Math.max(start, end); 1161 for (let i = minVal; i <= maxVal; i++) { 1162 parsed.add(i); 1163 } 1164 } 1165 } 1166 } else { 1167 const val = parseSingle(part); 1168 if (val !== null) parsed.add(val); 1169 } 1170 }); 1171 } 1172 let sampledCount = 0; 1173 if (parsed.size === 0) { 1174 sampledCount = Math.ceil(128 / slot.params.skipStep); 1175 } else { 1176 const sortedList = Array.from(parsed).sort((a, b) => a - b); 1177 sortedList.forEach((midi, idx) => { 1178 if (idx % slot.params.skipStep === 0) sampledCount++; 1179 }); 1180 } 1181 const fullInstrumentNotes = Math.ceil(128 / slot.params.skipStep); 1182 const estimatedSize = slot.outputSize / Math.max(1, sampledCount) * fullInstrumentNotes; 1183 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1184 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2", children: [ 1185 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Output Size:" }), 1186 " ", 1187 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200 font-mono", children: formatFileSize(slot.outputSize) }) 1188 ] }), 1189 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2", children: [ 1190 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Est. Full Size:" }), 1191 " ", 1192 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-zinc-850 dark:text-zinc-200 font-mono", children: formatFileSize(estimatedSize) }) 1193 ] }) 1194 ] }); 1195 })(), 1196 slot.origin_node_id !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-2 mt-2 pt-2 border-t border-zinc-200 dark:border-zinc-700", children: [
1197 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-450 dark:text-zinc-500 uppercase", children: "Files Hosted On:" }), 1198 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 px-2 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 font-mono text-xs border border-zinc-200 dark:border-zinc-700", children: [ 1199 "Node ", 1200 slot.origin_node_id, 1201 " ", 1202 slot.origin_node_id === 1 ? "(HK)" : slot.origin_node_id === 2 ? "(JP)" : "" 1203 ] }) 1204 ] }) 1205 ] }), 1206 /* @__PURE__ */ jsxRuntimeExports.jsx( 1207 CompactTestKeyboard, 1208 { 1209 previewSf: `_test_${slot.jobId}`, 1210 notesString: slot.params.notes, 1211 skipStep: slot.params.skipStep, 1212 program: slot.params.program, 1213 isSelected, 1214 fileSizes: slot.fileSizes 1215 } 1216 ) 1217 ] }) 1218 ] 1219 }, 1220 slot.jobId 1221 ); 1222 }) }) 1223 ] }) 1224 ] }) 1225 ] }); 1226}; 1227export { 1228 SoundfontQualityLab 1229};
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.