Spaces:
Running
Running
Update src/lib/audio-recorder.ts
Browse files- src/lib/audio-recorder.ts +77 -92
src/lib/audio-recorder.ts
CHANGED
@@ -1,120 +1,105 @@
|
|
1 |
// src/lib/audio-recorder.ts
|
2 |
|
3 |
-
import EventEmitter from
|
4 |
-
import { audioContext } from
|
5 |
-
import VolMeterWorket from
|
|
|
|
|
6 |
|
7 |
export class AudioRecorder extends EventEmitter {
|
8 |
-
|
9 |
-
private
|
10 |
-
private
|
11 |
private processor: ScriptProcessorNode | null = null;
|
12 |
-
private
|
13 |
-
private
|
14 |
|
15 |
-
|
16 |
-
|
17 |
-
}
|
18 |
|
19 |
-
|
20 |
-
|
21 |
-
this.
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
26 |
-
audio: {
|
27 |
-
sampleRate: 16000,
|
28 |
-
channelCount: 1,
|
29 |
-
echoCancellation: true,
|
30 |
-
noiseSuppression: true,
|
31 |
-
},
|
32 |
-
video: false,
|
33 |
-
});
|
34 |
-
} catch (err) {
|
35 |
-
console.error('Error getting media stream:', err);
|
36 |
-
this.emit('error', err);
|
37 |
-
return;
|
38 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
}
|
40 |
}
|
41 |
|
42 |
-
|
43 |
-
if (this.recording)
|
44 |
-
return;
|
45 |
-
}
|
46 |
-
|
47 |
-
await this.init();
|
48 |
|
49 |
-
|
50 |
-
|
51 |
-
return;
|
52 |
-
}
|
53 |
|
54 |
-
this.
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
// --- بخش کلیدی: اضافه کردن Volume Meter ---
|
67 |
-
try {
|
68 |
-
await this.audioContext.audioWorklet.addModule(VolMeterWorket);
|
69 |
-
this.volMeter = new AudioWorkletNode(this.audioContext, 'vumeter');
|
70 |
-
this.volMeter.port.onmessage = (event) => {
|
71 |
-
if(event.data.volume) {
|
72 |
-
// ارسال رویداد جدید به همراه حجم صدا
|
73 |
-
this.emit('volume', event.data.volume);
|
74 |
-
}
|
75 |
-
};
|
76 |
-
this.source.connect(this.volMeter);
|
77 |
-
} catch(err) {
|
78 |
-
console.error("Error adding AudioWorklet module", err);
|
79 |
}
|
80 |
-
// ------------------------------------------
|
81 |
-
|
82 |
-
this.source.connect(this.processor);
|
83 |
-
this.processor.connect(this.audioContext.destination);
|
84 |
-
|
85 |
-
this.emit('start');
|
86 |
}
|
87 |
|
88 |
-
|
89 |
-
|
90 |
-
|
|
|
91 |
}
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
this.source?.disconnect();
|
96 |
-
this.processor?.disconnect();
|
97 |
-
this.volMeter?.disconnect();
|
98 |
-
|
99 |
-
this.source = null;
|
100 |
-
this.processor = null;
|
101 |
-
this.volMeter = null;
|
102 |
}
|
103 |
-
|
104 |
-
|
105 |
-
private floatTo16BitPCM(input: Float32Array): Int16Array {
|
106 |
const output = new Int16Array(input.length);
|
107 |
for (let i = 0; i < input.length; i++) {
|
108 |
const s = Math.max(-1, Math.min(1, input[i]));
|
109 |
-
output[i] = s < 0 ? s * 0x8000 : s *
|
110 |
}
|
111 |
return output;
|
112 |
}
|
113 |
|
114 |
-
private
|
115 |
-
const
|
116 |
-
|
117 |
-
let binary = '';
|
118 |
for (let i = 0; i < bytes.byteLength; i++) {
|
119 |
binary += String.fromCharCode(bytes[i]);
|
120 |
}
|
|
|
1 |
// src/lib/audio-recorder.ts
|
2 |
|
3 |
+
import EventEmitter from "eventemitter3";
|
4 |
+
import { audioContext } from "./utils";
|
5 |
+
import VolMeterWorket from "./worklets/vol-meter";
|
6 |
+
|
7 |
+
const CHUNK_SIZE = 2048;
|
8 |
|
9 |
export class AudioRecorder extends EventEmitter {
|
10 |
+
recording = false;
|
11 |
+
private audioCtx: AudioContext | null = null;
|
12 |
+
private microphone: MediaStreamAudioSourceNode | null = null;
|
13 |
private processor: ScriptProcessorNode | null = null;
|
14 |
+
private worklet: AudioWorkletNode | null = null;
|
15 |
+
private stream: MediaStream | null = null;
|
16 |
|
17 |
+
async start(): Promise<void> {
|
18 |
+
if (this.recording) return;
|
|
|
19 |
|
20 |
+
try {
|
21 |
+
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
22 |
+
this.audioCtx = await audioContext({ id: "audio-in" });
|
23 |
+
|
24 |
+
if (this.audioCtx.state === "suspended") {
|
25 |
+
await this.audioCtx.resume();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
}
|
27 |
+
|
28 |
+
await this.audioCtx.audioWorklet.addModule(VolMeterWorket);
|
29 |
+
|
30 |
+
this.microphone = this.audioCtx.createMediaStreamSource(this.stream);
|
31 |
+
this.processor = this.audioCtx.createScriptProcessor(CHUNK_SIZE, 1, 1);
|
32 |
+
|
33 |
+
this.worklet = new AudioWorkletNode(this.audioCtx, "vumeter-in");
|
34 |
+
this.worklet.port.onmessage = (ev) => {
|
35 |
+
// رویداد جدید برای ارسال ولوم
|
36 |
+
this.emit("volume", ev.data.volume);
|
37 |
+
};
|
38 |
+
|
39 |
+
this.processor.onaudioprocess = (e: AudioProcessingEvent) => {
|
40 |
+
if (!this.recording) return;
|
41 |
+
const inputData = e.inputBuffer.getChannelData(0);
|
42 |
+
const pcm16Data = this.convertToPCM16(inputData);
|
43 |
+
const base64 = this.toBase64(pcm16Data);
|
44 |
+
// محاسبه ولوم در اینجا و ارسال آن همراه با دادهها
|
45 |
+
const volume = this.getVolume(inputData);
|
46 |
+
this.emit("data", base64, volume);
|
47 |
+
};
|
48 |
+
|
49 |
+
this.microphone.connect(this.processor);
|
50 |
+
this.microphone.connect(this.worklet); // اتصال worklet برای گرفتن ولوم
|
51 |
+
this.processor.connect(this.audioCtx.destination);
|
52 |
+
|
53 |
+
this.recording = true;
|
54 |
+
this.emit("start");
|
55 |
+
} catch (err) {
|
56 |
+
console.error("Error starting audio recording:", err);
|
57 |
+
this.emit("error", err);
|
58 |
}
|
59 |
}
|
60 |
|
61 |
+
stop(): void {
|
62 |
+
if (!this.recording) return;
|
|
|
|
|
|
|
|
|
63 |
|
64 |
+
this.recording = false;
|
65 |
+
this.emit("stop");
|
|
|
|
|
66 |
|
67 |
+
if (this.stream) {
|
68 |
+
this.stream.getTracks().forEach((track) => track.stop());
|
69 |
+
}
|
70 |
+
if (this.microphone) {
|
71 |
+
this.microphone.disconnect();
|
72 |
+
}
|
73 |
+
if (this.processor) {
|
74 |
+
this.processor.disconnect();
|
75 |
+
}
|
76 |
+
if (this.worklet) {
|
77 |
+
this.worklet.disconnect();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
}
|
80 |
|
81 |
+
private getVolume(data: Float32Array): number {
|
82 |
+
let sum = 0;
|
83 |
+
for (let i = 0; i < data.length; i++) {
|
84 |
+
sum += data[i] * data[i];
|
85 |
}
|
86 |
+
const rms = Math.sqrt(sum / data.length);
|
87 |
+
// نرمالسازی ولوم به یک مقدار بین 0 و 1
|
88 |
+
return Math.min(1, rms * 5); // ضریب 5 برای محسوستر کردن
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
}
|
90 |
+
|
91 |
+
private convertToPCM16(input: Float32Array): Int16Array {
|
|
|
92 |
const output = new Int16Array(input.length);
|
93 |
for (let i = 0; i < input.length; i++) {
|
94 |
const s = Math.max(-1, Math.min(1, input[i]));
|
95 |
+
output[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
96 |
}
|
97 |
return output;
|
98 |
}
|
99 |
|
100 |
+
private toBase64(pcm16Data: Int16Array): string {
|
101 |
+
const bytes = new Uint8Array(pcm16Data.buffer);
|
102 |
+
let binary = "";
|
|
|
103 |
for (let i = 0; i < bytes.byteLength; i++) {
|
104 |
binary += String.fromCharCode(bytes[i]);
|
105 |
}
|