Spaces:
Running
Running
// src/lib/audio-recorder.ts | |
import EventEmitter from "eventemitter3"; | |
import { audioContext } from "./utils"; | |
import VolMeterWorket from "./worklets/vol-meter"; | |
const CHUNK_SIZE = 2048; | |
export class AudioRecorder extends EventEmitter { | |
recording = false; | |
private audioCtx: AudioContext | null = null; | |
private microphone: MediaStreamAudioSourceNode | null = null; | |
private processor: ScriptProcessorNode | null = null; | |
private worklet: AudioWorkletNode | null = null; | |
private stream: MediaStream | null = null; | |
async start(): Promise<void> { | |
if (this.recording) return; | |
try { | |
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
this.audioCtx = await audioContext({ id: "audio-in" }); | |
if (this.audioCtx.state === "suspended") { | |
await this.audioCtx.resume(); | |
} | |
await this.audioCtx.audioWorklet.addModule(VolMeterWorket); | |
this.microphone = this.audioCtx.createMediaStreamSource(this.stream); | |
this.processor = this.audioCtx.createScriptProcessor(CHUNK_SIZE, 1, 1); | |
this.worklet = new AudioWorkletNode(this.audioCtx, "vumeter-in"); | |
this.worklet.port.onmessage = (ev) => { | |
// رویداد جدید برای ارسال ولوم | |
this.emit("volume", ev.data.volume); | |
}; | |
this.processor.onaudioprocess = (e: AudioProcessingEvent) => { | |
if (!this.recording) return; | |
const inputData = e.inputBuffer.getChannelData(0); | |
const pcm16Data = this.convertToPCM16(inputData); | |
const base64 = this.toBase64(pcm16Data); | |
// محاسبه ولوم در اینجا و ارسال آن همراه با دادهها | |
const volume = this.getVolume(inputData); | |
this.emit("data", base64, volume); | |
}; | |
this.microphone.connect(this.processor); | |
this.microphone.connect(this.worklet); // اتصال worklet برای گرفتن ولوم | |
this.processor.connect(this.audioCtx.destination); | |
this.recording = true; | |
this.emit("start"); | |
} catch (err) { | |
console.error("Error starting audio recording:", err); | |
this.emit("error", err); | |
} | |
} | |
stop(): void { | |
if (!this.recording) return; | |
this.recording = false; | |
this.emit("stop"); | |
if (this.stream) { | |
this.stream.getTracks().forEach((track) => track.stop()); | |
} | |
if (this.microphone) { | |
this.microphone.disconnect(); | |
} | |
if (this.processor) { | |
this.processor.disconnect(); | |
} | |
if (this.worklet) { | |
this.worklet.disconnect(); | |
} | |
} | |
private getVolume(data: Float32Array): number { | |
let sum = 0; | |
for (let i = 0; i < data.length; i++) { | |
sum += data[i] * data[i]; | |
} | |
const rms = Math.sqrt(sum / data.length); | |
// نرمالسازی ولوم به یک مقدار بین 0 و 1 | |
return Math.min(1, rms * 5); // ضریب 5 برای محسوستر کردن | |
} | |
private convertToPCM16(input: Float32Array): Int16Array { | |
const output = new Int16Array(input.length); | |
for (let i = 0; i < input.length; i++) { | |
const s = Math.max(-1, Math.min(1, input[i])); | |
output[i] = s < 0 ? s * 0x8000 : s * 0x7fff; | |
} | |
return output; | |
} | |
private toBase64(pcm16Data: Int16Array): string { | |
const bytes = new Uint8Array(pcm16Data.buffer); | |
let binary = ""; | |
for (let i = 0; i < bytes.byteLength; i++) { | |
binary += String.fromCharCode(bytes[i]); | |
} | |
return btoa(binary); | |
} | |
} |