사용자의 음성이 인식되고 있다는 것을 표현하기 위해 Oscillator 컴포넌트를 만들었습니다.

음성이 인식되고 있다는 것을 사용자에게 피드백하기 위해 위와 같은 컴포넌트를 디자인 했습니다.
이 디자인의 핵심은 사용자의 음성을 주파수의 파형으로 보여주는 오실리에이터입니다.
오실리에이터를 제가 어떻게 구현했는지 설명드리겠습니다.
1. Media Capture and Streams API를 사용하여 음성데이터 가져오기
사용자의 음성을 시각화하는 것이므로 가장 먼저 해야하는 것은 음성 데이터를 가져오는 것입니다.
이를 위해 Media Capture and Streams API의 MediaDevices의 getUserMedia 메서드를 사용하여 마이크 권한을 요청하고 반환값으로 MediaStream을 받습니다.
const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
2. AudioContext를 사용하여 MediaStreamAudioSourceNode 객체 생성하기
AudioContext란?
Web Audio API는 노드 기반 아키텍처를 사용합니다. 오디오 소스(Source) → 처리(Processing) → 출력(Destination)으로 이어지는 노드들을 연결하여 오디오 파이프라인을 구성하는데, AudioContext가 이 노드들을 생성하고 연결하는 역할을 합니다.
[MediaStream] → [SourceNode] → [AnalyserNode] → [Destination]
↑ AudioContext가 생성
MediaStream을 재생이나 조작하기 위해서는 MediaStreamAudioSourceNode 객체를 생성해야합니다.
const source = audioContext.createMediaStreamSource(mediaStream)
3. 주파수 분석을 위해 AnalyserNode 생성하기
AnalyserNode 인터페이스는 실시간 주파수와 시간 영역 분석 정보를 제공 가능한 노드를 표현합니다. 이것은 변경되지 않은 오디오 스트림을 입력에서 출력으로 전달하지만, 여러분은 생성된 데이터를 얻고, 그것을 처리하고, 오디오 시각화를 생성할 수 있습니다.
const analyser = audioContext.createAnalyser();
analyser.fftSize = 64;
source.connect(analyser);
FFT(Fast Fourier Transform)는 시간 영역의 오디오 신호를 주파수 영역으로 변환하는 알고리즘입니다. fftSize는 한번의 FFT 분석에 사용할 샘플 수를 결정하며, 값이 클수록 주파수 해상도는 높아지지만 시간 반응성은 낮아집니다.
4. 주파수 데이터를 저장할 배열 생성하기
AnalyserNode에서 추출한 주파수 데이터를 저장할 Uint8Array를 생성합니다.
const dataArray = new Uint8Array(analyser.frequencyBinCount);
frequencyBinCount는 fftSize / 2 값으로, 분석 결과로 얻을 수 있는 주파수 구간의 개수입니다. fftSize가 64이므로 32개의 주파수 데이터를 얻게 됩니다. 각 요소는 0~255 범위의 값을 가지며, 해당 주파수 대역의 볼륨을 나타냅니다.
5. requestAnimationFrame으로 시각화 애니메이션 구현하기
실시간 시각화를 위해 requestAnimationFrame을 사용하여 매 프레임마다 주파수 데이터를 갱신하고 화면에 그립니다.
const draw = () => {
analyser.getByteFrequencyData(dataArray);
drawBars(ctx, width, height, dataArray);
animationIdRef.current = requestAnimationFrame(draw);
};
draw();
저는 프로젝트에서 React를 사용해서 animationId를 ref에 저장했습니다.
6. drawBars 함수로 막대 그래프 그리기
const BAR_WIDTH = 4;
const BAR_GAP = 2;
const BAR_MIN_HEIGHT = 8;
const BAR_RADIUS = 2;
const BAR_COLOR = 'rgba(255, 255, 255, 0.6)';
const drawBars = (
ctx: CanvasRenderingContext2D,
width: number,
height: number,
dataArray: Uint8Array,
) => {
ctx.clearRect(0, 0, width, height);
const barCount = Math.min(
Math.floor(width / (BAR_WIDTH + BAR_GAP)),
dataArray.length,
);
const totalWidth = barCount * (BAR_WIDTH + BAR_GAP) - BAR_GAP;
const startX = (width - totalWidth) / 2;
const centerY = height / 2;
ctx.fillStyle = BAR_COLOR;
const mul = Math.floor(dataArray.length / barCount);
for (let i = 0; i < barCount; i++) {
let sum = 0;
for (
let dataIndex = i * mul;
dataIndex < (i + 1) * mul && dataIndex < dataArray.length;
dataIndex++
) {
sum += dataArray[dataIndex];
}
const avg = sum / mul;
const barHeight = (avg / 255) * (height * 0.8);
const clampedHeight = Math.max(barHeight, BAR_MIN_HEIGHT);
const x = startX + i * (BAR_WIDTH + BAR_GAP);
const y = centerY - clampedHeight / 2;
ctx.beginPath();
ctx.roundRect(x, y, BAR_WIDTH, clampedHeight, BAR_RADIUS);
ctx.fill();
}
};
결과
https://www.youtube.com/watch?v=4MB5F7jrZc0
위와 같이 사용자가 말하면 주파수 파형을 보여주는 컴포넌트를 만들었습니다.
처음 다루어보는 Web API라 우여곡절이 많았지만 잘 작동하네요.☺️