PageSourceSearch

https://www.hamienet.com/assets/VocalProcessorPage-Dz4yqXND.js

js hamienet.com collected 2026-10-02 05:00:17 UTC 86,787 bytes, 1,811 lines download raw bytes

1import { r as reactExports, R as React, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js";
2import { ee as v4, ef as useSensors, eg as useSensor, eh as DndContext, ei as closestCenter, ej as SortableContext, ek as verticalListSortingStrategy, el as sortableKeyboardCoordinates, em as KeyboardSensor, en as PointerSensor, eo as useSortable, ep as CSS, bU as Volume2, c$ as Minimize2, cg as Activity, eq as AudioLines, cq as Settings2, bs as Zap, er as SquareSplitHorizontal, es as ArrowDownUp, cF as Key, et as ShieldMinus, eu as Power, bi as Sparkles, cI as Disc, di as Square, bT as Mic, b3 as FileHeadphone, b2 as Upload, bV as Play, aX as ArrowLeft, b5 as Trash2, ev as Bookmark, b1 as CircleAlert } from "./vendor-D0P4XgNj.js";
3import { g as getAudioContext } from "./synth-Fr-K52Ox.js";
4import "./vendor-i18n-BC7ZMXWQ.js";
5import "./vendor-react-three-BCww3P1x.js";
6import "./utils-CEVb7nN1.js";
7import "./index-w5lXls15.js";
8import "./wasm-synth-adapter-DpNGIIyM.js";
9import "./vendor-spessasynth-DH8n6o_V.js";
10import "./vendor-audio-D-wTggbx.js";
11const createDefaultChain = () => [
12  { id: v4(), type: "noise-gate", name: "Noise Gate", enabled: false, params: { threshold: -40, attack: 5, release: 100, hold: 50 } },
13  { id: v4(), type: "pitch-correction", name: "Pitch Correction", enabled: false, params: { key: "C", scale: "Major", retuneSpeed: 50, humanize: true } },
14  { id: v4(), type: "pitch-shift", name: "Pitch Shift", enabled: false, params: { semitones: 0, cents: 0 } },
15  { id: v4(), type: "formant-shift", name: "Formant Shift", enabled: false, params: { semitones: 0 } },
16  { id: v4(), type: "robot", name: "Robot", enabled: false, params: { note: 60 } },
17  { id: v4(), type: "vocoder", name: "Vocoder", enabled: false, params: { carrierType: "sawtooth", carrierFreq: 110, bandCount: 16, release: 200, mix: 100 } },
18  { id: v4(), type: "harmonizer", name: "Harmonizer", enabled: false, params: { voices: [{ interval: 7, volume: 80, pan: -50 }, { interval: -5, volume: 80, pan: 50 }] } },
19  { id: v4(), type: "distortion", name: "Distortion", enabled: false, params: { type: "softClip", drive: 50, mix: 100 } },
20  { id: v4(), type: "eq", name: "EQ", enabled: false, params: { bass: 0, mid: 0, treble: 0 } },
21  { id: v4(), type: "compressor", name: "Compressor", enabled: false, params: { threshold: -24, ratio: 4, attack: 10, release: 100, makeupGain: 0 } },
22  { id: v4(), type: "delay", name: "Delay", enabled: false, params: { time: 300, feedback: 30, mix: 20 } },
23  { id: v4(), type: "reverb", name: "Reverb", enabled: false, params: { roomSize: "medium", decay: 2, mix: 30 } }
24];
25const initialState = {
26  input: {
27    mode: "mic"
28  },
29  chain: createDefaultChain(),
30  master: {
31    wetDry: 100,
32    outputVolume: 100,
33    bypass: false
34  },
35  preset: {
36    name: "Init Preset",
37    isDirty: false
38  },
39  ui: {
40    selectedModuleId: null,
41    isRecording: false
42  }
43};
44const VocalProcessorContext = reactExports.createContext({ state: initialState, dispatch: () => null });
45function vocalProcessorReducer(state, action) {
46  switch (action.type) {
47    case "SET_INPUT_MODE":
48      return { ...state, input: { ...state.input, mode: action.payload } };
49    case "SET_MIC_DEVICE":
50      return { ...state, input: { ...state.input, deviceId: action.payload } };
51    case "SET_FILE_NAME":
52      return { ...state, input: { ...state.input, fileName: action.payload } };
53    case "TOGGLE_MODULE":
54      return {
55        ...state,
56        preset: { ...state.preset, isDirty: true },
57        chain: state.chain.map(
58          (mod) => mod.id === action.payload ? { ...mod, enabled: !mod.enabled } : mod
59        )
60      };
61    case "UPDATE_MODULE_PARAMS":
62      return {
63        ...state,
64        preset: { ...state.preset, isDirty: true },
65        chain: state.chain.map(
66          (mod) => mod.id === action.payload.id ? { ...mod, params: { ...mod.params, ...action.payload.params } } : mod
67        )
68      };
69    case "REORDER_CHAIN": {
70      const { oldIndex, newIndex } = action.payload;
71      const newChain = [...state.chain];
72      const [moved] = newChain.splice(oldIndex, 1);
73      newChain.splice(newIndex, 0, moved);
74      return { ...state, preset: { ...state.preset, isDirty: true }, chain: newChain };
75    }
76    case "LOAD_PRESET": {
77      return {
78        ...state,
79        chain: action.payload.chain,
80        master: action.payload.master,
81        ui: { ...state.ui, selectedModuleId: null }
82      };
83    }
84    case "SELECT_MODULE":
85      return { ...state, ui: { ...state.ui, selectedModuleId: action.payload } };
86    case "SET_MASTER_WET_DRY":
87      return { ...state, master: { ...state.master, wetDry: action.payload } };
88    case "SET_MASTER_VOLUME":
89      return { ...state, master: { ...state.master, outputVolume: action.payload } };
90    case "TOGGLE_BYPASS":
91      return { ...state, master: { ...state.master, bypass: action.payload } };
92    case "SET_RECORDING_STATE":
93      return { ...state, ui: { ...state.ui, isRecording: action.payload } };
94    case "SET_PRESET":
95      return {
96        ...state,
97        preset: { name: action.payload.name, isDirty: false },
98        chain: action.payload.chain
99      };
100    default:
101      return state;
102  }
103}
104function VocalProcessorProvider({ children }) {
105  const [state, dispatch] = reactExports.useReducer(vocalProcessorReducer, initialState);
106  React.useEffect(() => {
107    if (!state.ui.selectedModuleId && state.chain.length > 0) {
108      dispatch({ type: "SELECT_MODULE", payload: state.chain[0].id });
109    }
110  }, []);
111  return /* @__PURE__ */ jsxRuntimeExports.jsx(VocalProcessorContext.Provider, { value: { state, dispatch }, children });
112}
113function useVocalProcessor() {
114  const context = reactExports.useContext(VocalProcessorContext);
115  if (!context) {
116    throw new Error("useVocalProcessor must be used within a VocalProcessorProvider");
117  }
118  return context;
119}
120const TYPE_ICONS = {
121  "noise-gate": ShieldMinus,
122  "pitch-correction": Key,
123  "pitch-shift": ArrowDownUp,
124  "formant-shift": SquareSplitHorizontal,
125  "robot": Zap,
126  "vocoder": Settings2,
127  "harmonizer": AudioLines,
128  "distortion": Activity,
129  "eq": Activity,
130  "compressor": Minimize2,
131  "reverb": Volume2,
132  "delay": Volume2
133};
134function SortableModuleItem({ module }) {
135  const { state, dispatch } = useVocalProcessor();
136  const {
137    attributes,
138    listeners,
139    setNodeRef,
140    transform,
141    transition
142  } = useSortable({ id: module.id });
143  const style = {
144    transform: CSS.Transform.toString(transform),
145    transition
146  };
147  const isSelected = state.ui.selectedModuleId === module.id;
148  const Icon = TYPE_ICONS[module.type] || Settings2;
149  const handleToggle = (e) => {
150    e.stopPropagation();
151    dispatch({ type: "TOGGLE_MODULE", payload: module.id });
152  };
153  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
154    "div",
155    {
156      ref: setNodeRef,
157      style,
158      className: `
159        flex items-center gap-3 p-3 mb-2 rounded-xl cursor-pointer
160        border transition-colors
161        ${isSelected ? "bg-zinc-800 border-zinc-600" : "bg-zinc-900/50 border-zinc-800 hover:border-zinc-700"}
162      `,
163      onClick: () => dispatch({ type: "SELECT_MODULE", payload: module.id }),
164      children: [
165        /* @__PURE__ */ jsxRuntimeExports.jsx(
166          "div",
167          {
168            ...attributes,
169            ...listeners,
170            className: "text-zinc-600 hover:text-zinc-400 cursor-grab active:cursor-grabbing p-1",
171            children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowDownUp, { className: "w-4 h-4" })
172          }
173        ),
174        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `p-1.5 rounded-lg ${module.enabled ? "bg-rose-500/20 text-rose-400" : "bg-zinc-800 text-zinc-500"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { className: "w-4 h-4" }) }),
175        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `font-medium text-sm leading-tight ${module.enabled ? "text-zinc-200" : "text-zinc-500"}`, children: module.name }) }),
176        /* @__PURE__ */ jsxRuntimeExports.jsx(
177          "button",
178          {
179            onClick: handleToggle,
180            className: `p-1.5 rounded-full transition-colors ${module.enabled ? "text-rose-400 hover:bg-rose-500/20" : "text-zinc-600 hover:bg-zinc-800"}`,
181            children: /* @__PURE__ */ jsxRuntimeExports.jsx(Power, { className: "w-4 h-4" })
182          }
183        )
184      ]
185    }
186  );
187}
188function SignalChainSidebar() {
189  const { state, dispatch } = useVocalProcessor();
190  const sensors = useSensors(
191    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
192    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
193  );
194  const handleDragEnd = (event) => {
195    const { active, over } = event;
196    if (over && active.id !== over.id) {
197      const oldIndex = state.chain.findIndex((mod) => mod.id === active.id);
198      const newIndex = state.chain.findIndex((mod) => mod.id === over.id);
199      dispatch({ type: "REORDER_CHAIN", payload: { oldIndex, newIndex } });
200    }
201  };
202  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-1 pb-20", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
203    DndContext,
204    {
205      sensors,
206      collisionDetection: closestCenter,
207      onDragEnd: handleDragEnd,
208      children: /* @__PURE__ */ jsxRuntimeExports.jsx(
209        SortableContext,
210        {
211          items: state.chain.map((m) => m.id),
212          strategy: verticalListSortingStrategy,
213          children: state.chain.map((module) => /* @__PURE__ */ jsxRuntimeExports.jsx(SortableModuleItem, { module }, module.id))
214        }
215      )
216    }
217  ) });
218}
219function ModuleParameterPanel() {
220  const { state, dispatch } = useVocalProcessor();
221  const selectedModule = state.chain.find((m) => m.id === state.ui.selectedModuleId);
222  if (!selectedModule) {
223    return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full text-zinc-500", children: "Select a module from the signal chain to edit its parameters." });
224  }
225  const updateParams = (newParams) => {
226    dispatch({
227      type: "UPDATE_MODULE_PARAMS",
228      payload: { id: selectedModule.id, params: newParams }
229    });
230  };
231  const renderControls = () => {
232    switch (selectedModule.type) {
233      case "noise-gate":
234        return /* @__PURE__ */ jsxRuntimeExports.jsx(NoiseGateControls, { params: selectedModule.params, onChange: updateParams });
235      case "eq":
236        return /* @__PURE__ */ jsxRuntimeExports.jsx(EQControls, { params: selectedModule.params, onChange: updateParams });
237      case "compressor":
238        return /* @__PURE__ */ jsxRuntimeExports.jsx(CompressorControls, { params: selectedModule.params, onChange: updateParams });
239      case "delay":
240        return /* @__PURE__ */ jsxRuntimeExports.jsx(DelayControls, { params: selectedModule.params, onChange: updateParams });
241      case "reverb":
242        return /* @__PURE__ */ jsxRuntimeExports.jsx(ReverbControls, { params: selectedModule.params, onChange: updateParams });
243      case "pitch-correction":
244        return /* @__PURE__ */ jsxRuntimeExports.jsx(PitchCorrectionControls, { params: selectedModule.params, onChange: updateParams });
245      case "pitch-shift":
246        return /* @__PURE__ */ jsxRuntimeExports.jsx(PitchShiftControls, { params: selectedModule.params, onChange: updateParams });
247      case "formant-shift":
248        return /* @__PURE__ */ jsxRuntimeExports.jsx(FormantShiftControls, { params: selectedModule.params, onChange: updateParams });
249      case "robot":
250        return /* @__PURE__ */ jsxRuntimeExports.jsx(RobotControls, { params: selectedModule.params, onChange: updateParams });
251      case "vocoder":
252        return /* @__PURE__ */ jsxRuntimeExports.jsx(VocoderControls, { params: selectedModule.params, onChange: updateParams });
253      case "harmonizer":
254        return /* @__PURE__ */ jsxRuntimeExports.jsx(HarmonizerControls, { params: selectedModule.params, onChange: updateParams });
255      case "distortion":
256        return /* @__PURE__ */ jsxRuntimeExports.jsx(DistortionControls, { params: selectedModule.params, onChange: updateParams });
257      default:
258        return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-zinc-500", children: "N
258o controls available for this module." });
259    }
260  };
261  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-6 max-w-2xl mx-auto", children: [
262    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between border-b border-zinc-800 pb-4", children: [
263      /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-medium text-white", children: selectedModule.name }),
264      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
265        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-500", children: "Bypass" }),
266        /* @__PURE__ */ jsxRuntimeExports.jsx(
267          "button",
268          {
269            onClick: () => dispatch({ type: "TOGGLE_MODULE", payload: selectedModule.id }),
270            className: `w-12 h-6 rounded-full transition-colors relative ${selectedModule.enabled ? "bg-rose-500" : "bg-zinc-800"}`,
271            children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute top-1 w-4 h-4 rounded-full bg-white transition-transform ${selectedModule.enabled ? "left-7" : "left-1"}` })
272          }
273        )
274      ] })
275    ] }),
276    /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pt-2", children: renderControls() })
277  ] });
278}
279function KnobSlider({ label, value, min, max, unit, onChange }) {
280  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
281    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-sm", children: [
282      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400", children: label }),
283      /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-200", children: [
284        value,
285        unit
286      ] })
287    ] }),
288    /* @__PURE__ */ jsxRuntimeExports.jsx(
289      "input",
290      {
291        type: "range",
292        min,
293        max,
294        value,
295        onChange: (e) => onChange(Number(e.target.value)),
296        className: "w-full accent-rose-500"
297      }
298    )
299  ] });
300}
301function NoiseGateControls({ params, onChange }) {
302  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-6", children: [
303    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Threshold", value: params.threshold, min: -80, max: 0, unit: " dB", onChange: (v) => onChange({ threshold: v }) }),
304    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Attack", value: params.attack, min: 1, max: 100, unit: " ms", onChange: (v) => onChange({ attack: v }) }),
305    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Release", value: params.release, min: 10, max: 1e3, unit: " ms", onChange: (v) => onChange({ release: v }) }),
306    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Hold", value: params.hold, min: 0, max: 500, unit: " ms", onChange: (v) => onChange({ hold: v }) })
307  ] });
308}
309function EQControls({ params, onChange }) {
310  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-3 gap-6", children: [
311    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Bass (250Hz)", value: params.bass, min: -12, max: 12, unit: " dB", onChange: (v) => onChange({ bass: v }) }),
312    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Mid (1kHz)", value: params.mid, min: -12, max: 12, unit: " dB", onChange: (v) => onChange({ mid: v }) }),
313    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Treble (4kHz)", value: params.treble, min: -12, max: 12, unit: " dB", onChange: (v) => onChange({ treble: v }) })
314  ] });
315}
316function CompressorControls({ params, onChange }) {
317  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-6", children: [
318    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Threshold", value: params.threshold, min: -60, max: 0, unit: " dB", onChange: (v) => onChange({ threshold: v }) }),
319    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Ratio", value: params.ratio, min: 1, max: 20, unit: ":1", onChange: (v) => onChange({ ratio: v }) }),
320    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Attack", value: params.attack, min: 1, max: 100, unit: " ms", onChange: (v) => onChange({ attack: v }) }),
321    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Release", value: params.release, min: 10, max: 1e3, unit: " ms", onChange: (v) => onChange({ release: v }) }),
322    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Makeup Gain", value: params.makeupGain, min: 0, max: 24, unit: " dB", onChange: (v) => onChange({ makeupGain: v }) })
323  ] });
324}
325function DelayControls({ params, onChange }) {
326  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-6", children: [
327    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Time", value: params.time, min: 10, max: 2e3, unit: " ms", onChange: (v) => onChange({ time: v }) }),
328    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Feedback", value: params.feedback, min: 0, max: 95, unit: "%", onChange: (v) => onChange({ feedback: v }) }),
329    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Mix", value: params.mix, min: 0, max: 100, unit: "%", onChange: (v) => onChange({ mix: v }) })
330  ] });
331}
332function ReverbControls({ params, onChange }) {
333  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-6", children: [
334    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
335      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm text-zinc-400", children: "Room Size" }),
336      /* @__PURE__ */ jsxRuntimeExports.jsxs(
337        "select",
338        {
339          className: "bg-zinc-900 border border-zinc-800 text-zinc-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-rose-500",
340          value: params.roomSize,
341          onChange: (e) => onChange({ roomSize: e.target.value }),
342          children: [
343            /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "small", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: "Small Room" }),
344            /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "medium", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: "Medium Hall" }),
345            /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "large", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: "Large Chamber" }),
346            /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "cathedral", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: "Cathedral" })
347          ]
348        }
349      )
350    ] }),
351    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Decay", value: params.decay, min: 0.1, max: 10, unit: " s", onChange: (v) => onChange({ decay: v }) }),
352    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Mix", value: params.mix, min: 0, max: 100, unit: "%", onChange: (v) => onChange({ mix: v }) })
353  ] });
354}
355function PitchCorrectionControls({ params, onChange }) {
356  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-6", children: [
357    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
358      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm text-zinc-400", children: "Key" }),
359      /* @__PURE__ */ jsxRuntimeExports.jsx(
360        "select",
361        {
362          className: "bg-zinc-900 border border-zinc-800 text-zinc-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-rose-500",
363          value: params.key,
364          onChange: (e) => onChange({ key: e.target.value }),
365          children: ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"].map((k) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: k, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: k }, k))
366        }
367      )
368    ] }),
369    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
370      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm text-zinc-400", children: "Scale" }),
371      /* @__PURE__ */ jsxRuntimeExports.jsx(
372        "select",
373        {
374          className: "bg-zinc-900 border border-zinc-800 text-zinc-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-rose-500",
375          value: params.scale,
376          onChange: (e) => onChange({ scale: e.target.value }),
377          children: ["Major", "Minor", "Chromatic", "Pentatonic"].map((s) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: s, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: s }, s))
378        }
379      )
380    ] }),
381    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Retune Speed", value: params.retuneSpeed, min: 0, max: 100, unit: "%", onChange: (v) => onChange({ retuneSpeed: v }) }),
382    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
383      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm text-zinc-400", children: "Humanize" }),
384      /* @__PURE__ */ jsxRuntimeExports.jsx(
385        "button",
386        {
387          onClick: () => onChange({ humanize: !params.humanize }),
388          className: `px-3 py-2 rounded-lg text-sm transition-colors border ${params.humanize ? "bg-rose-500/20 text-rose-400 border-rose-500/50" : "bg-zinc-900 text-zinc-400 border-zinc-800"}`,
389          children: params.humanize ? "Enabled" : "Disabled"
390        }
391      )
392    ] })
393  ] });
394}
395function PitchShiftControls({ params, onChange }) {
396  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-6", children: [
397    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Semitones", value: params.semitones, min: -24, max: 24, unit: " st", onChange: (v) => onChange({ semitones: v }) }),
398    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Cents", value: params.cents, min: -50, max: 50, unit: " ct", onChange: (v) => onChange({ cents: v }) })
399  ] });
400}
401function FormantShiftControls({ params, onChange }) {
402  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 gap-6", children: /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Shift Amount", value: params.semitones, min: -12, max: 12, unit: " st", onChange: (v) => onChange({ semitones: v }) }) });
403}
404function RobotControls({ params, onChange }) {
405  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 gap-6", children: [
406    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "MIDI Note", value: params.note, min: 36, max: 84, unit: "", onChange: (v) => onChange({ note: v }) }),
407    /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500 text-center", children: "Locks the voice to the selected MIDI note pitch." })
408  ] });
409}
410function VocoderControls({ params, onChange }) {
411  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-6", children: [
412    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
413      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm text-zinc-400", children: "Carrier Type" }),
414      /* @__PURE__ */ jsxRuntimeExports.jsx(
415        "select",
416        {
417          className: "bg-zinc-900 border border-zinc-800 text-zinc-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-rose-500",
418          value: params.carrierType,
419          onChange: (e) => onChange({ carrierType: e.target.value }),
420          children: ["sawtooth", "square", "triangle", "noise"].map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: t, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: t.charAt(0).toUpperCase() + t.slice(1) }, t))
421        }
422      )
423    ] }),
424    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
425      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm text-zinc-400", children: "Bands" }),
426      /* @__PURE__ */ jsxRuntimeExports.jsx(
427        "select",
428        {
429          className: "bg-zinc-900 border border-zinc-800 text-zinc-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-rose-500",
430          value: params.bandCount,
431          onChange: (e) => onChange({ bandCount: Number(e.target.value) }),
432          children: [8, 16, 32].map((b) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: b, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: [
433            b,
434            " Bands"
435          ] }, b))
436        }
437      )
438    ] }),
439    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Carrier Freq", value: params.carrierFreq, min: 50, max: 1e3, unit: " Hz", onChange: (v) => onChange({ carrierFreq: v }) }),
440    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Release", value: params.release, min: 10, max: 1e3, unit: " ms", onChange: (v) => onChange({ release: v }) }),
441    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Mix", value: params.mix, min: 0, max: 100, unit: "%", onChange: (v) => onChange({ mix: v }) })
442  ] });
443}
444function HarmonizerControls({ params, onChange }) {
445  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-4", children: params.voices.map((voice, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900/50 p-4 rounded-xl border border-zinc-800 flex flex-col gap-4", children: [
446    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm font-medium text-zinc-300", children: [
447      "Voice ",
448      idx + 1
449    ] }),
450    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
451      /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Interval", value: voice.interval, min: -12, max: 12, unit: " st", onChange: (v) => {
452        const newVoices = [...params.voices];
453        newVoices[idx].interval = v;
454        onChange({ voices: newVoices });
455      } }),
456      /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Volume", value: voice.volume, min: 0, max: 100, unit: "%", onChange: (v) => {
457        const newVoices = [...params.voices];
458        newVoices[idx].volume = v;
459        onChange({ voices: newVoices });
460      } }),
461      /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Pan", value: voice.pan, min: -100, max: 100, unit: "", onChange: (v) => {
462        const newVoices = [...params.voices];
463        newVoices[idx].pan = v;
464        onChange({ voices: newVoices });
465      } })
466    ] })
467  ] }, idx)) });
468}
469function DistortionControls({ params, onChange }) {
470  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-6", children: [
471    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
472      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm text-zinc-400", children: "Type" }),
473      /* @__PURE__ */ jsxRuntimeExports.jsx(
474        "select",
475        {
476          className: "bg-zinc-900 border border-zinc-800 text-zinc-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-rose-500",
477          value: params.type,
478          onChange: (e) => onChange({ type: e.target.value }),
479          children: ["clean", "softClip", "hardClip", "radio", "telephone", "megaphone", "8bit"].map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: t, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: t }, t))
480        }
481      )
482    ] }),
483    /* @__PURE__ */ jsxRuntimeExports.jsx("div", {}),
484    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Drive", value: params.drive, min: 0, max: 100, unit: "%", onChange: (v) => onChange({ drive: v }) }),
485    /* @__PURE__ */ jsxRuntimeExports.jsx(KnobSlider, { label: "Mix", value: params.mix, min: 0, max: 100, unit: "%", onChange: (v) => onChange({ mix: v }) })
486  ] });
487}
488class VocalEngineNode {
489  constructor(ctx) {
490    this.node = null;
491    this.initialized = false;
492    this.ctx = ctx;
493  }
494  async init() {
495    if (this.initialized) return;
496    try {
497      await this.ctx.audioWorklet.addModule("/worklets/vocal-engine-worklet.js");
498      this.node = new AudioWorkletNode(this.ctx, "vocal-engine-worklet", {
499        outputChannelCount: [2]
500      });
501      this.node.port.postMessage({ type: "SET_SAMPLE_RATE", payload: this.ctx.sampleRate });
502      this.node.port.onmessage = (e) => {
503        if (e.data.type === "PITCH_DATA") {
504          const payload = e.data.payload;
505          if (this.onPitchData) this.onPitchData(payload);
506          if (this.onPitchDetected) this.onPitchDetected(payload.detectedPitch);
507          window.dispatchEvent(new CustomEvent("VOCAL_PITCH_DATA", { detail: payload }));
508        } else if (e.data.type === "PITCH_DETECTED") {
509          if (this.onPitchDetected) this.onPitchDetected(e.data.payload);
510        }
511      };
512      this.initialized = true;
513    } catch (e) {
514      console.error("Failed to load vocal-engine-worklet:", e);
515    }
516  }
517  updateParams(modules) {
518    if (!this.node) return;
519    const params = {
520      pitchCorrectionEnabled: false,
521      pitchShiftEnabled: false,
522      formantShiftEnabled: false,
523      robotEnabled: false
524    };
525    for (const mod of modules) {
526      if (!mod.enabled) continue;
527      if (mod.type === "pitch-correction") {
528        params.pitchCorrectionEnabled = true;
529        params.pitchCorrectionKey = mod.params.key;
530        params.pitchCorrectionScale = mod.params.scale;
531        params.pitchCorrectionSpeed = mod.params.retuneSpeed;
532        params.humanize = mod.params.humanize !== false;
533      }
534      if (mod.type === "pitch-shift") {
535        params.pitchShiftEnabled = true;
536        params.pitchShiftSemitones = mod.params.semitones + mod.params.cents / 100;
537      }
538      if (mod.type === "formant-shift") {
539        params.formantShiftEnabled = true;
540        params.formantShiftSemitones = mod.params.semitones;
541      }
542      if (mod.type === "robot") {
543        params.robotEnabled = true;
544        params.robotNote = mod.params.note;
545      }
546    }
547    this.node.port.postMessage({ type: "SET_PARAMS", payload: params });
548  }
549  connect(destination) {
550    var _a;
551    (_a = this.node) == null ? void 0 : _a.connect(destination);
552  }
553  disconnect() {
554    var _a;
555    (_a = this.node) == null ? void 0 : _a.disconnect();
556  }
557}
558let activeSignalChain = null;
559const getActiveSignalChain = () => activeSignalChain;
560class VocalSignalChain {
561  constructor(ctx, inputNode, outputNode) {
562    this.masterParams = { wetDry: 100, outputVolume: 100, bypass: false };
563    this.currentChainNodes = {};
564    this.activeConnections = null;
565    this.currentModules = [];
566    this.ctx = ctx;
567    this.inputNode = inputNode;
568    this.outputNode = outputNode;
569    this.analyser = this.ctx.createAnalyser();
570    this.analyser.fftSize = 2048;
571    this.analyser.smoothingTimeConstant = 0.8;
572    this.masterGain = this.ctx.createGain();
573    this.wetGain = this.ctx.createGain();
574    this.dryGain = this.ctx.createGain();
575    this.wetGain.connect(this.masterGain);
576    this.dryGain.connect(this.masterGain);
577    this.masterGain.connect(this.analyser);
578    this.analyser.connect(this.outputNode);
579    this.applyMasterGains();
580    activeSignalChain = this;
581    this.engineNode = new VocalEngineNode(ctx);
582    this.engineNode.init().then(() => {
583      this.rebuildChain(this.currentModules);
584    });
585  }
586  setInputNode(node) {
587    if (this.inputNode === node) return;
588    try {
589      this.inputNode.disconnect();
590    } catch (e) {
591    }
592    this.inputNode = node;
593    this.rebuildChain(this.currentModules);
594  }
595  addRecordingDestination(dest) {
596    try {
597      this.masterGain.connect(dest);
598    } catch (e) {
599      if (this.activeConnections) {
600        this.activeConnections.output.connect(dest);
601      } else {
602        this.inputNode.connect(dest);
603      }
604    }
605  }
606  destroy() {
607    if (activeSignalChain === this) activeSignalChain = null;
608    this.disconnectAll();
609    try {
610      this.dryGain.disconnect();
611      this.wetGain.disconnect();
612      this.masterGain.disconnect();
613      this.analyser.disconnect();
614    } catch (e) {
615    }
616  }
617  disconnectAll() {
618    try {
619      this.inputNode.disconnect();
620    } catch (e) {
621    }
622    if (this.activeConnections) {
623      try {
624        this.activeConnections.output.disconnect();
625      } catch (e) {
626      }
627      this.activeConnections = null;
628    }
629  }
630  rebuildChain(modules) {
631    this.currentModules = modules;
632    this.disconnectAll();
633    this.currentChainNodes = {};
634    try {
635      this.inputNode.connect(this.dryGain);
636    } catch (e) {
637    }
638    let previousOutput = this.inputNode;
639    if (this.engineNode) {
640      this.engineNode.updateParams(modules);
641      const hasPitchFX = modules.some((m) => m.enabled && ["pitch-correction", "pitch-shift", "formant-shift", "robot", "harmonizer", "vocoder"].includes(m.type));
642      if (hasPitchFX && this.engineNode.node) {
643        previousOutput.connect(this.engineNode.node);
644        previousOutput = this.engineNode.node;
645      }
646    }
647    const activeModules = modules.filter((m) => m.enabled);
648    for (const module of activeModules) {
649      if (["pitch-correction", "pitch-shift", "formant-shift", "robot", "harmonizer", "vocoder"].includes(module.type)) {
650        continue;
651      }
652      const { input, output, nodes } = this.createModuleNodes(module);
653      previousOutput.connect(input);
654      previousOutput = output;
655      this.currentChainNodes[module.id] = nodes;
656    }
657    previousOutput.connect(this.wetGain);
658    this.activeConnections = { output: previousOutput };
659    this.applyMasterGains();
660  }
661  updateModuleParams(moduleId, type, params) {
662    if (["pitch-correction", "pitch-shift", "formant-shift", "robot", "harmonizer", "vocoder"].includes(type)) {
663      if (this.engineNode) this.engineNode.updateParams(this.currentModules);
664      return;
665    }
666    const nodes = this.currentChainNodes[moduleId];
667    if (!nodes) return;
668    switch (type) {
669      case "noise-gate":
670        this.updateNoiseGateParams(nodes[1], nodes[1].__analyser, params);
671        break;
672      case "eq":
673        this.updateEqParams(nodes, params);
674        break;
675      case "compressor":
676        this.updateCompressorParams(nodes, params);
677        break;
678      case "delay":
679        this.updateDelayParams(nodes, params);
680        break;
681      case "reverb":
682        this.updateReverbParams(nodes, params);
683        break;
684      case "distortion":
685        this.updateDistortionParams(nodes, params);
686        break;
687      case "lofi":
688        this.updateLofiParams(nodes, params);
689        break;
690    }
691  }
692  updateDistortionParams(nodes, params) {
693    const waveshaper = nodes[0];
694    const filter = nodes[1];
695    const k = typeof params.drive === "number" ? params.drive : 50;
696    const curve = new Float32Array(44100);
697    const deg = Math.PI / 180;
698    for (let i = 0; i < 44100; ++i) {
699      const x = i * 2 / 44100 - 1;
700      curve[i] = (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x));
701    }
702    waveshaper.curve = curve;
703    if (typeof params.tone === "number") {
704      filter.frequency.value = 200 + params.tone / 100 * 1e4;
705    }
706  }
707  updateLofiParams(nodes, params) {
708    const filter = nodes[0];
709    if (typeof params.sampleRate === "number") {
710      filter.frequency.value = Math.max(200, params.sampleRate / 100 * 8e3);
711    }
712  }
713  updateMasterParams(params) {
714    this.masterParams = { ...params };
715    this.applyMasterGains();
716  }
717  applyMasterGains() {
718    if (!this.wetGain || !this.dryGain || !this.masterGain) return;
719    const { wetDry, bypass, outputVolume } = this.masterParams;
720    const now = this.ctx.currentTime;
721    if (bypass) {
722      this.wetGain.gain.setTargetAtTime(0, now, 0.02);
723      this.dryGain.gain.setTargetAtTime(1, now, 0.02);
724    } else {
725      const wet = Math.max(0, Math.min(100, wetDry)) / 100;
726      const dry = 1 - wet;
727      this.wetGain.gain.setTargetAtTime(wet, now, 0.02);
728      this.dryGain.gain.setTargetAtTime(dry, now, 0.02);
729    }
730    const vol = Math.max(0, Math.min(100, outputVolume)) / 100;
731    this.masterGain.gain.setTargetAtTime(vol, now, 0.02);
732  }
733  createModuleNodes(module) {
734    switch (module.type) {
735      case "noise-gate": {
736        const gainNode = this.ctx.createGain();
737        const analyser = this.ctx.createAnalyser();
738        analyser.fftSize = 256;
739        const inputSplitter = this.ctx.createGain();
740        inputSplitter.connect(gainNode);
741        inputSplitter.connect(analyser);
742        gainNode.__analyser = analyser;
743        gainNode.__gateState = { isOpen: false, lastTrigger: 0 };
744        gainNode.__animFrame = null;
745        this.updateNoiseGateParams(gainNode, analyser, module.params);
746        return { input: inputSplitter, output: gainNode, nodes: [inputSplitter, gainNode] };
747      }
748      case "eq": {
749        const low = this.ctx.createBiquadFilter();
750        low.type = "lowshelf";
751        low.frequency.value = 250;
752        const mid = this.ctx.createBiquadFilter();
753        mid.type = "peaking";
754        mid.frequency.value = 1e3;
755        mid.Q.value = 1;
756        const high = this.ctx.createBiquadFilter();
757        high.type = "highshelf";
758        high.frequency.value = 4e3;
759        low.connect(mid);
760        mid.connect(high);
761        const nodes = [low, mid, high];
762        this.updateEqParams(nodes, module.params);
763        return { input: low, output: high, nodes };
764      }
765      case "delay": {
766        const delayNode = this.ctx.createDelay(5);
767        const feedbackGain = this.ctx.createGain();
768        const outputMix = this.ctx.createGain();
769        delayNode.connect(feedbackGain);
770        feedbackGain.connect(delayNode);
771        const dryGain = this.ctx.createGain();
772        const wetGain = this.ctx.createGain();
773        delayNode.connect(wetGain);
774        wetGain.connect(outputMix);
775        const inputGain = this.ctx.createGain();
776        inputGain.connect(delayNode);
777        inputGain.connect(dryGain);
778        dryGain.connect(outputMix);
779        const nodes = [inputGain, delayNode, feedbackGain, dryGain, wetGain, outputMix];
780        this.updateDelayParams(nodes, module.params);
781        return { input: inputGain, output: outputMix, nodes };
782      }
783      case "distortion": {
784        const waveshaper = this.ctx.createWaveShaper();
785        waveshaper.oversample = "4x";
786        const toneFilter = this.ctx.createBiquadFilter();
787        toneFilter.type = "lowpass";
788        waveshaper.connect(toneFilter);
789        this.updateDistortionParams([waveshaper, toneFilter], module.params);
790        return { input: waveshaper, output: toneFilter, nodes: [waveshaper, toneFilter] };
791      }
792      case "lofi": {
793        const filter = this.ctx.createBiquadFilter();
794        filter.type = "lowpass";
795        this.updateLofiParams([filter], module.params);
796        return { input: filter, output: filter, nodes: [filter] };
797      }
798      case "compressor": {
799        const comp = this.ctx.createDynamicsCompressor();
800        const makeup = this.ctx.createGain();
801        comp.connect(makeup);
802        comp.__makeup = makeup;
803        this.updateCompressorParams(comp, module.params);
804        return { input: comp, output: makeup, nodes: comp };
805      }
806      case "reverb": {
807        const inputGain = this.ctx.createGain();
808        const convolver = this.ctx.createConvolver();
809        const wetGain = this.ctx.createGain();
810        const dryGain = this.ctx.createGain();
811        const output = this.ctx.createGain();
812        inputGain.connect(dryGain);
813        dryGain.connect(output);
814        inputGain.connect(convolver);
815        convolver.connect(wetGain);
816        wetGain.connect(output);
817        const nodes = [inputGain, convolver, wetGain, dryGain, output];
818        this.updateReverbParams(nodes, module.params);
819        return { input: inputGain, output, nodes };
820      }
821      default: {
822        const bypass = this.ctx.createGain();
823        return { input: bypass, output: bypass, nodes: bypass };
824      }
825    }
826  }
827  // --- Parameter Updaters ---
828  updateNoiseGateParams(gain, analyser, params) {
829    const thresholdDb = params.threshold ?? -40;
830    const attack = (params.attack ?? 5) / 1e3;
831    const release = (params.release ?? 100) / 1e3;
832    const hold = params.hold ?? 50;
833    const gateState = gain.__gateState;
834    if (!gain.__animFrame) {
835      const data = new Float32Array(analyser.fftSize);
836      const loop = () => {
837        analyser.getFloatTimeDomainData(data);
838        let sum = 0;
839        for (let i = 0; i < data.length; i++) sum += data[i] * data[i];
840        const rms = Math.sqrt(sum / data.length);
841        const dbfs = 20 * Math.log10(rms || 1e-8);
842        const now = performance.now();
843        if (dbfs > thresholdDb) {
844          gateState.lastTrigger = now;
845          if (!gateState.isOpen) {
846            gateState.isOpen = true;
847            gain.gain.setTargetAtTime(1, this.ctx.currentTime, attack);
848          }
849        } else {
850          if (gateState.isOpen && now - gateState.lastTrigger > hold) {
851            gateState.isOpen = false;
852            gain.gain.setTargetAtTime(0, this.ctx.currentTime, release);
853          }
854        }
855        gain.__animFrame = requestAnimationFrame(loop);
856      };
857      gain.__animFrame = requestAnimationFrame(loop);
858    }
859  }
860  updateEqParams(nodes, params) {
861    nodes[0].gain.setTargetAtTime(params.bass ?? 0, this.ctx.currentTime, 0.05);
862    nodes[1].gain.setTargetAtTime(params.mid ?? 0, this.ctx.currentTime, 0.05);
863    nodes[2].gain.setTargetAtTime(params.treble ?? 0, this.ctx.currentTime, 0.05);
864  }
865  updateCompressorParams(comp, params) {
866    comp.threshold.setTargetAtTime(params.threshold ?? -24, this.ctx.currentTime, 0.05);
867    comp.ratio.setTargetAtTime(params.ratio ?? 4, this.ctx.currentTime, 0.05);
868    comp.attack.setTargetAtTime((params.attack ?? 10) / 1e3, this.ctx.currentTime, 0.05);
869    comp.release.setTargetAtTime((params.release ?? 100) / 1e3, this.ctx.currentTime, 0.05);
870    const makeupGain = comp.__makeup;
871    const linearGain = Math.pow(10, (params.makeupGain ?? 0) / 20);
872    makeupGain.gain.setTargetAtTime(linearGain, this.ctx.currentTime, 0.05);
873  }
874  updateDelayParams(nodes, params) {
875    const delay = nodes[1];
876    const feedback = nodes[2];
877    const wet = nodes[3];
878    const dry = nodes[4];
879    const time = (params.time ?? 300) / 1e3;
880    delay.delayTime.setTargetAtTime(time, this.ctx.currentTime, 0.05);
881    const fb = (params.feedback ?? 30) / 100;
882    feedback.gain.setTargetAtTime(fb, this.ctx.currentTime, 0.05);
883    const mix = (params.mix ?? 20) / 100;
884    wet.gain.setTargetAtTime(mix, this.ctx.currentTime, 0.05);
885    dry.gain.setTargetAtTime(1 - mix, this.ctx.currentTime, 0.05);
886  }
887  updateReverbParams(nodes, params) {
888    const convolver = nodes[1];
889    const wet = nodes[2];
890    const dry = nodes[3];
891    const decay = params.decay ?? 2;
892    const currentDecay = convolver.__decay;
893    if (decay !== currentDecay || !convolver.buffer) {
894      convolver.buffer = this.generateReverbIR(decay);
895      convolver.__decay = decay;
896    }
897    const mix = (params.mix ?? 30) / 100;
898    wet.gain.setTargetAtTime(mix, this.ctx.currentTime, 0.05);
899    dry.gain.setTargetAtTime(1 - mix, this.ctx.currentTime, 0.05);
900  }
901  generateReverbIR(duration) {
902    const sampleRate = this.ctx.sampleRate;
903    const length = Math.max(1, sampleRate * duration);
904    const impulse = this.ctx.createBuffer(2, length, sampleRate);
905    const left = impulse.getChannelData(0);
906    const right = impulse.getChannelData(1);
907    for (let i = 0; i < length; i++) {
908      const multiplier = Math.exp(-i / (sampleRate * (duration / 5)));
909      left[i] = (Math.random() * 2 - 1) * multiplier;
910      right[i] = (Math.random() * 2 - 1) * multiplier;
911    }
912    return impulse;
913  }
914}
915function PitchVisualizer() {
916  var _a, _b, _c;
917  const { state } = useVocalProcessor();
918  const canvasRef = reactExports.useRef(null);
919  const [pitchData, setPitchData] = reactExports.useState({
920    detectedPitch: 0,
921    targetPitch: 0,
922    noteName: "--",
923    cents: 0,
924    clarity: 0
925  });
926  const [viewMode, setViewMode] = reactExports.useState("both");
927  const [volumeLevel, setVolumeLevel] = reactExports.useState(0);
928  const pitchHistoryRef = reactExports.useRef([]);
929  const latestPitchRef = reactExports.useRef(pitchData);
930  latestPitchRef.current = pitchData;
931  const pitchCorrectionMod = state.chain.find((m) => m.type === "pitch-correction" && m.enabled);
932  const robotMod = state.chain.find((m) => m.type === "robot" && m.enabled);
933  reactExports.useEffect(() => {
934    const handlePitchData = (e) => {
935      const customEvent = e;
936      if (customEvent.detail) {
937        setPitchData(customEvent.detail);
938        if (customEvent.detail.detectedPitch > 40) {
939          pitchHistoryRef.current.push({
940            pitch: customEvent.detail.detectedPitch,
941            target: customEvent.detail.targetPitch,
942            time: Date.now()
943          });
944          if (pitchHistoryRef.current.length > 200) {
945            pitchHistoryRef.current.shift();
946          }
947        }
948      }
949    };
950    window.addEventListener("VOCAL_PITCH_DATA", handlePitchData);
951    return () => {
952      window.removeEventListener("VOCAL_PITCH_DATA", handlePitchData);
953    };
954  }, []);
955  reactExports.useEffect(() => {
956    let animId;
957    const canvas = canvasRef.current;
958    if (!canvas) return;
959    const ctx = canvas.getContext("2d");
960    if (!ctx) return;
961    const timeDomainBuffer = new Uint8Array(1024);
962    let lastRmsCalcTime = 0;
963    const render = () => {
964      const chain = getActiveSignalChain();
965      const analyser = chain == null ? void 0 : chain.analyser;
966      const width = canvas.clientWidth;
967      const height = canvas.clientHeight;
968      const dpr = window.devicePixelRatio || 1;
969      if (canvas.width !== width * dpr || canvas.height !== height * dpr) {
970        canvas.width = width * dpr;
971        canvas.height = height * dpr;
972      }
973      ctx.save();
974      ctx.scale(dpr, dpr);
975      ctx.fillStyle = "#09090b";
976      ctx.fillRect(0, 0, width, height);
977      ctx.strokeStyle = "#18181b";
978      ctx.lineWidth = 1;
979      const gridCountY = 6;
980      for (let i = 1; i < gridCountY; i++) {
981        const y = height / gridCountY * i;
982        ctx.beginPath();
983        ctx.moveTo(0, y);
984        ctx.lineTo(width, y);
985        ctx.stroke();
986      }
987      let rms = 0;
988      if (analyser) {
989        analyser.getByteTimeDomainData(timeDomainBuffer);
990        let sumSquares = 0;
991        for (let i = 0; i < timeDomainBuffer.length; i++) {
992          const norm = (timeDomainBuffer[i] - 128) / 128;
993          sumSquares += norm * norm;
994        }
995        rms = Math.sqrt(sumSquares / timeDomainBuffer.length);
996        const now = performance.now();
997        if (now - lastRmsCalcTime > 50) {
998          setVolumeLevel(Math.min(1, rms * 3.5));
999          lastRmsCalcTime = now;
1000        }
1001      }
1002      const drawWaveform = viewMode === "wave" || viewMode === "both";
1003      const drawPitchTrail = viewMode === "pitch" || viewMode === "both";
1004      if (drawWaveform && analyser) {
1005        ctx.lineWidth = 2;
1006        const gradient = ctx.createLinearGradient(0, 0, width, 0);
1007        gradient.addColorStop(0, "#f43f5e");
1008        gradient.addColorStop(0.5, "#a855f7");
1009        gradient.addColorStop(1, "#06b6d4");
1010        ctx.strokeStyle = gradient;
1011        ctx.shadowColor = "#f43f5e";
1012        ctx.shadowBlur = rms > 0.05 ? 8 : 2;
1013        const centerY = drawPitchTrail ? height * 0.35 : height * 0.5;
1014        const waveHeight = drawPitchTrail ? height * 0.28 : height * 0.42;
1015        ctx.beginPath();
1016        const sliceWidth = width / timeDomainBuffer.length;
1017        let x = 0;
1018        for (let i = 0; i < timeDomainBuffer.length; i++) {
1019          const v = timeDomainBuffer[i] / 128;
1020          const y = centerY + (v - 1) * waveHeight;
1021          if (i === 0) {
1022            ctx.moveTo(x, y);
1023          } else {
1024            ctx.lineTo(x, y);
1025          }
1026          x += sliceWidth;
1027        }
1028        ctx.stroke();
1029        ctx.shadowBlur = 0;
1030      }
1031      if (drawPitchTrail) {
1032        const pitchHistory = pitchHistoryRef.current;
1033        const trailTop = drawWaveform ? height * 0.55 : height * 0.15;
1034        const trailBottom = height - 15;
1035        const trailHeight = trailBottom - trailTop;
1036        const minPitch = 80;
1037        const maxPitch = 800;
1038        const logMin = Math.log2(minPitch);
1039        const logMax = Math.log2(maxPitch);
1040        const freqToY = (f) => {
1041          if (f <= minPitch) return trailBottom;
1042          if (f >= maxPitch) return trailTop;
1043          const ratio = (Math.log2(f) - logMin) / (logMax - logMin);
1044          return trailBottom - ratio * trailHeight;
1045        };
1046        const refNotes = [
1047          { name: "C3 (130.8 Hz)", f: 130.81 },
1048          { name: "A3 (220.0 Hz)", f: 220 },
1049          { name: "C4 (261.6 Hz)", f: 261.63 },
1050          { name: "A4 (440.0 Hz)", f: 440 },
1051          { name: "C5 (523.3 Hz)", f: 523.25 }
1052        ];
1053        ctx.font = "10px sans-serif";
1054        ctx.fillStyle = "#52525b";
1055        refNotes.forEach((note) => {
1056          const y = freqToY(note.f);
1057          ctx.strokeStyle = "#27272a";
1058          ctx.beginPath();
1059          ctx.moveTo(0, y);
1060          ctx.lineTo(width - 40, y);
1061          ctx.stroke();
1062          ctx.fillText(note.name, width - 100, y - 3);
1063        });
1064        if (pitchHistory.length > 1) {
1065          if (pitchCorrectionMod || robotMod) {
1066            ctx.strokeStyle = "#10b98188";
1067            ctx.lineWidth = 2;
1068            ctx.beginPath();
1069            for (let i = 0; i < pitchHistory.length; i++) {
1070              const pt = pitchHistory[i];
1071              const px = i / (pitchHistory.length - 1) * (width - 40);
1072              const py = freqToY(pt.target);
1073              if (i === 0) ctx.moveTo(px, py);
1074              else ctx.lineTo(px, py);
1075            }
1076            ctx.stroke();
1077          }
1078          ctx.strokeStyle = "#38bdf8";
1079          ctx.lineWidth = 3;
1080          ctx.shadowColor = "#38bdf8";
1081          ctx.shadowBlur = 6;
1082          ctx.beginPath();
1083          for (let i = 0; i < pitchHistory.length; i++) {
1084            const pt = pitchHistory[i];
1085            const px = i / (pitchHistory.length - 1) * (width - 40);
1086            const py = freqToY(pt.pitch);
1087            if (i === 0) ctx.moveTo(px, py);
1088            else ctx.lineTo(px, py);
1089          }
1090          ctx.stroke();
1091          ctx.shadowBlur = 0;
1092          const lastPt = pitchHistory[pitchHistory.length - 1];
1093          const headX = width - 40;
1094          const headY = freqToY(lastPt.pitch);
1095          ctx.fillStyle = Math.abs(latestPitchRef.current.cents) <= 6 ? "#10b981" : "#f43f5e";
1096          ctx.beginPath();
1097          ctx.arc(headX, headY, 5, 0, Math.PI * 2);
1098          ctx.fill();
1099        }
1100      }
1101      ctx.restore();
1102      animId = requestAnimationFrame(render);
1103    };
1104    animId = requestAnimationFrame(render);
1105    return () => cancelAnimationFrame(animId);
1106  }, [viewMode, pitchCorrectionMod, robotMod]);
1107  const isVoiced = pitchData.detectedPitch > 40;
1108  const clampedCents = Math.max(-50, Math.min(50, pitchData.cents));
1109  const isInTune = Math.abs(pitchData.cents) <= 6;
1110  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full h-full flex flex-col bg-zinc-950 select-none overflow-hidden", children: [
1111    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-3 left-4 right-4 z-10 flex items-center justify-between pointer-events-none", children: [
1112      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 pointer-events-auto bg-zinc-900/80 backdrop-blur-md px-3 py-1.5 rounded-xl border border-zinc-800", children: [
1113        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center w-9 h-9 rounded-lg bg-rose-500/10 text-rose-400 font-mono font-bold text-lg", children: isVoiced ? pitchData.noteName : "--" }),
1114        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [
1115          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400 font-medium", children: isVoiced ? `${pitchData.detectedPitch} Hz` : "Silent" }),
1116          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [
1117            /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `w-1.5 h-1.5 rounded-full ${isVoiced ? isInTune ? "bg-emerald-400 animate-pulse" : "bg-rose-400" : "bg-zinc-600"}` }),
1118            /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 uppercase tracking-wider font-semibold", children: isVoiced ? isInTune ? "In Tune" : `${pitchData.cents > 0 ? "+" : ""}${pitchData.cents}c` : "Awaiting Input" })
1119          ] })
1120        ] })
1121      ] }),
1122      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "hidden sm:flex flex-col items-center gap-1 bg-zinc-900/80 backdrop-blur-md px-4 py-1.5 rounded-xl border border-zinc-800 pointer-events-auto", children: [
1123        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1124          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "-50c" }),
1125          /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-40 h-2.5 bg-zinc-800 rounded-full overflow-hidden flex items-center", children: [
1126            /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-1/2 -translate-x-1/2 w-0.5 h-full bg-zinc-500 z-10" }),
1127            isVoiced && /* @__PURE__ */ jsxRuntimeExports.jsx(
1128              "div",
1129              {
1130                className: `absolute top-0 bottom-0 w-2 rounded-full transition-all duration-75 ${isInTune ? "bg-emerald-400 shadow-[0_0_8px_#34d399]" : "bg-rose-500"}`,
1131                style: {
1132                  left: `calc(50% + ${clampedCents / 50 * 45}% - 4px)`
1133                }
1134              }
1135            )
1136          ] }),
1137          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-mono", children: "+50c" })
1138        ] }),
1139        pitchCorrectionMod && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 text-[10px] text-emerald-400 font-medium", children: [
1140          /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { className: "w-3 h-3" }),
1141          /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
1142            "Auto-Tune: ",
1143            (_a = pitchCorrectionMod.params) == null ? void 0 : _a.key,
1144            " ",
1145            (_b = pitchCorrectionMod.params) == null ? void 0 : _b.scale
1146          ] })
1147        ] }),
1148        robotMod && !pitchCorrectionMod && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 text-[10px] text-amber-400 font-medium", children: [
1149          /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { className: "w-3 h-3" }),
1150          /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
1151            "Robot Voice: Note ",
1152            (_c = robotMod.params) == null ? void 0 : _c.note
1153          ] })
1154        ] })
1155      ] }),
1156      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 pointer-events-auto", children: [
1157        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 bg-zinc-900/80 backdrop-blur-md px-2.5 py-1.5 rounded-xl border border-zinc-800", children: [
1158          /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { className: "w-3.5 h-3.5 text-zinc-400" }),
1159          /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-2 bg-zinc-800 rounded-full overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1160            "div",
1161            {
1162              className: "h-full bg-gradient-to-r from-emerald-500 via-yellow-500 to-rose-500 transition-all duration-75",
1163              style: { width: `${Math.round(volumeLevel * 100)}%` }
1164            }
1165          ) })
1166        ] }),
1167        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex bg-zinc-900/80 backdrop-blur-md p-0.5 rounded-xl border border-zinc-800 text-xs", children: [
1168          /* @__PURE__ */ jsxRuntimeExports.jsx(
1169            "button",
1170            {
1171              onClick: () => setViewMode("wave"),
1172              className: `px-2 py-1 rounded-lg font-medium transition-colors ${viewMode === "wave" ? "bg-zinc-800 text-white" : "text-zinc-400 hover:text-zinc-200"}`,
1173              title: "Oscilloscope Waveform",
1174              children: "Wave"
1175            }
1176          ),
1177          /* @__PURE__ */ jsxRuntimeExports.jsx(
1178            "button",
1179            {
1180              onClick: () => setViewMode("pitch"),
1181              className: `px-2 py-1 rounded-lg font-medium transition-colors ${viewMode === "pitch" ? "bg-zinc-800 text-white" : "text-zinc-400 hover:text-zinc-200"}`,
1182              title: "Pitch Contour Trail",
1183              children: "Pitch"
1184            }
1185          ),
1186          /* @__PURE__ */ jsxRuntimeExports.jsx(
1187            "button",
1188            {
1189              onClick: () => setViewMode("both"),
1190              className: `px-2 py-1 rounded-lg font-medium transition-colors ${viewMode === "both" ? "bg-zinc-800 text-white" : "text-zinc-400 hover:text-zinc-200"}`,
1191              title: "Split View",
1192              children: "Both"
1193            }
1194          )
1195        ] })
1196      ] })
1197    ] }),
1198    /* @__PURE__ */ jsxRuntimeExports.jsx(
1199      "canvas",
1200      {
1201        ref: canvasRef,
1202        className: "w-full h-full block"
1203      }
1204    )
1205  ] });
1206}
1207function OutputControls() {
1208  const { state, dispatch } = useVocalProcessor();
1209  const [isRecording, setIsRecording] = reactExports.useState(false);
1210  const mediaRecorder = reactExports.useRef(null);
1211  const recordedChunks = reactExports.useRef([]);
1212  const toggleRecording = () => {
1213    var _a;
1214    if (isRecording) {
1215      (_a = mediaRecorder.current) == null ? void 0 : _a.stop();
1216      setIsRecording(false);
1217    } else {
1218      const ctx = getAudioContext();
1219      const dest = ctx.createMediaStreamDestination();
1220      const event = new CustomEvent("VOCAL_START_RECORDING", { detail: { destination: dest } });
1221      window.dispatchEvent(event);
1222      const recorder = new MediaRecorder(dest.stream);
1223      recordedChunks.current = [];
1224      recorder.ondataavailable = (e) => {
1225        if (e.data.size > 0) {
1226          recordedChunks.current.push(e.data);
1227        }
1228      };
1229      recorder.onstop = () => {
1230        const blob = new Blob(recordedChunks.current, { type: "audio/webm" });
1231        const url = URL.createObjectURL(blob);
1232        const a = document.createElement("a");
1233        a.style.display = "none";
1234        a.href = url;
1235        a.download = `Hamie_Vocal_Export_${(/* @__PURE__ */ new Date()).getTime()}.webm`;
1236        document.body.appendChild(a);
1237        a.click();
1238        setTimeout(() => {
1239          document.body.removeChild(a);
1240          URL.revokeObjectURL(url);
1241        }, 100);
1242      };
1243      recorder.start();
1244      mediaRecorder.current = recorder;
1245      setIsRecording(true);
1246    }
1247  };
1248  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex w-full items-center justify-between gap-6", children: [
1249    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 max-w-sm flex items-center gap-4", children: [
1250      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-zinc-400 w-12", children: "Dry" }),
1251      /* @__PURE__ */ jsxRuntimeExports.jsx(
1252        "input",
1253        {
1254          type: "range",
1255          min: "0",
1256          max: "100",
1257          value: state.master.wetDry,
1258          onChange: (e) => dispatch({ type: "SET_MASTER_WET_DRY", payload: Number(e.target.value) }),
1259          className: "flex-1 accent-rose-500"
1260        }
1261      ),
1262      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-rose-400 w-12 text-right", children: "Wet" })
1263    ] }),
1264    /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
1265      "button",
1266      {
1267        onClick: toggleRecording,
1268        className: `flex items-center gap-2 px-6 py-2 rounded-full font-medium transition-all ${isRecording ? "bg-rose-500/20 text-rose-500 border border-rose-500/50 animate-pulse" : "bg-zinc-800 text-white hover:bg-zinc-700"}`,
1269        children: isRecording ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1270          /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { className: "w-4 h-4 fill-current" }),
1271          " Stop Recording"
1272        ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1273          /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { className: "w-4 h-4" }),
1274          " Record Output"
1275        ] })
1276      }
1277    ) }),
1278    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 max-w-xs flex items-center gap-4 justify-end", children: [
1279      /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium text-zinc-400", children: "Out" }),
1280      /* @__PURE__ */ jsxRuntimeExports.jsx(
1281        "input",
1282        {
1283          type: "range",
1284          min: "0",
1285          max: "100",
1286          value: state.master.outputVolume,
1287          onChange: (e) => dispatch({ type: "SET_MASTER_VOLUME", payload: Number(e.target.value) }),
1288          className: "w-32 accent-zinc-300"
1289        }
1290      ),
1291      /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-sm text-zinc-400 w-12 text-right", children: [
1292        state.master.outputVolume,
1293        "%"
1294      ] })
1295    ] })
1296  ] });
1297}
1298class FileInput {
1299  constructor(ctx) {
1300    this.buffer = null;
1301    this.sourceNode = null;
1302    this.isPlaying = false;
1303    this.ctx = ctx;
1304  }
1305  async loadFile(file) {
1306    const arrayBuffer = await file.arrayBuffer();
1307    this.buffer = await this.ctx.decodeAudioData(arrayBuffer);
1308  }
1309  play() {
1310    this.stop();
1311    if (!this.buffer) return null;
1312    if (this.ctx.state === "suspended") {
1313      try {
1314        this.ctx.resume();
1315      } catch (e) {
1316      }
1317    }
1318    this.sourceNode = this.ctx.createBufferSource();
1319    this.sourceNode.buffer = this.buffer;
1320    this.sourceNode.loop = true;
1321    this.sourceNode.start();
1322    this.isPlaying = true;
1323    return this.sourceNode;
1324  }
1325  stop() {
1326    if (this.sourceNode) {
1327      try {
1328        this.sourceNode.stop();
1329        this.sourceNode.disconnect();
1330      } catch (e) {
1331      }
1332      this.sourceNode = null;
1333    }
1334    this.isPlaying = false;
1335  }
1336}
1337function InputSelector() {
1338  const { state, dispatch } = useVocalProcessor();
1339  const [devices, setDevices] = reactExports.useState([]);
1340  const fileInputRef = reactExports.useRef(null);
1341  const fileAudioHandler = reactExports.useRef(null);
1342  const [isPlayingFile, setIsPlayingFile] = reactExports.useState(false);
1343  reactExports.useEffect(() => {
1344  }, []);
1345  reactExports.useEffect(() => {
1346    if (state.input.mode === "mic") {
1347      navigator.mediaDevices.enumerateDevices().then((devices2) => {
1348        const audioInputs = devices2.filter((d) => d.kind === "audioinput");
1349        setDevices(audioInputs);
1350        if (audioInputs.length > 0 && !state.input.deviceId) {
1351          dispatch({ type: "SET_MIC_DEVICE", payload: audioInputs[0].deviceId });
1352        }
1353      });
1354    }
1355  }, [state.input.mode]);
1356  const handleFileUpload = async (e) => {
1357    var _a;
1358    const file = (_a = e.target.files) == null ? void 0 : _a[0];
1359    if (!file) return;
1360    dispatch({ type: "SET_FILE_NAME", payload: file.name });
1361    if (!fileAudioHandler.current) {
1362      fileAudioHandler.current = new FileInput(getAudioContext());
1363    }
1364    await fileAudioHandler.current.loadFile(file);
1365    setIsPlayingFile(false);
1366  };
1367  const handleTogglePlay = () => {
1368    if (!fileAudioHandler.current) return;
1369    if (isPlayingFile) {
1370      fileAudioHandler.current.stop();
1371      setIsPlayingFile(false);
1372    } else {
1373      const sourceNode = fileAudioHandler.current.play();
1374      if (sourceNode) {
1375        setIsPlayingFile(true);
1376        const event = new CustomEvent("VOCAL_FILE_PLAY", { detail: { node: sourceNode } });
1377        window.dispatchEvent(event);
1378      }
1379    }
1380  };
1381  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-4", children: [
1382    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex bg-zinc-900 rounded-lg p-1", children: [
1383      /* @__PURE__ */ jsxRuntimeExports.jsxs(
1384        "button",
1385        {
1386          className: `flex-1 flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md transition-colors ${state.input.mode === "mic" ? "bg-zinc-800 text-white" : "text-zinc-400 hover:text-zinc-200"}`,
1387          onClick: () => dispatch({ type: "SET_INPUT_MODE", payload: "mic" }),
1388          children: [
1389            /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { className: "w-4 h-4" }),
1390            " Live Mic"
1391          ]
1392        }
1393      ),
1394      /* @__PURE__ */ jsxRuntimeExports.jsxs(
1395        "button",
1396        {
1397          className: `flex-1 flex items-center justify-center gap-2 py-2 text-sm font-medium rounded-md transition-colors ${state.input.mode === "file" ? "bg-zinc-800 text-white" : "text-zinc-400 hover:text-zinc-200"}`,
1398          onClick: () => dispatch({ type: "SET_INPUT_MODE", payload: "file" }),
1399          children: [
1400            /* @__PURE__ */ jsxRuntimeExports.jsx(FileHeadphone, { className: "w-4 h-4" }),
1401            " File Upload"
1402          ]
1403        }
1404      )
1405    ] }),
1406    state.input.mode === "mic" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
1407      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-sm text-zinc-400", children: "Microphone Input" }),
1408      /* @__PURE__ */ jsxRuntimeExports.jsx(
1409        "select",
1410        {
1411          className: "w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-sm text-zinc-200 focus:outline-none focus:border-rose-500",
1412          value: state.input.deviceId || "",
1413          onChange: (e) => dispatch({ type: "SET_MIC_DEVICE", payload: e.target.value }),
1414          children: devices.map((device) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: device.deviceId, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: device.label || `Microphone ${device.deviceId.slice(0, 5)}...` }, device.deviceId))
1415        }
1416      )
1417    ] }),
1418    state.input.mode === "file" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3", children: [
1419      /* @__PURE__ */ jsxRuntimeExports.jsx(
1420        "input",
1421        {
1422          type: "file",
1423          ref: fileInputRef,
1424          className: "hidden",
1425          accept: "audio/*",
1426          onChange: handleFileUpload
1427        }
1428      ),
1429      /* @__PURE__ */ jsxRuntimeExports.jsxs(
1430        "button",
1431        {
1432          className: "w-full border border-dashed border-zinc-700 hover:border-rose-500 bg-zinc-900/50 rounded-lg p-4 flex flex-col items-center justify-center gap-2 text-zinc-400 hover:text-rose-400 transition-colors",
1433          onClick: () => {
1434            var _a;
1435            return (_a = fileInputRef.current) == null ? void 0 : _a.click();
1436          },
1437          children: [
1438            /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { className: "w-6 h-6" }),
1439            /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-medium", children: "Select Audio File" })
1440          ]
1441        }
1442      ),
1443      state.input.fileName && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between bg-zinc-900 border border-zinc-800 rounded-lg p-2 px-3", children: [
1444        /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-200 truncate pr-4", children: state.input.fileName }),
1445        /* @__PURE__ */ jsxRuntimeExports.jsx(
1446          "button",
1447          {
1448            className: `p-2 rounded-full ${isPlayingFile ? "bg-rose-500/20 text-rose-500" : "bg-zinc-800 text-zinc-300"} hover:bg-rose-500 hover:text-white transition-colors`,
1449            onClick: handleTogglePlay,
1450            children: isPlayingFile ? /* @__PURE__ */ jsxRuntimeExports.jsx(Square, { className: "w-4 h-4 fill-current" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { className: "w-4 h-4 fill-current ml-0.5" })
1451          }
1452        )
1453      ] })
1454    ] })
1455  ] });
1456}
1457const PresetBar = ({ currentUser, onNavigate }) => {
1458  const { state, dispatch } = useVocalProcessor();
1459  const [presets, setPresets] = reactExports.useState([]);
1460  const [selectedPresetId, setSelectedPresetId] = reactExports.useState("");
1461  const [showSaveDialog, setShowSaveDialog] = reactExports.useState(false);
1462  const [presetName, setPresetName] = reactExports.useState("");
1463  const [saveStatus, setSaveStatus] = reactExports.useState("idle");
1464  const [errorMessage, setErrorMessage] = reactExports.useState("");
1465  reactExports.useEffect(() => {
1466    fetchPresets();
1467  }, []);
1468  const fetchPresets = async () => {
1469    try {
1470      const res = await fetch("/api/vocal-processor/presets");
1471      if (res.ok) {
1472        const data = await res.json();
1473        setPresets(data);
1474      }
1475    } catch (e) {
1476      console.error("Failed to fetch vocal presets:", e);
1477    }
1478  };
1479  const savePreset = async () => {
1480    if (!presetName.trim()) return;
1481    setSaveStatus("saving");
1482    setErrorMessage("");
1483    try {
1484      const res = await fetch("/api/vocal-processor/presets", {
1485        method: "POST",
1486        headers: { "Content-Type": "application/json" },
1487        body: JSON.stringify({
1488          name: presetName.trim(),
1489          chain: state.chain,
1490          master: state.master
1491        })
1492      });
1493      if (res.ok) {
1494        const saved = await res.json();
1495        setShowSaveDialog(false);
1496        setPresetName("");
1497        setSaveStatus("idle");
1498        await fetchPresets();
1499        if (saved && saved.id) {
1500          setSelectedPresetId(saved.id);
1501        }
1502      } else {
1503        const err = await res.json().catch(() => ({}));
1504        setSaveStatus("error");
1505        setErrorMessage(err.error || "Failed to save preset. Please ensure you are logged in.");
1506      }
1507    } catch (e) {
1508      console.error(e);
1509      setSaveStatus("error");
1510      setErrorMessage("Network error while saving preset.");
1511    }
1512  };
1513  const loadPreset = (preset) => {
1514    setSelectedPresetId(preset.id);
1515    dispatch({
1516      type: "LOAD_PRESET",
1517      payload: {
1518        chain: preset.chain,
1519        master: preset.master || { wetDry: 100, outputVolume: 100, bypass: false }
1520      }
1521    });
1522  };
1523  const deletePreset = async (id) => {
1524    if (!id || id.startsWith("factory-")) return;
1525    if (!confirm("Are you sure you want to delete this custom preset?")) return;
1526    try {
1527      const res = await fetch(`/api/vocal-processor/presets/${id}`, { method: "DELETE" });
1528      if (res.ok) {
1529        setSelectedPresetId("");
1530        fetchPresets();
1531      }
1532    } catch (e) {
1533      console.error("Failed to delete preset:", e);
1534    }
1535  };
1536  const factoryPresets = presets.filter((p) => p.isFactory || typeof p.id === "string" && p.id.startsWith("factory-"));
1537  const userPresets = presets.filter((p) => !p.isFactory && !(typeof p.id === "string" && p.id.startsWith("factory-")));
1538  const currentPreset = presets.find((p) => p.id === selectedPresetId);
1539  const isCustomPreset = currentPreset && !currentPreset.isFactory && !(typeof currentPreset.id === "string" && currentPreset.id.startsWith("factory-"));
1540  return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1541    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-none h-14 border-b border-zinc-800 bg-zinc-900/95 backdrop-blur flex items-center justify-between px-4 z-20", children: [
1542      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
1543        onNavigate && /* @__PURE__ */ jsxRuntimeExports.jsxs(
1544          "button",
1545          {
1546            onClick: () => onNavigate("lab"),
1547            className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-zinc-300 hover:text-white bg-zinc-800/90 hover:bg-zinc-750 border border-zinc-700/80 rounded-lg transition-colors cursor-pointer shadow-sm",
1548            title: "Return to Hamie Lab",
1549            children: [
1550              /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { className: "w-3.5 h-3.5 text-zinc-400 group-hover:text-white" }),
1551              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Hamie Lab" })
1552            ]
1553          }
1554        ),
1555        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-5 w-px bg-zinc-800 mx-1 hidden sm:block" }),
1556        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-2 bg-rose-500/10 text-rose-500 rounded-lg", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { className: "w-5 h-5" }) }),
1557        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1558          /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "font-semibold text-zinc-100 text-sm sm:text-base leading-tight", children: "Vocal Processor" }),
1559          /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "px-2 py-0.5 text-[10px] font-semibold bg-rose-500/20 text-rose-400 rounded-full", children: "DSP Studio" })
1560        ] }) })
1561      ] }),
1562      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
1563        /* @__PURE__ */ jsxRuntimeExports.jsxs(
1564          "select",
1565          {
1566            className: "bg-zinc-800 text-zinc-200 px-3 py-1.5 rounded-lg text-sm border border-zinc-700 focus:outline-none focus:border-rose-500",
1567            value: selectedPresetId,
1568            onChange: (e) => {
1569              const val = e.target.value;
1570              if (val) {
1571                const p = presets.find((p2) => p2.id === val);
1572                if (p) loadPreset(p);
1573              } else {
1574                setSelectedPresetId("");
1575              }
1576            },
1577            children: [
1578              /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", disabled: true, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: "Load Preset..." }),
1579              factoryPresets.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("optgroup", { label: "Factory Presets", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: factoryPresets.map((p) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: p.id, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: p.name }, p.id)) }),
1580              userPresets.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("optgroup", { label: "My Presets", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: userPresets.map((p) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: p.id, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: p.name }, p.id)) })
1581            ]
1582          }
1583        ),
1584        isCustomPreset && /* @__PURE__ */ jsxRuntimeExports.jsx(
1585          "button",
1586          {
1587            onClick: () => deletePreset(selectedPresetId),
1588            className: "p-1.5 text-zinc-400 hover:text-rose-400 bg-zinc-800 hover:bg-zinc-750 border border-zinc-700 rounded-lg transition-colors",
1589            title: "Delete Custom Preset",
1590            children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { className: "w-4 h-4" })
1591          }
1592        ),
1593        /* @__PURE__ */ jsxRuntimeExports.jsxs(
1594          "button",
1595          {
1596            onClick: () => {
1597              setErrorMessage("");
1598              setSaveStatus("idle");
1599              setShowSaveDialog(true);
1600            },
1601            className: "flex items-center gap-1.5 bg-rose-600 hover:bg-rose-500 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors shadow-sm",
1602            children: [
1603              /* @__PURE__ */ jsxRuntimeExports.jsx(Bookmark, { className: "w-3.5 h-3.5" }),
1604              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Save Preset" }),
1605              /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "sm:hidden", children: "Save" })
1606            ]
1607          }
1608        )
1609      ] })
1610    ] }),
1611    showSaveDialog && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 p-6 rounded-2xl border border-zinc-800 w-full max-w-md 
1611shadow-2xl", children: [
1612      /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-white font-bold text-lg mb-1", children: "Save Vocal Preset" }),
1613      /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-400 mb-4", children: "Save current vocal chain modules and master DSP settings to your personal preset library." }),
1614      !currentUser && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-2.5 p-3 rounded-xl bg-amber-500/10 border border-amber-500/30 text-amber-300 text-xs mb-4", children: [
1615        /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "w-4 h-4 mt-0.5 shrink-0" }),
1616        /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
1617          /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-semibold mb-0.5", children: "Authentication Required" }),
1618          /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "You are currently browsing as a guest. Please sign in to permanently save custom presets to your account." })
1619        ] })
1620      ] }),
1621      errorMessage && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-300 text-xs mb-4", children: errorMessage }),
1622      /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-semibold text-zinc-400 uppercase tracking-wider mb-2", children: "Preset Name" }),
1623      /* @__PURE__ */ jsxRuntimeExports.jsx(
1624        "input",
1625        {
1626          type: "text",
1627          value: presetName,
1628          onChange: (e) => setPresetName(e.target.value),
1629          placeholder: "e.g. My Warm Lead Auto-Tune",
1630          className: "w-full bg-zinc-800 text-white border border-zinc-700 rounded-xl px-3.5 py-2.5 text-sm focus:outline-none focus:border-rose-500 mb-5",
1631          autoFocus: true,
1632          onKeyDown: (e) => {
1633            if (e.key === "Enter") savePreset();
1634          }
1635        }
1636      ),
1637      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2.5", children: [
1638        /* @__PURE__ */ jsxRuntimeExports.jsx(
1639          "button",
1640          {
1641            onClick: () => setShowSaveDialog(false),
1642            className: "px-4 py-2 text-sm text-zinc-400 hover:text-white transition-colors",
1643            disabled: saveStatus === "saving",
1644            children: "Cancel"
1645          }
1646        ),
1647        /* @__PURE__ */ jsxRuntimeExports.jsx(
1648          "button",
1649          {
1650            onClick: savePreset,
1651            disabled: saveStatus === "saving" || !presetName.trim(),
1652            className: "bg-rose-600 hover:bg-rose-500 disabled:opacity-50 text-white px-5 py-2 rounded-xl text-sm font-medium transition-colors shadow-sm",
1653            children: saveStatus === "saving" ? "Saving..." : "Save Preset"
1654          }
1655        )
1656      ] })
1657    ] }) })
1658  ] });
1659};
1660class MicrophoneInput {
1661  constructor(ctx) {
1662    this.stream = null;
1663    this.sourceNode = null;
1664    this.ctx = ctx;
1665  }
1666  async start(deviceId) {
1667    this.stop();
1668    if (this.ctx.state === "suspended") {
1669      try {
1670        await this.ctx.resume();
1671      } catch (e) {
1672      }
1673    }
1674    const constraints = {
1675      audio: {
1676        echoCancellation: false,
1677        noiseSuppression: false,
1678        autoGainControl: false,
1679        ...deviceId ? { deviceId: { exact: deviceId } } : {}
1680      }
1681    };
1682    this.stream = await navigator.mediaDevices.getUserMedia(constraints);
1683    this.sourceNode = this.ctx.createMediaStreamSource(this.stream);
1684    return this.sourceNode;
1685  }
1686  stop() {
1687    if (this.stream) {
1688      this.stream.getTracks().forEach((t) => t.stop());
1689      this.stream = null;
1690    }
1691    if (this.sourceNode) {
1692      this.sourceNode.disconnect();
1693      this.sourceNode = null;
1694    }
1695  }
1696}
1697function useVocalAudioSync() {
1698  const { state, dispatch } = useVocalProcessor();
1699  const chainRef = reactExports.useRef(null);
1700  const micInputRef = reactExports.useRef(null);
1701  const prevChainRef = reactExports.useRef(state.chain);
1702  const prevMasterRef = reactExports.useRef(state.master);
1703  const prevInputRef = reactExports.useRef(state.input);
1704  reactExports.useEffect(() => {
1705    const ctx = getAudioContext();
1706    const dummySource = ctx.createGain();
1707    const destination = ctx.destination;
1708    chainRef.current = new VocalSignalChain(ctx, dummySource, destination);
1709    chainRef.current.rebuildChain(state.chain);
1710    micInputRef.current = new MicrophoneInput(ctx);
1711    return () => {
1712      var _a, _b;
1713      (_a = chainRef.current) == null ? void 0 : _a.destroy();
1714      chainRef.current = null;
1715      (_b = micInputRef.current) == null ? void 0 : _b.stop();
1716    };
1717  }, []);
1718  reactExports.useEffect(() => {
1719    if (!chainRef.current || !micInputRef.current) return;
1720    const setupInput = async () => {
1721      var _a;
1722      if (state.input.mode === "mic") {
1723        try {
1724          const sourceNode = await micInputRef.current.start(state.input.deviceId);
1725          chainRef.current.setInputNode(sourceNode);
1726        } catch (e) {
1727          console.error("Failed to start mic", e);
1728        }
1729      } else {
1730        (_a = micInputRef.current) == null ? void 0 : _a.stop();
1731        const dummy = getAudioContext().createGain();
1732        chainRef.current.setInputNode(dummy);
1733      }
1734    };
1735    if (prevInputRef.current.mode !== state.input.mode || prevInputRef.current.deviceId !== state.input.deviceId) {
1736      setupInput();
1737    }
1738    if (prevInputRef.current === state.input && state.input.mode === "mic") {
1739      setupInput();
1740    }
1741    const handleFilePlay = (e) => {
1742      var _a;
1743      const customEvent = e;
1744      if (customEvent.detail && customEvent.detail.node) {
1745        (_a = chainRef.current) == null ? void 0 : _a.setInputNode(customEvent.detail.node);
1746      }
1747    };
1748    const handleRecordStart = (e) => {
1749      var _a;
1750      const customEvent = e;
1751      if (customEvent.detail && customEvent.detail.destination) {
1752        (_a = chainRef.current) == null ? void 0 : _a.addRecordingDestination(customEvent.detail.destination);
1753      }
1754    };
1755    window.addEventListener("VOCAL_FILE_PLAY", handleFilePlay);
1756    window.addEventListener("VOCAL_START_RECORDING", handleRecordStart);
1757    prevInputRef.current = state.input;
1758    return () => {
1759      window.removeEventListener("VOCAL_FILE_PLAY", handleFilePlay);
1760      window.removeEventListener("VOCAL_START_RECORDING", handleRecordStart);
1761    };
1762  }, [state.input.mode, state.input.deviceId]);
1763  reactExports.useEffect(() => {
1764    if (!chainRef.current) return;
1765    const prevChain = prevChainRef.current;
1766    const currentChain = state.chain;
1767    const structureChanged = prevChain.length !== currentChain.length || prevChain.some((mod, i) => mod.id !== currentChain[i].id || mod.enabled !== currentChain[i].enabled);
1768    if (structureChanged) {
1769      chainRef.current.rebuildChain(currentChain);
1770    } else {
1771      for (let i = 0; i < currentChain.length; i++) {
1772        if (prevChain[i].params !== currentChain[i].params) {
1773          chainRef.current.updateModuleParams(currentChain[i].id, currentChain[i].type, currentChain[i].params);
1774        }
1775      }
1776    }
1777    prevChainRef.current = currentChain;
1778  }, [state.chain]);
1779  reactExports.useEffect(() => {
1780    if (!chainRef.current) return;
1781    if (prevMasterRef.current !== state.master) {
1782      chainRef.current.updateMasterParams(state.master);
1783      prevMasterRef.current = state.master;
1784    }
1785  }, [state.master]);
1786}
1787function VocalWorkspace({ currentUser, onNavigate }) {
1788  const { state, dispatch } = useVocalProcessor();
1789  useVocalAudioSync();
1790  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-screen bg-zinc-950 text-white overflow-hidden", children: [
1791    /* @__PURE__ */ jsxRuntimeExports.jsx(PresetBar, { currentUser, onNavigate }),
1792    /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex overflow-hidden", children: [
1793      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-72 flex-none border-r border-zinc-800 bg-zinc-900/50 flex flex-col", children: [
1794        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 border-b border-zinc-800", children: /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xs font-semibold text-zinc-400 uppercase tracking-wider", children: "Signal Chain" }) }),
1795        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto p-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SignalChainSidebar, {}) })
1796      ] }),
1797      /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [
1798        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-none p-4 border-b border-zinc-800 bg-zinc-900/30", children: /* @__PURE__ */ jsxRuntimeExports.jsx(InputSelector, {}) }),
1799        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto p-6 bg-zinc-950", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ModuleParameterPanel, {}) }),
1800        /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-56 flex-none border-t border-zinc-800 bg-zinc-900 relative", children: /* @__PURE__ */ jsxRuntimeExports.jsx(PitchVisualizer, {}) })
1801      ] })
1802    ] }),
1803    /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-none h-16 border-t border-zinc-800 bg-zinc-900 flex items-center justify-between px-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx(OutputControls, {}) })
1804  ] });
1805}
1806function VocalProcessorPage({ currentUser, onNavigate }) {
1807  return /* @__PURE__ */ jsxRuntimeExports.jsx(VocalProcessorProvider, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(VocalWorkspace, { currentUser, onNavigate }) });
1808}
1809export {
1810  VocalProcessorPage as default
1811};

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.