원문
https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API
웹오디오 API는 웹에서 오디오를 다루기위한 강력하고 다양한 시스템을 제공한다. 개발자들이 오디오 소스들을 선택할 수 있게 하고, 오디오에 이펙트를 적용 할수 있으며 오디오 시각화와 공간적인 이펙트(패닝같은)를 적용할 수 있고 이외에도 더 많은 작업을 할 수 있다.
웹오디오API는 오디오 처리를 오디오 컨텍스트 안에 하게 되고 모듈러 라우팅이 가능하도록 설계되어 있다. 기본 오디오 처리는 오디오 라우팅 그래프의 형태로 서로 연결된 오디오 노드들을 이용해 수행된다. 다른 타입의 채널 레이아웃을 가진 몇개의 소스들이 한개의 컨텍스트에서라도 제공된다. 이런 모듈러 디자인은 다이나믹 이펙트들을 이용해 복잡한 오디오 기능을 만드는데 유연함을 제공한다.
오디오 노드들은 체인과 인풋과 아웃풋들의해 간단한 웹(그래프)에 연결된다. 일반적으로 하나 이상의 소스로 시작되고 소스는 매우 작은 시간 단위의 소리 세기(샘플)의 배열을 제공하는데 보통 초당 수만개이다. 이것들은 수학적으로 계산될 수 있고(OscillatorNode), 사운드나 비디오 파일(AudioBufferSourceNode, MediaElementAudioSourceNode) 그리고 오디오 스트림(MediaStreamAudioSourceNode)에서 녹음될 수 있다. 사실 사운드 파일은 마이크나 전자악기를 통해 얻어지고 하나의 복잡한 웨이브로 섞여진 소리 세기들의 기록이다. 이런 노드들의 아웃풋은 다른 노드의 인풋에 연결되어 믹스되거나 사운드 샘플의 스트림을 수정해 다른 스트림으로 바꾸게 된다. 가장 일반적인 수정은 시끄럽거나 조용하게 만들기위해 값으로 샘플을 증감시키는 것이다(GainNode). 사운드가 충분히 의도된 효과로 처리가 되면 destination(AudioContext.destination)의 인풋에 링크되어 스피커나 해드폰으로 출력되게 된다. 유저가 오디오를 듣게 하려면 이 마지막 연결이 필요하다.
간단하고 일반적인 웹오디오 워크플로우는 아래와 같다.
타이밍은 높은 정확도와 낮은 레이턴시로 컨트롤 조절되어 개발자가 정확하게 이벤트에 반응하고 정확한 샘플을 타겟으로 할 수 있다, 높은 샘플레이트에도 말이다. 그래서 드럼머신이나 시퀀서같은 어플리케이션에도 충분히 사용 가능하다.
웹오디오API는 오디오의 공간에 대한 컨트롤이 가능하다. 소스-리스너 모델을 이용하면 패닝을 조절하거나 거리에 의한 변화 혹은 움직이난 소스(혹은 움직이는 리스너)에 의한 도플러 효과를 적용 할수 있다.
Note: 웹오디오API의 이론에 관한 더 자세한 내용은 Basic concepts behind Web Audio API 아티클에서 확인 할 수 있다.
웹오디오API는 총 28개의 인터페이스와 연관된 이벤트들이 있다. 기능별로 9개의 카테고리로 구분해봤다.
웹오디오API에서 오디오 그래프를 만들 때 필요한 일반적인 컨테이너와 정의
AudioContext 인터페이스는 AudioNode로 표현되는 오디오 모듈들이 서로 연결되어 구성된 오디오 프로세싱 그래프를 뜻한다. 오디오 컨텍스트는 보유하고 노드들의 생성과 오디오 처리의 실행 혹은 디코딩을 컨트롤한다. 컨텍스트를 이용해 어떤 일을 하려고 한다면 무엇보다 먼저 AudioContext를 생성해야 한다.
AudioNode 인터페이스는 audio source(HTML
AudioParam 인터페이스는 AudioNode와 같이 오디오와 관련된 파라메터를 뜻한다. 명확한 값이나 값의 변화가 될 수 있고 특정한 시간에 동작하게 하거나 특정한 패턴을 따르도록 설정될 수 있다.
ended이벤트는 미디어가 끝까지 재생되어 재생이 멈춰졌을 때 발생한다.
웹오디오API에서 사용되는 오디오 소스들을 정의한 인터페이스
OscillatorNode 인터페이스는 사인파를 뜻한다. 주어진 주파수에 해당하는 사인파를 발생시키는 오디오 처리 모듈이다.
AudioBuffer 인터페이스는 메모리에 존재하는 작은 오디어 데이터 조각을 뜻하는데 이런 조각은 AudioContext.decodeAudioData() 메소드를 이용해서 오디오 파일로 부터 만들어지거나 AudioContext.createBuffer()를 이용한 raw 데이터로 만들어진다. 일단 이런 형태로 디코딩 되고 나면 오디오는 AudioBufferSourceNode에 들어가게 된다.
AudioBufferSourceNode 인터페이스는 AudioBuffer에 저장된 메모리상의 오디오 데이터로 구성된 오디오소스를 뜻하고 오디오 소스 역할을 하는 AudioNode이다.
MediaElementAudioSourceNode 인터페이스는 HTML5
MediaStreamAudioSourceNode 인터페이스는 WebRTC MediaStream(웹캠이나 마이크)으로 구성된 오디오소스이고 오디오 소스 역할을 하는 AudioNode이다.
오디오 소스에 적용 할 수 있는 이펙트 인터페이스
간단한 저수준의 필터 AudioNode로 음색을 조절하는 디바이스나 그래픽 이퀄라이저같은 여러종류의 필터를 뜻한다. BiquadFilterNode는 언제하나 한개의 인풋과 한개의 아웃풋을 가진다.
주어진 AudioBuffer에 Linear Convolution을 수행하는 AudioNode이다 리버브를 구현할때 자주 사용된다.
딜레이를 구현하는 AudioNode로 인풋으로 들어오는 데이터에 딜레이를 걸어 아웃풋으로 전달한다.
컴프레션 이펙트를 제공하는 인터페이스로 다수의 사운드혹은 중첩되어 한번에 플레이될때 발생되는 클리핑이나 왜곡을 피하기위해 신호의 음량의 큰 파트의 음량을 줄여 주는 이팩트이다.
불륨을 조절할 수 있게 해주는 인터페이스로 주어진 gain을 인풋 데이터에 적용해 그결과를 아웃풋에 전달한다
간단히 스테레오 패닝을 다룰수 있는 인터페이스로 오디오 스트림을 left 혹은 right로 패닝하는데 사용된다.
non-linear 디스토션을 제공하는 인터페이스로 커브를 이용해 신호에 waveshaping 디스토션을 적용한다. 분명히 디스토션 이펙트이긴 하지만 신호에 따듯한 느낌을 더하기위해 자주 사용된다.
주기적인 파형을 정의할때 사용되며 이는 OscilatorNode의 아웃풋을 다듬는데 사용된다.
모든 오디오 프로세스가 종료가 되면 audio destination을 통해 어디로 소리가 나가게 될지를 정의하게 된다.
주어진 컨텍스트안의 오디오소스의 최종 목적지를 뜻하는 인터페이스로 보통 스피커에 해당한다.
WebRTC MediaStream과 단일 AudioMediaStreamTrack로 구성된 audio destination을 뜻한다. Navigator.getUserMedia를 통해 얻어지는 MediaStream과 비슷한 방법으로 사용될 수 있다. audio destination역할을 하는 AudioNode이다.
오디오에서 시간, 주파수등 추가 데이터를 얻어내고 싶다면 AnalyserNode를 이용할 수 있다.
데이터 분석과 시각화를 위해 실시간 주파수나 시간기준 분석 정보를 제공해주는 노드이다.
오디오 채널을 합치거나 분할하고자 할때는 아래와 같은 인터페이스를 이용하면 된다.
오디오 소스의 각각의 다른 채널을 분리하여 개별적인 모노아웃풋으로 만든다.
분리된 다른 모노인풋들을 하나의 아웃풋으로 다시 합친다. 각각의 인풋은 아웃풋의 채널을 채우는데 사용된다.
오디오에 공간감을 제공하기위해 제공되는 이팩터.
오디오 공간감을 위해 오디오 씬을 듣는 고유한 사람의 방향과 위치를 뜻한다.
공간에서의 신호의 특징을 뜻한다. 오른손 좌표계를 이용해서 위치를 기술하고 벨로시티 백터를 이용해 움직임을 그리고 directionality cone을 이용해 방향성을 기술한다.
외부의 스크립트를 통해 오디오소스를 처리하고자 한다면 아래의 노드와 이벤트를 이용하면 된다.
Note: 2014년 8월 29일 웹오디오 API 스펙에 따르면 이 기능은 deprecated되는것으로 표시됬다. 이 기능은 곧 AudioWorker로 대체된다.
자바스크립트를 이용해 오디오의 생성, 처리 분석을 제공한다 현재의 인풋을 포함하는 버퍼와 아웃풋을 포함하는 버퍼로 연결되는 AudioNode다. 인풋버퍼에 새로운 데이터가 들어올 때마다 객체에 AudioProcessingEvent 인터페이스의 이벤트가 전달되고 이벤트 핸들러는 아웃풋 핸들러에 데이터를 채우고 종료한다.
웹오디오API의 ScriptProcessorNode의 인풋버퍼가 처리를 할 준비가 되면 발생한다.
AudioProcessingEvent 는 ScriptProcessorNode의 인풋 버퍼가 처리를 할 준비가 되면 발생되는 이벤트를 제공한다.
백그라운드 작업을 통해 오디오그래프의 처리나 렌더링을 빠르게 할수있다.(디바이스장치 보다는 AudioBuffer로 렌더링 하는등)
OfflineAudioContext도 AudioContext와 동일하게 AudioNode로 연결된 오디오 처리 그래프이다. 일반 AudioContext와의 차이점은 OfflineAudioContext는 실제 오디오로 렌더링 되는것이 아니라 최대한 빠르게 버퍼로 만들어낸다.
complete이벤트는 OfflineAudioContext의 렌더링이 중단되었을 때 발생한다.
OfflineAudioCompletionEvent는 OfflineAudioContext의 렌더링이 종료되었을 때 발생하는 이벤트들로 complete이벤트가 이 인터페이스를 구현한다.
오디오워커는 웹워커 컨텍스트에서 오디오 프로세싱을 직접 처리하는 방법을 제공한다. 몇개의 인터페이스로 구현 됬는데(2014년 8월 29일부터) 아직 브라우저에서 구현되진 않았다. 만약 구현이 되면 ScriptProcessorNode과 위에 언급한 오디오 처리 섹션을 대체하게 될것이다.
AudioWorkerNode는 워커 쓰레드로 오디오를 직접 생성하고 처리하고 분석하는 AudioNode로 표현되는 인터페이스이다.
AudioWorkerGlobalScope 인터페이스는 오디오 처리 스크립트가 실행되는 워커컨텍스트로 포현되는 DedicatedWorkerGlobalScope-derived 오브젝트이다 워커 스레드 안에서 자바스크립트를 이용해 오디오데이터를 생성하거나 처리하거나 분석할 수 있또록 설계되었다.
AudioWorkerGlobalScope에게 오디오 처리를 수행하도록 발생되는 이벤트이다.
아래의 인터페이서는 오래된 웹오디오 API 스펙에 정의 되었다가 다른 인터페이스에 의해 더이상 사용하지 않는 인터페이스이다.
자바스크립트로 오디오 프로세스를 다루기 위해 사용 되었다가 ScriptProcessorNode로 대체되었다.
주기적인 파형을 정의하기 위해 사용되었다가 PeriodicWave로 대체되었다.
이 예제는 웹오디오API의 다양한 기능들을 확인할 수 있고 Voice-change-o-matic 데모에서 동작하는 모습을 볼 수 있다. 이 데모는 목소리의 음을 낮추는 실험적인 목소리 변환기이다.
// define audio context
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// Webkit/blink browsers need prefix, Safari won't work without window.
// select box for selecting voice effect options
var voiceSelect = document.getElementById("voice");
// select box for selecting audio visualization options
var visualSelect = document.getElementById("visual");
var mute = document.querySelector('.mute'); // mute button
var drawVisual; // requestAnimationFrame
var analyser = audioCtx.createAnalyser();
var distortion = audioCtx.createWaveShaper();
var gainNode = audioCtx.createGain();
var biquadFilter = audioCtx.createBiquadFilter();
// function to make curve shape for distortion/wave shaper node to use
function makeDistortionCurve(amount) {
var k = typeof amount === 'number' ? amount : 50,
n_samples = 44100,
curve = new Float32Array(n_samples),
deg = Math.PI / 180,
i = 0,
x;
for ( ; i < n_samples; ++i ) {
x = i * 2 / n_samples - 1;
curve[i] = ( 3 + k ) * x * 20 * deg / ( Math.PI + k * Math.abs(x) );
}
return curve;
};
navigator.getUserMedia (
// constraints - only audio needed for this app
{
audio: true
},
// Success callback
function(stream) {
source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
analyser.connect(distortion);
distortion.connect(biquadFilter);
biquadFilter.connect(gainNode);
// connecting the different audio graph nodes together
gainNode.connect(audioCtx.destination);
visualize(stream);
voiceChange();
},
// Error callback
function(err) {
console.log('The following gUM error occured: ' + err);
}
);
function visualize(stream) {
WIDTH = canvas.width;
HEIGHT = canvas.height;
var visualSetting = visualSelect.value;
console.log(visualSetting);
if(visualSetting == "sinewave") {
analyser.fftSize = 2048;
var bufferLength = analyser.frequencyBinCount; // half the FFT value
var dataArray = new Uint8Array(bufferLength); // create an array to store the data
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
function draw() {
drawVisual = requestAnimationFrame(draw);
// get waveform data and put it into the array created above
analyser.getByteTimeDomainData(dataArray);
canvasCtx.fillStyle = 'rgb(200, 200, 200)'; // draw wave with canvas
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = 'rgb(0, 0, 0)';
canvasCtx.beginPath();
var sliceWidth = WIDTH * 1.0 / bufferLength;
var x = 0;
for(var i = 0; i < bufferLength; i++) {
var v = dataArray[i] / 128.0;
var y = v * HEIGHT/2;
if(i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height/2);
canvasCtx.stroke();
};
draw();
} else if(visualSetting == "off") {
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
canvasCtx.fillStyle = "red";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
}
}
function voiceChange() {
distortion.curve = new Float32Array;
// reset the effects each time the voiceChange function is run
biquadFilter.gain.value = 0;
var voiceSetting = voiceSelect.value;
console.log(voiceSetting);
if(voiceSetting == "distortion") {
// apply distortion to sound using waveshaper node
distortion.curve = makeDistortionCurve(400);
} else if(voiceSetting == "biquad") {
biquadFilter.type = "lowshelf";
biquadFilter.frequency.value = 1000;
biquadFilter.gain.value = 25; // apply lowshelf filter to sounds using biquad
} else if(voiceSetting == "off") {
console.log("Voice settings turned off"); // do nothing, as off option was chosen
}
}
// event listeners to change visualize and voice settings
visualSelect.onchange = function() {
window.cancelAnimationFrame(drawVisual);
visualize(stream);
}
voiceSelect.onchange = function() {
voiceChange();
}
mute.onclick = voiceMute;
function voiceMute() { // toggle to mute and unmute sound
if(mute.id == "") {
gainNode.gain.value = 0; // gain set to 0 to mute sound
mute.id = "activated";
mute.innerHTML = "Unmute";
} else {
gainNode.gain.value = 1; // gain set to 1 to unmute sound
mute.id = "";
mute.innerHTML = "Mute";
}
}