File size: 2,723 Bytes
ec17af3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useEffect, useRef, useState } from "react";

type AudioState = {
  rms: number;
  pitch: number;
  pauseMs: number;
  volumeLabel: "quiet" | "good" | "loud";
};

const computeRms = (data: Float32Array) => {
  let sum = 0;
  for (let i = 0; i < data.length; i += 1) {
    sum += data[i] * data[i];
  }
  return Math.sqrt(sum / data.length);
};

const autoCorrelate = (buffer: Float32Array, sampleRate: number) => {
  let bestOffset = -1;
  let bestCorrelation = 0;
  const size = buffer.length;
  for (let offset = 8; offset < 1000; offset += 1) {
    let correlation = 0;
    for (let i = 0; i < size - offset; i += 1) {
      correlation += buffer[i] * buffer[i + offset];
    }
    if (correlation > bestCorrelation) {
      bestCorrelation = correlation;
      bestOffset = offset;
    }
  }
  if (bestOffset === -1) return 0;
  return sampleRate / bestOffset;
};

export const useAudioAnalyzer = (stream: MediaStream | null, enabled: boolean) => {
  const [state, setState] = useState<AudioState>({ rms: 0, pitch: 0, pauseMs: 0, volumeLabel: "good" });
  const lastSoundRef = useRef<number>(performance.now());
  const lastUpdateRef = useRef<number>(0);

  useEffect(() => {
    if (!stream || !enabled) return;
    const AudioContextCtor =
      typeof window !== "undefined"
        ? window.AudioContext || (window as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext
        : undefined;
    if (!AudioContextCtor) return;
    const audioContext = new AudioContextCtor();
    const analyser = audioContext.createAnalyser();
    analyser.fftSize = 2048;
    const source = audioContext.createMediaStreamSource(stream);
    source.connect(analyser);

    const data = new Float32Array(analyser.fftSize);
    const minUpdateMs = 100;

    let rafId = 0;
    const tick = () => {
      analyser.getFloatTimeDomainData(data);
      const rms = computeRms(data);
      const normalized = Math.min(100, Math.max(0, rms * 200));
      const now = performance.now();
      let pauseMs = 0;
      if (normalized < 6) {
        pauseMs = now - lastSoundRef.current;
      } else {
        lastSoundRef.current = now;
      }
      const pitch = autoCorrelate(data, audioContext.sampleRate);
      const volumeLabel = normalized < 15 ? "quiet" : normalized > 70 ? "loud" : "good";
      if (now - lastUpdateRef.current >= minUpdateMs) {
        lastUpdateRef.current = now;
        setState({ rms: normalized, pitch, pauseMs, volumeLabel });
      }
      rafId = requestAnimationFrame(tick);
    };
    tick();

    return () => {
      cancelAnimationFrame(rafId);
      analyser.disconnect();
      source.disconnect();
      audioContext.close();
    };
  }, [stream, enabled]);

  return state;
};