1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/vendor-audio-DH3w0BzW.js","assets/vendor-react-Bay9Y7Lb.js","assets/vendor-dVNaLJx_.js","assets/vendor-i18n-R-bMoYp_.js","assets/vendor-DqKRdbjz.css"])))=>i.map(i=>d[i]); 2import { _ as __vitePreload } from "./vendor-react-three-MDLA5jog.js"; 3import { j as jsxRuntimeExports, r as reactExports } from "./vendor-react-Bay9Y7Lb.js"; 4import { g as getAudioContext, s as synth } from "./synth-BE1TSTD4.js"; 5import { cD as Headphones, c3 as RotateCcw, cL as RefreshCw, cc as Save, b4 as FolderOpen, b6 as LoaderCircle, c$ as Download, er as Power, eb as v4 } from "./vendor-dVNaLJx_.js"; 6import "./wasm-synth-adapter-BrqgBX1y.js"; 7import "./vendor-spessasynth-DH8n6o_V.js"; 8import "./vendor-audio-DH3w0BzW.js"; 9import "./vendor-i18n-R-bMoYp_.js"; 10class EQService { 11 constructor(audioContext) { 12 this.filters = []; 13 this.isBypassed = false; 14 this.audioContext = audioContext; 15 this.input = this.audioContext.createGain(); 16 this.output = this.audioContext.createGain(); 17 this.input.connect(this.output); 18 } 19 updateBands(bands) { 20 this.input.disconnect(); 21 this.filters.forEach((filter) => filter.disconnect()); 22 this.filters = []; 23 if (this.isBypassed || bands.length === 0) { 24 this.input.connect(this.output); 25 return; 26 } 27 const activeBands = bands.some((b) => b.solo) ? bands.filter((b) => b.solo) : bands; 28 this.filters = activeBands.map((band) => { 29 const filter = this.audioContext.createBiquadFilter(); 30 filter.type = band.type; 31 const safeFreq = Number.isFinite(band.freq) ? Math.max(0, band.freq) : 1e3; 32 const safeGain = Number.isFinite(band.gain) ? band.gain : 0; 33 const safeQ = Number.isFinite(band.q) ? Math.max(1e-4, band.q) : 1; 34 filter.frequency.setValueAtTime(safeFreq, this.audioContext.currentTime); 35 filter.gain.setValueAtTime(safeGain, this.audioContext.currentTime); 36 filter.Q.setValueAtTime(safeQ, this.audioContext.currentTime); 37 return filter; 38 }); 39 this.input.connect(this.filters[0]); 40 for (let i = 0; i < this.filters.length - 1; i++) { 41 this.filters[i].connect(this.filters[i + 1]); 42 } 43 this.filters[this.filters.length - 1].connect(this.output); 44 } 45 setBypass(bypass) { 46 this.isBypassed = bypass; 47 } 48 setOutputGain(gain) { 49 const safeGain = Number.isFinite(gain) ? gain : 0; 50 const linearGain = Math.pow(10, safeGain / 20); 51 this.output.gain.setValueAtTime(linearGain, this.audioContext.currentTime); 52 } 53 getFrequencyResponse(freqs, magResponse, phaseResponse) { 54 for (let i = 0; i < magResponse.length; i++) { 55 magResponse[i] = 1; 56 phaseResponse[i] = 0; 57 } 58 if (this.isBypassed || this.filters.length === 0) return; 59 const tempMag = new Float32Array(freqs.length); 60 const tempPhase = new Float32Array(freqs.length); 61 this.filters.forEach((filter) => { 62 filter.getFrequencyResponse(freqs, tempMag, tempPhase); 63 for (let i = 0; i < freqs.length; i++) { 64 magResponse[i] *= tempMag[i]; 65 phaseResponse[i] += tempPhase[i]; 66 } 67 }); 68 } 69 disconnect() { 70 this.input.disconnect(); 71 this.output.disconnect(); 72 this.filters.forEach((filter) => filter.disconnect()); 73 } 74} 75const filterTypes = ["peaking", "lowshelf", "highshelf", "highpass", "lowpass", "notch", "bandpass", "allpass"]; 76const filterDescriptions = { 77 peaking: "Boosts or cuts a specific frequency range. Good for general EQ adjustments.", 78 lowshelf: "Boosts or cuts all frequencies below the target frequency. Good for adding bass.", 79 highshelf: 'Boosts or cuts all frequencies above the target frequency. Good for adding "air" or brightness.', 80 highpass: "Cuts all frequencies below the target frequency. Good for removing low-end rumble.", 81 lowpass: "Cuts all frequencies above the target frequency. Good for removing harsh high frequencies.", 82 notch: "Sharply cuts a very specific frequency. Good for removing unwanted resonance or feedback.", 83 bandpass: "Passes frequencies within a certain range and rejects frequencies outside that range.", 84 allpass: "Passes all frequencies equally, but changes their phase relationship." 85};
86const BandControls = ({ selectedBand, onUpdateBand, defaultBandType = "peaking", setDefaultBandType }) => { 87 const handleUpdate = (param, value) => { 88 if (selectedBand) { 89 onUpdateBand(selectedBand.id, { [param]: value }); 90 } else if (param === "type" && setDefaultBandType) { 91 setDefaultBandType(value); 92 } 93 }; 94 const isDisabled = !selectedBand; 95 const currentType = (selectedBand == null ? void 0 : selectedBand.type) || defaultBandType; 96 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `h-24 border-t border-gray-800 bg-black/30 p-2 flex items-center gap-4 text-xs`, children: [ 97 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", title: `Filter Type: ${filterDescriptions[currentType]}`, children: [ 98 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-gray-400 w-16 cursor-help", children: "Type" }), 99 /* @__PURE__ */ jsxRuntimeExports.jsx( 100 "select", 101 { 102 value: currentType, 103 onChange: (e) => handleUpdate("type", e.target.value), 104 className: "bg-[#222] border border-gray-700 rounded-md px-2 py-1 w-32 cursor-pointer", 105 children: filterTypes.map((type) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: type, children: type }, type)) 106 } 107 ) 108 ] }), 109 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", title: "Frequency (Hz): The target pitch to adjust. Lower numbers are bass, higher numbers are treble.", children: [
110 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: `text-gray-400 w-16 cursor-help ${isDisabled ? "opacity-50" : ""}`, children: "Frequency" }), 111 /* @__PURE__ */ jsxRuntimeExports.jsx( 112 "input", 113 { 114 type: "number", 115 value: (selectedBand == null ? void 0 : selectedBand.freq) ?? 1e3, 116 onChange: (e) => { 117 const val = parseInt(e.target.value); 118 handleUpdate("freq", Number.isNaN(val) ? 1e3 : val); 119 }, 120 className: "bg-[#222] border border-gray-700 rounded-md px-2 py-1 w-24 disabled:opacity-50", 121 disabled: isDisabled 122 } 123 ) 124 ] }), 125 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", title: "Gain (dB): How much to boost (positive) or cut (negative) the target frequency.", children: [ 126 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: `text-gray-400 w-12 cursor-help ${isDisabled ? "opacity-50" : ""}`, children: "Gain" }), 127 /* @__PURE__ */ jsxRuntimeExports.jsx( 128 "input", 129 { 130 type: "number", 131 value: (selectedBand == null ? void 0 : selectedBand.gain) ?? 0, 132 onChange: (e) => { 133 const val = parseFloat(e.target.value); 134 handleUpdate("gain", Number.isNaN(val) ? 0 : val); 135 }, 136 className: "bg-[#222] border border-gray-700 rounded-md px-2 py-1 w-24 disabled:opacity-50", 137 disabled: isDisabled 138 } 139 ) 140 ] }), 141 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", title: "Q Factor: How wide or narrow the affected frequency range is. Higher numbers affect a narrower range.", children: [
142 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: `text-gray-400 w-8 cursor-help ${isDisabled ? "opacity-50" : ""}`, children: "Q" }), 143 /* @__PURE__ */ jsxRuntimeExports.jsx( 144 "input", 145 { 146 type: "number", 147 step: "0.1", 148 value: (selectedBand == null ? void 0 : selectedBand.q) ?? 1.41, 149 onChange: (e) => { 150 const val = parseFloat(e.target.value); 151 handleUpdate("q", Number.isNaN(val) ? 1.41 : val); 152 }, 153 className: "bg-[#222] border border-gray-700 rounded-md px-2 py-1 w-24 disabled:opacity-50", 154 disabled: isDisabled 155 } 156 ) 157 ] }), 158 /* @__PURE__ */ jsxRuntimeExports.jsx( 159 "button", 160 { 161 onClick: () => handleUpdate("solo", !(selectedBand == null ? void 0 : selectedBand.solo)), 162 title: "Solo Band: When active, only the frequencies affected by this band (and other soloed bands) will be heard. Useful for isolating specific frequencies.", 163 disabled: isDisabled, 164 className: `p-2 rounded-full disabled:opacity-50 ${(selectedBand == null ? void 0 : selectedBand.solo) ? "bg-yellow-500 text-white" : "bg-gray-700 hover:bg-gray-600"}`, 165 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Headphones, { size: 14 }) 166 } 167 ),
168 isDisabled && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-gray-500 ml-4 italic", children: "Click on the chart to add or select a band" }) 169 ] }); 170}; 171const MIN_FREQ = 20; 172const MAX_FREQ = 2e4; 173const MIN_GAIN = -30; 174const MAX_GAIN = 30; 175const BAND_HANDLE_RADIUS = 8; 176const freqToLogX = (freq, width) => { 177 const logMin = Math.log10(MIN_FREQ); 178 const logMax = Math.log10(MAX_FREQ); 179 const logFreq = Math.log10(freq); 180 return width * (logFreq - logMin) / (logMax - logMin); 181}; 182const xToLogFreq = (x, width) => { 183 const logMin = Math.log10(MIN_FREQ); 184 const logMax = Math.log10(MAX_FREQ); 185 const logFreq = x / width * (logMax - logMin) + logMin; 186 return Math.pow(10, logFreq); 187}; 188const gainToY = (gain, height) => { 189 return height * (1 - (gain - MIN_GAIN) / (MAX_GAIN - MIN_GAIN)); 190}; 191const yToGain = (y, height) => { 192 return (1 - y / height) * (MAX_GAIN - MIN_GAIN) + MIN_GAIN; 193}; 194const ParametricEQ = ({ inputNode: initialInputNode, availableNodes = [], tracks = [], selectedTrackIndex = "master", onSelectTrack, trackId, userId }) => { 195 const [localSelectedNodeIndex, setLocalSelectedNodeIndex] = reactExports.useState("master"); 196 const [isRendering, setIsRendering] = reactExports.useState(false); 197 const [showLoadModal, setShowLoadModal] = reactExports.useState(false); 198 const [savedSettings, setSavedSettings] = reactExports.useState([]); 199 const activeTrackIndex = onSelectTrack ? selectedTrackIndex : localSelectedNodeIndex; 200 const setActiveTrackIndex = onSelectTrack || setLocalSelectedNodeIndex; 201 const [eqSettings, setEqSettings] = reactExports.useState({}); 202 reactExports.useEffect(() => { 203 setEqSettings({}); 204 setSelectedBandId(null); 205 setDraggedBandId(null); 206 }, [trackId]); 207 const currentSettings = eqSettings[activeTrackIndex] || { bands: [], isBypassed: false, outputGain: 0 }; 208 const bands = currentSettings.bands || []; 209 const isBypassed = currentSettings.isBypassed || false; 210 const outputGain = currentSettings.outputGain ?? 0; 211 const updateCurrentSettings = (updates) => { 212 setEqSettings((prev) => { 213 const current = prev[activeTrackIndex] || { bands: [], isBypassed: false, outputGain: 0 }; 214 const resolvedUpdates = typeof updates === "function" ? updates(current) : updates; 215 return { 216 ...prev, 217 [activeTrackIndex]: { 218 ...current, 219 ...resolvedUpdates 220 } 221 }; 222 }); 223 }; 224 const setBands = (newBands) => { 225 updateCurrentSettings((prev) => ({ 226 bands: typeof newBands === "function" ? newBands(prev.bands || []) : newBands 227 })); 228 }; 229 const setIsBypassed = (val) => updateCurrentSettings({ isBypassed: val }); 230 const setOutputGain = (val) => updateCurrentSettings({ outputGain: val }); 231 const [draggedBandId, setDraggedBandId] = reactExports.useState(null); 232 const [selectedBandId, setSelectedBandId] = reactExports.useState(null); 233 const [defaultBandType, setDefaultBandType] = reactExports.useState("peaking"); 234 const canvasRef = reactExports.useRef(null); 235 const containerRef = reactExports.useRef(null); 236 const eqServicesRef = reactExports.useRef({}); 237 const activeInputNode = reactExports.useMemo(() => { 238 if (activeTrackIndex === "master") return initialInputNode; 239 const node = availableNodes.find((n) => n.index === activeTrackIndex); 240 return node ? node.gainNode : null; 241 }, [activeTrackIndex, initialInputNode, availableNodes]); 242 reactExports.useEffect(() => { 243 const audioContext = getAudioContext(); 244 if (!audioContext) return; 245 const currentEqServices = eqServicesRef.current; 246 const activeKeys = /* @__PURE__ */ new Set(); 247 if (initialInputNode) { 248 activeKeys.add("master"); 249 if (!currentEqServices["master"]) { 250 const eq = new EQService(audioContext); 251 try { 252 initialInputNode.disconnect(audioContext.destination); 253 initialInputNode.connect(eq.input); 254 eq.output.connect(audioContext.destination); 255 } catch { 256 initialInputNode.connect(eq.input); 257 eq.output.connect(audioContext.destination); 258 } 259 currentEqServices["master"] = eq; 260 } 261 }
262 availableNodes.forEach((node) => { 263 const key = node.index.toString(); 264 activeKeys.add(key); 265 if (!currentEqServices[key]) { 266 const eq = new EQService(audioContext); 267 try { 268 node.gainNode.disconnect(initialInputNode || audioContext.destination); 269 node.gainNode.connect(eq.input); 270 eq.output.connect(initialInputNode || audioContext.destination); 271 } catch { 272 node.gainNode.connect(eq.input); 273 eq.output.connect(initialInputNode || audioContext.destination); 274 } 275 currentEqServices[key] = eq; 276 } 277 }); 278 Object.keys(currentEqServices).forEach((key) => { 279 if (!activeKeys.has(key)) { 280 const eq = currentEqServices[key]; 281 try { 282 if (key === "master" && initialInputNode) { 283 initialInputNode.disconnect(eq.input); 284 eq.output.disconnect(audioContext.destination); 285 initialInputNode.connect(audioContext.destination); 286 } else { 287 const node = availableNodes.find((n) => n.index.toString() === key); 288 if (node) { 289 node.gainNode.disconnect(eq.input); 290 eq.output.disconnect(initialInputNode || audioContext.destination); 291 node.gainNode.connect(initialInputNode || audioContext.destination); 292 } 293 } 294 } catch { 295 } 296 eq.disconnect(); 297 delete currentEqServices[key]; 298 } 299 }); 300 return () => { 301 }; 302 }, [initialInputNode, availableNodes]); 303 reactExports.useEffect(() => { 304 return () => { 305 const audioContext = getAudioContext(); 306 if (!audioContext) return; 307 Object.keys(eqServicesRef.current).forEach((key) => { 308 const eq = eqServicesRef.current[key]; 309 try { 310 if (key === "master" && initialInputNode) { 311 initialInputNode.disconnect(eq.input); 312 eq.output.disconnect(audioContext.destination); 313 initialInputNode.connect(audioContext.destination); 314 } else { 315 eq.output.disconnect(); 316 } 317 } catch { 318 } 319 eq.disconnect(); 320 }); 321 eqServicesRef.current = {}; 322 }; 323 }, [initialInputNode]); 324 reactExports.useEffect(() => { 325 Object.keys(eqServicesRef.current).forEach((key) => { 326 const eq = eqServicesRef.current[key]; 327 const settings = eqSettings[key] || { bands: [], isBypassed: false, outputGain: 0 }; 328 eq.setBypass(settings.isBypassed || false); 329 eq.updateBands(settings.bands || []); 330 eq.setOutputGain(settings.outputGain ?? 0); 331 }); 332 }, [eqSettings, availableNodes, initialInputNode]); 333 const draw = reactExports.useCallback(() => { 334 const canvas = canvasRef.current; 335 const ctx = canvas == null ? void 0 : canvas.getContext("2d"); 336 if (!canvas || !ctx) return; 337 const eq = eqServicesRef.current[activeTrackIndex.toString()]; 338 const { width, height } = canvas; 339 ctx.clearRect(0, 0, width, height); 340 ctx.fillStyle = "#0a0a0a"; 341 ctx.fillRect(0, 0, width, height); 342 ctx.strokeStyle = "#222"; 343 ctx.lineWidth = 1; 344 ctx.fillStyle = "#555"; 345 ctx.font = "10px Inter, sans-serif"; 346 ctx.textAlign = "center"; 347 const freqs = [20, 50, 100, 200, 500, 1e3, 2e3, 5e3, 1e4, 2e4]; 348 const hasSolo = bands.some((b) => b.solo); 349 freqs.forEach((f) => { 350 const x = Math.floor(freqToLogX(f, width)); 351 ctx.beginPath(); 352 ctx.moveTo(x, 0); 353 ctx.lineTo(x, height); 354 ctx.stroke(); 355 const label = f >= 1e3 ? `${f / 1e3}k` : `${f}`; 356 ctx.fillText(label, x, height - 8); 357 }); 358 ctx.textAlign = "right"; 359 ctx.textBaseline = "middle"; 360 for (let g = MIN_GAIN; g <= MAX_GAIN; g += 6) { 361 if (g === 0) { 362 ctx.strokeStyle = "#444"; 363 ctx.lineWidth = 1.5; 364 } else { 365 ctx.strokeStyle = "#222"; 366 ctx.lineWidth = 1; 367 } 368 const y = Math.round(gainToY(g, height)); 369 ctx.beginPath(); 370 ctx.moveTo(0, y); 371 ctx.lineTo(width, y); 372 ctx.stroke(); 373 if (g !== 0) { 374 ctx.fillText(`${g > 0 ? "+" : ""}${g}`, 25, y); 375 } 376 } 377 if (eq && width > 0 && height > 0) { 378 const freqPoints = new Float32Array(width); 379 for (let i = 0; i < width; i++) { 380 freqPoints[i] = xToLogFreq(i, width); 381 } 382 const magResponse = new Float32Array(width); 383 const phaseResponse = new Float32Array(width); 384 eq.getFrequencyResponse(freqPoints, magResponse, phaseResponse); 385 ctx.strokeStyle = "#10b981"; 386 ctx.lineWidth = 3; 387 ctx.shadowBlur = 10; 388 ctx.shadowColor = "rgba(16, 185, 129, 0.4)"; 389 ctx.beginPath(); 390 for (let i = 0; i < width; i++) { 391 if (!Number.isFinite(magResponse[i]) || magResponse[i] <= 0) continue; 392 const db = 20 * Math.log10(magResponse[i]); 393 const y = gainToY(db, height); 394 if (!Number.isFinite(y)) continue; 395 if (i === 0) ctx.moveTo(i, y); 396 else ctx.lineTo(i, y); 397 } 398 ctx.stroke(); 399 ctx.shadowBlur = 0; 400 } 401 bands.forEach((band) => { 402 const x = freqToLogX(band.freq, width); 403 const y = gainToY(band.gain, height); 404 if (!Number.isFinite(x) || !Number.isFinite(y)) return; 405 const isSelected = selectedBandId === band.id; 406 const isSoloed = band.solo; 407 const isDimmed = hasSolo && !isSoloed; 408 if (isSelected || isSoloed) { 409 ctx.shadowBlur = 15; 410 ctx.shadowColor = isSoloed ? "#f59e0b" : "#10b981"; 411 } 412 ctx.fillStyle = isDimmed ? "#333" : isSoloed ? "#f59e0b" : isSelected ? "#10b981" : "#3b82f6"; 413 ctx.beginPath(); 414 ctx.arc(x, y, isSelected ? BAND_HANDLE_RADIUS + 2 : BAND_HANDLE_RADIUS, 0, 2 * Math.PI); 415 ctx.fill(); 416 ctx.strokeStyle = isDimmed ? "#444" : "white"; 417 ctx.lineWidth = 2; 418 ctx.stroke(); 419 ctx.shadowBlur = 0; 420 }); 421 }, [bands, selectedBandId, activeTrackIndex]); 422 reactExports.useEffect(() => { 423 let animationFrameId; 424 const render = () => { 425 draw(); 426 animationFrameId = requestAnimationFrame(render); 427 }; 428 render(); 429 return () => { 430 cancelAnimationFrame(animationFrameId); 431 }; 432 }, [draw]); 433 reactExports.useEffect(() => { 434 const handleResize = () => { 435 if (containerRef.current && canvasRef.current) { 436 const { clientWidth, clientHeight } = containerRef.current; 437 if (canvasRef.current.width !== clientWidth) { 438 canvasRef.current.width = clientWidth; 439 } 440 if (canvasRef.current.height !== clientHeight) { 441 canvasRef.current.height = clientHeight; 442 } 443 } 444 }; 445 handleResize(); 446 window.addEventListener("resize", handleResize); 447 let observer = null; 448 if (containerRef.current) { 449 observer = new ResizeObserver(handleResize); 450 observer.observe(containerRef.current); 451 } 452 return () => { 453 window.removeEventListener("resize", handleResize); 454 if (observer) observer.disconnect(); 455 }; 456 }, []); 457 const getBandAtPos = (x, y) => { 458 const canvas = canvasRef.current; 459 if (!canvas) return null; 460 for (let i = bands.length - 1; i >= 0; i--) { 461 const band = bands[i]; 462 const bandX = freqToLogX(band.freq, canvas.width); 463 const bandY = gainToY(band.gain, canvas.height); 464 const dx = x - bandX; 465 const dy = y - bandY; 466 if (Math.sqrt(dx * dx + dy * dy) <= BAND_HANDLE_RADIUS + 6) { 467 return band; 468 } 469 } 470 return null; 471 }; 472 const handleMouseDown = (e) => { 473 const canvas = canvasRef.current; 474 if (!canvas) return; 475 const rect = canvas.getBoundingClientRect(); 476 const x = e.clientX - rect.left; 477 const y = e.clientY - rect.top; 478 const clickedBand = getBandAtPos(x, y); 479 if (clickedBand) { 480 setDraggedBandId(clickedBand.id); 481 setSelectedBandId(clickedBand.id); 482 } else { 483 let freq = xToLogFreq(x, canvas.width); 484 let gain = yToGain(y, canvas.height); 485 freq = Number.isFinite(freq) ? Math.max(MIN_FREQ, Math.min(MAX_FREQ, freq)) : 1e3; 486 gain = Number.isFinite(gain) ? Math.max(MIN_GAIN, Math.min(MAX_GAIN, gain)) : 0; 487 const newBand = { 488 id: v4(), 489 freq: Math.round(freq), 490 gain: parseFloat(gain.toFixed(1)), 491 q: 1.41, 492 type: defaultBandType, 493 solo: false 494 }; 495 setBands((prev) => [...prev, newBand]); 496 setDraggedBandId(newBand.id); 497 setSelectedBandId(newBand.id); 498 } 499 }; 500 const handleMouseMove = (e) => { 501 if (!draggedBandId) return; 502 const canvas = canvasRef.current; 503 if (!canvas) return; 504 const rect = canvas.getBoundingClientRect(); 505 const x = e.clientX - rect.left; 506 const y = e.clientY - rect.top; 507 let freq = xToLogFreq(x, canvas.width); 508 let gain = yToGain(y, canvas.height); 509 freq = Number.isFinite(freq) ? Math.max(MIN_FREQ, Math.min(MAX_FREQ, freq)) : 1e3; 510 gain = Number.isFinite(gain) ? Math.max(MIN_GAIN, Math.min(MAX_GAIN, gain)) : 0; 511 setBands((prev) => prev.map( 512 (b) => b.id === draggedBandId ? { ...b, freq: Math.round(freq), gain: parseFloat(gain.toFixed(1)) } : b 513 )); 514 }; 515 const handleMouseUp = () => { 516 setDraggedBandId(null); 517 }; 518 const handleMouseLeave = () => { 519 setDraggedBandId(null); 520 }; 521 const handleDoubleClick = (e) => { 522 const canvas = canvasRef.current; 523 if (!canvas) return; 524 const rect = canvas.getBoundingClientRect(); 525 const x = e.clientX - rect.left; 526 const y = e.clientY - rect.top; 527 const clickedBand = getBandAtPos(x, y); 528 if (clickedBand) { 529 setBands((prev) => prev.filter((b) => b.id !== clickedBand.id)); 530 if (selectedBandId === clickedBand.id) { 531 setSelectedBandId(null); 532 } 533 if (draggedBandId === clickedBand.id) { 534 setDraggedBandId(null); 535 } 536 } 537 }; 538 const handleUpdateBand = (id, updates) => { 539 setBands((prev) => prev.map((b) => b.id === id ? { ...b, ...updates } : b)); 540 }; 541 const selectedBand = reactExports.useMemo(() => bands.find((b) => b.id === selectedBandId), [bands, selectedBandId]); 542 const handleSaveEQ = async () => { 543 if (!trackId || userId === void 0 || userId === null) return alert("Please login to save EQ settings"); 544 const name = prompt("Enter a name for this EQ setting:"); 545 if (!name) return; 546 try { 547 const res = await fetch(`/api/tracks/${trackId}/eq-settings`, { 548 method: "POST", 549 headers: { "Content-Type": "application/json" }, 550 body: JSON.stringify({ userId, name, settings: currentSettings }) 551 }); 552 const data = await res.json(); 553 if (!res.ok) throw new Error(data.error || "Failed to save"); 554 alert("EQ Settings saved successfully!"); 555 } catch (e) { 556 alert(e.message); 557 } 558 }; 559 const loadSavedSettings = async () => { 560 if (!trackId || userId === void 0 || userId === null) return alert("Please login to load settings"); 561 try { 562 const res = await fetch(`/api/tracks/${trackId}/eq-settings?userId=${userId}`); 563 const data = await res.json(); 564 if (res.ok) { 565 setSavedSettings(data); 566 setShowLoadModal(true); 567 } 568 } catch (e) { 569 console.error(e); 570 } 571 }; 572 const applySavedSetting = (setting) => { 573 const savedData = setting.settings; 574 if (savedData && !Array.isArray(savedData.bands) && (savedData["master"] || Object.keys(savedData).some((k) => !isNaN(Number(k))))) { 575 const legacySettings = savedData["master"] || Object.values(savedData)[0] || { bands: [], isBypassed: fal
575se, outputGain: 0 }; 576 updateCurrentSettings(legacySettings); 577 } else { 578 updateCurrentSettings(savedData || { bands: [], isBypassed: false, outputGain: 0 }); 579 } 580 setShowLoadModal(false); 581 }; 582 const deleteSavedSetting = async (settingId) => { 583 if (!trackId || userId === void 0 || userId === null) return; 584 try { 585 const res = await fetch(`/api/tracks/${trackId}/eq-settings/${settingId}`, { 586 method: "DELETE", 587 headers: { "Content-Type": "application/json" }, 588 body: JSON.stringify({ userId }) 589 }); 590 if (res.ok) { 591 setSavedSettings((prev) => prev.filter((s) => s.id !== settingId)); 592 } 593 } catch (e) { 594 console.error(e); 595 } 596 }; 597 const handleDownloadOpus = async () => { 598 if (!trackId || userId === void 0 || userId === null) return alert("Please login to download"); 599 try { 600 const res = await fetch(`/api/tracks/${trackId}/charge-eq-download`, { 601 method: "POST", 602 headers: { "Content-Type": "application/json" }, 603 body: JSON.stringify({ userId }) 604 }); 605 const data = await res.json(); 606 if (!res.ok) throw new Error(data.error || "Failed to charge credits"); 607 setIsRendering(true); 608 const channels = synth.getChannelBuffers(); 609 if (!channels.length || !channels[0].buffer) throw new Error("Audio not loaded"); 610 const sampleRate = channels[0].buffer.sampleRate; 611 const length = channels[0].buffer.length; 612 const offlineCtx = new OfflineAudioContext(2, length, sampleRate); 613 const masterGain = offlineCtx.createGain(); 614 masterGain.connect(offlineCtx.destination); 615 const masterSettings = eqSettings["master"]; 616 if (masterSettings && !masterSettings.isBypassed) { 617 const masterEq = new EQService(offlineCtx); 618 masterEq.updateBands(masterSettings.bands); 619 masterEq.setOutputGain(masterSettings.outputGain); 620 masterGain.disconnect(); 621 masterGain.connect(masterEq.input); 622 masterEq.output.connect(offlineCtx.destination); 623 } 624 channels.forEach((ch, idx) => { 625 if (!ch.buffer) return; 626 const source = offlineCtx.createBufferSource(); 627 source.buffer = ch.buffer; 628 const trackGain = offlineCtx.createGain(); 629 trackGain.gain.value = ch.userVolume; 630 source.connect(trackGain); 631 const trackSettings = eqSettings[idx]; 632 if (trackSettings && !trackSettings.isBypassed) { 633 const trackEq = new EQService(offlineCtx); 634 trackEq.updateBands(trackSettings.bands); 635 trackEq.setOutputGain(trackSettings.outputGain); 636 trackGain.connect(trackEq.input); 637 trackEq.output.connect(masterGain); 638 } else { 639 trackGain.connect(masterGain); 640 } 641 source.start(0); 642 }); 643 const renderedBuffer = await offlineCtx.startRendering(); 644 const lamejs = await __vitePreload(() => import("./vendor-audio-DH3w0BzW.js").then((n) => n.i), true ? __vite__mapDeps([0,1,2,3,4]) : void 0); 645 const mp3encoder = new lamejs.Mp3Encoder(2, sampleRate, 128); 646 const mp3Data = []; 647 const left = renderedBuffer.getChannelData(0); 648 const right = renderedBuffer.getChannelData(1); 649 const sampleBlockSize = 1152; 650 for (let i = 0; i < left.length; i += sampleBlockSize) { 651 const leftChunk = left.subarray(i, i + sampleBlockSize); 652 const rightChunk = right.subarray(i, i + sampleBlockSize); 653 const leftInt = new Int16Array(leftChunk.length); 654 const rightInt = new Int16Array(rightChunk.length); 655 for (let j = 0; j < leftChunk.length; j++) { 656 const sL = Math.max(-1, Math.min(1, leftChunk[j])); 657 const sR = Math.max(-1, Math.min(1, rightChunk[j])); 658 leftInt[j] = sL < 0 ? sL * 32768 : sL * 32767; 659 rightInt[j] = sR < 0 ? sR * 32768 : sR * 32767; 660 } 661 const mp3buf2 = mp3encoder.encodeBuffer(leftInt, rightInt); 662 if (mp3buf2.length > 0) { 663 mp3Data.push(mp3buf2); 664 } 665 } 666 const mp3buf = mp3encoder.flush(); 667 if (mp3buf.length > 0) { 668 mp3Data.push(mp3buf); 669 } 670 const blob = new Blob(mp3Data, { type: "audio/mp3" }); 671 const url = URL.createObjectURL(blob); 672 const a = document.createElement("a"); 673 a.href = url; 674 a.download = `eq-modified-${trackId}.opus`; 675 a.click(); 676 URL.revokeObjectURL(url); 677 alert(`Downloaded successfully! Remaining credits: ${data.newCredits}`); 678 } catch (e) { 679 alert(e.message); 680 } finally { 681 setIsRendering(false); 682 } 683 }; 684 const handleResetTrack = () => {
685 updateCurrentSettings({ bands: [], isBypassed: false, outputGain: 0 }); 686 setSelectedBandId(null); 687 setDraggedBandId(null); 688 }; 689 const handleResetAll = () => { 690 setEqSettings({}); 691 setSelectedBandId(null); 692 setDraggedBandId(null); 693 }; 694 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 695 "div", 696 { 697 className: "w-full h-full bg-[#050505] text-white flex flex-col p-4 rounded-lg select-none relative", 698 onClick: (e) => e.stopPropagation(), 699 children: [ 700 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-2", children: [ 701 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [ 702 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-sm font-bold uppercase tracking-widest text-zinc-500", children: "Parametric EQ" }), 703 /* @__PURE__ */ jsxRuntimeExports.jsxs( 704 "select", 705 { 706 value: activeTrackIndex, 707 onChange: (e) => setActiveTrackIndex(e.target.value === "master" ? "master" : parseInt(e.target.value)), 708 className: "bg-zinc-900 border border-zinc-800 rounded px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-emerald-500", 709 children: [
710 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "master", children: "Master Output" }), 711 tracks && tracks.length > 0 ? tracks.map((track, idx) => { 712 const t = track; 713 if (t.originalName === "Informational Track" || t.displayName === "Informational Track") return null; 714 return /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: idx, children: t.displayName }, idx); 715 }) : availableNodes.map((node) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: node.index, children: node.name }, node.index)) 716 ] 717 } 718 ), 719 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 ml-2", children: [ 720 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: handleResetTrack, className: "px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-xs rounded text-white flex items-center gap-1 transition-colors", title: "Reset Current Track", children: [
721 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 14 }), 722 " Reset Track" 723 ] }), 724 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: handleResetAll, className: "px-2 py-1 bg-red-900/30 hover:bg-red-900/50 text-red-400 text-xs rounded flex items-center gap-1 transition-colors", title: "Reset All Tracks", children: [ 725 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { size: 14 }), 726 " Reset All" 727 ] }) 728 ] }) 729 ] }), 730 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 731 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: handleSaveEQ, className: "px-3 py-1 bg-zinc-800 hover:bg-zinc-700 text-xs rounded text-white flex items-center gap-1 transition-colors", children: [ 732 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 14 }), 733 " Save Preset (1 Credit)" 734 ] }), 735 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: loadSavedSettings, className: "px-3 py-1 bg-zinc-800 hover:bg-zinc-700 text-xs rounded text-white flex items-center gap-1 transition-colors", children: [
736 /* @__PURE__ */ jsxRuntimeExports.jsx(FolderOpen, { size: 14 }), 737 " Load" 738 ] }), 739 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: handleDownloadOpus, disabled: isRendering, className: "px-3 py-1 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed text-xs rounded text-white flex items-center gap-1 transition-colors", children: [ 740 isRendering ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 14 }), 741 isRendering ? "Rendering..." : "Download Opus (2 Credits)" 742 ] }) 743 ] }) 744 ] }), 745 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: containerRef, className: "flex-1 min-h-0 bg-black rounded-lg border border-zinc-900 relative overflow-hidden", children: [ 746 /* @__PURE__ */ jsxRuntimeExports.jsx( 747 "canvas", 748 { 749 ref: canvasRef, 750 className: "absolute top-0 left-0 w-full h-full cursor-crosshair", 751 onMouseDown: handleMouseDown, 752 onMouseMove: handleMouseMove, 753 onMouseUp: handleMouseUp, 754 onMouseLeave: handleMouseLeave, 755 onDoubleClick: handleDoubleClick 756 } 757 ), 758 !activeInputNode && activeTrackIndex !== "master" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 text-sm font-bold tracking-widest uppercase", children: "Track not loaded yet. EQ will apply when played." }) }) 759 ] }),
760 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 flex flex-col gap-2 shrink-0", children: [ 761 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 762 BandControls, 763 { 764 selectedBand, 765 onUpdateBand: handleUpdateBand, 766 defaultBandType, 767 setDefaultBandType 768 } 769 ) }), 770 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 bg-zinc-900/50 p-2 rounded-lg border border-zinc-800", children: [ 771 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex items-center gap-3", children: [ 772 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[10px] uppercase text-zinc-500 font-bold whitespace-nowrap", children: "Output Gain" }), 773 /* @__PURE__ */ jsxRuntimeExports.jsx( 774 "input", 775 { 776 type: "range", 777 min: "-24", 778 max: "12", 779 step: "0.1", 780 value: outputGain, 781 onChange: (e) => { 782 const val = parseFloat(e.target.value); 783 setOutputGain(Number.isNaN(val) ? 0 : val); 784 }, 785 className: "flex-1 h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-emerald-500" 786 } 787 ),
788 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-emerald-500 text-xs w-12 text-right", children: [ 789 outputGain.toFixed(1), 790 " dB" 791 ] }) 792 ] }), 793 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-6 bg-zinc-800 mx-2" }), 794 /* @__PURE__ */ jsxRuntimeExports.jsxs( 795 "button", 796 { 797 onClick: () => setIsBypassed(!isBypassed), 798 className: `px-4 py-1.5 rounded font-bold text-[10px] flex items-center gap-2 transition-colors ${isBypassed ? "bg-red-500/20 text-red-500 border border-red-500/30" : "bg-emerald-500/20 text-emerald-500 border border-emerald-500/30"}`, 799 title: isBypassed ? "Bypass Active" : "EQ Active", 800 children: [ 801 /* @__PURE__ */ jsxRuntimeExports.jsx(Power, { size: 12 }), 802 isBypassed ? "BYPASSED" : "ACTIVE" 803 ] 804 } 805 ) 806 ] }) 807 ] }), 808 showLoadModal && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 border border-zinc-800 rounded-xl p-6 w-full max-w-md
808shadow-2xl", children: [ 809 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-bold text-white mb-4", children: "Load EQ Settings" }), 810 savedSettings.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-400 text-sm mb-6", children: "No saved settings found for this track." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-2 mb-6 max-h-60 overflow-y-auto pr-2", children: savedSettings.map((setting) => { 811 const s = setting; 812 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between bg-zinc-800/50 p-3 rounded-lg border border-zinc-700/50", children: [ 813 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-200 font-medium", children: s.name }), 814 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 815 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => applySavedSetting(s), className: "px-3 py-1 bg-emerald-600 hover:bg-emerald-500 text-xs rounded text-white transition-colors", children: "Load" }),
816 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => deleteSavedSetting(s.id), className: "px-3 py-1 bg-red-600/20 hover:bg-red-600/40 text-red-500 text-xs rounded transition-colors", children: "Delete" }) 817 ] }) 818 ] }, s.id); 819 }) }), 820 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowLoadModal(false), className: "px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-sm rounded text-white transition-colors", children: "Close" }) }) 821 ] }) }) 822 ] 823 } 824 ); 825}; 826export { 827 ParametricEQ as default 828};
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.