오디오 시각화 애니메이션 컴포넌트 만들기 (Audio Visualization)

2023. 8. 4. 18:08·프론트엔드/React

오디오 시각화란

오디오 데이터를 시각화 하는 것

마이크를 사용했을 때 생기는 오디오 소스로부터 주파수나, 파형, 기타 데이터들을 활용해서 데이터 시각화를 하는 것이다.

 

오디오 시각화가 필요했던 이유

유저가 마이크를 사용할 때 자신의 목소리가 잘 녹음되고 있는지 인지시켜야함.

현 회사에서는 영어 교육 서비스를 제공하고 있는데, 유저가 스피킹 시험을 볼 때 마이크를 사용한다. 유저의 목소리가 작으면 녹음 퀄리티 저하로 이어지게 되는데, 문제는 마이크 녹음할 때 유저는 자신의 목소리가 작은지 인지하지 못한다. 그래서 마이크를 사용할 때 목소리 크기를 시각적으로 확인하는 장치가 필요했다. 즉 오디오 스트림 데이터를 시각화하여 유저는 목소리의 크기를 실시간으로 인지하면서 녹음할 수 있다. 이로써 녹음 품질을 개선시킬 수 있을 뿐더러 유저가 재시험을 봐야하는 번거로움을 없앨 수 있었다.

 

컴포넌트 작성 방법

여기서는 MDN 문서를 기반으로 기초적인 리액트 컴포넌트를 작성합니다.

기초 코드

const AudioVisualizer = () => {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const requestIdRef = useRef<number | null>(null);
  const streamRef = useRef<MediaStream | null>(null);

  const start = () => {};
  const cancel = () => {};

  useEffect(() => {
    start();

    return () => {
      cancel();
    };
  }, []);
  
  
  return (
    <div ref={containerRef} style={{ width: '100%' }}>
      <canvas ref={canvasRef} height={120} />
    </div>
  );
}

 

1. 유저 미디어 연결

const start = async () => {
  try {
    // 사용자에게 미디어 사용 권한을 요청하고 오디오 스트림을 받아옵니다.
    streamRef.current = await navigator.mediaDevices.getUserMedia({
      audio: true
    });
  } catch (error) {
    console.error(error);
  }
}

 

2. 오디오 스트림을 생성하고 오디오 분석기 연결

const start = async () => {
  // 오디오 분석기를 생성하고 설정합니다.
  const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
  const analyser = audioCtx.createAnalyser();
  analyser.minDecibels = -90;
  analyser.maxDecibels = -10;
  analyser.smoothingTimeConstant = 0.85;
  analyser.fftSize = 512;
  
  try {
    streamRef.current = await navigator.mediaDevices.getUserMedia({ audio: true });
    
	// 입력받은 오디오 스트림을 오디오 객체로 변환합니다.
    const audioSource = audioCtx.createMediaStreamSource(streamRef.current);
    audioSource.connect(analyser); // 오디오 소스를 분석기에 연결합니다.
  } catch (error) {
    console.error(error);
  }
}
  • minDecibels, maxDecibels: -100dB ~ 0dB 사이에 값을 설정한다. MDN 예제에서 설정된 -90dB, -10dB 값들은 대부분의 오디오 장비들이 유효한 신호로 간주하고 처리하기 시작하는 범위이다.
  • smoothingTimeConstant:  0~1 사이에 값으로 설정한다. 낮을 수록 값이 빠르게 변화하고 높을 수록 매끄럽게 값이 변함. 이전 버퍼와 현재 버퍼 사이의 값을 얼마나 평균화할 건지 설정하는 값.
  • fftSize: 2^5 ~ 2^15 사이에 값으로 설정한다. 값이 높을 수록 세밀한 주파수를 얻을 수 있다. 하지만 값이 높을 수록 주파수의 데이터가 오래된 것일 수 있음.

 

3. 오디오 페인트

const start = async () => {
    ...
    // 캔버스 그릴 준비
    const canvasCtx = canvasRef?.current?.getContext('2d');
    const containerWidth = containerRef?.current?.clientWidth.toString() || '0';
    canvasRef.current?.setAttribute('width', containerWidth);

     // 오디오 시각화
    const visualizeAudio = () => {
      const width = canvasRef.current?.width || 0;
      const height = canvasRef.current?.height || 0;

      // 얼마나 많은 주파수 데이터를 수집할지 셋팅한다.
      // frequencyBinCount는 위에서 설정한 fftSize 반의 길이다.
      const bufferLength = analyser.frequencyBinCount;
      // 주파수 데이터를 저장하기위한 객체 생성
      const dataArray = new Float32Array(bufferLength);

      // 이전에 그려진 캔버스를 지운다.
      canvasCtx?.clearRect(0, 0, width, height);

      const draw = () => {
        if (!canvasCtx) {
          return;
        }

        requestIdRef.current = requestAnimationFrame(draw);

        // 주파수 데이터를 가져옵니다.
        analyser.getFloatFrequencyData(dataArray);

        canvasCtx.fillStyle = 'rgb(0, 0, 0)';
        canvasCtx.fillRect(0, 0, width, height);

        // 주파수 데이터가 부족한 경우 캔버스가 비어 보이지 않도록 barWidth를 크게 표현하기위해 2.5를 곱해줍니다.
        const barWidth = (width / bufferLength) * 2.5;
        let barHeight = 0;
        let x = 0; // 캔버스 왼쪽 끝에서부터 시작

        // dataArray 주파수 데이터를 순회하며 캔버스에 그려줍니다.
        for (let i = 0; i < bufferLength; i++) {
          // 막대기 높이 계산
          barHeight = (dataArray[i] + 120) * 2;

          // 막대기가 높을 수록 밝은 색으로 표시됩니다.
          canvasCtx.fillStyle =
            'rgb(' + Math.floor(barHeight + 100) + ',50,50)';

          canvasCtx.fillRect(
            x,
            height - barHeight / 2, // 막대기가 사각형 밑변 중간에 걸칠 수 있게 y좌표 offset 조정
            barWidth,
            barHeight,
          );

          x += barWidth + 1;
        }
      };

      draw();
    };
    
    try {
     ...
     
     visualizeAudio();
    } catch (error {
      ...
    }

 

4. 녹음 및 페인팅 취소

const AudioVisualizer = () => {
...

 const cancel = () => {
   if (requestIdRef.current) {
     cancelAnimationFrame(requestIdRef.current);
     streamRef.current?.getTracks()[0].stop();
   }
 };
  
...
}

 

 

전체 코드

더보기
import { useEffect, useRef } from 'react';

const AudioVisualizer = () => {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const requestIdRef = useRef<number | null>(null);
  const streamRef = useRef<MediaStream | null>(null);

  const start = async () => {
    const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();

    const analyser = audioCtx.createAnalyser();
    analyser.minDecibels = -90;
    analyser.maxDecibels = -10;
    analyser.smoothingTimeConstant = 0.85;
    analyser.fftSize = 512;

    const canvasCtx = canvasRef?.current?.getContext('2d');
    const containerWidth = containerRef?.current?.clientWidth.toString() || '0';
    canvasRef.current?.setAttribute('width', containerWidth);

    const visualizeAudio = () => {
      const width = canvasRef.current?.width || 0;
      const height = canvasRef.current?.height || 0;

      const bufferLength = analyser.frequencyBinCount;
      const dataArray = new Float32Array(bufferLength);

      canvasCtx?.clearRect(0, 0, width, height);

      const draw = () => {
        if (!canvasCtx) {
          return;
        }

        requestIdRef.current = requestAnimationFrame(draw);

        analyser.getFloatFrequencyData(dataArray);

        canvasCtx.fillStyle = 'rgb(0, 0, 0)';
        canvasCtx.fillRect(0, 0, width, height);

        const barWidth = (width / bufferLength) * 2.5;
        let barHeight = 0;
        let x = 0;

        for (let i = 0; i < bufferLength; i++) {
          barHeight = (dataArray[i] + 100) * 2;

          canvasCtx.fillStyle =
            'rgb(' + Math.floor(barHeight + 100) + ',50,50)';

          canvasCtx.fillRect(
            x,
            height - barHeight / 2,
            barWidth,
            barHeight,
          );

          x += barWidth + 1;
        }
      };

      draw();
    };

    try {
      streamRef.current = await navigator.mediaDevices.getUserMedia({
        audio: true,
      });
      const audioSource = audioCtx.createMediaStreamSource(streamRef.current);
      audioSource.connect(analyser);

      visualizeAudio();

    } catch (error) {
      console.error(error);
    }
  };

  const cancel = () => {
    if (requestIdRef.current) {
      cancelAnimationFrame(requestIdRef.current);
      streamRef.current?.getTracks()[0].stop();
    }
  };

  useEffect(() => {
    start();

    return () => {
      cancel();
    };
  }, []);

  return (
    <div ref={containerRef} style={{ width: '100%' }}>
      <canvas ref={canvasRef} height={120} />
    </div>
  );
};

'프론트엔드 > React' 카테고리의 다른 글

Next/image 컴포넌트의 내부 동작 원리를 알아보자!  (0) 2023.08.01
리액트 image 어디에다 저장해야할까 (public vs src)  (0) 2022.09.25
현업에서 바로 적용해보는 프론트엔드 클린코드  (0) 2021.10.28
리액트 불변성이란 무엇이고, 왜 지켜야 할까?  (2) 2021.06.24
리액트에서 cryptoJS 사용하는 법  (0) 2021.04.15
'프론트엔드/React' 카테고리의 다른 글
  • Next/image 컴포넌트의 내부 동작 원리를 알아보자!
  • 리액트 image 어디에다 저장해야할까 (public vs src)
  • 현업에서 바로 적용해보는 프론트엔드 클린코드
  • 리액트 불변성이란 무엇이고, 왜 지켜야 할까?
꿀표
꿀표
양봉업자
  • 꿀표
    꿀로그
    꿀표
  • 전체
    오늘
    어제
    • 분류 전체보기 (82)
      • 인디해커 (0)
      • AI (0)
      • 프론트엔드 (34)
        • Javascript (17)
        • React (9)
        • Git (2)
        • Web Env (4)
        • Typescript (1)
        • 웹접근성 (1)
        • 상태관리 (0)
      • CS (8)
        • Network (3)
        • 알고리즘 (5)
      • 글쓰기 (3)
        • 생각 (2)
        • 서적 (1)
      • JAVA (37)
        • JAVA 기초 (22)
        • JSP (15)
  • 블로그 메뉴

    • 방명록
  • 인기 글

  • 태그

    알고리즘
    구명보트
    greedy
    프로그래머스
    network
    js
    javascript
    react
    cross browsing
    그리디
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
꿀표
오디오 시각화 애니메이션 컴포넌트 만들기 (Audio Visualization)
상단으로

티스토리툴바