PageSourceSearch

https://www.hamienet.com/assets/ParametricEQ-wkxmF0lG.js

js hamienet.com collected 2026-09-24 19:00:01 UTC 38,966 bytes, 828 lines download raw bytes

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.