sungyup's.

Tale of Two Clocks

Web Audio API의 시계와 JavaScript의 시계

Feb 19, 2025

원본 글 보기
Web

들어가며

웹에서 오디오의 시간 을 관리하는 것은 처음엔 쉬워보이지만 애플리케이션이 복잡해질수록 정확하게 시간을 관리하기가 어려워진다.

보통 웹에서 오디오의 시간에 접근할 때는 Web Audio AudioContext 오브젝트의 currentTime이라는 property에 접근하곤 한다. 시퀀서, 신디사이저 뿐 아니라 드럼 머신, 게임, 음악 플레이어 등 다양한 애플리케이션들이 정확한 시간 관리(scheduling)를 요구하는데, 원하는 타이밍에 원하는 소리가 나오지 않거나 살짝만 밀려도 아주 glitchy하게 들리기 쉽다.

Web Audio Clock : 정확한 시계

AudioContext의 currentTime property는 AudioContext가 생성된 이후로 흐른 시간을 double 형태의 floating-point 수로 보관한다. Audio Clock이라고 불리는 이 시계는 높은 정확성을 위한 것이며, 샘플 레이트가 높더라도 개별 사운드들의 시간을 정확히 관리할 수 있게 해준다.(샘플 레이트와 개별 사운드 시간 관리와의 관계에 관한 설명... emphasisbox같은걸로?) Web Audio API는 오디오 하드웨어의 시계를 사용하기에 매우 정확하다.

Audio Clock은 Web Audio API의 오디오 이벤트들(start()loop() 등)과 파라미터(set*ValueAtTime() 등의 메소드들의)를 스케줄링하는데 쓰인다.

이것이 중요한 이유는, Web Audio의 타이밍을 단지 start/stop으로만 맞추면 문제가 생기기 때문이다.

예를 들어, 아래는 2마디 하이햇 패턴이다:

Javascript
for (const bar = 0; bar < 2; bar++) { const time = startTime + bar * 8 * eighthNoteTime; // Play the hi-hat every eighth note. for (const i = 0; i < 8; ++i) { playSound(hihat, time + i * eighthNoteTime); } }

이 코드는 잘 작동하지만, 재생되는 두 마디 사이에 템포를 바꾸거나, 두 마디가 끝나기 전에 중지하려고 하면 문제가 생긴다. 따라서, 템포를 바꾸거나 gain을 바꾸거나 하려면 큐에 너무 많은 오디오 이벤트를 넣으면 안된다.

Javascript Clock : 최악의 시계

자바스크립트에도 Date.now()setTimeout()로 대변되는 시계가 있다. 이 시계의 좋은 점은 window.setTimeout()window.setIneterval()과 같은, 시스템이 일정 시간이 지난 후 코드를 호출해주는 유용한 메소드를 제공한다는 것이다.

반면, 자바스크립트 시계의 단점은 부정확하다는 것이다. Date.now()는 값을 ms(밀리초) 단위로 제공하는데, 따라서 정확성이 1 밀리초 단위로만 확보된다. 이게 아주 정확해보일 수 있지만, 44.1kHz라는 상대적으로 낮은 샘플 레이트에서 44.1배 느려지게 된다면 오디오들을 스케줄링하는 시계로는 적합하지 않다는 것을 알 수 있다.

이 부분은 window.performance.now()가 등장하며 보다 정확한 시계가 되었지만, 사실 이 부분은 자바스크립트 시계의 최대 단점이 아니다.

자바스크립트 시계의 최대 단점은 이 밀리초 단위의 정확성이 레이아웃, 가비지 컬렉션, 그리고 XMLHTTPRequest 또는 다른 콜백들에 의해 왜곡(skewed)될 수 있다는 것이다. 다시 말해, 메인 실행 스레드에서 일어나는 수많은 일들에 의해 이 시계는 방해받고 왜곡된다.

앞서 말한 '정확한 시계'인 Web Audio API는 '오디오 이벤트'들을 별개의 스레드에서 처리하기에, 메인 스레드가 일시적으로 복잡한 무언가를 처리하느라 멈춘 동안에도 정확한 타이밍에 처리된다. 디버거의 브레이크포인트에서 멈추거나 메인 스레드가 얼어붙은 동안에도 오디오 스레드는 계속 스케줄링된 음악을 연주하는 것이 이를 증명한다.

오디오 앱에서 JavaScript의 setTimeout() 사용하기

자바스크립트의 메인 스레드가 이처럼 쉽게 방해받기 때문에, JavaScript의 setTimeout으로 오디오 이벤트를 스케줄링하는 것은 별로 좋은 생각이 아니다. 창을 줄이고 키우거나, 스크롤링 하거나, 레이아웃이 많거나 하는 등 다양한 이유로 오디오 이벤트들은 밀릴 것이다.

오디오 이벤트를 통한 콜백을 호출하는 방식을 써도 마찬가지다. 이벤트들이 결국 자바스크립트의 메인 스레드에서 실행되는 한, 정확한 타이밍을 맞출 수 없다.

그러면 어떻게 해야할까? 타이밍을 맞추기 위해서 가장 좋은 방법은 JavaScript 타이머(setTimeout(), setInterval() 또는 requestAnimationFrame())을 오디오 하드웨어 스케줄링과 함께 쓰는 것이다.

정확한 오디오 스케줄링 기법 - 미리 내다보기

작동 중에 템포가 변해도 정확하게 작동하는 메트로놈 웹 앱을 만든다고 해보자.

JavaScript의 setTimeout으로 소리를 직접 내는 간격을 조절하는 것은 정확하지 않다. 그렇다고 Web Audio API만으로는 템포 변화에 반응할 수 없다.

우선 정확한 타이밍에 소리를 출력하기 위해서 Web Audio API로 스케줄링을 해야하는 것은 필연적이다. 이 때, 템포 변화에 반응하기 위해 setTimeout을 자주 발동하며 템포에 따른 오디오 스케줄링을 한다.

setTimeout의 타이머가 발동될 때마다, Audio Context의 currentTime을 확인하고, 일정 시간을 내다본다.

Javascript
while (nextNoteTime < audioContext.currentTime + scheduleAheadTime) { scheduleNote(current16thNote, nextNoteTime); nextNote(); }