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