웹 오디오 API 시작하기

HTML5 <audio> 요소가 도입되기 전에는 웹의 침묵을 깨기 위해 플래시나 다른 플러그인이 필요했습니다. 웹의 오디오에는 더 이상 플러그인이 필요하지 않지만 오디오 태그는 정교한 게임과 양방향 애플리케이션을 구현하는 데 상당한 제한이 있습니다.

Web Audio API는 웹 애플리케이션에서 오디오를 처리하고 합성하기 위한 고급 JavaScript API입니다. 이 API의 목표는 최신 게임 오디오 엔진에 있는 기능과 최신 데스크톱 오디오 제작 애플리케이션에 있는 일부 믹싱, 처리, 필터링 작업을 포함하는 것입니다. 다음은 이 강력한 API 사용에 관한 간단한 소개입니다.

AudioContext 시작하기

AudioContext는 모든 사운드를 관리하고 재생하는 데 사용됩니다. Web Audio API를 사용하여 사운드를 생성하려면 하나 이상의 사운드 소스를 만들고 AudioContext 인스턴스에서 제공하는 사운드 대상에 연결합니다. 이 연결은 직접 연결일 필요는 없으며 오디오 신호의 처리 모듈 역할을 하는 임의의 수의 중간 AudioNode를 통과할 수 있습니다. 이 라우팅은 웹 오디오 사양에 자세히 설명되어 있습니다.

AudioContext의 단일 인스턴스는 여러 음원 입력과 복잡한 오디오 그래프를 지원할 수 있으므로 만드는 각 오디오 애플리케이션에 하나만 있으면 됩니다.

다음 스니펫은 AudioContext를 만듭니다.

var context;
window.addEventListener('load', init, false);
function init() {
    try {
    context = new AudioContext();
    }
    catch(e) {
    alert('Web Audio API is not supported in this browser');
    }
}

이전 WebKit 기반 브라우저의 경우 webkitAudioContext와 마찬가지로 webkit 접두사를 사용합니다.

AudioNode 만들기, 오디오 파일 데이터 디코딩 등 흥미로운 Web Audio API 기능 중 다수는 AudioContext의 메서드입니다.

소리 로드 중

Web Audio API는 짧은 길이에서 중간 길이의 사운드에 AudioBuffer를 사용합니다. 기본적인 접근 방식은 사운드 파일을 가져오는 데 XMLHttpRequest를 사용하는 것입니다.

이 API는 WAV, MP3, AAC, OGG 기타 형식의 오디오 파일 데이터 로드를 지원합니다. 다양한 오디오 형식에 관한 브라우저의 지원 여부는 다릅니다.

다음 스니펫은 사운드 샘플을 로드하는 방법을 보여줍니다.

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    // Decode asynchronously
    request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer = buffer;
    }, onError);
    }
    request.send();
}

오디오 파일 데이터는 텍스트가 아닌 바이너리이므로 요청의 responseType'arraybuffer'로 설정합니다. ArrayBuffers에 관한 자세한 내용은 이 XHR2 관련 도움말을 참고하세요.

디코딩되지 않은 오디오 파일 데이터가 수신되면 이후 디코딩을 위해 보관하거나 AudioContext decodeAudioData() 메서드를 사용하여 즉시 디코딩할 수 있습니다. 이 메서드는 request.response에 저장된 오디오 파일 데이터의 ArrayBuffer를 가져와 비동기식으로 디코딩합니다 (기본 JavaScript 실행 스레드를 차단하지 않음).

decodeAudioData()가 완료되면 디코딩된 PCM 오디오 데이터를 AudioBuffer로 제공하는 콜백 함수를 호출합니다.

소리 재생

간단한 오디오 그래프
간단한 오디오 그래프

하나 이상의 AudioBuffers가 로드되면 사운드를 재생할 준비가 된 것입니다. 개가 짖는 소리와 함께 AudioBuffer를 방금 로드했고 로드가 완료되었다고 가정해 보겠습니다. 그런 다음 다음 코드로 이 버퍼를 재생할 수 있습니다.

var context = new AudioContext();

function playSound(buffer) {
    var source = context.createBufferSource(); // creates a sound source
    source.buffer = buffer;                    // tell the source which sound to play
    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source.noteOn(0);                          // play the source now
}

playSound() 함수는 누군가가 키를 누르거나 마우스로 무언가를 클릭할 때마다 호출될 수 있습니다.

noteOn(time) 함수를 사용하면 게임 및 기타 시간에 민감한 애플리케이션에서 정확한 사운드 재생을 쉽게 예약할 수 있습니다. 하지만 이 예약이 제대로 작동하려면 사운드 버퍼가 미리 로드되어야 합니다.

Web Audio API 추상화

물론 이 특정 사운드 로드에 하드코딩되지 않은 더 일반적인 로드 시스템을 만드는 것이 좋습니다. 오디오 애플리케이션이나 게임에서 사용할 수많은 짧은 길이에서 중간 길이의 사운드를 처리하는 방법에는 여러 가지가 있습니다. 다음은 BufferLoader (웹 표준의 일부가 아님)를 사용하는 한 가지 방법입니다.

다음은 BufferLoader 클래스를 사용하는 방법의 예입니다. 두 개의 AudioBuffers를 만들어 보겠습니다. 로드되자마자 동시에 재생하겠습니다.

window.onload = init;
var context;
var bufferLoader;

function init() {
    context = new AudioContext();

    bufferLoader = new BufferLoader(
    context,
    [
        '../sounds/hyper-reality/br-jam-loop.wav',
        '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

    bufferLoader.load();
}

function finishedLoading(bufferList) {
    // Create two sources and play them both together.
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource();
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];

    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.noteOn(0);
    source2.noteOn(0);
}

시간 처리: 리듬으로 소리 재생

Web Audio API를 사용하면 개발자가 재생을 정확하게 예약할 수 있습니다. 이를 보여주기 위해 간단한 리듬 트랙을 설정해 보겠습니다. 가장 널리 알려진 드럼 키트 패턴은 다음과 같습니다.

간단한 록 드럼 패턴
간단한 록 드럼 패턴

4/4 박자에서 하이햇은 8분음표마다 연주되고 킥과 스네어는 4분음표마다 번갈아 연주됩니다.

kick, snare, hihat 버퍼를 로드했다고 가정하면 이를 실행하는 코드는 간단합니다.

for (var bar = 0; bar < 2; bar++) {
    var 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 (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
    }
}

여기서는 악보에 표시된 무제한 루프 대신 한 번만 반복합니다. playSound 함수는 다음과 같이 지정된 시간에 버퍼를 재생하는 메서드입니다.

function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.noteOn(time);
}

소리의 볼륨 변경

사운드에 대해 실행할 수 있는 가장 기본적인 작업 중 하나는 볼륨을 변경하는 것입니다. Web Audio API를 사용하면 볼륨을 조작하기 위해 AudioGainNode를 통해 소스를 대상으로 라우팅할 수 있습니다.

이득 노드가 있는 오디오 그래프
이득 노드가 있는 오디오 그래프

이 연결 설정은 다음과 같이 실행할 수 있습니다.

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

그래프가 설정된 후 다음과 같이 gainNode.gain.value를 조작하여 볼륨을 프로그래매틱 방식으로 변경할 수 있습니다.

// Reduce the volume.
gainNode.gain.value = 0.5;

두 사운드 간의 크로스페이딩

이제 여러 사운드를 재생하지만 사운드 간에 크로스페이드를 적용하려는 약간 더 복잡한 시나리오를 가정해 보겠습니다. 이는 두 개의 턴테이블이 있고 한 음원에서 다른 음원으로 패닝할 수 있어야 하는 DJ와 같은 애플리케이션에서 흔히 발생하는 사례입니다.

다음 오디오 그래프로 이 작업을 수행할 수 있습니다.

게인 노드를 통해 연결된 두 소스가 있는 오디오 그래프
게인 노드를 통해 연결된 2개의 소스가 있는 오디오 그래프

이렇게 설정하려면 두 개의 AudioGainNodes를 만들고 다음 함수를 사용하여 노드를 통해 각 소스를 연결합니다.

function createSource(buffer) {
    var source = context.createBufferSource();
    // Create a gain node.
    var 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
    };
}

동일한 전력 크로스페이딩

기본 선형 크로스페이드 접근방식에서는 샘플을 이동할 때 볼륨이 낮아집니다.

선형 크로스페이드
선형 크로스페이드

이 문제를 해결하기 위해 상응하는 게인 곡선이 비선형이고 더 높은 진폭에서 교차하는 등호 곡선을 사용합니다. 이렇게 하면 오디오 영역 간의 볼륨 감소가 최소화되어 수준이 약간 다를 수 있는 영역 간에 더 균일한 크로스페이드가 발생합니다.

동일한 전력 크로스페이드
동일한 전력 교차 페이드

재생목록 크로스페이딩

또 다른 일반적인 크로스페이더 애플리케이션은 음악 플레이어 애플리케이션용입니다. 노래가 바뀔 때 불편한 전환을 방지하기 위해 현재 트랙을 페이드 아웃하고 새 트랙을 페이드 인해야 합니다. 이렇게 하려면 앞으로 크로스페이드를 예약합니다. setTimeout를 사용하여 일정을 예약할 수는 있지만 이는 정확하지 않습니다. Web Audio API를 사용하면 AudioParam 인터페이스를 사용하여 AudioGainNode의 이득 값과 같은 매개변수의 향후 값을 예약할 수 있습니다.

따라서 재생목록이 주어지면 현재 재생 중인 트랙의 게인 감소와 다음 트랙의 게인 증가를 모두 현재 트랙 재생이 끝나기 직전에 예약하여 트랙 간에 전환할 수 있습니다.

function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var 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.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) - 1000);
}

Web Audio API는 linearRampToValueAtTimeexponentialRampToValueAtTime와 같은 매개변수의 값을 점진적으로 변경하는 편리한 RampToValue 메서드 세트를 제공합니다.

전환 타이밍 함수는 위와 같이 기본 제공 선형 및 지수 함수 중에서 선택할 수 있지만 setValueCurveAtTime 함수를 사용하여 값 배열을 통해 고유한 값 곡선을 지정할 수도 있습니다.

사운드에 간단한 필터 효과 적용

BiquadFilterNode가 있는 오디오 그래프
BiquadFilterNode가 있는 오디오 그래프

Web Audio API를 사용하면 한 오디오 노드에서 다른 오디오 노드로 사운드를 파이핑하여 잠재적으로 복잡한 프로세서 체인을 만들어 사운드 양식에 복잡한 효과를 추가할 수 있습니다.

이를 위한 한 가지 방법은 사운드 소스와 대상 사이에 BiquadFilterNode를 배치하는 것입니다. 이 유형의 오디오 노드는 그래픽 이퀄라이저를 빌드하는 데 사용할 수 있는 다양한 저차 필터와 더 복잡한 효과를 빌드하는 데 사용할 수 있는 다양한 저차 필터를 실행할 수 있습니다. 주로 강조할 사운드의 주파수 스펙트럼 부분과 감쇠할 부분을 선택하는 것과 관련이 있습니다.

지원되는 필터 유형은 다음과 같습니다.

  • 저역 통과 필터
  • 고역 통과 필터
  • 대역 통과 필터
  • 저주파 필터
  • 높은 선반 필터
  • 피킹 필터
  • 노치 필터
  • 모두 통과 필터

모든 필터에는 일정량의 이득, 필터를 적용할 빈도, 품질 계수를 지정하는 매개변수가 포함되어 있습니다. 저역 필터는 낮은 주파수 범위를 유지하지만 높은 주파수는 삭제합니다. 중단점은 주파수 값에 의해 결정되며 Q 계수는 단위가 없으며 그래프의 모양을 결정합니다. 게인은 저역 및 피크 필터와 같은 특정 필터에만 영향을 미치며 이 저역 통과 필터에는 영향을 주지 않습니다.

사운드 샘플에서 베이스만 추출하는 간단한 로우 패스 필터를 설정해 보겠습니다.

// Create the filter
var 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);

일반적으로 인간의 청력 자체가 동일한 원리(즉, A4는 440hz, A5는 880hz)로 작동하므로 로그 스케일에서 작동하도록 주파수 컨트롤을 조정해야 합니다. 자세한 내용은 위의 소스 코드 링크에서 FilterSample.changeFrequency 함수를 참고하세요.

마지막으로, 샘플 코드를 사용하면 필터를 연결 및 연결 해제하여 AudioContext 그래프를 동적으로 변경할 수 있습니다. node.disconnect(outputNumber)를 호출하여 그래프에서 AudioNode의 연결을 해제할 수 있습니다. 예를 들어 필터를 통과하는 그래프의 경로를 직접 연결로 다시 변경하려면 다음을 실행하면 됩니다.

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

추가 청취

오디오 샘플을 로드하고 재생하는 등 API의 기본사항을 살펴봤습니다. 이득 노드와 필터로 오디오 그래프를 빌드하고, 일반적인 음향 효과를 사용 설정하기 위해 사운드 및 오디오 매개변수 조정을 예약했습니다. 이제 멋진 웹 오디오 애플리케이션을 빌드할 준비가 되었습니다.

아이디어를 얻고 싶다면 이미 많은 개발자가 Web Audio API를 사용하여 훌륭한 작품을 만들었습니다. 제가 특히 흥미롭게 생각하는 세션은 다음과 같습니다.

  • SoundCloud 퍼머링 링크를 사용하는 브라우저 내 사운드 스플라이싱 도구인 AudioJedit
  • 3D 블록을 쌓아 소리를 만드는 사운드 시퀀서인 ToneCraft
  • 웹 오디오 및 웹 소켓을 사용하는 공동 음악 제작 게임인 Plink