SotiproAlpha2 / src /lib /audio-recorder.ts
Ezmary's picture
Update src/lib/audio-recorder.ts
29d8bf1 verified
// src/lib/audio-recorder.ts
import { audioContext } from "./utils";
import AudioRecordingWorklet from "./worklets/audio-processing";
import SafariAudioRecordingWorklet from "./worklets/safari-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);
}
async function createSafariAudioContext(sampleRate: number): Promise<AudioContext> {
const AudioContextClass = (window as any).webkitAudioContext || window.AudioContext;
const ctx = new AudioContextClass({ sampleRate, latencyHint: 'interactive' });
if (ctx.state === 'suspended') {
await ctx.resume();
}
return ctx;
}
export class AudioRecorder extends EventEmitter {
stream?: MediaStream;
audioContext?: AudioContext;
source?: MediaStreamAudioSourceNode;
recording: boolean = false;
recordingWorklet?: AudioWorkletNode;
vuWorklet?: AudioWorkletNode;
private starting: Promise<void> | null = null;
isSafari: boolean;
constructor(public sampleRate = 16000) {
super();
this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}
async start() {
if (this.recording || this.starting) return;
this.starting = new Promise(async (resolve, reject) => {
try {
const constraints = {
audio: this.isSafari
? { sampleRate: this.sampleRate, channelCount: 1 }
: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, sampleRate: this.sampleRate }
};
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
this.audioContext = this.isSafari
? await createSafariAudioContext(this.sampleRate)
: await audioContext({ sampleRate: this.sampleRate });
this.source = this.audioContext.createMediaStreamSource(this.stream);
const recordingWorkletSrc = this.isSafari ? SafariAudioRecordingWorklet : AudioRecordingWorklet;
await this.audioContext.audioWorklet.addModule(createWorketFromSrc("recorder", recordingWorkletSrc));
await this.audioContext.audioWorklet.addModule(createWorketFromSrc("vumeter", VolMeterWorket));
this.recordingWorklet = new AudioWorkletNode(this.audioContext, "recorder", { processorOptions: { sampleRate: this.sampleRate } });
this.vuWorklet = new AudioWorkletNode(this.audioContext, "vumeter");
let lastVolume = 0;
this.recordingWorklet.port.onmessage = (ev: MessageEvent) => {
const arrayBuffer = ev.data.data?.int16arrayBuffer;
if (arrayBuffer) {
const base64 = arrayBufferToBase64(arrayBuffer);
// ارسال همزمان دیتا و آخرین حجم صدای دریافتی
this.emit("data", base64, lastVolume);
}
};
this.vuWorklet.port.onmessage = (ev: MessageEvent) => {
if (typeof ev.data.volume === 'number') {
lastVolume = ev.data.volume;
}
};
this.source.connect(this.recordingWorklet);
this.source.connect(this.vuWorklet);
this.recording = true;
resolve();
} catch (error) {
console.error('Failed to start recording:', error);
this.stop();
reject(error);
} finally {
this.starting = null;
}
});
return this.starting;
}
stop() {
if (!this.recording && !this.starting) return;
const handleStop = () => {
this.recording = false;
this.source?.disconnect();
this.stream?.getTracks().forEach(track => track.stop());
if (this.audioContext?.state === 'running') {
this.audioContext.close();
}
this.stream = undefined;
this.audioContext = undefined;
this.source = undefined;
this.recordingWorklet = undefined;
this.vuWorklet = undefined;
this.emit("stop");
};
if (this.starting) {
this.starting.then(handleStop).catch(handleStop);
} else {
handleStop();
}
}
}