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