웹 오디오 API 시작하기

HTML5 <audio> 요소 이전에는 웹의 침묵을 깨기 위해 Flash나 다른 플러그인이 필요했습니다. 웹의 오디오에는 더 이상 플러그인이 필요하지 않지만 오디오 태그에는 정교한 게임 및 대화형 애플리케이션을 구현하기 위한 상당한 제한사항이 있습니다.

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

AudioContext 시작하기

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

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) 함수를 사용하면 게임 및 시간이 중요한 기타 애플리케이션의 정확한 사운드 재생을 쉽게 예약할 수 있습니다. 그러나 이 예약이 제대로 작동하려면 사운드 버퍼가 미리 로드되어 있어야 합니다.

웹 오디오 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번 번갈아 연주합니다.

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와 유사한 애플리케이션의 일반적인 사례로, 두 개의 턴테이블이 있고 한 사운드 소스에서 다른 음원으로 이동할 수 있습니다.

다음 오디오 그래프를 사용하면 됩니다.

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

이를 설정하려면 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의 기본사항을 설명했습니다. Google은 몇 가지 일반적인 사운드 효과를 지원하기 위해 게인 노드 및 필터와 예약된 사운드 및 오디오 매개변수 조정을 사용하여 오디오 그래프를 빌드했습니다. 이제 멋진 웹 오디오 애플리케이션을 빌드할 준비가 되었습니다.

아이디어를 얻고 싶다면 이미 많은 개발자가 Web Audio API를 사용하여 훌륭한 작업을 만든 경험이 있습니다. 제가 좋아하는 몇 가지는 다음과 같습니다.

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