PageSourceSearch

https://www.hamienet.com/assets/SoundfontQualityLab-B9zmg6dJ.js

js hamienet.com collected 2026-09-24 19:00:51 UTC 62,800 bytes, 1,229 lines download raw bytes

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.