웹 오디오 API를 사용하며 웹 오디오 API에 대해 찾아보다가 우연히 잘 정리한 글을 발견해 정리한다.
우리가 할 일
- Web Audio API가 무엇인지, <audio>요소와 어떻게 다른지 알아보기
- GainNode를 사용하여 간단한 볼륨 컨트롤러 구축
- 더 복잡한 오디오 노드 탐색
<audio>태그와 다른 점
예전에는 브라우저에서 오디오를 재생하는 첫 번째 방법이 <bgsound> 태그 였습니다. 이 태그는 오디오를 전혀 제어할 수 없으며 Internet Explorer에서만 사용이 가능합니다. 이는 결코 표준화되지 않았으며 지금은 너무 구식이어서 caniuse.com 웹사이트에서도 다음과 같이 경고할 것입니다.
이러한 요소/태그는 더 이상 사용되지 않으므로 사용해서는 안됩니다. 이로 인해 브라우저 지원이 되질 않습니다.
Flash는 웹에서 오디오를 재생하는 최초의 크로스 브라우저 방식이었지만 실행하려면 플러그인이 필요하다는 심각한 단점이 있습니다. 기억하시는 분들도 계시겠지만, 초기의 Youtube에서도 동영상을 표시하려면 플래시가 필요했습니다.
아시다시피, Apple은 Flash를 그다지 좋아하지 않았으며 이에 맞서 전생을 벌이기로 결정하여 기본적으로 iPhone에서 플래서리르 비활성화했습니다. 2005년에 애플은 Webkit 프로젝트를 오픈 소스로 만들었습니다.(지금까지 이 회사가 시작한 유일한 오픈소스 프로젝트였습니다.) 애플이 주요 브라우저에 도입하고 싶었던 것 중 하나는 플래시 위젯에 미디어를 표시하는 대안으로 <audio>와 <video>태그였습니다.
2012년에는 클라이언트측 개발이 더욱 활발해지면서 Web Audio API가 W3C사양의 일부로 도입되었습니다. 이는 자바스크립트 개발자가 오디오 노드를 사용하여 자신만의 오디오 효과를 구성할 수 있게 해주는 획기적인 변화였습니다.
1년 후, Google은 Webkit의 WebCore 구성요소를 Blink라는 이름으로 포크했습니다.
이때 WebAudio API가 발전하면서 Blink 기능이 중요한 역할을 하기 시작했습니다. 최근에 도입된 Audio Worklets 기능을 사용하면 별도의 오디오 스레드에서 오디오를 렌더링할 수 있으므로 Game/DAW/미디어 편집자를 위한 완벽한 제품군이 됩니다.
WebAudio API의 기본 개념
Web Audio API와 관련된 모든 작업은 모듈식 라우팅을 통해 오디오 컨텍스트내에서 발생합니다. 일반적으로 창당 하나의 오디오 컨텍스트 인스턴스만 있지만 브라우저에서는 동시에 최대 5개의 인스턴스를 보유할 수 잇습니다. 단일 프로젝트에 둘 이상의 오디오 컨텍스트 인스턴스를 갖는 것은 좋은 습관으로 간주되지 않습니다. 오디오 컨텍스트는 샘플링 속도와 같은 하드웨어에 대한 메타데이터를 보유합니다.
기본 작업은 AudioNodes에 의해 수행됩니다. Web Audio API에서 제공하고 W3C 사양에 설명된 몇가지 기본 AudioNode가 있습니다. 가장 일반적인 것들은 다음과 같습니다
- MediaStreamAudioSourceNode, MediaElementAudioSourceNode(오디오 소스)
- OscillatorNode(주기적인 파형을 생성하는 오디오 노드)
- DelayNode, GainNode, BiqadFilterNode(오디오 처리)
그러나 개발자는 기본 노드에만 국한되지 않습니다. ScriptProcessorNode 및 AudioWorkletNode를 사용하면 스피커 장치로 전달되는 모든 데이터 스트림을 제공할 수 있습니다. WebAssembly 모듈과 같은 낮은 수준의 오디오 처리 계층을 사용하여 추가 성능을 얻을 수 있습니다.
Input으로 다음을 사용할 수 있습니다.
- .mp3 과 같은 정적 오디오 버퍼 소스
- Stream 데이터
- OscillatorNode를 사용하여 나만의 음악을 생성합니다.
그럼 무엇을 만들 것인가?
간단한 볼륨 컨트롤러를 구축할 예정이지만 <audio>태그와 볼륨 속성을 사용하는 대신 GainNode를 사용하겠습니다.
오디오 컨텍스트 인스턴스화부터 시작하겠습니다.
<!doctype html>
<head>
<title>Web Audio Basics</title>
</head>
<body>
<input type="file" id="file-input">
<input type="range" id="volume-input" min="0" max="1" value="1" step="0.02">
<script defer>
(() => {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();
})()
</script>
</body>
</html>
// js
const fileInputEl = document.getElementById('file-input');
const volumeInputEl = document.getElementById('volume-input');
const source = audioCtx.createBufferSource(); // once user will a select file, we are going to fill buffer of that node
const gainNode = audioCtx.createGain(); // this node allows us to control volume
// this is just an util function that takes File element and returns AudioBuffer
const getAudioBuffer = (file) => {
const reader = new FileReader();
const promise = new Promise((resolve) => {
reader.onload = _ => audioCtx.decodeAudioData(reader.result, (audioBuffer) => resolve(audioBuffer))
});
reader.readAsArrayBuffer(new Blob([file]));
return promise
};
fileInputEl.addEventListener('change', async ({ srcElement }) => {
const [file] = srcElement.files;
source.buffer = await getAudioBuffer(file); // connecting AudioBuffer source (audio file) with gain node (volume)
});
다음 단계는 선택한 파일의 AudioBuffer를 AudioContext 내부의 미디어 소스노드로 사용하는 것입니다.
// connecting AudioBuffer source (audio file) with gain node (volume)
source.connect(gainNode);
// next step would be to connect output device (destination) and start playback
gainNode.connect(audioCtx.destination);
// perfect, our graph should look like: SourceNode -> GainNode -> Output Device
source.start();
마지막으로 사용자가 슬라이더를 움직일 때 노드 값을 업데이트 해야합니다.
다시 한번 change 이벤트를 바인딩 할 예정이지만 이번에는 슬라이더 요소에 적용됩니다.
volumeInputEl.addEventListener('change', (e) => {
gainNode.gain.value = Number(e.target.value)
});
<audio>태그 대신 AudioContext 및 GainNode를 사용하는 간단한 볼륨 슬라이더를 만들었씁니다.
<!doctype html>
<head>
<title>Web Audio Basics</title>
</head>
<body>
<input type="file" id="file-input">
<input type="range" id="volume-input" min="0" max="1" value="1" step="0.02">
<script defer> // 'defer' is equivalent of DOMContentLoaded, watch of browser support
(() => {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();
const fileInputEl = document.getElementById('file-input');
const volumeInputEl = document.getElementById('volume-input');
const source = audioCtx.createBufferSource(); // once user will a select file, we are going to fill buffer of that node
const gainNode = audioCtx.createGain(); // this node allows us to control volume
// this is just an util function that takes File element and returns AudioBuffer
const getAudioBuffer = (file) => {
const reader = new FileReader();
const promise = new Promise((resolve) => {
reader.onload = _ => audioCtx.decodeAudioData(reader.result, (audioBuffer) => resolve(audioBuffer))
});
reader.readAsArrayBuffer(new Blob([file]));
return promise
};
fileInputEl.addEventListener('change', async ({srcElement}) => {
const [file] = srcElement.files;
source.buffer = await getAudioBuffer(file);
source.connect(gainNode); // connecting AudioBuffer source (audio file) with gain node (volume)
// last step would be to connect output device (destination) and start playback
gainNode.connect(audioCtx.destination);
source.start();
});
// this 'change' handler is responsible for updating gain node value
volumeInputEl.addEventListener('change', (e) => {
gainNode.gain.value = Number(e.target.value)
});
})()
</script>
</body>
</html>
오디오 그래프는 매우 단순해 보일 것입니다. Firefox 디버거를 사용하면 시각화를 볼 수 있습니다.
출처 : Medium