File size: 3,469 Bytes
36348bf
7f2a14a
0362c08
 
 
 
 
7f2a14a
 
0362c08
 
 
36348bf
0362c08
 
36348bf
0362c08
 
7f2a14a
0362c08
 
 
 
 
 
36348bf
0362c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36348bf
 
7f2a14a
0362c08
 
7f2a14a
0362c08
 
7f2a14a
0362c08
 
 
 
 
 
 
 
 
 
 
36348bf
7f2a14a
 
0362c08
 
 
 
7f2a14a
0362c08
 
 
36348bf
0362c08
 
36348bf
 
 
0362c08
36348bf
 
 
 
0362c08
 
 
36348bf
 
 
 
7f2a14a
36348bf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 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);
  }
}