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.