Files
gibbz/public/audio-manager.js
T
mats 2888a54259
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 10m18s
fix mic meter and distribute participant positions
2026-09-22 17:24:34 +02:00

74 lines
2.5 KiB
JavaScript

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();
}
}