UNPKG

murmuraba

Version:

Real-time audio noise reduction with advanced chunked processing for web applications

137 lines (136 loc) 4 kB
/** * Optimized Gain Management Service * Reduces re-renders and provides centralized gain control */ import { GainController, validateGain } from '../utils/gain-control'; export class GainService { constructor(options = {}) { this.updateListeners = new Set(); this.gainController = new GainController(options.initialGain); this.throttleMs = options.throttleMs ?? 16; // ~60fps this.enableSmoothing = options.enableSmoothing ?? true; this.smoothingTimeConstant = options.smoothingTimeConstant ?? 0.01; // 10ms } /** * Initialize the service with an audio context */ initialize(audioContext) { this.audioContext = audioContext; this.createGainNode(); } /** * Get the gain node for audio graph connection */ getGainNode() { return this.gainNode; } /** * Set gain with throttling to prevent excessive updates */ setGain(gain) { const validatedGain = validateGain(gain); // Update controller immediately for sync state this.gainController.setGain(validatedGain); // Throttle actual audio node updates this.pendingGain = validatedGain; if (this.throttleTimer) { return; // Update already scheduled } this.throttleTimer = setTimeout(() => { this.applyPendingGain(); this.throttleTimer = undefined; }, this.throttleMs); } /** * Get current gain value */ getCurrentGain() { return this.gainController.currentGain; } /** * Apply gain preset */ applyPreset(preset) { this.gainController.applyPreset(preset); this.applyPendingGain(); } /** * Get gain description for UI */ getDescription() { return this.gainController.getDescription(); } /** * Get gain in decibels for display */ getDbValue() { return this.gainController.getDbValue(); } /** * Subscribe to gain changes */ onGainChange(callback) { this.updateListeners.add(callback); return () => this.updateListeners.delete(callback); } /** * Cleanup resources */ destroy() { if (this.throttleTimer) { clearTimeout(this.throttleTimer); } this.gainController.destroy(); this.updateListeners.clear(); this.gainNode?.disconnect(); } createGainNode() { if (!this.audioContext) { throw new Error('AudioContext not initialized'); } this.gainNode = this.audioContext.createGain(); this.gainNode.gain.value = this.gainController.currentGain; } applyPendingGain() { if (!this.gainNode || this.pendingGain === undefined) { return; } const targetGain = this.pendingGain; this.pendingGain = undefined; if (this.enableSmoothing && this.audioContext) { // Apply smooth transition to prevent audio clicks this.gainNode.gain.setTargetAtTime(targetGain, this.audioContext.currentTime, this.smoothingTimeConstant); } else { this.gainNode.gain.value = targetGain; } // Notify listeners with throttled updates this.notifyListeners(targetGain); } notifyListeners(gain) { this.updateListeners.forEach(callback => { try { callback(gain); } catch (error) { console.warn('Gain service listener error:', error); } }); } } /** * Singleton instance for global use */ let globalGainService = null; export function getGainService(options) { if (!globalGainService) { globalGainService = new GainService(options); } return globalGainService; } export function destroyGainService() { if (globalGainService) { globalGainService.destroy(); globalGainService = null; } }