오디오 시각화란
오디오 데이터를 시각화 하는 것
마이크를 사용했을 때 생기는 오디오 소스로부터 주파수나, 파형, 기타 데이터들을 활용해서 데이터 시각화를 하는 것이다.
오디오 시각화가 필요했던 이유
유저가 마이크를 사용할 때 자신의 목소리가 잘 녹음되고 있는지 인지시켜야함.
현 회사에서는 영어 교육 서비스를 제공하고 있는데, 유저가 스피킹 시험을 볼 때 마이크를 사용한다. 유저의 목소리가 작으면 녹음 퀄리티 저하로 이어지게 되는데, 문제는 마이크 녹음할 때 유저는 자신의 목소리가 작은지 인지하지 못한다. 그래서 마이크를 사용할 때 목소리 크기를 시각적으로 확인하는 장치가 필요했다. 즉 오디오 스트림 데이터를 시각화하여 유저는 목소리의 크기를 실시간으로 인지하면서 녹음할 수 있다. 이로써 녹음 품질을 개선시킬 수 있을 뿐더러 유저가 재시험을 봐야하는 번거로움을 없앨 수 있었다.
컴포넌트 작성 방법
여기서는 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 |