[React] Web Audio API로 실시간 음성 녹음 볼륨 변화 추적하기

김지훈·2025년 9월 11일
post-thumbnail

음성 녹음과 실시간으로 음량 변화를 시각화하는 기능을 구현하면서 학습한 Web Audio API 관련 내용에 대해 정리해 보았다.


🎵 Web Audio API 기본 개념

1) AudioContext

AudioContext는 Web Audio API의 핵심 엔진으로, 모든 오디오 노드가 생성되고 연결되는 컨테이너 역할을 한다. 오디오 처리는 이 컨텍스트에서 시작되며 전체 오디오 그래프를 구성하고 제어하는 작업 공간으로 활용된다.

즉, 오디오 데이터를 생성, 변형, 분석, 출력할 수 있는 시간 기반 처리 환경을 제공하고, 내부적으로는 노드 간의 신호 흐름을 관리한다.


2) AudioNode

Web Audio API는 오디오 신호를 노드 기반 구조로 처리한다. AudioNode는 그래프의 기본 단위로, 각 노드는 입력·출력·필터링·분석 등 특정한 오디오 처리 기능을 수행한다.

여러 AudioNode를 연결해 입력부터 출력까지 파이프라인을 구성하며, 이 과정을 통해 오디오 데이터가 흐르고 변형된다. 즉, AudioNode는 오디오 신호를 처리하고 전달하는 모듈 단위이다.

노드역할주요 용도
MediaStreamAudioSourceNodeMediaStream을 입력으로 연결마이크 입력, 라이브 오디오 처리
AnalyserNode주파수/시간 도메인 분석오디오 시각화, 음성 감지
GainNode볼륨 조절페이드 인/아웃, 음소거, 믹싱
BiquadFilterNode주파수 필터링노이즈 제거, 이퀄라이징
AudioBufferSourceNode오디오 버퍼 재생샘플 재생, 효과음
OscillatorNode파형 생성테스트 톤, 신디사이저
PannerNode공간적인 오디오 위치 지정3D 사운드, 스테레오 패닝
ConvolverNode리버브 효과 적용공간감, 에코 구현
DynamicsCompressorNode오디오 압축마스터링, 볼륨 억제
DelayNode오디오 지연에코, 시간 기반 효과

🎤 useRecording 커스텀 훅 구현

1) 상태 관리

const [status, setStatus] = useState<RecordingStatus>('idle');
const [recordingTime, setRecordingTime] = useState(0);
const [volume, setVolume] = useState(0);
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
const [recordingError, setRecordingError] = useState(false);
  • status: 녹음 단계 ('idle' → 'recording' → 'converting')
  • recordingTime: 타이머 등 UI 피드백 제공
  • volume: 실시간 볼륨 정보 (비주얼라이저 렌더링에 활용)
  • audioBlob: 녹음 완료 후 재생 및 업로드용 데이터
  • recordingError: 오류 발생 시 상태 표시

2) 리소스 관리

const intervalRef = useRef<NodeJS.Timeout | null>(null);
const volumeIntervalRef = useRef<NodeJS.Timeout | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const audioChunksRef = useRef<Blob[]>([]);
const audioRef = useRef<HTMLAudioElement | null>(null);
  • intervalRef: 녹음 시간 타이머 ID
  • volumeIntervalRef: 실시간 볼륨 측정 주기 ID
  • audioContextRefAudioContext 인스턴스
  • analyserRef: 오디오 파형 분석용 AnalyserNode
  • streamRefgetUserMedia()로 획득한 마이크 입력 스트림
  • mediaRecorderRef: 브라우저 내장 MediaRecorder
  • audioChunksRef: 녹음된 오디오 조각 데이터
  • audioRef<audio> 요소 제어용 참조

3) 녹음 시작 로직

const startRecording = async () => {
  try {
    // 1. 마이크 권한 요청
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    streamRef.current = stream;

    // 2. Web Audio API 설정
    const audioContext = new AudioContext();
    const analyser = audioContext.createAnalyser();
    const microphone = audioContext.createMediaStreamSource(stream);

    analyser.fftSize = 256;
    microphone.connect(analyser);

    audioContextRef.current = audioContext;
    analyserRef.current = analyser;

    // 3. MediaRecorder 설정
    const mediaRecorder = new MediaRecorder(stream);
    mediaRecorderRef.current = mediaRecorder;
    audioChunksRef.current = [];

    mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0) audioChunksRef.current.push(event.data);
    };

    mediaRecorder.onstop = async () => {
      const webmBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
      setAudioBlob(webmBlob);
      setStatus('idle');
    };

    // 4. 녹음 시작 및 타이머
    mediaRecorder.start();
    setStatus('recording');
    setRecordingTime(0);

    // 시간 추적
    intervalRef.current = setInterval(() => {
      setRecordingTime((prev) => {
        if (prev >= 900) { // 15분 제한
          stopRecording();
          return prev;
        }
        return prev + 1;
      });
    }, 1000);

    // 볼륨 업데이트
    volumeIntervalRef.current = setInterval(updateVolume, 30);
  } catch {
    setRecordingError(true);
  }
};

작동 순서

  1. getUserMedia로 마이크 스트림 획득
  2. Web Audio API로 분석 파이프라인 구성
  3. MediaRecorder로 녹음 처리
  4. 타이머로 녹음 시간·볼륨 업데이트

fftSize는 FFT 연산에 사용되는 샘플 크기이며, 반드시 2^5~2^15 범위의 2의 제곱수여야 한다. 기본값은 2048이며, 값이 커질수록 주파수 해상도는 높아지지만 반응 속도가 느려질 수 있다.

이번 프로젝트에서는 볼륨만 시각화하면 되므로, fftSize = 256으로 설정해 성능을 최적화했다.


4) 실시간 볼륨 측정

const calculateAverageVolume = (dataArray: Uint8Array, bufferLength: number) => {
  let total = 0;
  for (let i = 0; i < bufferLength; i++) total += dataArray[i];
  return total / bufferLength;
};

const updateVolume = () => {
  if (analyserRef.current) {
    const bufferLength = analyserRef.current.frequencyBinCount;
    const dataArray = new Uint8Array(bufferLength);
    analyserRef.current.getByteFrequencyData(dataArray);
    const vol = calculateAverageVolume(dataArray, bufferLength);
    setVolume(Math.floor((vol / 256) * 100));
  }
};

작동 순서

  1. frequencyBinCount: FFT 결과 배열 크기 (fftSize 절반)
  2. getByteFrequencyData(): 주파수 데이터를 Uint8Array로 획득
  3. 평균값을 계산해 전체 볼륨 추정
  4. 0~256 범위를 0~100 범위로 정규화

5) 리소스 정리

const stopRecording = () => {
  setStatus('converting');
  setVolume(0);

  // 타이머 정리
  if (intervalRef.current) {
    clearInterval(intervalRef.current);
  }
  if (volumeIntervalRef.current) {
    clearInterval(volumeIntervalRef.current);
  }
    
  // MediaRecorder 정지
  if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
    mediaRecorderRef.current.stop();
  }

  // 스트림 정리
  if (streamRef.current) {
    streamRef.current.getTracks().forEach((track) => track.stop());
  }

  // AudioContext 정리
  if (audioContextRef.current) audioContextRef.current.close();
};

Web Audio API 리소스는 해제하지 않으면 백그라운드에서 계속 실행될 수 있다. 따라서 사용 후에는 반드시 정리하여 메모리 누수를 방지한다.

0개의 댓글