Getting Started with Web Audio API
Web Audio API 입문: AudioContext와 Audio Graph의 이해
Feb 11, 2025
원본 글 보기들어가며
회사에서 음악 관련 웹 애플리케이션을 기획/제작할 수 있는 기회를 받았다. 음악 관련 산업에 종사하고 싶었던 마음에 개발자의 세계로 뛰어든 프론트엔드 개발자로써, 이번 기회에 최선을 다해 관련 지식을 공부하고 기록으로 남겨야겠다는 생각이 들었다.
이번 포스팅 시리즈는 그간 잘 모르고 썼던 <audio>
element 및 Web Audio API에 대해 자세히 알아보고자 좋은 글들을 찾아 정리하고, 그 과정에서 잘 이해가 안되는 부분은 보완해 포스팅을 남기고자 한다.
Web Audio API의 등장 배경
HTML5의 <audio>
element 이전에는 웹에서 소리가 나게 하려면 Flash나 기타 plugin이 필요했다. <audio>
element의 등장으로 plugin은 더 이상 필요하지 않지만, 복잡한 컨트롤 또는 element와 상호작용하는 애플리케이션을 만들 때는 여전히 제한사항이 있다.
이러한 제한 사항을 극복하고 <audio>
를 웹 애플리케이션에서 프로세싱하고 합성하기 위해 등장한 고수준 JavaScript API가 Web Audio API이다.
AudioContext
AudioContext는 Web Audio API에서 제공하는, 사운드를 관리/재생하기 위한 JavaScript Object이다. Web Audio API로 사운드를 생성하려면, Sound Source를 하나 또는 그 이상 만든 후 AudioContext 인스턴스에서 제공하는 Sound Destination으로 연결해야 한다.

이 연결은 직접적일 필요는 없으며, 다양한 AudioNode를 거쳐도 문제 없다. AudioNode는 오디오 신호를 프로세싱하는 모듈이다.
한 개의 AudioContext 인스턴스는 여러 사운드 인풋을 처리할 수 있을 뿐 아니라, 각각의 오디오들이 어떤 노드를 거쳐(소리를 변형하기 위해) 어떻게 destination으로 갈지도 처리할 수 있다(이를 Audio Graph라고 한다. 위의 이미지처럼 사운드 소스가 어떻게 Destination으로 가는지를 표현하는 도식이라고 생각하면 된다). 그렇기에 오디오 애플리케이션을 만들때 AudioContext는 한 개만 만들면 된다.
사운드 로드
Web Audio API는 WAV, MP3, AAC, OGG 등의 포맷을 기본적으로 지원하지만, 브라우저마다 또 약간씩 다르다.
Web Audio API는 짧은 ~ 중간 길이의 사운드는 AudioBuffer를 써서 사운드를 로드한다. 이 방식은 XMLHttpRequest로 사운드 파일을 fetch하는 것이다. 실제 오디오 파일 데이터는 binary이고 text가 아니기 때문에, responseType은 arraybuffer로 설정해야 한다.
디코딩이 되지 않은 오디오 파일을 받아온 후, 해당 오디오 파일은 이후에 디코딩 할 수도 있고 AudioContext의 decodeAudioData()
메소드로 바로 디코드할 수도 있다. 이 메소드는 request.response로 저장된 오디오 파일 데이터의 ArrayBuffer를 받아 asynchronous하게 디코드한다. 즉, JavaScript의 메인 스레드를 막지 않는다.
decodeAudioData()
가 끝났으면, WEb Audio API는 디코드된 PCM(pulse code modulation) 오디오 데이터를 AudioBuffer로 제공하는 콜백 함수를 호출한다.
사운드 재생
AudioBuffer가 로드되었으면, 재생할 준비가 된 것이다.
javascriptconst context = new AudioContext(); function playSound(buffer) { const source = context.createBufferSource(); // sound source 생성 source.buffer = buffer; // 소스에 audio buffer 연결 source.connect(context.destination); // 소스에 audio context의 destination(스피커) 연결 source.noteOn(0); // 바로 소스 재생시키기 }
noteOn(time)
함수는 사운드를 정확한 타이밍에 스케줄링 할 수 있게 해준다. 물론, sound buffer가 모두 로드된 상태여야만 한다.
이렇게 만들어진 playSound()
함수는 언제든 호출할 수 있다.
박자 처리하기 : 리듬이 있는 사운드 재생하기
Web Audio API는 사운드 재생을 정확하게 스케줄링 할 수 있게 해준다.
예를 들면, 아래는 기본 락 비트이다.
javascriptfor (const bar = 0; bar < 2; bar++) { const time = startTime + bar * 8 * eighthNoteTime; // Play the bass (kick) drum on beats 1, 5 playSound(kick, time); playSound(kick, time + 4 * eighthNoteTime); // Play the snare drum on beats 3, 7 playSound(snare, time + 2 * eighthNoteTime); playSound(snare, time + 6 * eighthNoteTime); // Play the hi-hat every eighth note. for (const i = 0; i < 8; ++i) { playSound(hihat, time + i * eighthNoteTime); } }
사운드의 볼륨 조절

Web Audio API로 볼륨을 조절하려면, AudioGainNode라는 노드를 소스에서 거쳐 destination으로 보내면 된다.
javascript// gainNode 생성 const gainNode = context.createGainNode(); // 소스를 gainNode에 연결 source.connect(gainNode); // gainNode를 destination에 연결 gainNode.connect(context.destination);
이렇게 연결하고 나면 gainNode.gain.value를 조정함으로 볼륨 조절을 할 수 있다.
javascript// 볼륨 줄이기 gainNode.gain.value = 0.5;
두 사운드 간 크로스-페이딩
DJ들이 하는 것처럼, 두 소리를 자연스럽게 이어지게 하고 싶을 수 있다.

이렇게 하기 위해서는 두 개의 AudioGainNode를 만들고, 각각의 소스를 노드에 연결한다.
javascript// 두 개의 볼륨 노드 생성 function createSource(buffer) { const source = context.createBufferSource(); // Create a gain node. const gainNode = context.createGainNode(); source.buffer = buffer; // Turn on looping. source.loop = true; // Connect source to gain. source.connect(gainNode); // Connect gain to destination. gainNode.connect(context.destination); return { source: source, gainNode: gainNode, }; }
동일 파워 크로스페이딩
선형적으로 크로스페이딩을 하는 것보다는 동일 파워 크로스페이딩이 더 자연스럽다.

플레이리스트 크로스페이딩
음악 재생기 애플리케이션에서는 DJ 케이스와 달리, 한 오디오가 다 페이드아웃 되고나서 새로운 오디오가 페이드인 되어야 한다. 이런 크로스페이딩을 하려면, 크로스페이딩을 스케줄링 해야한다. 이런 종류의 작업엔 setTimeout
을 쓸 것 같지만, setTimeout
은 정확하지 않다. Web Audio API로는 AudioParam 인터페이스를 사용해 미래 파라미터 값들을 스케줄링한다.
javascriptfunction playHelper(bufferNow, bufferLater) { const playNow = createSource(bufferNow); const source = playNow.source; const gainNode = playNow.gainNode; const duration = bufferNow.duration; const currTime = context.currentTime; // Fade the playNow track in. gainNode.gain.linearRampToValueAtTime(0, currTime); gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME); // Play the playNow track. source.noteOn(0); // At the end of the track, fade it out. gainNode.gain.linearRampToValueAtTime(1, currTime + duration - ctx.FADE_TIME); gainNode.gain.linearRampToValueAtTime(0, currTime + duration); // Schedule a recursive track change with the tracks swapped. const recurse = arguments.callee; ctx.timer = setTimeout(function () { recurse(bufferLater, bufferNow); }, duration - ctx.FADE_TIME - 1000); }
Web Audio API는 파라미터 값을 부드럽게 바꾸기 위해 linearRampToValueAtTime
과 exponentialRampToValueAtTime
메소드를 제공한다. 커스터마이징도 할 수 있는데, setValueCurveAtTime
함수로 파라미터 값들의 array를 제공하면 된다.
사운드에 필터 더하기

사운드 소스에서 destination으로 이어지는 파이프라인 사이에는 다양한 노드가 들어가 소리를 변형할 수 있는데, 그 중 BiquadFilterNode가 있다.
제공되는 필터는 아래와 같다:
- Low pass filter
- High pass filter
- Band pass filter
- Low shelf filter
- High shelf filter
- Peaking filter
- Notch filter
- All pass filter
모든 필터들은 얼마나 필터를 적용할지(gain)를 지정하는 파라미터, 필터를 적용할 주파수대를 지정하는 파라미터 및 성질 계수(quality factor)를 지정할 수 있다.
예를 들면, 아래는 Low-pass filter이다.
javascript// 필터 생성 const filter = context.createBiquadFilter(); // Create the audio graph. source.connect(filter); filter.connect(context.destination); // Create and specify parameters for the low-pass filter. filter.type = 0; // Low-pass filter. See BiquadFilterNode docs filter.frequency.value = 440; // Set cutoff to 440 HZ // Playback the sound. source.noteOn(0);
필터는 코드로 제거가 가능해, AudioContext 그래프를 다이나믹하게 변경할 수 있다. node.disconnect(outputNumber)가 필터를 제거해주는 메소드이다.
javascript// Disconnect the source and filter. source.disconnect(0); filter.disconnect(0); // Connect the source directly. source.connect(context.destination);