File: //opt/textanalyse/live_api_frontend/src/lib/audio-recorder.ts
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { audioContext } from "./utils";
import AudioRecordingWorklet from "./worklets/audio-processing";
import VolMeterWorket from "./worklets/vol-meter";
import { createWorketFromSrc } from "./audioworklet-registry";
import EventEmitter from "eventemitter3";
function arrayBufferToBase64(buffer: ArrayBuffer) {
var binary = "";
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
export class AudioRecorder extends EventEmitter {
stream: MediaStream | undefined;
audioContext: AudioContext | undefined;
source: MediaStreamAudioSourceNode | undefined;
recording: boolean = false;
recordingWorklet: AudioWorkletNode | undefined;
vuWorklet: AudioWorkletNode | undefined;
private starting: Promise<void> | null = null;
constructor(public sampleRate = 16000) {
super();
}
async start() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error("Could not request user media");
}
this.starting = new Promise(async (resolve, reject) => {
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.audioContext = await audioContext({ sampleRate: this.sampleRate });
this.source = this.audioContext.createMediaStreamSource(this.stream);
const workletName = "audio-recorder-worklet";
const src = createWorketFromSrc(workletName, AudioRecordingWorklet);
if (this.audioContext.audioWorklet) {
console.log("AudioWorklet verfügbar, verwende AudioWorkletNode");
await this.audioContext.audioWorklet.addModule(src);
this.recordingWorklet = new AudioWorkletNode(this.audioContext, workletName);
this.recordingWorklet.port.onmessage = async (ev: MessageEvent) => {
const arrayBuffer = ev.data.data.int16arrayBuffer;
if (arrayBuffer) {
const arrayBufferString = arrayBufferToBase64(arrayBuffer);
this.emit("data", arrayBufferString);
}
};
this.source.connect(this.recordingWorklet);
} else {
console.warn("AudioWorklet nicht verfügbar, fallback auf ScriptProcessorNode");
alert("AudioWorklet nicht verfügbar – bitte verwende Chrome für Audio-Funktionalität.");
const bufferSize = 4096;
const spNode = this.audioContext.createScriptProcessor(bufferSize, 1, 1);
spNode.onaudioprocess = (evt: any) => {
const input = evt.inputBuffer.getChannelData(0);
const int16array = new Int16Array(input.length);
for (let i = 0; i < input.length; i++) {
int16array[i] = input[i] * 0x7fff;
}
const arrayBufferString = arrayBufferToBase64(int16array.buffer);
this.emit("data", arrayBufferString);
};
this.source.connect(spNode);
spNode.connect(this.audioContext.destination);
}
// vu meter worklet
const vuWorkletName = "vu-meter";
await this.audioContext.audioWorklet.addModule(
createWorketFromSrc(vuWorkletName, VolMeterWorket),
);
this.vuWorklet = new AudioWorkletNode(this.audioContext, vuWorkletName);
this.vuWorklet.port.onmessage = (ev: MessageEvent) => {
this.emit("volume", ev.data.volume);
};
this.source.connect(this.vuWorklet);
this.recording = true;
resolve();
this.starting = null;
});
}
stop() {
// its plausible that stop would be called before start completes
// such as if the websocket immediately hangs up
const handleStop = () => {
this.source?.disconnect();
this.stream?.getTracks().forEach((track) => track.stop());
this.stream = undefined;
this.recordingWorklet = undefined;
this.vuWorklet = undefined;
};
if (this.starting) {
this.starting.then(handleStop);
return;
}
handleStop();
}
}