audioProcessing.ts
1 import { ref } from "vue"; 2 3 export interface EQFilter { 4 type: BiquadFilterType; 5 frequency: { value: number }; 6 Q?: { value: number }; 7 gain?: { value: number }; 8 enabled: boolean; 9 } 10 11 export interface EQPreset { 12 mainOut: { 13 gain: number; 14 muted: boolean; 15 }; 16 filters: { 17 [key: string]: EQFilter; 18 }; 19 } 20 21 // State 22 export const filters = ref<BiquadFilterNode[]>([]); 23 export const activeFilters = ref<BiquadFilterNode[]>([]); 24 export const lastFilter = ref<BiquadFilterNode | null>(null); 25 26 // Add mute state tracking 27 export const isMuted = ref(false); 28 export let rememberedVolume = 1.0; 29 30 export let gainNode: GainNode | null = null; 31 export let source: MediaStreamAudioSourceNode; 32 export let audioCtx: AudioContext; 33 34 // Add a cache to store user-adjusted filter values 35 const userAdjustedFilterValues = new Map< 36 string, 37 { 38 frequency: number; 39 Q?: number; 40 gain?: number; 41 } 42 >(); 43 44 // Add this utility function at the top with other utility functions 45 function bandwidthToQ(bandwidth: number): number { 46 // Convert bandwidth (0-100) to Q (0.1-10) 47 // Higher bandwidth = lower Q, Lower bandwidth = higher Q 48 return 10.1 - bandwidth / 10; 49 } 50 51 // Setup audio context and capture 52 export async function startCapture( 53 preset: EQPreset 54 ): Promise<MediaStream | null> { 55 return new Promise((resolve) => { 56 chrome.tabCapture.capture({ audio: true }, (stream) => { 57 if (!stream) { 58 resolve(null); 59 return; 60 } 61 62 const AudioContext = window.AudioContext; 63 audioCtx = new AudioContext(); 64 source = audioCtx.createMediaStreamSource(stream); 65 gainNode = audioCtx.createGain(); 66 source.connect(gainNode); 67 68 // Initialize mute state and volume 69 if (preset?.mainOut) { 70 isMuted.value = preset.mainOut.muted || false; 71 rememberedVolume = preset.mainOut.gain || 1.0; 72 gainNode.gain.value = isMuted.value ? 0 : rememberedVolume; 73 } 74 75 // Create and initialize filter nodes with correct initial values 76 filters.value = Object.entries(preset.filters).map( 77 ([type, filterData]) => { 78 const filter = audioCtx.createBiquadFilter(); 79 filter.type = type as BiquadFilterType; 80 81 // Set initial frequency 82 filter.frequency.value = filterData.frequency.value; 83 84 // Set Q value based on filter type 85 if (filterData.Q) { 86 switch (type) { 87 case "bandpass": 88 filter.Q.value = Math.max(0.1, filterData.Q.value); // Q must be > 0 89 break; 90 case "peaking": 91 filter.Q.value = filterData.Q.value || 2.0; // Default Q for peaking 92 break; 93 case "lowpass": 94 case "highpass": 95 filter.Q.value = filterData.Q.value || 0.707; // Butterworth response 96 break; 97 default: 98 filter.Q.value = filterData.Q.value || 1.0; 99 } 100 } 101 102 // Set gain for filters that support it 103 if ( 104 filterData.gain && 105 (type === "lowshelf" || type === "highshelf" || type === "peaking") 106 ) { 107 filter.gain.value = filterData.gain.value; 108 } 109 110 return filter; 111 } 112 ); 113 114 gainNode.connect(audioCtx.destination); 115 resolve(stream); 116 }); 117 }); 118 } 119 120 // Update toggle mute function 121 export function toggleMute(muted: boolean, volume: number) { 122 if (!gainNode || !audioCtx) return; 123 124 // Store mute state 125 isMuted.value = muted; 126 127 if (muted) { 128 // Remember current volume but set gain to 0 129 rememberedVolume = volume; 130 gainNode.gain.value = 0; 131 } else { 132 // Restore remembered volume 133 gainNode.gain.value = rememberedVolume; 134 } 135 } 136 137 // Update volume setting function 138 export function setVolume(volume: number) { 139 if (!gainNode) return; 140 141 // Always store the requested volume 142 rememberedVolume = volume; 143 144 // Only apply it if not muted 145 if (!isMuted.value) { 146 gainNode.gain.value = volume; 147 } 148 // If muted, we keep gain at 0 but remember the new volume for when unmuted 149 } 150 151 export function updateFilterValue( 152 filterType: BiquadFilterType, 153 value: number, 154 secondaryValue?: number 155 ) { 156 const filter = filters.value.find((filter) => filter.type === filterType); 157 if (!filter) return; 158 159 // Always update frequency 160 filter.frequency.value = value; 161 162 // Store user adjusted values in our cache 163 let cachedValues = userAdjustedFilterValues.get(filterType) || { 164 frequency: value, 165 }; 166 cachedValues.frequency = value; 167 168 // Handle secondary parameter based on filter type 169 if (secondaryValue !== undefined) { 170 switch (filterType) { 171 case "bandpass": 172 // Convert bandwidth value to Q 173 filter.Q.value = bandwidthToQ(secondaryValue); 174 cachedValues.Q = secondaryValue; // Store bandwidth value 175 break; 176 case "lowpass": 177 case "highpass": 178 filter.Q.value = Math.max(0.1, secondaryValue); // Resonance control 179 cachedValues.Q = secondaryValue; 180 break; 181 case "peaking": 182 case "lowshelf": 183 case "highshelf": 184 filter.gain.value = secondaryValue; // Gain control in dB 185 cachedValues.gain = secondaryValue; 186 break; 187 } 188 } 189 190 // Update the cache 191 userAdjustedFilterValues.set(filterType, cachedValues); 192 } 193 194 export function connectToFilter( 195 filterType: string, 196 enabled: boolean, 197 preset: EQPreset 198 ) { 199 const targetFilter = filters.value.find( 200 (filter) => filter.type === filterType 201 ); 202 if (!targetFilter || !preset.filters) return; 203 204 const filterData = preset.filters[filterType]; 205 filterData.enabled = enabled; 206 207 if (enabled) { 208 // Use cached user-adjusted values if available, otherwise use preset values 209 const cachedValues = userAdjustedFilterValues.get(filterType); 210 211 if (cachedValues) { 212 // Restore user-adjusted values from cache 213 targetFilter.frequency.value = cachedValues.frequency; 214 215 if (filterType === "bandpass" && cachedValues.Q !== undefined) { 216 targetFilter.Q.value = bandwidthToQ(cachedValues.Q); 217 } else if (cachedValues.Q !== undefined) { 218 targetFilter.Q.value = cachedValues.Q; 219 } 220 221 if ( 222 cachedValues.gain !== undefined && 223 (filterType === "lowshelf" || 224 filterType === "highshelf" || 225 filterType === "peaking") 226 ) { 227 targetFilter.gain.value = cachedValues.gain; 228 } 229 } else { 230 // First-time enable, use preset values 231 targetFilter.frequency.value = filterData.frequency.value; 232 233 // Handle Q and gain values properly 234 if (filterType === "bandpass" && filterData.Q) { 235 targetFilter.Q.value = bandwidthToQ(filterData.Q.value); 236 } else if (filterData.Q) { 237 targetFilter.Q.value = filterData.Q.value; 238 } 239 240 if ( 241 filterData.gain && 242 (filterType === "lowshelf" || 243 filterType === "highshelf" || 244 filterType === "peaking") 245 ) { 246 targetFilter.gain.value = filterData.gain.value; 247 } 248 249 // Initialize the cache with preset values 250 userAdjustedFilterValues.set(filterType, { 251 frequency: filterData.frequency.value, 252 Q: filterData.Q?.value, 253 gain: filterData.gain?.value, 254 }); 255 } 256 257 try { 258 if (activeFilters.value.length === 0) { 259 // First filter being added 260 source.disconnect(); 261 source.connect(targetFilter); 262 targetFilter.connect(gainNode); 263 activeFilters.value.push(targetFilter); 264 lastFilter.value = targetFilter; 265 } else { 266 // Adding filter to chain 267 const lastActiveFilter = 268 activeFilters.value[activeFilters.value.length - 1]; 269 if (lastActiveFilter) { 270 lastActiveFilter.disconnect(); 271 lastActiveFilter.connect(targetFilter); 272 targetFilter.connect(gainNode); 273 activeFilters.value.push(targetFilter); 274 lastFilter.value = targetFilter; 275 } 276 } 277 } catch (e) { 278 console.warn("Error connecting filter:", e); 279 } 280 } else { 281 // Disabling filter 282 const filterIndex = activeFilters.value.findIndex( 283 (f) => f.type === filterType 284 ); 285 if (filterIndex === -1) return; 286 287 try { 288 if (activeFilters.value.length === 1) { 289 // Last filter being removed 290 targetFilter.disconnect(); 291 source.disconnect(); 292 source.connect(gainNode); 293 activeFilters.value = []; 294 lastFilter.value = null; 295 } else if (filterIndex === 0) { 296 // First filter in chain being removed 297 targetFilter.disconnect(); 298 source.disconnect(); 299 source.connect(activeFilters.value[1]); 300 activeFilters.value.splice(0, 1); 301 lastFilter.value = activeFilters.value[activeFilters.value.length - 1]; 302 } else if (filterIndex === activeFilters.value.length - 1) { 303 // Last filter in chain being removed 304 const prevFilter = activeFilters.value[filterIndex - 1]; 305 targetFilter.disconnect(); 306 prevFilter.disconnect(); 307 prevFilter.connect(gainNode); 308 activeFilters.value.splice(filterIndex, 1); 309 lastFilter.value = prevFilter; 310 } else { 311 // Middle filter being removed 312 const prevFilter = activeFilters.value[filterIndex - 1]; 313 const nextFilter = activeFilters.value[filterIndex + 1]; 314 targetFilter.disconnect(); 315 prevFilter.disconnect(); 316 prevFilter.connect(nextFilter); 317 activeFilters.value.splice(filterIndex, 1); 318 lastFilter.value = activeFilters.value[activeFilters.value.length - 1]; 319 } 320 } catch (e) { 321 console.warn("Error disconnecting filter:", e); 322 } 323 } 324 } 325 326 export function handleMonoToggle(isMono: boolean, currentGain: number) { 327 if (!source || !gainNode || !audioCtx) return false; 328 329 const sourceNode = lastFilter.value || source; 330 sourceNode.disconnect(); 331 332 if (isMono) { 333 // Convert to mono 334 const splitter = audioCtx.createChannelSplitter(2); 335 const merger = audioCtx.createChannelMerger(2); 336 337 sourceNode.connect(splitter); 338 339 // Mix both channels equally for true mono 340 splitter.connect(merger, 0, 0); // Left to left 341 splitter.connect(merger, 1, 0); // Right to left 342 splitter.connect(merger, 0, 1); // Left to right 343 splitter.connect(merger, 1, 1); // Right to right 344 345 // Each channel now gets 50% of each input 346 const gainCompensation = audioCtx.createGain(); 347 gainCompensation.gain.value = 0.5; 348 349 merger.connect(gainCompensation); 350 gainCompensation.connect(gainNode); 351 } else { 352 // Restore stereo 353 sourceNode.connect(gainNode); 354 } 355 356 gainNode.gain.value = currentGain; 357 return !isMono; 358 } 359 360 export function resetFilters() { 361 try { 362 source?.disconnect(); 363 filters.value.forEach((filter) => filter.disconnect()); 364 activeFilters.value = []; 365 lastFilter.value = null; 366 source?.connect(gainNode); 367 } catch (e) { 368 console.warn("Error resetting connections:", e); 369 } 370 }