Ecobot_Scoala_Verde/frontend/js/audio.js
Stefan Caramizoiu d7a7d2cafd Initial commit
2026-04-01 11:14:26 +03:00

172 lines
5.7 KiB
JavaScript

class AudioHandler {
constructor() {
this.mediaRecorder = null;
this.audioChunks = [];
this.isRecording = false;
this.audioContext = null;
this.selectedMicId = localStorage.getItem('selectedMicId') || null;
this.selectedSpeakerId = localStorage.getItem('selectedSpeakerId') || null;
this.micGain = parseFloat(localStorage.getItem('micGain') || '3.0');
}
async init() {
const constraints = {
audio: this.selectedMicId
? {
deviceId: { exact: this.selectedMicId },
autoGainControl: true,
noiseSuppression: true,
echoCancellation: true,
}
: {
autoGainControl: true,
noiseSuppression: true,
echoCancellation: true,
},
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
// Apply gain boost through Web Audio API
const ctx = new AudioContext();
const source = ctx.createMediaStreamSource(stream);
const gainNode = ctx.createGain();
gainNode.gain.value = this.micGain;
const dest = ctx.createMediaStreamDestination();
source.connect(gainNode);
gainNode.connect(dest);
this._liveAudioCtx = ctx;
this._liveGainNode = gainNode;
this.mediaRecorder = new MediaRecorder(dest.stream, { mimeType: 'audio/webm' });
this.mediaRecorder.ondataavailable = (event) => {
this.audioChunks.push(event.data);
};
}
setMic(deviceId) {
this.selectedMicId = deviceId;
localStorage.setItem('selectedMicId', deviceId);
}
setSpeaker(deviceId) {
this.selectedSpeakerId = deviceId;
localStorage.setItem('selectedSpeakerId', deviceId);
}
setGain(value) {
this.micGain = value;
localStorage.setItem('micGain', value.toString());
if (this._liveGainNode) {
this._liveGainNode.gain.value = value;
}
}
startRecording() {
this.audioChunks = [];
this.mediaRecorder.start();
this.isRecording = true;
}
stopRecording() {
return new Promise((resolve) => {
this.mediaRecorder.onstop = async () => {
const blob = new Blob(this.audioChunks, { type: 'audio/webm' });
const wavBlob = await this.convertToWav(blob);
resolve(wavBlob);
};
this.mediaRecorder.stop();
this.isRecording = false;
});
}
async convertToWav(blob) {
if (!this.audioContext) {
this.audioContext = new AudioContext({ sampleRate: 16000 });
}
const arrayBuffer = await blob.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
const numChannels = 1;
const sampleRate = 16000;
const bitsPerSample = 16;
const offlineCtx = new OfflineAudioContext(numChannels, audioBuffer.duration * sampleRate, sampleRate);
const source = offlineCtx.createBufferSource();
source.buffer = audioBuffer;
source.connect(offlineCtx.destination);
source.start();
const resampled = await offlineCtx.startRendering();
const samples = resampled.getChannelData(0);
// Normalize audio - find peak and scale up
let peak = 0;
for (let i = 0; i < samples.length; i++) {
const abs = Math.abs(samples[i]);
if (abs > peak) peak = abs;
}
const normalizeGain = peak > 0.01 ? 0.9 / peak : 1;
const dataLength = samples.length * (bitsPerSample / 8);
const buffer = new ArrayBuffer(44 + dataLength);
const view = new DataView(buffer);
this.writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + dataLength, true);
this.writeString(view, 8, 'WAVE');
this.writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * numChannels * (bitsPerSample / 8), true);
view.setUint16(32, numChannels * (bitsPerSample / 8), true);
view.setUint16(34, bitsPerSample, true);
this.writeString(view, 36, 'data');
view.setUint32(40, dataLength, true);
let offset = 44;
for (let i = 0; i < samples.length; i++) {
const amplified = samples[i] * normalizeGain;
const s = Math.max(-1, Math.min(1, amplified));
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
offset += 2;
}
return new Blob([buffer], { type: 'audio/wav' });
}
writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
playAudioFromBase64(base64Data, mimeType = 'audio/mpeg') {
return new Promise((resolve) => {
const byteChars = atob(base64Data);
const byteArray = new Uint8Array(byteChars.length);
for (let i = 0; i < byteChars.length; i++) {
byteArray[i] = byteChars.charCodeAt(i);
}
const blob = new Blob([byteArray], { type: mimeType });
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
if (this.selectedSpeakerId && audio.setSinkId) {
audio.setSinkId(this.selectedSpeakerId).catch(() => {});
}
audio.onended = () => {
URL.revokeObjectURL(url);
resolve();
};
audio.play();
});
}
}