export class AudioManager { constructor() { this.context = null; this.rawStream = null; this.outputStream = null; this.source = null; this.gainNode = null; this.analyser = null; this.meterSink = null; this.meterData = new Float32Array(1024); this.gain = Number(localStorage.getItem('voicepoc.micGain') || 1); } async ensureContext() { if (!this.context) this.context = new AudioContext(); await this.context.resume().catch(() => {}); return this.context; } async attachInput(stream) { await this.ensureContext(); this.detachInput(); this.rawStream = stream; this.source = this.context.createMediaStreamSource(stream); this.gainNode = this.context.createGain(); this.gainNode.gain.value = this.gain; this.analyser = this.context.createAnalyser(); this.analyser.fftSize = 1024; this.meterSink = this.context.createGain(); this.meterSink.gain.value = 0; const destination = this.context.createMediaStreamDestination(); this.source.connect(this.gainNode).connect(this.analyser); // Keep the analyser alive without sending the microphone to the speakers. this.analyser.connect(this.meterSink).connect(this.context.destination); this.gainNode.connect(destination); this.outputStream = destination.stream; return this.outputStream; } detachInput() { for (const node of [this.source, this.gainNode, this.analyser, this.meterSink]) { try { node?.disconnect(); } catch {} } this.outputStream?.getTracks().forEach((track) => track.stop()); this.rawStream?.getTracks().forEach((track) => track.stop()); this.source = this.gainNode = this.analyser = this.meterSink = null; this.outputStream = this.rawStream = null; } setGain(value) { this.gain = Math.max(0, Math.min(2, Number(value) || 0)); localStorage.setItem('voicepoc.micGain', String(this.gain)); if (this.gainNode) this.gainNode.gain.setTargetAtTime(this.gain, this.context.currentTime, 0.015); } getLevel() { if (!this.analyser) return { rms: 0, peak: 0, clipping: false }; this.analyser.getFloatTimeDomainData(this.meterData); let sum = 0; let peak = 0; for (const sample of this.meterData) { const magnitude = Math.abs(sample); sum += sample * sample; peak = Math.max(peak, magnitude); } const rms = Math.sqrt(sum / this.meterData.length); return { rms, peak, clipping: peak >= 0.98 }; } stop() { this.detachInput(); } }