오디오 Worklet 입력

Hongchan Choi

Chrome 64는 많은 기대를 모은 Web Audio API의 새로운 기능인 AudioWorklet을 제공합니다. 이 도움말에서는 JavaScript 코드로 맞춤 오디오 프로세서를 만들려는 사용자를 위해 데이터 프로세서의 개념과 사용법을 소개합니다. GitHub에서 실시간 데모를 살펴보세요. 시리즈의 다음 문서인 오디오 Worklet 디자인 패턴도 고급 오디오 앱을 빌드하는 데 유용할 수 있습니다.

백그라운드: ScriptProcessorNode

Web Audio API의 오디오 처리는 기본 UI 스레드와 별도의 스레드에서 실행되므로 원활하게 실행됩니다. 자바스크립트에서 맞춤 오디오 처리를 사용 설정하기 위해 Web Audio API는 이벤트 핸들러를 사용하여 기본 UI 스레드에서 사용자 스크립트를 호출하는 ScriptProcessorNode를 제안했습니다.

이 설계에는 두 가지 문제가 있습니다. 이벤트 처리는 설계상 비동기식이고 코드 실행은 기본 스레드에서 발생합니다. 전자는 지연 시간을 유발하며, 후자는 일반적으로 다양한 UI 및 DOM 관련 작업으로 가득 찬 기본 스레드를 압박하여 UI가 '버벅거림'을 일으키거나 오디오에 '결함'을 일으킵니다. 이러한 기본적인 설계 결함으로 인해 ScriptProcessorNode는 사양에서 지원 중단되고 AudioWorklet으로 대체되었습니다.

개념

Audio Worklet은 사용자가 제공한 JavaScript 코드를 모두 오디오 처리 스레드 내에 원활하게 유지합니다. 즉, 오디오를 처리하기 위해 기본 스레드로 이동할 필요가 없습니다. 즉, 사용자 제공 스크립트 코드가 다른 내장 AudioNode와 함께 오디오 렌더링 스레드 (AudioWorkletGlobalScope)에서 실행되므로 추가 지연 시간이 없고 동기 렌더링이 가능합니다.

기본 전역 범위 및 오디오 Worklet 범위 다이어그램
그림 1

등록 및 인스턴스화

오디오 Worklet 사용은 AudioWorkletProcessor와 AudioWorkletNode라는 두 부분으로 구성됩니다. 이는 ScriptProcessorNode를 사용하는 것보다 더 복잡하지만 개발자에게 맞춤 오디오 처리를 위한 하위 수준의 기능을 제공하는 데 필요합니다. AudioWorkletProcessor는 JavaScript 코드로 작성된 실제 오디오 프로세서를 나타내며 AudioWorkletGlobalScope에 있습니다. AudioWorkletNode는 AudioWorkletProcessor의 상응 요소로, 기본 스레드의 다른 AudioNode와의 연결을 처리합니다. 기본 전역 범위에서 노출되며 일반 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 객체와 문자열인 프로세서 이름이 필요합니다. 프로세서 정의는 새 Audio Worklet 객체의 addModule() 호출을 통해 로드하고 등록할 수 있습니다. Audio Worklet을 포함한 Worklet API는 보안 컨텍스트에서만 사용할 수 있으므로 http://localhost가 로컬 테스트에서는 안전한 것으로 간주되지만 이를 사용하는 페이지는 HTTPS를 통해 제공되어야 합니다.

AudioWorkletNode를 서브클래스로 분류하여 Worklet에서 실행되는 프로세서가 지원하는 맞춤 노드를 정의할 수도 있습니다.

// This is "processor.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()의 프로미스가 확인되어 기본 전역 범위에서 클래스 정의를 사용할 준비가 되었음을 사용자에게 알립니다.

맞춤 AudioParam

AudioNode의 유용한 점 중 하나는 AudioParams를 사용한 예약 가능한 매개변수 자동화입니다. AudioWorkletNode는 이를 사용하여 오디오 속도에서 자동으로 제어할 수 있는 노출된 매개변수를 가져올 수 있습니다.

오디오 Worklet 노드 및 프로세서 다이어그램
그림 2

사용자 정의 AudioParams는 AudioParamDescriptors 집합을 설정하여 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가 제공됩니다. 이 채널을 통해 모든 종류의 커스텀 데이터를 교환할 수 있습니다.

Fig.2
그림 2

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!');
});
/* "processor.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 모듈을 전송할 수 있는 Transferable을 지원합니다. 이는 오디오 Worklet 시스템을 활용하는 방법에 관한 무수한 가능성을 열어줍니다.

둘러보기: GainNode 빌드

다음은 AudioWorkletNode 및 AudioWorkletProcessor를 기반으로 빌드된 GainNode의 전체 예입니다.

Index.html

<!doctype html>
<html>
<script>
  const context = new AudioContext();

  // Loads module script via 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>

게인 프로세서.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);

오디오 Worklet 시스템의 기본 사항을 다룹니다. 라이브 데모는 Chrome WebAudio팀의 GitHub 저장소에서 확인할 수 있습니다.

기능 전환: 실험용에서 안정화 버전으로 전환

Chrome 66 이상에서는 Audio Worklet이 기본적으로 사용 설정됩니다. Chrome 64 및 65에서는 이 기능이 실험용 플래그보다 중요했습니다.