murmuraba
Version:
Real-time audio noise reduction with advanced chunked processing for web applications
137 lines (136 loc) • 4 kB
JavaScript
/**
* 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;
}
}