Ezmary commited on
Commit
0362c08
·
verified ·
1 Parent(s): 89adfc6

Update src/lib/audio-recorder.ts

Browse files
Files changed (1) hide show
  1. 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 'eventemitter3';
4
- import { audioContext } from './utils';
5
- import VolMeterWorket from './worklets/vol-meter';
 
 
6
 
7
  export class AudioRecorder extends EventEmitter {
8
- public recording = false;
9
- private mediaStream: MediaStream | null = null;
10
- private audioContext: AudioContext | null = null;
11
  private processor: ScriptProcessorNode | null = null;
12
- private source: MediaStreamAudioSourceNode | null = null;
13
- private volMeter: AudioWorkletNode | null = null;
14
 
15
- constructor() {
16
- super();
17
- }
18
 
19
- private async init() {
20
- if (!this.audioContext) {
21
- this.audioContext = await audioContext({ id: 'audio-in' });
22
- }
23
- if (!this.mediaStream) {
24
- try {
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
- public async start() {
43
- if (this.recording) {
44
- return;
45
- }
46
-
47
- await this.init();
48
 
49
- if (!this.audioContext || !this.mediaStream) {
50
- console.error("AudioContext or MediaStream not available.");
51
- return;
52
- }
53
 
54
- this.recording = true;
55
- this.source = this.audioContext.createMediaStreamSource(this.mediaStream);
56
- this.processor = this.audioContext.createScriptProcessor(1024, 1, 1);
57
-
58
- this.processor.onaudioprocess = (e) => {
59
- if (!this.recording) return;
60
- const inputData = e.inputBuffer.getChannelData(0);
61
- const pcm16 = this.floatTo16BitPCM(inputData);
62
- const base64 = this.pcm16ToBase64(pcm16);
63
- this.emit('data', base64);
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
- public stop() {
89
- if (!this.recording) {
90
- return;
 
91
  }
92
- this.recording = false;
93
- this.emit('stop');
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 * 0x7FFF;
110
  }
111
  return output;
112
  }
113
 
114
- private pcm16ToBase64(pcm16: Int16Array): string {
115
- const buffer = pcm16.buffer;
116
- const bytes = new Uint8Array(buffer);
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
  }