PageSourceSearch

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

js hamienet.com collected 2026-09-24 18:59:35 UTC 86,727 bytes, 1,809 lines download raw bytes

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

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.