Spaces:
Running
Running
// src/lib/audio-recorder.ts | |
import EventEmitter from 'eventemitter3'; | |
import { audioContext } from './utils'; | |
import VolMeterWorket from './worklets/vol-meter'; | |
export class AudioRecorder extends EventEmitter { | |
public recording = false; | |
private mediaStream: MediaStream | null = null; | |
private audioContext: AudioContext | null = null; | |
private processor: ScriptProcessorNode | null = null; | |
private source: MediaStreamAudioSourceNode | null = null; | |
private volMeter: AudioWorkletNode | null = null; | |
constructor() { | |
super(); | |
} | |
private async init() { | |
if (!this.audioContext) { | |
this.audioContext = await audioContext({ id: 'audio-in' }); | |
} | |
if (!this.mediaStream) { | |
try { | |
this.mediaStream = await navigator.mediaDevices.getUserMedia({ | |
audio: { | |
sampleRate: 16000, | |
channelCount: 1, | |
echoCancellation: true, | |
noiseSuppression: true, | |
}, | |
video: false, | |
}); | |
} catch (err) { | |
console.error('Error getting media stream:', err); | |
this.emit('error', err); | |
return; | |
} | |
} | |
} | |
public async start() { | |
if (this.recording) { | |
return; | |
} | |
await this.init(); | |
if (!this.audioContext || !this.mediaStream) { | |
console.error("AudioContext or MediaStream not available."); | |
return; | |
} | |
this.recording = true; | |
this.source = this.audioContext.createMediaStreamSource(this.mediaStream); | |
this.processor = this.audioContext.createScriptProcessor(1024, 1, 1); | |
this.processor.onaudioprocess = (e) => { | |
if (!this.recording) return; | |
const inputData = e.inputBuffer.getChannelData(0); | |
const pcm16 = this.floatTo16BitPCM(inputData); | |
const base64 = this.pcm16ToBase64(pcm16); | |
this.emit('data', base64); | |
}; | |
// --- بخش کلیدی: اضافه کردن Volume Meter --- | |
try { | |
await this.audioContext.audioWorklet.addModule(VolMeterWorket); | |
this.volMeter = new AudioWorkletNode(this.audioContext, 'vumeter'); | |
this.volMeter.port.onmessage = (event) => { | |
if(event.data.volume) { | |
// ارسال رویداد جدید به همراه حجم صدا | |
this.emit('volume', event.data.volume); | |
} | |
}; | |
this.source.connect(this.volMeter); | |
} catch(err) { | |
console.error("Error adding AudioWorklet module", err); | |
} | |
// ------------------------------------------ | |
this.source.connect(this.processor); | |
this.processor.connect(this.audioContext.destination); | |
this.emit('start'); | |
} | |
public stop() { | |
if (!this.recording) { | |
return; | |
} | |
this.recording = false; | |
this.emit('stop'); | |
this.source?.disconnect(); | |
this.processor?.disconnect(); | |
this.volMeter?.disconnect(); | |
this.source = null; | |
this.processor = null; | |
this.volMeter = null; | |
} | |
// توابع کمکی برای تبدیل فرمت صدا | |
private floatTo16BitPCM(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 pcm16ToBase64(pcm16: Int16Array): string { | |
const buffer = pcm16.buffer; | |
const bytes = new Uint8Array(buffer); | |
let binary = ''; | |
for (let i = 0; i < bytes.byteLength; i++) { | |
binary += String.fromCharCode(bytes[i]); | |
} | |
return btoa(binary); | |
} | |
} |