PageSourceSearch

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

js hamienet.com collected 2026-10-02 05:00:59 UTC 39,026 bytes, 830 lines download raw bytes

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.