Chrome 64에는 Web Audio API의 새로운 기능인 AudioWorklet이 포함되어 있습니다. 여기에서는 JavaScript 코드로 맞춤 오디오 프로세서를 만드는 개념과 사용법을 알아봅니다. 라이브 데모를 살펴보세요. 고급 오디오 앱을 빌드하는 데 관심이 있다면 시리즈의 다음 도움말인 오디오 워크렛 디자인 패턴을 읽어보세요.
배경: ScriptProcessorNode
Web Audio API의 오디오 처리는 기본 UI 스레드와 별도의 스레드에서 실행되므로 원활하게 실행됩니다. JavaScript에서 맞춤 오디오 처리를 사용 설정하기 위해 Web Audio API는 이벤트 핸들러를 사용하여 기본 UI 스레드에서 사용자 스크립트를 호출하는 ScriptProcessorNode를 제안했습니다.
이 설계에는 두 가지 문제가 있습니다. 이벤트 처리는 설계상 비동기식이며 코드 실행은 기본 스레드에서 이루어집니다. 전자는 지연을 유도하고 후자는 일반적으로 다양한 UI 및 DOM 관련 작업으로 가득 찬 기본 스레드에 부담을 주어 UI가 '버벅거림' 또는 오디오가 '글리치'가 발생합니다. 이러한 근본적인 설계 결함으로 인해 ScriptProcessorNode
는 사양에서 지원 중단되고 AudioWorklet으로 대체되었습니다.
개념
오디오 워크렛은 사용자 제공 JavaScript 코드를 모두 오디오 처리 스레드 내에 유지합니다. 즉, 오디오를 처리하기 위해 기본 스레드로 이동할 필요가 없습니다. 즉, 사용자가 제공한 스크립트 코드는 다른 내장 AudioNodes
와 함께 오디오 렌더링 스레드 (AudioWorkletGlobalScope
)에서 실행되므로 추가 지연 시간과 동기 렌더링이 없습니다.
등록 및 인스턴스화
오디오 워크렛 사용은 AudioWorkletProcessor
및 AudioWorkletNode
의 두 부분으로 구성됩니다. 이는 ScriptProcessorNode를 사용하는 것보다 복잡하지만 개발자에게 맞춤 오디오 처리를 위한 하위 수준 기능을 제공하는 데 필요합니다. AudioWorkletProcessor
는 JavaScript 코드로 작성된 실제 오디오 프로세서를 나타내며 AudioWorkletGlobalScope
에 있습니다.
AudioWorkletNode
는 AudioWorkletProcessor
의 대응 항목이며 기본 스레드에서 다른 AudioNodes
와의 연결을 처리합니다. 기본 전역 범위에 노출되며 일반 AudioNode
와 같이 작동합니다.
다음은 등록 및 인스턴스화를 보여주는 코드 스니펫 쌍입니다.
// The code in the main global scope.
class MyWorkletNode extends AudioWorkletNode {
constructor(context) {
super(context, 'my-worklet-processor');
}
}
let context = new AudioContext();
context.audioWorklet.addModule('processors.js').then(() => {
let node = new MyWorkletNode(context);
});
AudioWorkletNode
를 만들려면 AudioContext 객체와 프로세서 이름을 문자열로 추가해야 합니다. 프로세서 정의는 새 오디오 워크렛 객체의 addModule()
호출을 통해 로드되고 등록될 수 있습니다.
오디오 워크렛을 비롯한 워크렛 API는 보안 컨텍스트에서만 사용할 수 있으므로 이를 사용하는 페이지는 HTTPS를 통해 제공되어야 합니다. http://localhost
는 로컬 테스트를 위한 보안으로 간주되지만 예외는 아닙니다.
AudioWorkletNode
를 서브클래스화하여 워크렛에서 실행되는 프로세서가 지원하는 맞춤 노드를 정의할 수 있습니다.
// This is the "processors.js" file, evaluated in AudioWorkletGlobalScope
// upon audioWorklet.addModule() call in the main global scope.
class MyWorkletProcessor extends AudioWorkletProcessor {
constructor() {
super();
}
process(inputs, outputs, parameters) {
// audio processing code here.
}
}
registerProcessor('my-worklet-processor', MyWorkletProcessor);
AudioWorkletGlobalScope
의 registerProcessor()
메서드는 등록할 프로세서의 이름과 클래스 정의를 위한 문자열을 취합니다.
전역 범위에서 스크립트 코드 평가가 완료되면 AudioWorklet.addModule()
의 약속이 확인되어 사용자에게 클래스 정의가 기본 전역 범위에서 사용할 준비가 되었다고 알립니다.
맞춤 오디오 매개변수
AudioNode의 유용한 점 중 하나는 AudioParam
를 사용한 예약 가능한 매개변수 자동화입니다. AudioWorkletNodes는 이를 사용하여 오디오 속도로 자동으로 제어할 수 있는 노출된 매개변수를 가져올 수 있습니다.
사용자 정의 오디오 매개변수는 AudioParamDescriptor
집합을 설정하여 AudioWorkletProcessor
클래스 정의에서 선언할 수 있습니다. 기본 WebAudio 엔진은 AudioWorkletNode를 생성하는 동안 이 정보를 가져와서 적절하게 AudioParam
객체를 만들고 노드에 연결합니다.
/* A separate script file, like "my-worklet-processor.js" */
class MyWorkletProcessor extends AudioWorkletProcessor {
// Static getter to define AudioParam objects in this custom processor.
static get parameterDescriptors() {
return [{
name: 'myParam',
defaultValue: 0.707
}];
}
constructor() { super(); }
process(inputs, outputs, parameters) {
// |myParamValues| is a Float32Array of either 1 or 128 audio samples
// calculated by WebAudio engine from regular AudioParam operations.
// (automation methods, setter) Without any AudioParam change, this array
// would be a single value of 0.707.
const myParamValues = parameters.myParam;
if (myParamValues.length === 1) {
// |myParam| has been a constant value for the current render quantum,
// which can be accessed by |myParamValues[0]|.
} else {
// |myParam| has been changed and |myParamValues| has 128 values.
}
}
}
AudioWorkletProcessor.process()
메서드
실제 오디오 처리는 AudioWorkletProcessor
의 process()
콜백 메서드에서 이루어집니다. 클래스 정의에서 사용자가 구현해야 합니다. WebAudio 엔진은 이 함수를 동기식으로 호출하여 입력과 매개변수를 제공하고 출력을 가져옵니다.
/* AudioWorkletProcessor.process() method */
process(inputs, outputs, parameters) {
// The processor may have multiple inputs and outputs. Get the first input and
// output.
const input = inputs[0];
const output = outputs[0];
// Each input or output may have multiple channels. Get the first channel.
const inputChannel0 = input[0];
const outputChannel0 = output[0];
// Get the parameter value array.
const myParamValues = parameters.myParam;
// if |myParam| has been a constant value during this render quantum, the
// length of the array would be 1.
if (myParamValues.length === 1) {
// Simple gain (multiplication) processing over a render quantum
// (128 samples). This processor only supports the mono channel.
for (let i = 0; i < inputChannel0.length; ++i) {
outputChannel0[i] = inputChannel0[i] * myParamValues[0];
}
} else {
for (let i = 0; i < inputChannel0.length; ++i) {
outputChannel0[i] = inputChannel0[i] * myParamValues[i];
}
}
// To keep this processor alive.
return true;
}
또한 process()
메서드의 반환 값을 사용하여 AudioWorkletNode
의 전체 기간을 제어할 수 있으므로 개발자가 메모리 사용량을 관리할 수 있습니다. process()
메서드에서 false
를 반환하면 프로세서가 비활성 상태로 표시되고 WebAudio
엔진이 더 이상 메서드를 호출하지 않습니다. 프로세서를 활성 상태로 유지하려면 메서드가 true
를 반환해야 합니다.
그렇지 않으면 노드와 프로세서 쌍이 결국 시스템에서 가비지 컬렉션됩니다.
MessagePort와의 양방향 통신
커스텀 AudioWorkletNode
가 AudioParam
에 매핑되지 않는 컨트롤을 노출하려고 할 때도 있습니다(예: 커스텀 필터를 제어하는 데 사용되는 문자열 기반 type
속성). 이 목적을 위해 AudioWorkletNode
및 AudioWorkletProcessor
에는 양방향 통신을 위한 MessagePort
가 장착되어 있습니다. 이 채널을 통해 모든 종류의 맞춤 데이터를 교환할 수 있습니다.
MessagePort는 노드와 프로세서 모두에서 .port
속성을 사용하여 액세스할 수 있습니다. 노드의 port.postMessage()
메서드는 연결된 프로세서의 port.onmessage
핸들러에 메시지를 전송하고 그 반대로도 전송합니다.
/* The code in the main global scope. */
context.audioWorklet.addModule('processors.js').then(() => {
let node = new AudioWorkletNode(context, 'port-processor');
node.port.onmessage = (event) => {
// Handling data from the processor.
console.log(event.data);
};
node.port.postMessage('Hello!');
});
/* "processors.js" file. */
class PortProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.port.onmessage = (event) => {
// Handling data from the node.
console.log(event.data);
};
this.port.postMessage('Hi!');
}
process(inputs, outputs, parameters) {
// Do nothing, producing silent output.
return true;
}
}
registerProcessor('port-processor', PortProcessor);
MessagePort
는 전송 가능한 것을 지원하므로 스레드 경계를 통해 데이터 저장소 또는 WASM 모듈을 전송할 수 있습니다. 이렇게 하면 오디오 워크렛 시스템을 사용하는 방법에 관한 수많은 가능성이 열립니다.
둘러보기: GainNode 빌드
다음은 AudioWorkletNode
및 AudioWorkletProcessor
위에 빌드된 GainNode의 전체 예입니다.
index.html
파일:
<!doctype html>
<html>
<script>
const context = new AudioContext();
// Loads module script with AudioWorklet.
context.audioWorklet.addModule('gain-processor.js').then(() => {
let oscillator = new OscillatorNode(context);
// After the resolution of module loading, an AudioWorkletNode can be
// constructed.
let gainWorkletNode = new AudioWorkletNode(context, 'gain-processor');
// AudioWorkletNode can be interoperable with other native AudioNodes.
oscillator.connect(gainWorkletNode).connect(context.destination);
oscillator.start();
});
</script>
</html>
gain-processor.js
파일:
class GainProcessor extends AudioWorkletProcessor {
// Custom AudioParams can be defined with this static getter.
static get parameterDescriptors() {
return [{ name: 'gain', defaultValue: 1 }];
}
constructor() {
// The super constructor call is required.
super();
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const gain = parameters.gain;
for (let channel = 0; channel < input.length; ++channel) {
const inputChannel = input[channel];
const outputChannel = output[channel];
if (gain.length === 1) {
for (let i = 0; i < inputChannel.length; ++i)
outputChannel[i] = inputChannel[i] * gain[0];
} else {
for (let i = 0; i < inputChannel.length; ++i)
outputChannel[i] = inputChannel[i] * gain[i];
}
}
return true;
}
}
registerProcessor('gain-processor', GainProcessor);
오디오 워크렛 시스템의 기본사항을 다룹니다. 실시간 데모는 Chrome WebAudio팀의 GitHub 저장소에서 확인할 수 있습니다.
기능 전환: 실험용에서 안정화 버전으로
오디오 워크렛은 Chrome 66 이상에서 기본적으로 사용 설정되어 있습니다. Chrome 64 및 65에서는 이 기능이 실험 플래그 뒤에 있었습니다.