[Web Audio API #9] 사운드 시각화 - Sound Visualization
재생 중인 음원의 주파수, 시간영역 데이터를 시각화 할 수 있다. 시각화를 위해서는 먼저 사운드 데이터를 분석하고 그 결과를 가져와야 한다.
사운드데이터 분석을 위해 analyser 노드가 사용된다. Anlayser 노드는 음원 신호에는 영향을 주지 않으며 주파수 성분을 알아낸다. 예를 들어 아래와 같이 음원재생 노드를 구성하면 사운드 시각화를 위한 분석을 수행할 수 있다.
Analyser 노드는 입력되는 음원 데이터를 변경시키지 않는다. 따라서 오디오 그래프에서 음원 데이터 분석이 필요한 단계에 넣어주면 된다. 예를 들어 일정한 주파수의 소리를 만들어 내는 오디오 그래프를 아래와 같이 구성할 수 있다.
const audioContext = new(window.AudioContext || window.webkitAudioContext)();
// Oscillator 노드는 일정한 주파수의 소리를 만들어낸다.
const osc = audioContext.createOscillator();
const analyser = audioContext.createAnalyser();
// 500Hz 주파수를 가진 사인파(sine wave) 소리를 생성하도록 설정
osc.frequency.setValueAtTime(500, audioContext.currentTime);
osc.type = 'sine';
// 주파수를 512개의 데이터로 표현한다.
analyser.fftSize = 512;
osc.connect(analyser);
analyser.connect(audioContext.destination);
Analyser 노드로 부터 데이터를 얻어 화면에 시각화 할 수 있다. 시각화를 위해 보통 HTML canvas 엘리먼트가 사용된다.
<div id="sound_canvas_div">
<canvas id="sound_canvas" width="600" height="300" style="border:1px solid gray"></canvas>
</div>
Canvas에 해당하는 객체를 JavaScript로 얻어서 그림을 그리기 위한 정보들을 얻는다.
const soundCanvas = document.querySelector('#sound_canvas');
const drawContext = soundCanvas.getContext("2d");
const figWidth = soundCanvas.clientWidth;
const figHeight = soundCanvas.clientHeight;
Analyser 노드로부터 주파수 데이터를 얻기 위해 getByteFrequencyData() 메서드를 사용하고, 시간 영역 음향 데이터를 얻기 위해서는 getByteTimeDomainData() 메서드를 사용한다.
var freqDomain = new Uint8Array(analyser.frequencyBinCount); // value: [0~255]
analyser.getByteFrequencyData(freqDomain);
var timeDomain = new Uint8Array(analyser.frequencyBinCount); // value: [0~255]
analyser.getByteTimeDomainData(timeDomain);
Analyser는 음향 데이터 분석을 위해 FFT(Fast Fourier Transform)를 사용한다. Analyser 노드의 frequencyBinCount 속성은 분석 결과 데이터의 개수를 나타낸다. 분석 대상의 데이터를 앞서 512개로 설정했는데(analyser.fftSize = 512), 결과 데이터의 개수는 그 1/2인 256개가 된다. 이는 FFT의 특성과 관련이 있다. FFT 결과 Real, Imaginary 두 종류 데이터가 256개씩, 총 512개의 데이터가 생성되는데 Real, Imaginary 데이터가 같은 값을 갖기 때문에 그 중 하나인 256개의 값만 저장한다. Anlayser 노드는 시간 영역의 데이터도 주파수 영역의 데이터의 수와 같은 개수로 만들어서 돌려준다.
이제 canvas에 각 frequency data를 사각형(rect)으로 나타내 보자.
for(var i = 0; i < analyser.frequencyBinCount; i++){
var value = freqDomain[i];
var percent = value/256; // value가 0~255의 값을 갖기 때문이다.
var height = figHeight*percent; // 주파수 성분의 크기를 canvas 크기의 비율로 환산한다.
var offset = figHeight - height - 1; // 일반 XY좌표를 디스플레이 평면 좌표로 변환한다.
var hue = (i/analyser.frequencyBinCount)*360; // 주파수에 따라 서로 다른 색상(hue)값을 준다.
drawContext.fillStyle = 'hsl(' + hue + ', 100%, 50%)'; // 색상(hue), 채도(saturation), 명도(luminance)
drawContext.fillRect(i*barWidth, offset, barWidth, height); // upper left x, upper left y, 가로길이, 높이
}
예제 코드에서는 주파수 값을 서로 다른 색으로 구분할 수 있게 하기 위해 색상(Hue), 채도(Saturation), 명도(Luminance) 색좌표를 사용했다. 채도와 명도는 변경하지 않고 색상(Hue) 값만 변경하였다.
화면에 사각형을 그리기 위해 fillRect 함수를 사용했다. fillRect함수는 사각형을 그리기 위해 좌상(upper left) X 좌표, 좌상(upper left) Y 좌표, 가로길이, 높이 값들을 인자로 사용한다. 이 때 Y 좌표로 주파수 성분을 canvas 크기 비율로 환산한 높이(height)값을 사용하지 않고 canvas 높이에서 주파수 성분 높이(height)를 뺀 offset 값을 사용했다. 그 이유는 일반적인 XY 평면에서는 위로 이동할수록 Y좌표 값이 증가하는데 반해 디스플레이를 위해 사용하는 디스플레이 평면에서는 평면의 위가 0이고 평면의 아래로 이동할수록 Y좌표의 값이 증가하기 때문이다. 따라서 화면 위를 0으로 놓고 계산한 Y 좌표(=offset)가 필요하다.
주파수 데이터를 시각화 한 방법으로 analyser 노드에서 가져온 시간에 따른 음향 데이터도 시각화 할 수 있다. 시간축 데이터는 색상으로 구분하지 않고 모두 검정색 점으로 표시하였다.
for(var i = 0; i < analyser.frequencyBinCount; i++){
var value = timeDomain[i];
var percent = value/256; // 8bit resolution = 256
var height = figHeight*percent; // size of the component
var offset = figHeight - height - 1; // 디스플레이 좌표로 환산한 Y 좌표값
drawContext.fillStyle = 'black';
drawContext.fillRect(i*barWidth, offset, 1, 1); // 변의 길이가 1인 사각형 = pixel
}
위 내용들을 결합하여 주파수 성분 값, 음향 신호의 시간에 따른 크기 데이터를 얻어서 화면에 표시하는 루틴을 만들면 아래의 visualizationSound() 함수와 같다.
const soundCanvas = document.querySelector('#sound_canvas');
const drawContext = soundCanvas.getContext("2d");
const figWidth = soundCanvas.clientWidth;
const figHeight = soundCanvas.clientHeight;
const visualizeSound = () => {
// 주파수 데이터를 얻는다.
var freqDomain = new Uint8Array(analyser.frequencyBinCount); // value: [0~255]
analyser.getByteFrequencyData(freqDomain);
var barWidth = figWidth/analyser.frequencyBinCount; // canvas에 표시될 각 주파수 성분 사각형의 너비
// 기존 그림은 지워준다.
drawContext.clearRect(0, 0, figWidth, figHeight);
// 주파수 성분 표시
for(var i = 0; i < analyser.frequencyBinCount; i++){
var value = freqDomain[i];
var percent = value/256; // 8bit resolution = 256
var height = figHeight*percent;
var offset = figHeight - height - 1;
var hue = (i/analyser.frequencyBinCount)*360; // 주파수에 따른 coloring
drawContext.fillStyle = 'hsl(' + hue + ', 100%, 50%)'; // hue, saturation, and luminance;
drawContext.fillRect(i*barWidth, offset, barWidth, height);
}
// 시간에 따른 음향 데이터를 얻는다.
var timeDomain = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteTimeDomainData(timeDomain);
// 음향 성분 표시
for(var i = 0; i < analyser.frequencyBinCount; i++){
var value = timeDomain[i];
var percent = value/256;
var height = figHeight*percent;
var offset = figHeight - height - 1;
drawContext.fillStyle = 'black';
drawContext.fillRect(i*barWidth, offset, 1, 1);
}
}
visualizeSound 함수는 호출될 때 마다 호출 시점의 음향 데이터를 그림으로 보여준다. 이제 window의 requestAnimationFrame 함수를 이용해서 주기적으로 이 visualizeSound 함수를 호출해주면 매번 새 음향 데이터를 화면에 보여줄 수 있다.
let rAFId = null;
let second = 0;
const updateSoundDisplay = () => {
if((second % 5) == 0){ // 12fps
visualizeSound();
}
second = second + 1;
// Play 상태일 때만 화면에 음향 데이터를 표시하도록 하였다.
if(PlayPauseButton.dataset.playing == 'true'){
rAFId = window.requestAnimationFrame(updateSoundDisplay);
}
else{
cancelAnimationFrame(rAFId)
second = 0;
}
} // updateSoundDisplay = ()
requestAnimationFrame 함수는 웹 브라우저 스크린이 업데이트 되는 시점(일반적으로 1초에 60번)에 실행되어야 할 내용을 인자로 받는다. requestAnimationFrame 함수를 반복적으로 호출해줌으로써 매번 웹 브라우저 스크린이 업데이트 될 때마다 음향 데이터 값을 화면에 새로 표시해준다.
아래 그림에서 그래프 부분이 음향 데이터를 표시하는 canvas 영역이고 그 아래 요소들은 순서대로 oscillator의 주파수를 조정하는 입력바, 그리고 볼륨조절 입력, 그리고 재생/멈춤 버튼이다.
위의 예제 코드에서는 1초에 12번만 새로운 데이터를 화면에 표시해 주도록 하였다. 이 정도면 시각화를 통해 음향 데이터를 보는데 무리가 없기 때문이다.
■