1import { r as reactExports, j as jsxRuntimeExports, R as React } from "./vendor-react-Bay9Y7Lb.js"; 2import { a as api } from "./index-w5lXls15.js"; 3import { j as formatFileSize, l as GM_INSTRUMENTS } from "./utils-CEVb7nN1.js"; 4import { SoundfontQualityLab } from "./SoundfontQualityLab-BxeAHII3.js"; 5import { u as useSoundfontKeyboard, a as useKeyboardInput, d as getSoundfontName, r as renderSharedKeyboard, g as getMidiFromKeyboardClick } from "./SharedKeyboard-B8ktwnUd.js"; 6import { u as useTranslation } from "./vendor-i18n-BC7ZMXWQ.js"; 7import { bg as Music, cM as RefreshCw, cC as ChevronDown, bF as ChevronRight, bs as Zap, b1 as CircleAlert, b5 as Trash2, bM as Clock, bv as CircleCheckBig, cG as Keyboard, du as Folder, bi as Sparkles, cg as Activity, bA as Eye, bz as EyeOff, b3 as FileHeadphone, c9 as ArrowUpDown, aW as X, cd as Save, c_ as ChevronLeft } from "./vendor-D0P4XgNj.js"; 8import "./vendor-react-three-BCww3P1x.js"; 9import "./useMidiKeyboard-DlyEIN8T.js"; 10import "./wasm-synth-adapter-DpNGIIyM.js"; 11import "./vendor-spessasynth-DH8n6o_V.js"; 12const getBaseName = (name) => { 13 return name.replace(/(_JS|_HSF3Bank|_SF3Bank)(?:_.*)?$/i, "").replace(/(?:_test_\d*)?\.sf[23]$/i, ""); 14}; 15const getSoundfontFormatType = (lib) => { 16 const nameLower = lib.name.toLowerCase(); 17 if (lib.isHsf3Bank || nameLower.endsWith("_hsf3bank") || nameLower.endsWith("_sf3bank") || nameLower.endsWith(".hsf3")) { 18 return "HSF3"; 19 } 20 if (nameLower.endsWith(".sf3")) { 21 return "SF3"; 22 } 23 if (nameLower.endsWith(".sf2")) { 24 return "SF2"; 25 } 26 if (nameLower.endsWith(".sfark")) { 27 return "SFARK"; 28 } 29 return "JS"; 30}; 31const SoundfontKeyboardDock = ({ 32 selectedSf, 33 program, 34 setProgram, 35 bank, 36 setBank, 37 onClose, 38 gain 39}) => { 40 const processedInstruments = reactExports.useMemo(() => [{ program, midiChannel: bank === 128 ? 9 : 0 }], [program, bank]); 41 const { playNote, stopNote, stopAllNotes, audioContextRef, isLoading, loadingProgress } = useSoundfontKeyboard(0, processedInstruments, void 0, selectedSf, gain); 42 const canvasRef = reactExports.useRef(null); 43 const renderActiveNotesRef = reactExports.useRef(/* @__PURE__ */ new Set()); 44 const [baseOctave, setBaseOctave] = reactExports.useState(4); 45 const baseOctaveRef = reactExports.useRef(4); 46 const activeKeysRef = reactExports.useRef(/* @__PURE__ */ new Map()); 47 const mouseActiveKeyRef = reactExports.useRef(null); 48 const [labelMode, setLabelMode] = reactExports.useState("pc"); 49 const [velocity, setVelocity] = reactExports.useState(100); 50 const changeOctave = reactExports.useCallback((newOctave) => { 51 const bounded = Math.max(0, Math.min(8, newOctave)); 52 setBaseOctave(bounded); 53 baseOctaveRef.current = bounded; 54 }, []); 55 const handleKeyOn = reactExports.useCallback((midi) => { 56 if (audioContextRef.current && audioContextRef.current.state === "suspended") { 57 audioContextRef.current.resume().catch(() => { 58 }); 59 } 60 playNote(midi, void 0, velocity); 61 renderActiveNotesRef.current.add(midi); 62 }, [playNote, audioContextRef, velocity]); 63 const handleKeyOff = reactExports.useCallback((midi) => { 64 stopNote(midi); 65 renderActiveNotesRef.current.delete(midi); 66 }, [stopNote]); 67 useKeyboardInput({ 68 playNote: handleKeyOn, 69 stopNote: handleKeyOff, 70 baseOctaveRef, 71 activeKeysRef, 72 enabled: true, 73 onOctaveChange: setBaseOctave 74 }); 75 reactExports.useEffect(() => { 76 let animationFrameId; 77 const render = () => { 78 if (canvasRef.current) { 79 const canvas = canvasRef.current; 80 const ctx = canvas.getContext("2d"); 81 if (ctx) { 82 const width = canvas.width; 83 const height = canvas.height; 84 ctx.clearRect(0, 0, width, height); 85 renderSharedKeyboard( 86 ctx, 87 width, 88 height, 89 height, 90 renderActiveNotesRef.current, 91 labelMode, 92 baseOctaveRef.current 93 ); 94 } 95 } 96 animationFrameId = requestAnimationFrame(render); 97 }; 98 render(); 99 return () => cancelAnimationFrame(animationFrameId); 100 }, [labelMode]); 101 const handlePointerDown = (e) => { 102 e.preventDefault(); 103 const canvas = canvasRef.current; 104 if (!canvas) return; 105 const rect = canvas.getBoundingClientRect(); 106 const x = e.clientX - rect.left; 107 const y = e.clientY - rect.top; 108 const midi = getMidiFromKeyboardClick(x, y, rect.width, rect.height); 109 if (midi !== null) { 110 mouseActiveKeyRef.current = midi; 111 handleKeyOn(midi); 112 } 113 }; 114 const handlePointerMove = (e) => { 115 e.preventDefault(); 116 if (e.buttons !== 1) return; 117 const canvas = canvasRef.current; 118 if (!canvas) return; 119 const rect = canvas.getBoundingClientRect(); 120 const x = e.clientX - rect.left; 121 const y = e.clientY - rect.top; 122 const midi = getMidiFromKeyboardClick(x, y, rect.width, rect.height); 123 if (midi !== null && midi !== mouseActiveKeyRef.current) { 124 if (mouseActiveKeyRef.current !== null) { 125 handleKeyOff(mouseActiveKeyRef.current); 126 } 127 mouseActiveKeyRef.current = midi; 128 handleKeyOn(midi); 129 } 130 }; 131 const handlePointerUp = (e) => { 132 e.preventDefault(); 133 if (mouseActiveKeyRef.current !== null) { 134 handleKeyOff(mouseActiveKeyRef.current); 135 mouseActiveKeyRef.current = null; 136 } 137 }; 138 const isSF2 = selectedSf.toLowerCase().endsWith(".sf2"); 139 const isSF3 = selectedSf.toLowerCase().endsWith(".sf3"); 140 const isHSF3 = selectedSf.toLowerCase().endsWith("_hsf3bank") || selectedSf.toLowerCase().endsWith("_sf3bank"); 141 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed bottom-0 left-0 right-0 z-50 bg-zinc-900/95 backdrop-blur-md border-t border-zinc-700 text-white shadow-2xl p-3 flex flex-col gap-2", children: [
142 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4 text-xs", children: [ 143 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 144 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 145 /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { className: "w-4 h-4 text-indigo-400" }), 146 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-sm text-indigo-300", children: "Shared Keyboard" }) 147 ] }), 148 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 bg-black/20 px-2 py-1 rounded", children: [ 149 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-400 font-mono flex items-center gap-1", title: "Note Velocity (Loudness)", children: [ 150 /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { className: "w-3 h-3 text-amber-500" }), 151 "VEL:" 152 ] }),
153 /* @__PURE__ */ jsxRuntimeExports.jsx( 154 "input", 155 { 156 type: "range", 157 min: "1", 158 max: "127", 159 value: velocity, 160 onChange: (e) => setVelocity(parseInt(e.target.value) || 100), 161 onMouseUp: (e) => e.target.blur(), 162 onTouchEnd: (e) => e.target.blur(), 163 className: "w-20 accent-amber-500", 164 title: `Velocity: ${velocity}` 165 } 166 ), 167 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-amber-400 font-mono font-bold w-6 text-right", children: velocity }) 168 ] }), 169 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 bg-zinc-800/80 px-2.5 py-1 rounded-lg border border-zinc-700", children: [ 170 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-400 font-bold uppercase", children: "Soundfont:" }), 171 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold text-white max-w-[180px] truncate", title: selectedSf, children: selectedSf }),
172 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[9px] px-1.5 py-0.5 font-black rounded uppercase ${isHSF3 ? "bg-emerald-500/20 text-emerald-400" : isSF3 ? "bg-cyan-500/20 text-cyan-400" : isSF2 ? "bg-indigo-500/20 text-indigo-400" : "bg-green-500/20 text-green-400"}`, children: isHSF3 ? "HSF3" : isSF3 ? "SF3" : isSF2 ? "SF2" : "JS" }) 173 ] }), 174 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 bg-zinc-800/80 px-2.5 py-1 rounded-lg border border-zinc-700", children: [ 175 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-400 font-bold uppercase", children: "Gain:" }), 176 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono font-bold text-white max-w-[80px]", children: [ 177 gain ?? 1, 178 "x" 179 ] }) 180 ] }), 181 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 bg-zinc-800/80 px-2.5 py-1 rounded-lg border border-zinc-700", children: [
182 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-400 font-bold uppercase", children: "Bank:" }), 183 /* @__PURE__ */ jsxRuntimeExports.jsxs( 184 "select", 185 { 186 value: bank, 187 onChange: (e) => setBank(parseInt(e.target.value)), 188 className: "bg-zinc-900 text-zinc-200 border border-zinc-700 rounded px-2 py-0.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500 font-mono", 189 children: [ 190 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 0, children: "0 (Melodic)" }), 191 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: 128, children: "128 (Percussion)" }) 192 ] 193 } 194 ) 195 ] }), 196 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 bg-zinc-800/80 px-2.5 py-1 rounded-lg border border-zinc-700", children: [ 197 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-zinc-400 font-bold uppercase", children: [ 198 "Program (", 199 program, 200 "):" 201 ] }),
202 /* @__PURE__ */ jsxRuntimeExports.jsx( 203 "select", 204 { 205 value: program, 206 onChange: (e) => setProgram(parseInt(e.target.value)), 207 className: "bg-zinc-900 text-zinc-200 border border-zinc-700 rounded px-2 py-0.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500 font-mono max-w-[160px]", 208 children: Array.from({ length: 128 }).map((_, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: i, className: "text-zinc-200 bg-zinc-900", children: [ 209 i, 210 ": ", 211 getSoundfontName(i).replace(/_/g, " ") 212 ] }, i)) 213 } 214 ) 215 ] }) 216 ] }), 217 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 218 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 bg-zinc-800/80 px-2 py-0.5 rounded-lg border border-zinc-700", children: [ 219 /* @__PURE__ */ jsxRuntimeExports.jsx( 220 "button", 221 { 222 onClick: () => changeOctave(baseOctave - 1), 223 disabled: baseOctave <= 0, 224 className: "p-1 hover:bg-zinc-700 rounded text-zinc-300 disabled:opacity-30", 225 title: "Previous Octave (L-Shift)", 226 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronLeft, { className: "w-3.5 h-3.5" }) 227 } 228 ),
229 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-[11px] font-bold text-zinc-200 px-1", children: [ 230 "Oct ", 231 baseOctave, 232 " (C", 233 baseOctave, 234 "-B", 235 baseOctave, 236 ")" 237 ] }), 238 /* @__PURE__ */ jsxRuntimeExports.jsx( 239 "button", 240 { 241 onClick: () => changeOctave(baseOctave + 1), 242 disabled: baseOctave >= 8, 243 className: "p-1 hover:bg-zinc-700 rounded text-zinc-300 disabled:opacity-30", 244 title: "Next Octave (R-Shift)", 245 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "w-3.5 h-3.5" }) 246 } 247 ) 248 ] }), 249 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 bg-zinc-800/80 p-0.5 rounded-lg border border-zinc-700 text-[10px]", children: [ 250 /* @__PURE__ */ jsxRuntimeExports.jsx( 251 "button", 252 { 253 onClick: () => setLabelMode("pc"), 254 className: `px-2 py-0.5 rounded font-bold transition-colors ${labelMode === "pc" ? "bg-indigo-600 text-white" : "text-zinc-400 hover:text-white"}`, 255 children: "PC Keys" 256 } 257 ),
258 /* @__PURE__ */ jsxRuntimeExports.jsx( 259 "button", 260 { 261 onClick: () => setLabelMode("notes"), 262 className: `px-2 py-0.5 rounded font-bold transition-colors ${labelMode === "notes" ? "bg-indigo-600 text-white" : "text-zinc-400 hover:text-white"}`, 263 children: "Notes" 264 } 265 ), 266 /* @__PURE__ */ jsxRuntimeExports.jsx( 267 "button", 268 { 269 onClick: () => setLabelMode("off"), 270 className: `px-2 py-0.5 rounded font-bold transition-colors ${labelMode === "off" ? "bg-indigo-600 text-white" : "text-zinc-400 hover:text-white"}`, 271 children: "Off" 272 } 273 ) 274 ] }), 275 /* @__PURE__ */ jsxRuntimeExports.jsx( 276 "button", 277 { 278 onClick: stopAllNotes, 279 className: "px-2.5 py-1 bg-zinc-800 hover:bg-red-900/50 text-zinc-300 hover:text-red-300 border border-zinc-700 rounded-lg text-[10px] font-bold transition-colors", 280 title: "Stop all playing notes", 281 children: "Panic" 282 } 283 ),
284 /* @__PURE__ */ jsxRuntimeExports.jsx( 285 "button", 286 { 287 onClick: onClose, 288 className: "p-1 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors", 289 title: "Close Keyboard", 290 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "w-5 h-5" }) 291 } 292 ) 293 ] }) 294 ] }), 295 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative h-28 w-full border border-zinc-700 bg-zinc-950 rounded-lg overflow-hidden flex select-none touch-none shadow-inner", children: [ 296 isLoading && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center p-8 bg-zinc-800 rounded-lg animate-pulse absolute inset-0 z-10", children: [ 297 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-3", children: [ 298 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-6 h-6 animate-spin text-zinc-400" }), 299 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 font-mono text-sm", children: "Loading soundfont engine..." }) 300 ] }),
301 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-64 bg-zinc-900 rounded-full h-2 overflow-hidden border border-zinc-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 302 "div", 303 { 304 className: "bg-indigo-500 h-2 transition-all duration-300", 305 style: { width: `${Math.round(loadingProgress * 100)}%` } 306 } 307 ) }), 308 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-500 text-xs mt-2 font-mono", children: [ 309 Math.round(loadingProgress * 100), 310 "%" 311 ] }) 312 ] }), 313 /* @__PURE__ */ jsxRuntimeExports.jsx( 314 "canvas", 315 { 316 ref: canvasRef, 317 width: 1400, 318 height: 112, 319 className: "w-full h-full cursor-pointer", 320 onPointerDown: handlePointerDown, 321 onPointerMove: handlePointerMove, 322 onPointerUp: handlePointerUp, 323 onPointerLeave: handlePointerUp 324 } 325 ) 326 ] }), 327 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-400 text-center font-medium", children: "Play chords using PC Keyboard (Z X C V...) or click/drag keys. Press L-Shift / R-Shift to shift octaves." }) 328 ] }); 329}; 330const SoundfontManager = () => { 331 var _a; 332 const { t } = useTranslation(); 333 const [soundfonts, setSoundfonts] = reactExports.useState([]); 334 const [downloadedLibraries, setDownloadedLibraries] = reactExports.useState([]); 335 const [jobs, setJobs] = reactExports.useState([]); 336 const [loading, setLoading] = reactExports.useState(true); 337 const [activeSf, setActiveSf] = reactExports.useState(null); 338 const [expandedLibraries, setExpandedLibraries] = reactExports.useState(/* @__PURE__ */ new Set()); 339 const [expandedFolders, setExpandedFolders] = reactExports.useState(/* @__PURE__ */ new Set()); 340 const [customUrl, setCustomUrl] = reactExports.useState(""); 341 const [preRenderedSf, setPreRenderedSf] = reactExports.useState("GeneralUser-GS"); 342 const [metadata, setMetadata] = reactExports.useState({}); 343 const [defaultLiveSf, setDefaultLiveSf] = reactExports.useState("MusyngKite"); 344 const [defaultConverterSf, setDefaultConverterSf] = reactExports.useState("GeneralUser-GS.sf2"); 345 const [defaultWasmSf, setDefaultWasmSf] = reactExports.useState("SoundBlasterOld.sf2"); 346 const [defaultKeyboardSf, setDefaultKeyboardSf] = reactExports.useState("MusyngKite"); 347 const [defaultJsSf, setDefaultJsSf] = reactExports.useState("MusyngKite"); 348 const [showEngine, setShowEngine] = reactExports.useState(false); 349 const [showEngineOrder, setShowEngineOrder] = reactExports.useState(false); 350 const [showExternal, setShowExternal] = reactExports.useState(false); 351 const [showJobs, setShowJobs] = reactExports.useState(false); 352 const [showAssets, setShowAssets] = reactExports.useState(true); 353 const [showKeyboard, setShowKeyboard] = reactExports.useState(false); 354 const [selectedSfForKeyboard, setSelectedSfForKeyboard] = reactExports.useState(""); 355 const [keyboardProgram, setKeyboardProgram] = reactExports.useState(0); 356 const [keyboardBank, setKeyboardBank] = reactExports.useState(0); 357 const [engineOrder, setEngineOrder] = reactExports.useState({ 358 watchDesktop: ["opus", "singleopus", "wasmsynth", "synth", "fmsynth", "chiptune", "webmidi"], 359 watchMobile: ["singleopus", "opus", "wasmsynth", "chiptune", "fmsynth", "synth", "webmidi"], 360 slowMobile: ["singleopus", "chiptune", "wasmsynth", "fmsynth", "synth", "webmidi"], 361 miniPlayer: ["singleopus", "wasmsynth", "synth", "fmsynth", "chiptune", "webmidi"] 362 }); 363 const [profiles, setProfiles] = reactExports.useState({}); 364 const [autoLevelingLibs, setAutoLevelingLibs] = reactExports.useState({}); 365 const handleSaveGain = async (libName, newGain) => { 366 const baseName = getBaseName(libName); 367 const existing = profiles[baseName] || { 368 skipStep: 1, 369 noteDuration: 2, 370 opusBitrate: "64k", 371 gain: 1.8, 372 fadeOutDuration: 0.3, 373 reverbLevel: 0.5, 374 chorusLevel: 0.3, 375 notes: "60-72", 376 program: 0 377 }; 378 const nextProfiles = { 379 ...profiles, 380 [baseName]: { 381 ...existing, 382 gain: newGain, 383 savedAt: (/* @__PURE__ */ new Date()).toISOString() 384 } 385 }; 386 try { 387 await api.saveSystemSetting("SOUNDFONT_QUALITY_PROFILES", nextProfiles); 388 setProfiles(nextProfiles); 389 } catch (e) { 390 console.error("Failed to save soundfont gain profile", e); 391 } 392 }; 393 const handleAutoLevelForRow = async (libName) => { 394 const baseName = getBaseName(libName); 395 let targetSf2Path = libName.endsWith(".sf2") ? libName : null; 396 if (!targetSf2Path) { 397 const found = soundfonts.find((f) => { 398 var _a2; 399 return ((_a2 = f.path) == null ? void 0 : _a2.endsWith(".sf2")) && f.path.includes(baseName); 400 }); 401 if (found) targetSf2Path = found.path; 402 else targetSf2Path = `${baseName}.sf2`; 403 } 404 setAutoLevelingLibs((prev) => ({ ...prev, [libName]: true })); 405 try {
406 const res = await api.autoLevelSoundfont(targetSf2Path, 0, -18); 407 if (res.recommendedGain) { 408 const newGain = parseFloat(Math.min(10, Math.max(0.1, res.recommendedGain)).toFixed(2)); 409 await handleSaveGain(baseName, newGain); 410 } 411 } catch (e) { 412 alert(`Auto level failed: ${e.message || "Make sure the source .sf2 file exists in storage."}`); 413 } finally { 414 setAutoLevelingLibs((prev) => ({ ...prev, [libName]: false })); 415 } 416 }; 417 const [draggedItem, setDraggedItem] = reactExports.useState(null); 418 const handleDragStart = (context, index) => { 419 setDraggedItem({ type: "list", context, index }); 420 }; 421 const handleMatrixDragStart = (id) => { 422 setDraggedItem({ type: "matrix", id }); 423 }; 424 const handleRemoveListMode = (context, index) => { 425 const currentList = [...engineOrder[context]]; 426 currentList.splice(index, 1); 427 setEngineOrder((prev) => ({ 428 ...prev, 429 [context]: currentList 430 })); 431 }; 432 const handleDragOver = (e) => { 433 e.preventDefault(); 434 }; 435 const handleDrop = (context, targetIndex) => { 436 if (!draggedItem) return; 437 const currentList = [...engineOrder[context]]; 438 if (draggedItem.type === "matrix" && draggedItem.id) { 439 if (currentList.includes(draggedItem.id)) return; 440 currentList.splice(targetIndex, 0, draggedItem.id); 441 } else if (draggedItem.type === "list" && draggedItem.context === context && draggedItem.index !== void 0) { 442 const [movedItem] = currentList.splice(draggedItem.index, 1); 443 currentList.splice(targetIndex, 0, movedItem); 444 } else { 445 return; 446 } 447 setEngineOrder((prev) => ({ 448 ...prev, 449 [context]: currentList 450 })); 451 setDraggedItem(null); 452 }; 453 const saveEngineOrder = async (nextOrder) => { 454 try { 455 await api.saveSystemSetting("PLAYBACK_ENGINE_ORDER", nextOrder); 456 setEngineOrder(nextOrder); 457 alert("Playback engine priority order saved successfully!"); 458 } catch (e) { 459 alert("Failed to save engine order"); 460 } 461 }; 462 const ENGINE_DETAILS = { 463 opus: { label: "STEMS", color: "#ff5500", desc: "Instrument Stems (Opus)", condition: "Watch Page + Processed Stems" }, 464 singleopus: { label: "OPUS", color: "#3b82f6", desc: "Single-file Opus", condition: "Master Audio + Pitch = 0" }, 465 synth: { label: "JS", color: "#10b981", desc: "Web Audio JS SoundFont", condition: "JS Opus notes (Web Audio)" }, 466 wasmsynth: { label: "SF", color: "#06b6d4", desc: "Direct SF2/SF3 SoundFont (WASM)", condition: "WASM & AudioWorklet (HTTPS)" }, 467 fmsynth: { label: "FM", color: "#8b5cf6", desc: "16-bit FM Synth", condition: "Real-time FM synth (Low CPU)" }, 468 chiptune: { label: "8BIT", color: "#f59e0b", desc: "Chiptune 8-bit", condition: "Retro 8-bit waves (Ultra Low CPU)" }, 469 webmidi: { label: "MIDI", color: "#6366f1", desc: "Web MIDI Output", condition: "Web MIDI API + Connected Device" } 470 }; 471 const renderEngineOrderEditor = () => { 472 const contexts = [ 473 { id: "watchDesktop", name: "Watch Page (Desktop)", desc: "Preferred playback engine layout on desktop watch page." }, 474 { id: "watchMobile", name: "Watch Page (Mobile)", desc: "Preferred playback engine layout on mobile watch page." }, 475 { id: "slowMobile", name: "Slow / Low-End Devices", desc: "Fallback priority on low-end CPUs or slow network connections." }, 476 { id: "miniPlayer", name: "Mini Player (Previews)", desc: "Preferred playback engine layout for home/search previews." } 477 ]; 478 const resetToDefault = (context) => { 479 const defaults = { 480 watchDesktop: ["opus", "singleopus", "synth", "wasmsynth", "fmsynth", "chiptune", "webmidi"], 481 watchMobile: ["singleopus", "opus", "chiptune", "wasmsynth", "fmsynth", "synth", "webmidi"], 482 slowMobile: ["singleopus", "chiptune", "wasmsynth", "fmsynth", "synth", "webmidi"], 483 miniPlayer: ["singleopus", "synth", "wasmsynth", "fmsynth", "chiptune", "webmidi"] 484 }; 485 setEngineOrder((prev) => ({ 486 ...prev, 487 [context]: defaults[context] 488 })); 489 }; 490 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 mb-6", children: [
491 /* @__PURE__ */ jsxRuntimeExports.jsx( 492 "div", 493 { 494 onClick: () => setShowEngineOrder(!showEngineOrder), 495 className: `p-4 ${showEngineOrder ? "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`, 496 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 497 showEngineOrder ? /* @__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" }), 498 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 499 /* @__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: [ 500 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowUpDown, { className: "w-4 h-4 text-indigo-500" }), 501 "Playback Engine Priority Order & Rules" 502 ] }),
503 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-zinc-500 mt-0.5", children: "Drag and drop cards within each context column to rearrange selection priority. The engine at the top will be selected first if its prerequisite condition is satisfied." }) 504 ] }) 505 ] }) 506 } 507 ), 508 showEngineOrder && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-5 space-y-6", children: [ 509 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4", children: contexts.map((ctx) => { 510 const engines = engineOrder[ctx.id] || []; 511 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50/50 dark:bg-zinc-950/20 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4 flex flex-col h-[520px]", children: [ 512 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-3 flex justify-between items-center", children: [ 513 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
514 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-xs text-zinc-800 dark:text-zinc-200 uppercase tracking-tight", children: ctx.name }), 515 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[9px] text-zinc-400 mt-0.5 leading-tight", children: ctx.desc }) 516 ] }), 517 /* @__PURE__ */ jsxRuntimeExports.jsx( 518 "button", 519 { 520 onClick: () => resetToDefault(ctx.id), 521 className: "text-[9px] font-bold text-zinc-400 hover:text-indigo-500 transition-colors uppercase shrink-0", 522 children: "Reset" 523 } 524 ) 525 ] }), 526 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto space-y-2 custom-scrollbar pr-1", children: engines.map((engineId, idx) => { 527 const details = ENGINE_DETAILS[engineId] || { label: engineId.toUpperCase(), color: "#71717a", desc: "Unknown Engine", condition: "None" }; 528 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 529 "div", 530 { 531 draggable: true, 532 onDragStart: () => handleDragStart(ctx.id, idx), 533 onDragOver: handleDragOver, 534 onDrop: () => handleDrop(ctx.id, idx), 535 className: "flex flex-col gap-1.5 p-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 hover:border-indigo-500/50 rounded-lg cursor-grab active:cursor-grabbing transition-all select-none shadow-sm group hover:scale-[1.01] relative", 536 children: [
537 /* @__PURE__ */ jsxRuntimeExports.jsx( 538 "button", 539 { 540 onClick: (e) => { 541 e.stopPropagation(); 542 handleRemoveListMode(ctx.id, idx); 543 }, 544 className: "absolute -top-2 -right-2 bg-zinc-200 dark:bg-zinc-700 hover:bg-red-500 hover:text-white dark:hover:bg-red-500 rounded-full p-1 shadow-sm opacity-0 group-hover:opacity-100 transition-opacity", 545 title: "Remove Engine", 546 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "w-2.5 h-2.5" }) 547 } 548 ), 549 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 550 /* @__PURE__ */ jsxRuntimeExports.jsx( 551 "div", 552 { 553 className: "font-mono text-[8px] font-black px-1.5 py-0.5 rounded border tracking-widest shrink-0 text-center min-w-[38px] bg-zinc-950", 554 style: { 555 color: details.color, 556 textShadow: `0 0 3px ${details.color}` 557 }, 558 children: details.label 559 } 560 ),
561 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col min-w-0 flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-700 dark:text-zinc-300 truncate", children: details.desc }) }), 562 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[8px] text-zinc-400 font-mono shrink-0", children: [ 563 "#", 564 idx + 1 565 ] }) 566 ] }), 567 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-[8px] text-zinc-500 dark:text-zinc-400 bg-zinc-100 dark:bg-zinc-950 px-1.5 py-0.5 rounded font-mono", children: [ 568 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "truncate", children: [ 569 "â¡ ", 570 details.condition 571 ] }), 572 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowUpDown, { className: "w-2.5 h-2.5 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity" }) 573 ] }) 574 ] 575 }, 576 engineId 577 ); 578 }) }) 579 ] }, ctx.id); 580 }) }),
581 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950/40 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4 space-y-3 mt-6", children: [ 582 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "font-bold text-xs uppercase tracking-wider text-zinc-700 dark:text-zinc-300 flex items-center gap-2", children: [ 583 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { className: "w-3.5 h-3.5 text-indigo-500" }), 584 "Playback Engine Prerequisite & Fallback Matrix" 585 ] }), 586 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-zinc-500 mb-2", children: "Drag an item from below into a column above to add it to the priority list." }), 587 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 text-[10px]", children: [ 588 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { draggable: true, onDragStart: () => handleMatrixDragStart("opus"), className: "p-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg space-y-1 cursor-grab active:cursor-grabbing hover:border-indigo-500/50 transition-colors", children: [
589 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-amber-500 block", children: "ðµ STEMS (Opus Multi-Track)" }), 590 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500", children: "Requires track to be fully processed with individual stem files available. Only supported on Watch Page." }) 591 ] }), 592 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { draggable: true, onDragStart: () => handleMatrixDragStart("singleopus"), className: "p-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg space-y-1 cursor-grab active:cursor-grabbing hover:border-indigo-500/50 transition-colors", children: [ 593 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-blue-400 block", children: "ð§ OPUS (Single Pre-mixed)" }), 594 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500", children: "Requires pre-rendered master audio. Automatically bypassed if user applies Pitch Shift â 0." }) 595 ] }),
596 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { draggable: true, onDragStart: () => handleMatrixDragStart("wasmsynth"), className: "p-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg space-y-1 cursor-grab active:cursor-grabbing hover:border-indigo-500/50 transition-colors", children: [ 597 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-cyan-400 block", children: "ð¹ SF (Direct WASM SoundFont)" }), 598 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500", children: "Synthesizes raw .sf2 or .sf3 files in browser via WASM AudioWorklet. Requires HTTPS/localhost context." }) 599 ] }), 600 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { draggable: true, onDragStart: () => handleMatrixDragStart("synth"), className: "p-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg space-y-1 cursor-grab active:cursor-grabbing hover:border-indigo-500/50 transition-colors", children: [
601 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-emerald-400 block", children: "â¡ JS (Web Audio JS SoundFont)" }), 602 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500", children: "Decodes pre-rendered Opus note samples per instrument. Works on plain HTTP. Heavy CPU on mobile." }) 603 ] }), 604 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { draggable: true, onDragStart: () => handleMatrixDragStart("fmsynth"), className: "p-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg space-y-1 cursor-grab active:cursor-grabbing hover:border-indigo-500/50 transition-colors", children: [ 605 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-purple-400 block", children: "ð» FM (16-bit Arcade FM)" }), 606 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500", children: "Generates retro FM synthesis in real time. Works everywhere with minimal CPU overhead." }) 607 ] }),
608 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { draggable: true, onDragStart: () => handleMatrixDragStart("chiptune"), className: "p-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg space-y-1 cursor-grab active:cursor-grabbing hover:border-indigo-500/50 transition-colors", children: [ 609 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-yellow-400 block", children: "ð¹ï¸ 8BIT (Chiptune Waves)" }), 610 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500", children: "Ultra-light 8-bit square/triangle waves. Recommended for slow devices or weak connections." }) 611 ] }), 612 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { draggable: true, onDragStart: () => handleMatrixDragStart("webmidi"), className: "p-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg space-y-1 cursor-grab active:cursor-grabbing hover:border-indigo-500/50 transition-colors", children: [
613 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-indigo-400 block", children: "ð MIDI (Web MIDI Output)" }), 614 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500", children: "Sends raw MIDI signals to external hardware or loopback MIDI devices. Requires browser support." }) 615 ] }) 616 ] }) 617 ] }), 618 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end pt-2", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 619 "button", 620 { 621 onClick: () => saveEngineOrder(engineOrder), 622 className: "px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold rounded-lg transition-colors flex items-center gap-1.5 shadow-sm", 623 children: [ 624 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { className: "w-3.5 h-3.5" }), 625 "Save Priority Order" 626 ] 627 } 628 ) }), 629 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950/40 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4 space-y-3 mt-6", children: [
630 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "font-bold text-xs uppercase tracking-wider text-zinc-700 dark:text-zinc-300 flex items-center gap-2", children: [ 631 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { className: "w-3.5 h-3.5 text-emerald-500" }), 632 "Automatic JS Engine Fallback Configuration" 633 ] }), 634 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-zinc-500 mb-2", children: "When a track falls back to the JS (Web Audio) engine, but the active soundfont does not have JS files available, the player will automatically try to load the fallback soundfont selected below." }), 635 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-w-md", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 636 "select", 637 { 638 value: defaultJsSf || "MusyngKite", 639 onChange: (e) => saveGlobalSetting("DEFAULT_JS_SOUNDFONT", e.target.value), 640 className: "w-full text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3 py-2 text-xs outline-none focus:ring-2 focus:ring-indigo-500 mb-1", 641 children: downloadedLibraries.filter((lib) => lib.name !== "Root").sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base", numeric: true })).map((lib) => { 642 const typeSuffix = getSoundfontFormatType(lib); 643 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 644 "option", 645 { 646 value: lib.name, 647 className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900", 648 children: [ 649 lib.name, 650 " (", 651 typeSuffix, 652 ") (", 653 formatFileSize(lib.size), 654 ")" 655 ] 656 }, 657 lib.name 658 ); 659 }) 660 } 661 ) }) 662 ] }) 663 ] }) 664 ] }); 665 }; 666 const FALLBACK_METADATA = { 667 "MusyngKite": { name: "MusyngKite", desc: "High Fidelity" }, 668 "FluidR3_GM": { name: "FluidR3 GM", desc: "Orchestral" }, 669 "FatBoy": { name: "FatBoy", desc: "Punchy/Modern" }, 670 "Chaos": { name: "Chaos", desc: "Aggressive/Raw" }, 671 "GeneralUser-GS": { name: "GeneralUser GS", desc: "Balanced" }, 672 "GravisUltraSound": { name: "Gravis UltraSound", desc: "Retro/Classic" }, 673 "MuseScore_General": { name: "MuseScore General", desc: "Balanced/Classical" }, 674 "TimGM6mb": { name: "TimGM6mb", desc: "Ultralight" }, 675 "Aspirin": { name: "Aspirin DX", desc: "Warm/Balanced" } 676 }; 677 const presets = [ 678 { name: "Chaos (SF2)", url: "https://github.com/surikov/webaudiofontdata/raw/master/sf2/chaos.sf2" }, 679 { name: "GeneralUser GS (SF2)", url: "https://github.com/surikov/webaudiofontdata/raw/master/sf2/GeneralUserGS.sf2" }, 680 { name: "MuseScore General (SF2)", url: "https://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General.sf2" }, 681 { name: "FluidR3 GM (SF2)", url: "https://github.com/fhunleth/midi_synth/releases/download/v0.1.0/FluidR3_GM.sf2" }, 682 { name: "TimGM6mb (SF2)", url: "https://github.com/craffel/pretty-midi/raw/main/pretty_midi/TimGM6mb.sf2" }, 683 { name: "Aspirin (SF2)", url: "https://github.com/surikov/webaudiofontdata/raw/master/sf2/Aspirin.sf2" } 684 ]; 685 const toggleLibrary = (baseName) => { 686 setExpandedLibraries((prev) => { 687 const next = new Set(prev); 688 if (next.has(baseName)) next.delete(baseName); 689 else next.add(baseName); 690 return next; 691 }); 692 }; 693 const toggleFolder = (folderName, e) => { 694 e.stopPropagation(); 695 setExpandedFolders((prev) => { 696 const next = new Set(prev); 697 if (next.has(folderName)) next.delete(folderName); 698 else next.add(folderName); 699 return next; 700 }); 701 }; 702 const fetchData = async (force = false) => { 703 setLoading(true); 704 try { 705 const [sfData, jobData, preSet, metaSet, defaultLiveSet, defaultConverterSet, defaultWasmSet, defaultKeyboardSet, defaultJsSet, profileSet, engineOrderSet] = await Promise.all([ 706 api.getSoundfonts({ scan: force }), 707 api.getSoundfontJobs(), 708 api.getSystemSetting("PRE_RENDERED_SOUNDFONT"), 709 api.getSystemSetting("SOUNDFONT_METADATA"), 710 api.getSystemSetting("DEFAULT_LIVE_SOUNDFONT"), 711 api.getSystemSetting("DEFAULT_CONVERTER_SOUNDFONT"), 712 api.getSystemSetting("DEFAULT_WASM_SOUNDFONT"), 713 api.getSystemSetting("DEFAULT_KEYBOARD_SOUNDFONT"), 714 api.getSystemSetting("DEFAULT_JS_SOUNDFONT"), 715 api.getSystemSetting("SOUNDFONT_QUALITY_PROFILES"), 716 api.getSystemSetting("PLAYBACK_ENGINE_ORDER") 717 ]); 718 setSoundfonts(sfData.files || []); 719 setDownloadedLibraries(sfData.libraries || []); 720 if (sfData.libraries && sfData.libraries.length > 0) { 721 setSelectedSfForKeyboard((prev) => prev || sfData.libraries[0].name); 722 const allBaseNames = new Set(sfData.libraries.map((lib) => getBaseName(lib.name))); 723 setExpandedLibraries((prev) => prev.size === 0 ? allBaseNames : prev); 724 }
725 setActiveSf(sfData.active); 726 setJobs(jobData); 727 if (preSet) setPreRenderedSf(preSet.value); 728 if (defaultLiveSet) setDefaultLiveSf(defaultLiveSet.value || "MusyngKite"); 729 if (defaultConverterSet) setDefaultConverterSf(defaultConverterSet.value || "GeneralUser-GS.sf2"); 730 if (defaultWasmSet) setDefaultWasmSf(defaultWasmSet.value || "SoundBlasterOld.sf2"); 731 if (defaultKeyboardSet) setDefaultKeyboardSf(defaultKeyboardSet.value || "MusyngKite"); 732 if (defaultJsSet) setDefaultJsSf(defaultJsSet.value || "MusyngKite"); 733 if (profileSet && profileSet.value) { 734 try { 735 setProfiles(typeof profileSet.value === "string" ? JSON.parse(profileSet.value) : profileSet.value); 736 } catch { 737 } 738 } 739 if (engineOrderSet && engineOrderSet.value) { 740 try { 741 const parsed = typeof engineOrderSet.value === "string" ? JSON.parse(engineOrderSet.value) : engineOrderSet.value; 742 const defaults = { 743 watchDesktop: ["opus", "singleopus", "wasmsynth", "synth", "fmsynth", "chiptune", "webmidi"], 744 watchMobile: ["singleopus", "opus", "wasmsynth", "chiptune", "fmsynth", "synth", "webmidi"], 745 slowMobile: ["singleopus", "chiptune", "wasmsynth", "fmsynth", "synth", "webmidi"], 746 miniPlayer: ["singleopus", "wasmsynth", "synth", "fmsynth", "chiptune", "webmidi"] 747 }; 748 setEngineOrder({ ...defaults, ...parsed || {} }); 749 } catch { 750 } 751 } 752 if (metaSet && metaSet.value) { 753 try { 754 const parsed = typeof metaSet.value === "string" ? JSON.parse(metaSet.value) : metaSet.value; 755 const nextMeta = { ...parsed || {} }; 756 Object.keys(FALLBACK_METADATA).forEach((key) => { 757 if (!nextMeta[key]) { 758 nextMeta[key] = { ...FALLBACK_METADATA[key], visible: true, enabledWasm: true, access: "free" }; 759 } 760 }); 761 setMetadata(nextMeta); 762 } catch (e) { 763 console.error("Metadata JSON corrupt:", e); 764 const fallback = { ...FALLBACK_METADATA }; 765 setMetadata(fallback); 766 } 767 } else if (metaSet) { 768 const fallback = { ...FALLBACK_METADATA }; 769 setMetadata(fallback); 770 } else { 771 console.warn("Failed to fetch SOUNDFONT_METADATA. Keeping current local state."); 772 } 773 } catch (e) { 774 console.error("Failed to fetch soundfonts:", e); 775 } finally { 776 setLoading(false); 777 } 778 }; 779 const fetchJobsOnly = async () => { 780 try { 781 const jobData = await api.getSoundfontJobs(); 782 const hadActiveJobs = jobs.some((j) => j.status === "pending" || j.status === "processing"); 783 const hasActiveJobsNow = jobData.some((j) => j.status === "pending" || j.status === "processing" || j.status === "running"); 784 setJobs(jobData); 785 if (hadActiveJobs && !hasActiveJobsNow) { 786 fetchData(); 787 } 788 } catch (e) { 789 console.error("Failed to fetch jobs:", e); 790 } 791 }; 792 const saveGlobalSetting = async (key, value) => { 793 try { 794 await api.saveSystemSetting(key, value); 795 fetchData(); 796 } catch (e) { 797 alert("Failed to save setting"); 798 } 799 }; 800 reactExports.useEffect(() => { 801 fetchData(); 802 }, []); 803 reactExports.useEffect(() => { 804 const hasActiveJobs = jobs.some((j) => j.status === "pending" || j.status === "processing"); 805 if (!hasActiveJobs) return; 806 const interval = setInterval(fetchJobsOnly, 5e3); 807 return () => clearInterval(interval); 808 }, [jobs]); 809 const clearFinishedJobs = async () => { 810 const finished = jobs.filter((j) => j.status === "completed"); 811 if (finished.length === 0) return; 812 try { 813 await Promise.all(finished.map((j) => api.deleteSoundfontJob(j.id))); 814 fetchData(); 815 } catch (e) { 816 alert("Failed to clear jobs"); 817 } 818 }; 819 const startSf2Conversion = async (sfPath) => { 820 if (!confirm("Generating a JS library from an SF2 takes 5-15 minutes and can use significant storage (up to 400MB+). Continue?")) return; 821 try { 822 await api.startSoundfontJob(`Sample SF2 to JS: ${sfPath.split("/").pop()}`, sfPath, "SF2_TO_JS"); 823 fetchData(); 824 } catch (e) { 825 alert("Failed to start SF2 sampling"); 826 } 827 }; 828 const startHsf3BankConversion = async (sfPath) => { 829 if (!confirm("Convert this SF2 to HSF3 Bank (Split instrument files + DRM obfuscated)?")) return; 830 try { 831 await api.startSoundfontJob(`Convert SF2 â HSF3 Bank: ${sfPath.split("/").pop()}`, sfPath, "SF2_TO_HSF3_BANK"); 832 fetchData(); 833 } catch (e) { 834 alert("Failed to start HSF3 Bank conversion"); 835 } 836 }; 837 const startSf3Conversion = async (sfPath) => { 838 if (!confirm("Convert this SF2 to SF3 (OGG Vorbis compressed)? The output will be ~90% smaller than the original SF2.")) return; 839 try { 840 await api.startSoundfontJob(`Convert SF2 â SF3: ${sfPath.split("/").pop()}`, sfPath, "SF2_TO_SF3"); 841 fetchData(); 842 } catch (e) { 843 alert("Failed to start SF3 conversion"); 844 } 845 }; 846 const startBulkSf2Conversion = async () => { 847 const sf2Libs = downloadedLibraries.filter((lib) => lib.name.toLowerCase().endsWith(".sf2")); 848 if (sf2Libs.length === 0) { 849 alert("No .sf2 soundfonts found to convert."); 850 return; 851 } 852 const existingJsFolders = new Set( 853 downloadedLibraries.filter((lib) => !lib.name.toLowerCase().endsWith(".sf2") && !lib.name.toLowerCase().endsWith(".sf3") && !lib.name.toLowerCase().endsWith(".sfark")).map((lib) => lib.name.toLowerCase()) 854 ); 855 const unconvertedLibs = sf2Libs.filter((lib) => !existingJsFolders.has(lib.name.toLowerCase().replace(/\.sf2$/, ""))); 856 const alreadyConvertedCount = sf2Libs.length - unconvertedLibs.length; 857 let targetsToQueue = unconvertedLibs; 858 if (alreadyConvertedCount > 0) { 859 if (unconvertedLibs.length === 0) { 860 const reConvert = confirm(`All ${sf2Libs.length} .sf2 soundfont(s) have already been sampled to JS. 861 862Do you want to FORCE re-sample all ${sf2Libs.length} soundfonts?`); 863 if (!reConvert) return; 864 targetsToQueue = sf2Libs; 865 } else { 866 const msg = `Found ${sf2Libs.length} .sf2 soundfont(s): 867⢠${unconvertedLibs.length} pending JS sampling 868⢠${alreadyConvertedCount} already sampled to JS 869
870Click OK to sample ONLY the ${unconvertedLibs.length} pending soundfonts. 871(Skipping the ${alreadyConvertedCount} already-sampled files)`; 872 if (!confirm(msg)) return; 873 targetsToQueue = unconvertedLibs; 874 } 875 } else { 876 if (!confirm(`Are you sure you want to queue ${targetsToQueue.length} .sf2 soundfont(s) for JS conversion? This may take time.`)) return; 877 } 878 let successCount = 0; 879 let failCount = 0; 880 for (const lib of targetsToQueue) { 881 try { 882 await api.startSoundfontJob(`Sample SF2 to JS: ${lib.name.split("/").pop()}`, lib.name, "SF2_TO_JS"); 883 successCount++; 884 } catch (e) { 885 console.error(`Failed to queue ${lib.name}`, e); 886 failCount++; 887 } 888 } 889 fetchData(); 890 if (failCount > 0) { 891 alert(`Queued ${successCount} jobs, but failed to queue ${failCount}.`); 892 } else { 893 alert(`Successfully queued ${successCount} conversion jobs.`); 894 } 895 }; 896 const startBulkSf3Conversion = async () => { 897 const sf2Libs = downloadedLibraries.filter((lib) => lib.name.toLowerCase().endsWith(".sf2")); 898 if (sf2Libs.length === 0) { 899 alert("No .sf2 soundfonts found to convert."); 900 return; 901 } 902 const existingSf3s = new Set( 903 downloadedLibraries.filter((lib) => lib.name.toLowerCase().endsWith(".sf3")).map((lib) => lib.name.toLowerCase().replace(/\.sf3$/, "")) 904 ); 905 const unconvertedLibs = sf2Libs.filter((lib) => !existingSf3s.has(lib.name.toLowerCase().replace(/\.sf2$/, ""))); 906 const alreadyConvertedCount = sf2Libs.length - unconvertedLibs.length; 907 let targetsToQueue = unconvertedLibs; 908 if (alreadyConvertedCount > 0) { 909 if (unconvertedLibs.length === 0) { 910 const reConvert = confirm(`All ${sf2Libs.length} .sf2 soundfont(s) have already been converted to SF3. 911 912Do you want to FORCE re-convert all ${sf2Libs.length} soundfonts?`); 913 if (!reConvert) return; 914 targetsToQueue = sf2Libs; 915 } else { 916 const msg = `Found ${sf2Libs.length} .sf2 soundfont(s): 917⢠${unconvertedLibs.length} pending conversion 918⢠${alreadyConvertedCount} already converted to SF3 919 920Click OK to convert ONLY the ${unconvertedLibs.length} pending soundfonts. 921(Skipping the ${alreadyConvertedCount} already-converted files)`; 922 if (!confirm(msg)) return; 923 targetsToQueue = unconvertedLibs; 924 } 925 } else { 926 if (!confirm(`Convert ${targetsToQueue.length} .sf2 soundfont(s) to SF3? This compresses samples with OGG Vorbis (~90% smaller).`)) return; 927 } 928 let successCount = 0; 929 let failCount = 0; 930 for (const lib of targetsToQueue) { 931 try { 932 await api.startSoundfontJob(`Convert SF2 â SF3: ${lib.name.split("/").pop()}`, lib.name, "SF2_TO_SF3"); 933 successCount++; 934 } catch (e) { 935 console.error(`Failed to queue ${lib.name}`, e); 936 failCount++; 937 } 938 } 939 fetchData(); 940 if (failCount > 0) { 941 alert(`Queued ${successCount} jobs, but failed to queue ${failCount}.`); 942 } else { 943 alert(`Successfully queued ${successCount} SF3 conversion jobs.`); 944 } 945 }; 946 const startBulkHsf3BankConversion = async () => { 947 const sf2Libs = downloadedLibraries.filter((lib) => lib.name.toLowerCase().endsWith(".sf2")); 948 if (sf2Libs.length === 0) { 949 alert("No .sf2 soundfonts found to convert."); 950 return; 951 } 952 const existingHsf3s = new Set( 953 downloadedLibraries.filter((lib) => lib.isHsf3Bank || lib.name.toLowerCase().endsWith("_hsf3bank")).map((lib) => lib.name.toLowerCase().replace(/_hsf3bank$/, "")) 954 ); 955 const unconvertedLibs = sf2Libs.filter((lib) => !existingHsf3s.has(lib.name.toLowerCase().replace(/\.sf2$/, ""))); 956 const alreadyConvertedCount = sf2Libs.length - unconvertedLibs.length; 957 let targetsToQueue = unconvertedLibs; 958 if (alreadyConvertedCount > 0) { 959 if (unconvertedLibs.length === 0) { 960 const reConvert = confirm(`All ${sf2Libs.length} .sf2 soundfont(s) have already been converted to HSF3 Banks. 961 962Do you want to FORCE re-convert all ${sf2Libs.length} soundfonts?`); 963 if (!reConvert) return; 964 targetsToQueue = sf2Libs; 965 } else { 966 const overwrite = confirm(`Found ${sf2Libs.length} .sf2 soundfont(s): 967⢠${unconvertedLibs.length} pending conversion 968⢠${alreadyConvertedCount} already converted to HSF3 Banks 969 970Do you want to FORCE RE-CONVERT ALL ${sf2Libs.length} soundfonts (including existing)? 971 972Click OK to overwrite all. 973Click Cancel to proceed with ONLY the ${unconvertedLibs.length} pending ones.`); 974 if (overwrite) { 975 targetsToQueue = sf2Libs; 976 } else { 977 const proceed = confirm(`Proceeding with ONLY the ${unconvertedLibs.length} pending soundfonts. Are you sure?`); 978 if (!proceed) return; 979 targetsToQueue = unconvertedLibs; 980 } 981 } 982 } else { 983 if (!confirm(`Convert ${targetsToQueue.length} .sf2 soundfont(s) to HSF3 Banks? This creates split DRM obfuscated instrument files.`)) return; 984 } 985 let successCount = 0; 986 let failCount = 0; 987 for (const lib of targetsToQueue) {
988 try { 989 await api.startSoundfontJob(`Convert SF2 â HSF3 Bank: ${lib.name.split("/").pop()}`, lib.name, "SF2_TO_HSF3_BANK"); 990 successCount++; 991 } catch (e) { 992 console.error(`Failed to queue ${lib.name}`, e); 993 failCount++; 994 } 995 } 996 fetchData(); 997 if (failCount > 0) { 998 alert(`Queued ${successCount} jobs, but failed to queue ${failCount}.`); 999 } else { 1000 alert(`Successfully queued ${successCount} HSF3 conversion jobs.`); 1001 } 1002 }; 1003 const saveTimeoutRef = reactExports.useRef(null); 1004 const pendingSaveRef = reactExports.useRef(null); 1005 const saveMetadataBulk = (ids, updates) => { 1006 setMetadata((prev) => { 1007 const nextMeta = { ...prev || {} }; 1008 for (const id of ids) { 1009 const current = nextMeta[id] || { name: id, desc: "" }; 1010 nextMeta[id] = { ...current, ...updates }; 1011 } 1012 pendingSaveRef.current = nextMeta; 1013 return nextMeta; 1014 }); 1015 if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); 1016 saveTimeoutRef.current = setTimeout(() => { 1017 if (pendingSaveRef.current) { 1018 api.saveSystemSetting("SOUNDFONT_METADATA", pendingSaveRef.current).catch((e) => { 1019 console.error("Failed to save metadata bulk:", e); 1020 }); 1021 } 1022 }, 500); 1023 }; 1024 const saveMetadata = (id, updates) => { 1025 console.log(`[SoundfontManager] saveMetadata called for id: ${id}`, updates); 1026 setMetadata((prev) => { 1027 const current = prev[id] || { name: id, desc: "" }; 1028 const updated = { ...current, ...updates }; 1029 console.log(`[SoundfontManager] Previous state for ${id}:`, current); 1030 console.log(`[SoundfontManager] Updated state for ${id}:`, updated); 1031 const nextMeta = { ...prev || {}, [id]: updated }; 1032 pendingSaveRef.current = nextMeta; 1033 return nextMeta; 1034 }); 1035 console.log(`[SoundfontManager] Scheduling save for latest pending state.`); 1036 if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); 1037 saveTimeoutRef.current = setTimeout(() => { 1038 if (pendingSaveRef.current) { 1039 console.log(`[SoundfontManager] Executing save timeout. Payload being sent:`, pendingSaveRef.current); 1040 api.saveSystemSetting("SOUNDFONT_METADATA", pendingSaveRef.current).then(() => { 1041 console.log(`[SoundfontManager] Save successful.`); 1042 }).catch((e) => { 1043 console.error("Failed to save metadata:", e); 1044 alert(`Failed to save metadata: ${(e == null ? void 0 : e.message) || e}`); 1045 }); 1046 } 1047 }, 500); 1048 }; 1049 const handleDeleteSoundfont = async (filename) => { 1050 if (!confirm(`Are you sure you want to delete ${filename}? This cannot be undone.`)) return; 1051 try { 1052 await api.deleteSoundfont(filename); 1053 fetchData(); 1054 } catch (e) { 1055 alert("Failed to delete soundfont"); 1056 } 1057 }; 1058 const startDownload = async (libName) => { 1059 const isUrl = libName == null ? void 0 : libName.startsWith("http"); 1060 const displayName = isUrl ? libName.split("/").pop() : libName; 1061 try { 1062 await api.startSoundfontJob(`Download ${displayName}`, libName, "DOWNLOAD"); 1063 fetchData(); 1064 } catch (e) { 1065 alert("Failed to start download"); 1066 } 1067 }; 1068 const deleteJob = async (id) => { 1069 try { 1070 await api.deleteSoundfontJob(id); 1071 fetchData(); 1072 } catch (e) { 1073 alert("Failed to delete job"); 1074 } 1075 }; 1076 const groupedLibraries = reactExports.useMemo(() => { 1077 const groups = {}; 1078 for (const lib of downloadedLibraries) { 1079 if (lib.name === "Root") continue; 1080 const baseName = getBaseName(lib.name); 1081 if (!groups[baseName]) { 1082 groups[baseName] = { baseName, formats: [], totalSize: 0 }; 1083 } 1084 groups[baseName].formats.push(lib); 1085 groups[baseName].totalSize += lib.size || 0; 1086 if (!groups[baseName].createdAt || lib.createdAt && new Date(lib.createdAt) > new Date(groups[baseName].createdAt)) { 1087 groups[baseName].createdAt = lib.createdAt; 1088 } 1089 if (lib.name.toLowerCase().endsWith(".sf2")) groups[baseName].sf2 = lib; 1090 else if (lib.name.toLowerCase().endsWith(".sf3")) groups[baseName].sf3 = lib; 1091 else if (lib.name.endsWith("_JS")) groups[baseName].js = lib; 1092 else if (lib.name.endsWith("_HSF3Bank") || lib.name.endsWith("_SF3Bank")) groups[baseName].hsf3 = lib; 1093 else { 1094 const filesInFolder = soundfonts.filter((f) =>
1094 f.path.startsWith(lib.name + "/")); 1095 if (filesInFolder.some((f) => f.path.toLowerCase().endsWith(".hsf3"))) { 1096 groups[baseName].hsf3 = lib; 1097 } else if (filesInFolder.some((f) => f.path.toLowerCase().endsWith(".sf3"))) { 1098 groups[baseName].sf3 = lib; 1099 } else if (filesInFolder.some((f) => f.path.toLowerCase().endsWith(".sf2"))) { 1100 groups[baseName].sf2 = lib; 1101 } else { 1102 if (!groups[baseName].sf2) groups[baseName].sf2 = lib; 1103 } 1104 } 1105 } 1106 return Object.values(groups).sort((a, b) => a.baseName.localeCompare(b.baseName)); 1107 }, [downloadedLibraries, soundfonts]); 1108 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 md:p-6 lg:p-8 w-full max-w-[1700px] mx-auto space-y-6", children: [ 1109 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 1110 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1111 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-xl font-bold flex items-center gap-2", children: [
1112 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { className: "w-6 h-6 text-indigo-500" }), 1113 "Soundfont Management" 1114 ] }), 1115 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500", children: "Manage SF2 files and convert JS soundfonts to Opus." }) 1116 ] }), 1117 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1118 "button", 1119 { 1120 onClick: () => fetchData(true), 1121 className: "flex items-center gap-2 px-3 py-2 bg-white dark:bg-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-700 border border-zinc-200 dark:border-zinc-700 rounded-lg transition-all text-xs font-bold text-zinc-600 dark:text-zinc-300 shadow-sm", 1122 title: "Scan disk for new soundfonts", 1123 children: [ 1124 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: `w-4 h-4 ${loading ? "animate-spin text-indigo-500" : "text-zinc-400"}` }), 1125 "Scan Disk" 1126 ] 1127 } 1128 ) 1129 ] }), 1130 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-indigo-50 dark:bg-indigo-900/10 border border-indigo-100 dark:border-indigo-900/30 rounded-xl overflow-hidden mb-6", children: [
1131 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1132 "div", 1133 { 1134 onClick: () => setShowEngine(!showEngine), 1135 className: `p-4 ${showEngine ? "border-b border-indigo-100 dark:border-indigo-900/30" : ""} flex items-center justify-between cursor-pointer select-none`, 1136 children: [ 1137 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1138 showEngine ? /* @__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" }), 1139 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-sm uppercase tracking-wider text-indigo-600 dark:text-indigo-400 flex items-center gap-2", children: [ 1140 /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { className: "w-4 h-4" }), 1141 "Soundfont Engine" 1142 ] }) 1143 ] }), 1144 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-bold bg-white dark:bg-zinc-800 px-2 py-1 rounded-md border border-zinc-200 dark:border-zinc-700", children: "Global Defaults & Engine Configs" }) 1145 ] 1146 } 1147 ), 1148 showEngine && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4", children: [
1149 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1.5", children: [ 1150 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] font-bold uppercase text-zinc-500", children: "Pre-rendered Default (Worker)" }), 1151 /* @__PURE__ */ jsxRuntimeExports.jsx( 1152 "select", 1153 { 1154 value: preRenderedSf || "", 1155 onChange: (e) => saveGlobalSetting("PRE_RENDERED_SOUNDFONT", e.target.value), 1156 className: "w-full text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3 py-2 text-xs outline-none focus:ring-2 focus:ring-indigo-500 mb-1", 1157 children: soundfonts.filter((f) => { 1158 var _a2, _b, _c; 1159 return ((_a2 = f.path) == null ? void 0 : _a2.endsWith(".sf2")) || ((_b = f.path) == null ? void 0 : _b.endsWith(".sf3")) || ((_c = f.path) == null ? void 0 : _c.endsWith(".hsf3")); 1160 }).sort((a, b) => a.path.localeCompare(b.path, void 0, { sensitivity: "base", numeric: true })).map((sf) => { 1161 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1162 "option", 1163 { 1164 value: sf.path, 1165 className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900", 1166 children: [ 1167 sf.path, 1168 " (", 1169 formatFileSize(sf.size), 1170 ")" 1171 ] 1172 }, 1173 sf.path 1174 ); 1175 }) 1176 } 1177 ),
1178 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[9px] text-zinc-500 italic mt-1", children: [ 1179 "MIDI worker uses this ", 1180 /* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: ".sf2" }), 1181 " to render stems." 1182 ] }) 1183 ] }), 1184 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1.5", children: [ 1185 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] font-bold uppercase text-zinc-500", children: "Default Live (Fallback)" }), 1186 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1187 "select", 1188 { 1189 value: defaultLiveSf || "", 1190 onChange: (e) => saveGlobalSetting("DEFAULT_LIVE_SOUNDFONT", e.target.value), 1191 className: "w-full text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3 py-2 text-xs outline-none focus:ring-2 focus:ring-indigo-500 mb-1", 1192 children: [ 1193 /* @__P
1193URE__ */ jsxRuntimeExports.jsx("option", { value: "", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900", children: "-- Select Live Soundfont --" }), 1194 downloadedLibraries.filter((lib) => lib.name !== "Root").sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base", numeric: true })).map((lib) => { 1195 const typeSuffix = getSoundfontFormatType(lib); 1196 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1197 "option", 1198 { 1199 value: lib.name, 1200 className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900", 1201 children: [ 1202 lib.name, 1203 " (", 1204 typeSuffix, 1205 ") (", 1206 formatFileSize(lib.size), 1207 ")" 1208 ] 1209 }, 1210 lib.name 1211 ); 1212 }) 1213 ] 1214 } 1215 ),
1216 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[9px] text-zinc-500 italic mt-1", children: "Interactive keyboard source when live." }) 1217 ] }), 1218 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1.5", children: [ 1219 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] font-bold uppercase text-zinc-500", children: "Default Converter SF2" }), 1220 /* @__PURE__ */ jsxRuntimeExports.jsx( 1221 "select", 1222 { 1223 value: defaultConverterSf || "", 1224 onChange: (e) => saveGlobalSetting("DEFAULT_CONVERTER_SOUNDFONT", e.target.value), 1225 className: "w-full text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3 py-2 text-xs outline-none focus:ring-2 focus:ring-indigo-500 mb-1", 1226 children: soundfonts.filter((f) => { 1227 var _a2, _b, _c; 1228 return ((_a2 = f.path) == null ? void 0 : _a2.endsWith(".sf2")) || ((_b = f.path) == null ? void 0 : _b.endsWith(".sf3")) || ((_c = f.path) == null ? void 0 : _c.endsWith(".hsf3")); 1229 }).sort((a, b) => a.path.localeCompare(b.path, void 0, { sensitivity: "base", numeric: true })).map((sf) => { 1230 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1231 "option", 1232 { 1233 value: sf.path, 1234 className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900", 1235 children: [ 1236 sf.path, 1237 " (", 1238 formatFileSize(sf.size), 1239 ")" 1240 ] 1241 }, 1242 sf.path 1243 ); 1244 }) 1245 } 1246 ),
1247 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[9px] text-zinc-500 italic mt-1", children: "Default selected on MIDI Converter page." }) 1248 ] }), 1249 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1.5", children: [ 1250 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] font-bold uppercase text-zinc-500", children: "Default Keyboard SF" }), 1251 /* @__PURE__ */ jsxRuntimeExports.jsx( 1252 "select", 1253 { 1254 value: defaultKeyboardSf || "MusyngKite", 1255 onChange: (e) => saveGlobalSetting("DEFAULT_KEYBOARD_SOUNDFONT", e.target.value), 1256 className: "w-full text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3 py-2 text-xs outline-none focus:ring-2 focus:ring-indigo-500 mb-1", 1257 children: downloadedLibraries.filter((lib) => lib.name !== "Root").sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base", numeric: true })).map((lib) => { 1258 const typeSuffix = getSoundfontFormatType(lib); 1259 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1260 "option", 1261 { 1262 value: lib.name, 1263 className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900", 1264 children: [ 1265 lib.name, 1266 " (", 1267 typeSuffix, 1268 ") (", 1269 formatFileSize(lib.size), 1270 ")" 1271 ] 1272 }, 1273 lib.name 1274 ); 1275 }) 1276 } 1277 ),
1278 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[9px] text-zinc-500 italic mt-1", children: "Initial soundfont for the Keyboard emulator." }) 1279 ] }), 1280 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1.5", children: [ 1281 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] font-bold uppercase text-zinc-500", children: "Default WASM SF2" }), 1282 /* @__PURE__ */ jsxRuntimeExports.jsx( 1283 "select", 1284 { 1285 value: defaultWasmSf || "SoundBlasterOld.sf2", 1286 onChange: (e) => saveGlobalSetting("DEFAULT_WASM_SOUNDFONT", e.target.value), 1287 className: "w-full text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3 py-2 text-xs outline-none focus:ring-2 focus:ring-indigo-500 mb-1", 1288 children: soundfonts.filter((f) => { 1289 var _a2, _b, _c; 1290 return ((_a2 = f.path) == null ? void 0 : _a2.endsWith(".sf2")) || ((_b = f.path) == null ? void 0 : _b.endsWith(".sf3")) || ((_c = f.path) == null ? void 0 : _c.endsWith(".hsf3")); 1291 }).sort((a, b) => a.path.localeCompare(b.path, void 0, { sensitivity: "base", numeric: true })).map((sf) => { 1292 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 1293 "option", 1294 { 1295 value: sf.path, 1296 className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900", 1297 children: [ 1298 sf.path, 1299 " (", 1300 formatFileSize(sf.size), 1301 ")" 1302 ] 1303 }, 1304 sf.path 1305 ); 1306 }) 1307 } 1308 ),
1309 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[9px] text-zinc-500 italic mt-1", children: [ 1310 "Default ", 1311 /* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: ".sf2" }), 1312 " loaded in WASM mode." 1313 ] }) 1314 ] }) 1315 ] }) }) 1316 ] }), 1317 /* @__PURE__ */ jsxRuntimeExports.jsx( 1318 SoundfontQualityLab, 1319 { 1320 sf2Files: soundfonts.filter((f) => { 1321 var _a2; 1322 return (_a2 = f.path) == null ? void 0 : _a2.endsWith(".sf2"); 1323 }).sort((a, b) => a.path.localeCompare(b.path, void 0, { sensitivity: "base", numeric: true })), 1324 onRefreshProfiles: fetchData 1325 } 1326 ), 1327 renderEngineOrderEditor(), 1328 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-6", children: [ 1329 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden", children: [ 1330 /* @__PURE__ */ jsxRuntimeExports.jsx( 1331 "div", 1332 { 1333 onClick: () => setShowExternal(!showExternal), 1334 className: `p-4 ${showExternal ? "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`, 1335 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1336 showExternal ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "w-4 h-4 text-zinc-400 shrink-0" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "w-4 h-4 text-zinc-400 shrink-0" }),
1337 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-sm uppercase tracking-wider text-zinc-500", children: "External Soundfont Sources" }) 1338 ] }) 1339 } 1340 ), 1341 showExternal && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 space-y-4", children: [ 1342 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-2", children: presets.map((preset) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 1343 "button", 1344 { 1345 onClick: () => startDownload(preset.url), 1346 className: "p-3 bg-zinc-100 dark:bg-zinc-800 hover:bg-indigo-500 hover:text-white rounded-lg text-left transition-all group", 1347 children: [ 1348 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-bold uppercase tracking-tight", children: preset.name }), 1349 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] opacity-60 truncate", children: preset.url }) 1350 ] 1351 }, 1352 preset.name 1353 )) }),
1354 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 1355 /* @__PURE__ */ jsxRuntimeExports.jsx( 1356 "input", 1357 { 1358 type: "text", 1359 value: customUrl, 1360 onChange: (e) => setCustomUrl(e.target.value), 1361 placeholder: "https://example.com/soundfont.sf2", 1362 className: "flex-1 bg-zinc-100 dark:bg-zinc-800 border-none rounded-lg px-3 py-2 text-xs focus:ring-2 focus:ring-indigo-500 outline-none" 1363 } 1364 ), 1365 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1366 "button", 1367 { 1368 onClick: () => { 1369 if (customUrl) { 1370 startDownload(customUrl); 1371 setCustomUrl(""); 1372 } 1373 }, 1374 className: "px-4 py-2 bg-zinc-800 dark:bg-zinc-700 hover:bg-indigo-500 text-white text-xs font-bold rounded-lg transition-colors flex items-center gap-2", 1375 children: [
1376 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 14 }), 1377 "Download URL" 1378 ] 1379 } 1380 ) 1381 ] }), 1382 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-zinc-400 italic", children: "* Supports direct links to .sf2 or .js files. GitHub links are automatically converted to raw." }) 1383 ] }) 1384 ] }), 1385 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden", children: [ 1386 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1387 "div", 1388 { 1389 onClick: () => setShowJobs(!showJobs), 1390 className: `p-4 ${showJobs ? "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`, 1391 children: [ 1392 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1393 showJobs ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "w-4 h-4 text-zinc-400 shrink-0" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "w-4 h-4 text-zinc-400 shrink-0" }),
1394 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-sm uppercase tracking-wider text-zinc-500", children: "Active Jobs" }) 1395 ] }), 1396 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", onClick: (e) => e.stopPropagation(), children: [ 1397 jobs.some((j) => j.status === "error") && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1398 "button", 1399 { 1400 onClick: () => { 1401 if (confirm("Dismiss all errored jobs? You will lose their error details.")) { 1402 const errorJobs = jobs.filter((j) => j.status === "error"); 1403 Promise.all(errorJobs.map((j) => api.deleteSoundfontJob(j.id))).then(() => fetchData()); 1404 } 1405 }, 1406 className: "text-[10px] font-bold text-red-500 hover:text-red-600 uppercase flex items-center gap-1 transition-colors", 1407 children: [
1408 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 12 }), 1409 "Dismiss Errors (", 1410 jobs.filter((j) => j.status === "error").length, 1411 ")" 1412 ] 1413 } 1414 ), 1415 jobs.some((j) => j.status === "completed") && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1416 "button", 1417 { 1418 onClick: () => clearFinishedJobs(), 1419 className: "text-[10px] font-bold text-emerald-500 hover:text-emerald-600 uppercase flex items-center gap-1 transition-colors", 1420 children: [ 1421 /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 12 }), 1422 "Clear Finished" 1423 ] 1424 } 1425 ) 1426 ] }) 1427 ] 1428 } 1429 ), 1430 showJobs && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "divide-y divide-zinc-200 dark:divide-zinc-800 max-h-[400px] overflow-y-auto", children: jobs.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-8 text-center text-zinc-500 text-sm", children: "No active or recent jobs." }) : jobs.map((job) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 space-y-2", children: [
1431 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 1432 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1433 job.status === "pending" && /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { className: "w-3 h-3 text-zinc-500" }), 1434 job.status === "processing" && /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3 h-3 text-indigo-500 animate-spin" }), 1435 job.status === "completed" && /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheckBig, { className: "w-3 h-3 text-green-500" }), 1436 job.status === "error" && /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "w-3 h-3 text-red-500" }), 1437 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium", children: job.name }) 1438 ] }), 1439 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => deleteJob(job.id), className: "text-zinc-400 hover:text-red-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-4 h-4" }) }) 1440 ] }),
1441 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full bg-zinc-100 dark:bg-zinc-800 h-1 rounded-full overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `h-full transition-all duration-500 ${job.status === "error" ? "bg-red-500" : "bg-indigo-500"}`, style: { width: `${job.progress}%` } }) }), 1442 job.details && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 dark:text-zinc-400 italic", children: job.details }) 1443 ] }, job.id)) }) 1444 ] }), 1445 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl lg:col-span-2", children: [ 1446 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1447 "div", 1448 { 1449 onClick: () => setShowAssets(!showAssets), 1450 className: `p-4 ${showAssets ? "border-b border-zinc-200 dark:border-zinc-800" : ""} bg-zinc-50/50 dark:bg-zinc-800/50 flex items-center justify-between flex-wrap gap-2 cursor-pointer select-none`, 1451 children: [
1452 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 1453 showAssets ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "w-4 h-4 text-zinc-400 shrink-0" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "w-4 h-4 text-zinc-400 shrink-0" }), 1454 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-sm uppercase tracking-wider text-zinc-500", children: "Available Soundfont Assets" }) 1455 ] }), 1456 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 flex-wrap", onClick: (e) => e.stopPropagation(), children: [ 1457 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1458 "button", 1459 { 1460 onClick: () => { 1461 if (!selectedSfForKeyboard && downloadedLibraries.length > 0) { 1462 setSelectedSfForKeyboard(downloadedLibraries[0].name); 1463 } 1464 setShowKeyboard(!showKeyboard); 1465 }, 1466 className: `px-2.5 py-1 text-[10px] font-bold rounded uppercase flex items-center gap-1.5 transition-all shadow-sm ${showKeyboard ? "bg-indigo-600 text-white ring-2 ring-indigo-400" : "bg-zinc-200 dark:bg-zinc-700 hover:bg-indigo-500 hover:text-white text-zinc-700 dark:text-zinc-200"}`, 1467 title: "Toggle full 88-key interactive keyboard at bottom of screen", 1468 children: [
1469 /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { className: "w-3.5 h-3.5" }), 1470 showKeyboard ? "Keyboard Active" : "Toggle Keyboard" 1471 ] 1472 } 1473 ), 1474 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1475 "button", 1476 { 1477 onClick: startBulkSf2Conversion, 1478 className: "px-2 py-1 bg-indigo-500 hover:bg-indigo-600 text-white text-[10px] font-bold rounded uppercase flex items-center gap-1 transition-all shadow-sm", 1479 title: "Sample all .sf2 files to JS", 1480 children: [ 1481 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3 h-3" }), 1482 "Resample All .sf2" 1483 ] 1484 } 1485 ), 1486 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1487 "button", 1488 { 1489 onClick: startBulkSf3Conversion, 1490 className: "px-2 py-1 bg-teal-500 hover:bg-teal-600 text-white text-[10px] font-bold rounded uppercase flex items-center gap-1 transition-all shadow-sm", 1491 title: "Convert all .sf2 files to SF3 (OGG compressed)", 1492 children: [
1493 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3 h-3" }), 1494 "Convert All â SF3" 1495 ] 1496 } 1497 ), 1498 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1499 "button", 1500 { 1501 onClick: startBulkHsf3BankConversion, 1502 className: "px-2 py-1 bg-emerald-500 hover:bg-emerald-600 text-white text-[10px] font-bold rounded uppercase flex items-center gap-1 transition-all shadow-sm", 1503 title: "Convert all .sf2 files to HSF3 Bank (Split instrument files + DRM obfuscated)", 1504 children: [ 1505 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3 h-3" }), 1506 "Convert All â HSF3" 1507 ] 1508 } 1509 ), 1510 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] bg-zinc-200 dark:bg-zinc-700 px-2 py-0.5 rounded-full font-bold", children: [ 1511 soundfonts.length, 1512 " Files" 1513 ] }) 1514 ] }) 1515 ] 1516 } 1517 ), 1518 showAssets && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full transition-all", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-left border-collapse min-w-[1250px]", children: [
1519 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 shadow-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "bg-zinc-100 dark:bg-zinc-800 text-[10px] uppercase tracking-wider text-zinc-500 font-bold border-b border-zinc-200 dark:border-zinc-800", children: [ 1520 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 w-8" }), 1521 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 min-w-[160px]", children: "Soundfont / Format" }), 1522 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 min-w-[150px]", children: "Friendly Name" }), 1523 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 min-w-[150px]", children: "Description" }), 1524 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 w-28 text-center", children: "Type & Size" }),
1525 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 w-36 text-center", children: "Target Gain" }), 1526 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 w-32 text-center", title: "Smart Auto-Routing Threshold", children: "Auto-Route" }), 1527 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 w-24 text-center", children: "Playback" }), 1528 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 w-24 text-center", children: "Converter" }), 1529 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 w-24 text-center", children: "Keyboard" }), 1530 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 w-28 text-center", children: "WASM Engine" }),
1531 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 w-24 text-center", children: "Tier Access" }), 1532 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "sticky top-0 z-20 bg-zinc-100 dark:bg-zinc-800 px-3 py-3 min-w-[180px] text-right", children: "Actions" }) 1533 ] }) }), 1534 /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-zinc-200 dark:divide-zinc-800", children: groupedLibraries.map((group) => { 1535 var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n; 1536 const baseName = group.baseName; 1537 const isExpanded = expandedLibraries.has(baseName); 1538 const bestSfForKeyboard = ((_a2 = group.sf2) == null ? void 0 : _a2.name) || ((_b = group.sf3) == null ? void 0 : _b.name) || ((_c = group.js) == null ? void 0 : _c.name) || baseName; 1539 const isGroupSelectedForKb = selectedSfForKeyboard === bestSfForKeyboard; 1540 const currentGain = ((_d = profiles[baseName]) == null ? void 0 : _d.gain) ?? 1.8; 1541 const isLeveling = !!autoLevelingLibs[baseName]; 1542 group.formats.length > 0 ? group.formats.some((f) => { 1543 var _a3; 1544 return ((_a3 = metadata[f.name]) == null ? void 0 : _a3.visible) !== false; 1545 }) : ((_e = metadata[baseName]) == null ? void 0 : _e.visible) !== false; 1546 group.formats.length > 0 ? group.formats.some((f) => { 1547 var _a3; 1548 return ((_a3 = metadata[f.name]) == null ? void 0 : _a3.visibleConverter) !== false; 1549 }) : ((_f = metadata[baseName]) == null ? void 0 : _f.visibleConverter) !== false; 1550 group.formats.length > 0 ? group.formats.some((f) => { 1551 var _a3; 1552 return ((_a3 = metadata[f.name]) == null ? void 0 : _a3.visibleKeyboard) !== false; 1553 }) : ((_g = metadata[baseName]) == null ? void 0 : _g.visibleKeyboard) !== false; 1554 const isFolderPro = group.formats.length > 0 ? group.formats.some((f) => { 1555 var _a3; 1556 return ((_a3 = metadata[f.name]) == null ? void 0 : _a3.access) === "pro"; 1557 }) : ((_h = metadata[baseName]) == null ? void 0 : _h.access) === "pro"; 1558 group.formats.length > 0 && group.formats.every((f) => f.name.endsWith("_JS")); 1559 const sf3Size = ((_i = group.sf3) == null ? void 0 : _i.size) || 0; 1560 const hsf3TotalSize = ((_j = group.hsf3) == null ? void 0 : _j.size) || 0; 1561 const calcThreshold = sf3Size > 0 && hsf3TotalSize > 0 ? Math.floor(sf3Size / (hsf3TotalSize / 128)) : 15; 1562 const isAutoRoute = ((_k = metadata[baseName]) == null ? void 0 : _k.autoRoute) === true; 1563 const overrideThreshold = (_l = metadata[baseName]) == null ? void 0 : _l.autoRouteThresholdOverride; 1564 return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
1565 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1566 "tr", 1567 { 1568 onClick: () => toggleLibrary(baseName), 1569 className: `bg-zinc-100/60 dark:bg-zinc-800/60 hover:bg-zinc-200/60 dark:hover:bg-zinc-750 transition-colors group cursor-pointer border-b border-zinc-200/80 dark:border-zinc-800 ${isGroupSelectedForKb ? "border-l-4 border-l-indigo-500" : ""}`, 1570 children: [ 1571 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: (e) => { 1572 e.stopPropagation(); 1573 toggleLibrary(baseName); 1574 }, className: "text-zinc-400 hover:text-indigo-500 transition-colors", children: isExpanded ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "w-4 h-4 text-indigo-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "w-4 h-4 text-zinc-400" }) }) }), 1575 /* @__P
1575URE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [ 1576 /* @__PURE__ */ jsxRuntimeExports.jsx(Folder, { className: "w-4 h-4 text-indigo-500 shrink-0" }), 1577 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs font-bold text-zinc-900 dark:text-zinc-100 truncate max-w-[180px]", title: baseName, children: baseName }), 1578 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[9px] bg-indigo-500/10 text-indigo-500 border border-indigo-500/20 font-bold px-1.5 py-0.5 rounded-full shrink-0", children: [ 1579 group.formats.length, 1580 " ", 1581 group.formats.length === 1 ? "Format" : "Formats" 1582 ] }) 1583 ] }) }), 1584 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1585 "input", 1586 { 1587 type: "text", 1588 value: ((_m = metadata[baseName]) == null ? void 0 : _m.name) || baseName.replace(/_/g, " "), 1589 onChange: (e) => saveMetadata(baseName, { name: e.target.value }), 1590 placeholder: "Group Friendly Name", 1591 className: "w-full bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-indigo-500 outline-none font-semibold text-zinc-800 dark:text-zinc-200" 1592 } 1593 ) }),
1594 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1595 "input", 1596 { 1597 type: "text", 1598 value: ((_n = metadata[baseName]) == null ? void 0 : _n.desc) || "", 1599 onChange: (e) => saveMetadata(baseName, { desc: e.target.value }), 1600 placeholder: "Group Description...", 1601 className: "w-full bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-indigo-500 outline-none text-zinc-500 dark:text-zinc-400" 1602 } 1603 ) }), 1604 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3 text-center whitespace-nowrap", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-bold text-zinc-700 dark:text-zinc-300", children: [ 1605 "Total: ", 1606 formatFileSize(group.totalSize) 1607 ] }) }),
1608 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3 text-center", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-center gap-1.5", children: [ 1609 /* @__PURE__ */ jsxRuntimeExports.jsx( 1610 "input", 1611 { 1612 type: "number", 1613 step: 0.1, 1614 min: 0.1, 1615 max: 10, 1616 defaultValue: currentGain, 1617 onBlur: (e) => { 1618 const val = parseFloat(e.target.value); 1619 if (!isNaN(val) && val !== currentGain) { 1620 handleSaveGain(baseName, val); 1621 } 1622 }, 1623 className: "w-14 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded px-1.5 py-0.5 text-xs text-right font-mono font-bold outline-none focus:ring-1 focus:ring-indigo-500 text-zinc-800 dark:text-zinc-200 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none", 1624 title: "Target Gain Multiplier (Shared by all formats of this soundfont)" 1625 }, 1626 `gain-${baseName}-${currentGain}` 1627 ),
1628 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-400 font-bold", children: "x" }), 1629 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1630 "button", 1631 { 1632 onClick: () => handleAutoLevelForRow(baseName), 1633 disabled: isLeveling, 1634 className: "p-1.5 rounded 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 font-bold transition-all disabled:opacity-50 flex items-center gap-1 text-[10px]", 1635 title: "Auto Level Volume (-18 dB Target baseline)", 1636 children: [ 1637 isLeveling ? /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3 h-3 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "w-3 h-3" }), 1638 /* @__P
1638URE__ */ jsxRuntimeExports.jsx("span", { children: "Auto" }) 1639 ] 1640 } 1641 ) 1642 ] }) }), 1643 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3 text-center", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center gap-1", children: [ 1644 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1645 "button", 1646 { 1647 onClick: () => saveMetadata(baseName, { autoRoute: !isAutoRoute }), 1648 className: `px-1.5 py-0.5 rounded transition-colors flex items-center gap-1 text-[9px] font-bold uppercase tracking-wider border ${isAutoRoute ? "text-amber-500 bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800/50" : "text-zinc-400 bg-zinc-100 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-700"}`, 1649 title: isAutoRoute ? "Smart Auto-Routing Enabled" : "Enable Smart Auto-Routing", 1650 children: [
1651 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { className: "w-3 h-3" }), 1652 isAutoRoute ? "Auto" : "Manual" 1653 ] 1654 } 1655 ), 1656 isAutoRoute && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-1", title: `Calculated Optimal Break-Even Threshold: ${calcThreshold} instruments 1657(Leave empty to use auto)`, children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1658 "input", 1659 { 1660 type: "number", 1661 placeholder: calcThreshold.toString(), 1662 value: overrideThreshold || "", 1663 onChange: (e) => saveMetadata(baseName, { autoRouteThresholdOverride: e.target.value ? parseInt(e.target.value) : null }), 1664 className: "w-10 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded px-1 py-0.5 text-[9px] text-center font-mono font-bold outline-none focus:ring-1 focus:ring-amber-500 text-amber-600 dark:text-amber-500 placeholder:text-zinc-300 dark:placeholder:text-zinc-600 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" 1665 } 1666 ) }) 1667 ] }) }),
1668 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3 text-center text-zinc-300 dark:text-zinc-700 select-none", children: "-" }), 1669 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3 text-center text-zinc-300 dark:text-zinc-700 select-none", children: "-" }), 1670 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3 text-center text-zinc-300 dark:text-zinc-700 select-none", children: "-" }), 1671 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3 text-center text-zinc-300 dark:text-zinc-700 select-none", children: "-" }), 1672 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3 text-center", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 1673 "select", 1674 { 1675 value: isFolderPro ? "pro" : "free", 1676 onChange: (e) => saveMetadataBulk([...group.formats.map((f) => f.name), baseName], { access: e.target.value }), 1677 className: `text-[10px] font-bold px-2 py-1 rounded outline-none border transition-colors ${isFolderPro ? "bg-purple-50 text-purple-600 border-purple-200 dark:bg-purple-900/20 dark:text-purple-400 dark:border-purple-800/50" : "bg-emerald-50 text-emerald-600 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800/50"}`, 1678 children: [
1679 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "free", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: "FREE" }), 1680 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "pro", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: "PRO" }) 1681 ] 1682 } 1683 ) }), 1684 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-3 text-right", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-end gap-1.5 min-w-[170px]", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1685 "button", 1686 { 1687 onClick: () => confirm(`Permanently delete all formats of ${baseName}?`) && group.formats.forEach((sf) => handleDeleteSoundfont(sf.name)), 1688 className: "p-1.5 text-red-500 hover:bg-red-50 dark:hover:bg-red-950/40 rounded transition-colors border border-red-200 dark:border-red-900/50", 1689 title: "Delete All Formats of this Soundfont", 1690 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-3.5 h-3.5" }) 1691 } 1692 ) }) }) 1693 ] 1694 } 1695 ), 1696 isExpanded && group.formats.map((lib) => { 1697 var _a3, _b2, _c2, _d2, _e2, _f2, _g2, _h2, _i2, _j2, _k2, _l2; 1698 const formatName = lib.name; 1699 let isJS = formatName.endsWith("_JS"); 1700 let isSf2 = formatName.toLowerCase().endsWith(".sf2"); 1701 let isSf3 = formatName.toLowerCase().endsWith(".sf3"); 1702 let isHsf3 = formatName.endsWith("_HSF3Bank") || formatName.endsWith("_SF3Bank") || lib.isHsf3Bank; 1703 if (!isJS && !isSf2 && !isSf3 && !isHsf3) { 1704 const filesInFolder = soundfonts.filter((f) => f.path.startsWith(formatName + "/")); 1705 if (filesInFolder.some((f) => f.path.toLowerCase().endsWith(".hsf3"))) { 1706 isHsf3 = true; 1707 } else if (filesInFolder.some((f) => f.path.toLowerCase().endsWith(".sf3"))) { 1708 isSf3 = true; 1709 } else if (filesInFolder.some((f) => f.path.toLowerCase().endsWith(".sf2"))) { 1710 isSf2 = true; 1711 } else if (filesInFolder.some((f) => f.path.toLowerCase().endsWith(".js"))) { 1712 isJS = true; 1713 } else { 1714 isSf2 = true; 1715 } 1716 } 1717 const isSfBinary = isSf2 || isSf3; 1718 const isSelectedForKb = selectedSfForKeyboard === formatName; 1719 return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
1720 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1721 "tr", 1722 { 1723 onClick: () => setSelectedSfForKeyboard(formatName), 1724 className: `hover:bg-indigo-50/50 dark:hover:bg-indigo-950/20 transition-colors group cursor-pointer bg-white dark:bg-zinc-900/90 border-b border-zinc-100 dark:border-zinc-800/50 ${isSelectedForKb ? "bg-indigo-50 dark:bg-indigo-950/40 border-l-4 border-l-indigo-500 font-medium" : ""}`, 1725 children: [ 1726 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-right", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-indigo-400 font-bold text-xs select-none", children: "â³" }) }), 1727 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-3 py-2 pl-4", children: [ 1728 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [ 1729 (isJS || isHsf3) && lib.children && lib.children.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: (e) => toggleFolder(formatName, e), className: "p-0.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded transition-colors text-zinc-500", children: expandedFolders.has(formatName) ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "w-3.5 h-3.5" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "w-3.5 h-3.5" }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-4 shrink-0" }),
1730 isJS || isHsf3 ? /* @__PURE__ */ jsxRuntimeExports.jsx(Folder, { className: `w-3.5 h-3.5 shrink-0 ${isHsf3 ? "text-purple-400" : "text-indigo-400"}` }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { className: "w-3.5 h-3.5 text-zinc-400 shrink-0" }), 1731 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-[11px] text-zinc-700 dark:text-zinc-300 font-medium truncate max-w-[170px]", title: formatName, children: formatName }), 1732 isJS && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9.5px] text-zinc-400 dark:text-zinc-500 font-normal italic shrink-0 hidden sm:inline-block", title: "Used only by the Persistent Player as a fallback for older devices or mobile.", children: "(Web Player Fallback)" }), 1733 isHsf3 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9.5px] text-purple-400/80 font-normal italic shrink-0 hidden sm:inline-block", title: "Primary high-performance WASM synthesizer format.", children: "(Primary Synth Engine)" }), 1734 isSelectedForKb && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px] bg-indigo-600 text-white font-bold px-1.5 py-0.5 rounded shadow-sm shrink-0", children: "Active Target" }) 1735 ] }), 1736 lib.createdAt && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[9px] text-zinc-400 dark:text-zinc-500 font-mono mt-0.5 flex items-center gap-1 select-none", children: [
1737 /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { className: "w-2.5 h-2.5" }), 1738 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: new Date(lib.createdAt).toLocaleString(void 0, { 1739 year: "numeric", 1740 month: "short", 1741 day: "numeric", 1742 hour: "2-digit", 1743 minute: "2-digit" 1744 }) }) 1745 ] }) 1746 ] }), 1747 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-center text-zinc-300 dark:text-zinc-700 select-none", children: "-" }), 1748 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-center text-zinc-300 dark:text-zinc-700 select-none", children: "-" }), 1749 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-center whitespace-nowrap", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-0.5", children: [ 1750 isSf2 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-bold text-zinc-600 dark:text-zinc-300 bg-zinc-200 dark:bg-zinc-700 px-1.5 py-0.5 rounded", children: "SF2" }), 1751 isSf3 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-bold text-cyan-400 bg-cyan-500/10 px-1.5 py-0.5 rounded border border-cyan-500/20", children: "SF3" }), 1752 isJS && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-bold text-green-500 bg-green-500/10 px-1.5 py-0.5 rounded border border-green-500/20", children: "JS" }), 1753 isHsf3 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-bold text-purple-500 bg-purple-500/10 px-1.5 py-0.5 rounded border border-purple-500/20", children: "HSF3" }),
1754 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-500", children: formatFileSize(lib.size) }) 1755 ] }) }), 1756 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "px-3 py-2 text-center text-[10px] font-mono font-semibold text-zinc-400", children: [ 1757 currentGain, 1758 "x" 1759 ] }), 1760 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-center text-zinc-300 dark:text-zinc-700 select-none", children: "-" }), 1761 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-center", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1762 "button", 1763 { 1764 onClick: () => { 1765 var _a4; 1766 return saveMetadata(formatName, { visible: ((_a4 = metadata[formatName]) == null ? void 0 : _a4.visible) === false ? true : false }); 1767 }, 1768 className: `p-1.5 rounded transition-colors ${((_a3 = metadata[formatName]) == null ? void 0 : _a3.visible) !== false ? "text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20" : "text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"}`, 1769 title: ((_b2 = metadata[formatName]) == null ? void 0 : _b2.visible) !== false ? "Visible on Main Site" : "Hidden from Main Site", 1770 children: ((_c2 = metadata[formatName]) == null ? void 0 : _c2.visible) !== false ? /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "w-4 h-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "w-4 h-4" }) 1771 } 1772 ) }),
1773 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-center", onClick: (e) => e.stopPropagation(), children: !isJS ? /* @__PURE__ */ jsxRuntimeExports.jsx( 1774 "button", 1775 { 1776 onClick: () => { 1777 var _a4; 1778 return saveMetadata(formatName, { visibleConverter: ((_a4 = metadata[formatName]) == null ? void 0 : _a4.visibleConverter) === false ? true : false }); 1779 }, 1780 className: `p-1.5 rounded transition-colors ${((_d2 = metadata[formatName]) == null ? void 0 : _d2.visibleConverter) !== false ? "text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20" : "text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"}`, 1781 title: ((_e2 = metadata[formatName]) == null ? void 0 : _e2.visibleConverter) !== false ? "Visible in Converter page" : "Hidden from Converter page", 1782 children: ((_f2 = metadata[formatName]) == null ? void 0 : _f2.visibleConverter) !== false ? /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "w-4 h-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "w-4 h-4" }) 1783 } 1784 ) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-400 font-medium italic select-none", title: "JS is not a valid output format for the server-side MIDI Converter.", children: "N/A" }) }),
1785 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-center", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1786 "button", 1787 { 1788 onClick: () => { 1789 var _a4; 1790 return saveMetadata(formatName, { visibleKeyboard: ((_a4 = metadata[formatName]) == null ? void 0 : _a4.visibleKeyboard) === false ? true : false }); 1791 }, 1792 className: `p-1.5 rounded transition-colors ${((_g2 = metadata[formatName]) == null ? void 0 : _g2.visibleKeyboard) !== false ? "text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20" : "text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"}`, 1793 title: ((_h2 = metadata[formatName]) == null ? void 0 : _h2.visibleKeyboard) !== false ? "Visible in Keyboard Emulator dropdown" : "Hidden from Keyboard Emulator", 1794 children: ((_i2 = metadata[formatName]) == null ? void 0 : _i2.visibleKeyboard) !== false ? /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "w-4 h-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "w-4 h-4" }) 1795 } 1796 ) }),
1797 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-center", onClick: (e) => e.stopPropagation(), children: isSfBinary ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 1798 "button", 1799 { 1800 onClick: () => { 1801 var _a4; 1802 return saveMetadata(formatName, { enabledWasm: ((_a4 = metadata[formatName]) == null ? void 0 : _a4.enabledWasm) === false ? true : false }); 1803 }, 1804 className: `px-2 py-0.5 rounded text-[10px] font-bold flex items-center justify-center gap-1 mx-auto transition-all ${((_j2 = metadata[formatName]) == null ? void 0 : _j2.enabledWasm) !== false ? "text-cyan-600 bg-cyan-50 dark:bg-cyan-950/40 border border-cyan-200 dark:border-cyan-800" : "text-zinc-400 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 opacity-60"}`, 1805 title: ((_k2 = metadata[formatName]) == null ? void 0 : _k2.enabledWasm) !== false ? "Enabled for WebAssembly (WASM) engine" : "Disabled for WASM engine", 1806 children: [
1807 /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { className: "w-3 h-3 text-cyan-500" }), 1808 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: ((_l2 = metadata[formatName]) == null ? void 0 : _l2.enabledWasm) !== false ? "Enabled" : "Disabled" }) 1809 ] 1810 } 1811 ) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-400 italic", title: "JS Soundfonts do not use WASM", children: "â" }) }), 1812 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-center text-zinc-300 dark:text-zinc-700 select-none", children: "-" }), 1813 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-2 text-right", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-end gap-1.5 flex-wrap min-w-[170px]", children: [ 1814 /* @__P
1814URE__ */ jsxRuntimeExports.jsx( 1815 "button", 1816 { 1817 onClick: () => { 1818 setSelectedSfForKeyboard(formatName); 1819 if (!showKeyboard) setShowKeyboard(true); 1820 }, 1821 className: `p-1.5 rounded transition-colors border ${isSelectedForKb ? "text-indigo-600 bg-indigo-100 dark:bg-indigo-950/80 border-indigo-500" : "text-zinc-400 hover:text-indigo-500 hover:bg-zinc-100 dark:hover:bg-zinc-800 border-zinc-200 dark:border-zinc-700"}`, 1822 title: "Play specifically this format on Keyboard", 1823 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { className: "w-3.5 h-3.5" }) 1824 } 1825 ), 1826 isJS && /* @__PURE__ */ jsxRuntimeExports.jsx( 1827 "button", 1828 { 1829 onClick: () => confirm(`Rebuild JS Library for ${formatName}?`) && handleDeleteSoundfont(formatName), 1830 className: "p-1.5 text-amber-500 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded transition-colors border border-amber-200 dark:border-amber-800/50", 1831 title: "Rebuild JS Library", 1832 children: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3.5 h-3.5" }) 1833 } 1834 ), 1835 isHsf3 && /* @__PURE__ */ jsxRuntimeExports.jsx( 1836 "button", 1837 { 1838 onClick: () => { 1839 if (confirm(`Rebuild HSF3 Bank for ${formatName}?`)) { 1840 const sf2Path = formatName.replace(/_HSF3Bank$/i, ".sf2"); 1841 startHsf3BankConversion(sf2Path); 1842 } 1843 }, 1844 className: "p-1.5 text-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded transition-colors border border-purple-200 dark:border-purple-800/50", 1845 title: "Rebuild HSF3 Bank", 1846 children: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3.5 h-3.5" }) 1847 } 1848 ), 1849 isSf2 && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1850 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1851 "button", 1852 { 1853 onClick: () => startSf2Conversion(formatName), 1854 className: "px-2 py-1 bg-indigo-500 hover:bg-indigo-600 text-white text-[9px] font-bold rounded uppercase flex items-center gap-1 transition-all shadow-sm", 1855 title: "Sample to JS", 1856 children: [ 1857 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3.5 h-3.5" }), 1858 "Sample" 1859 ] 1860 } 1861 ), 1862 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1863 "button", 1864 { 1865 onClick: () => startSf3Conversion(formatName), 1866 className: "px-2 py-1 bg-teal-500 hover:bg-teal-600 text-white text-[9px] font-bold rounded uppercase flex items-center gap-1 transition-all shadow-sm", 1867 title: "Convert to SF3 (OGG compressed)", 1868 children: [
1869 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3.5 h-3.5" }), 1870 "SF3" 1871 ] 1872 } 1873 ), 1874 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1875 "button", 1876 { 1877 onClick: () => startHsf3BankConversion(formatName), 1878 className: "px-2 py-1 bg-purple-500 hover:bg-purple-600 text-white text-[9px] font-bold rounded uppercase flex items-center gap-1 transition-all shadow-sm", 1879 title: "Convert to HSF3 Bank (Split instrument files + DRM obfuscated)", 1880 children: [ 1881 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "w-3.5 h-3.5" }), 1882 "HSF3 Bank" 1883 ] 1884 } 1885 ) 1886 ] }),
1887 /* @__PURE__ */ jsxRuntimeExports.jsx( 1888 "button", 1889 { 1890 onClick: () => confirm(`Delete format ${formatName}?`) && handleDeleteSoundfont(formatName), 1891 className: "p-1.5 text-zinc-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors", 1892 title: `Delete ${formatName}`, 1893 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-3.5 h-3.5" }) 1894 } 1895 ) 1896 ] }) }) 1897 ] 1898 } 1899 ), 1900 expandedFolders.has(formatName) && lib.children && lib.children.map((child) => { 1901 var _a4; 1902 return /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "bg-zinc-50/50 dark:bg-zinc-900/50 border-b border-zinc-100 dark:border-zinc-800/50 text-[10px]", children: [
1903 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-1.5 text-right", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 dark:text-zinc-700 font-mono", children: "â³" }) }), 1904 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-1.5 pl-10", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-2 text-zinc-500 dark:text-zinc-400 font-mono", children: [ 1905 /* @__PURE__ */ jsxRuntimeExports.jsx(FileHeadphone, { className: "w-3 h-3 mt-1 shrink-0" }), 1906 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 1907 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: child.name || ((_a4 = child.path) == null ? void 0 : _a4.split("/").pop()) || "Unknown" }), 1908 (() => { 1909 const name = child.name || child.path || ""; 1910 const match = name.match(/_(\d+)\.hsf3$/i) || name.match(/_(\d+)_.*\.hsf3$/i); 1911 if (match) { 1912 const prog = parseInt(match[1], 10); 1913 return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] text-zinc-400 truncate", children: GM_INSTRUMENTS[prog] }); 1914 } 1915 return null; 1916 })() 1917 ] }) 1918 ] }) }),
1919 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { colSpan: 2 }), 1920 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-1.5 text-center text-zinc-400 font-mono font-medium", children: formatFileSize(child.size) }), 1921 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { colSpan: 6 }) 1922 ] }, `${formatName}/${child.name}`); 1923 }) 1924 ] }, formatName); 1925 }) 1926 ] }, baseName); 1927 }) }) 1928 ] }) }) 1929 ] }) 1930 ] }), 1931 showKeyboard && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { height: 350 } }), 1932 showKeyboard && selectedSfForKeyboard && /* @__PURE__ */ jsxRuntimeExports.jsx( 1933 SoundfontKeyboardDock, 1934 { 1935 selectedSf: selectedSfForKeyboard, 1936 program: keyboardProgram, 1937 setProgram: setKeyboardProgram, 1938 bank: keyboardBank, 1939 setBank: setKeyboardBank, 1940 onClose: () => setShowKeyboard(false), 1941 gain: ((_a = profiles[selectedSfForKeyboard.replace(/_JS$/, "").replace(/_HSF3Bank$/i, "").replace(/_SF3Bank$/i, "").replace(/\.h?sf[23]$/i, "")]) == null ? void 0 : _a.gain) ?? 1.8 1942 } 1943 ) 1944 ] }); 1945}; 1946export { 1947 SoundfontManager as default 1948};
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.