웹 오디오 API 시작하기

HTML5 <audio> 요소 이전에는 웹의 침묵을 깨기 위해 Flash 또는 다른 플러그인이 필요했습니다. 웹에서 오디오를 재생하는 데 더 이상 플러그인이 필요하지 않지만 오디오 태그는 정교한 게임과 대화형 애플리케이션을 구현하는 데 상당한 제한을 가져옵니다.

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

AudioContext 시작하기

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

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 기반 브라우저의 경우 webkit 프리픽스를 사용합니다. webkitAudioContext

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를 사용하면 개발자가 재생을 정확하게 예약할 수 있습니다. 이를 보여주기 위해 간단한 리듬 트랙을 설정해 보겠습니다. 아마도 가장 널리 알려진 드럼 키트 패턴은 다음과 같습니다.

간단한 록 드럼 패턴

여기서 하이햇은 8분음표마다 재생되고 킥과 스네어는 4/4 박자에서 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를 통해 소스를 대상으로 라우팅하여 볼륨을 조작할 수 있습니다.

소스에서 GainNode를 거쳐 Destination으로 흐르는 오디오를 보여주는 간단한 흐름도

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

// 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와 같은 애플리케이션에서 흔히 발생하는 경우입니다. 턴테이블이 두 개 있고 한 사운드 소스에서 다른 사운드 소스로 패닝할 수 있기를 원합니다.

이 작업은 다음 오디오 그래프를 사용하여 실행할 수 있습니다.

별도의 게인 노드에 연결된 두 소스가 동일한 대상으로 라우팅되는 오디오 그래프

이를 설정하려면 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는 매개변수의 값을 점진적으로 변경하는 편리한 RampToValue 메서드 집합을 제공합니다. 예를 들어 linearRampToValueAtTimeexponentialRampToValueAtTime 등이 있습니다.

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

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

오디오가 소스에서 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를 사용하여 만들었습니다. 제가 가장 좋아하는 것은 다음과 같습니다.