Worklet âm thanh hiện có sẵn theo mặc định

Hongchan Choi

Chrome 64 có một tính năng mới rất được mong đợi trong API Âm thanh trên web – AudioWorklet. Tại đây, bạn sẽ tìm hiểu các khái niệm và cách sử dụng để tạo trình xử lý âm thanh tuỳ chỉnh bằng mã JavaScript. Hãy xem các bản minh hoạ trực tiếp. Bài viết tiếp theo trong loạt bài này, Mẫu thiết kế Worklet âm thanh, có thể là một bài đọc thú vị để xây dựng một ứng dụng âm thanh nâng cao.

Nền: ScriptProcessorNode

Quá trình xử lý âm thanh trong API Âm thanh trên web chạy trong một luồng riêng biệt với luồng giao diện người dùng chính, nhờ đó, quá trình này sẽ diễn ra suôn sẻ. Để bật tính năng xử lý âm thanh tuỳ chỉnh trong JavaScript, Web Audio API đã đề xuất một ScriptProcessorNode sử dụng trình xử lý sự kiện để gọi tập lệnh của người dùng trong luồng giao diện người dùng chính.

Có hai vấn đề trong thiết kế này: việc xử lý sự kiện không đồng bộ theo thiết kế và quá trình thực thi mã diễn ra trên luồng chính. Luồng trước gây ra độ trễ và luồng sau gây áp lực cho luồng chính thường bị nhiều tác vụ liên quan đến giao diện người dùng và DOM làm cho giao diện người dùng "giật" hoặc âm thanh "lỗi". Do lỗi thiết kế cơ bản này, ScriptProcessorNode không còn được dùng trong quy cách và được thay thế bằng AudioWorklet.

Khái niệm

Audio Worklet giữ mã JavaScript do người dùng cung cấp trong luồng xử lý âm thanh. Điều đó có nghĩa là luồng này không phải chuyển sang luồng chính để xử lý âm thanh. Điều này có nghĩa là mã tập lệnh do người dùng cung cấp sẽ chạy trên luồng kết xuất âm thanh (AudioWorkletGlobalScope) cùng với các AudioNodes tích hợp khác, đảm bảo không có độ trễ bổ sung và kết xuất đồng bộ.

Phạm vi toàn cầu chính và sơ đồ phạm vi của Audio Worklet
Hình 1

Đăng ký và tạo bản sao

Việc sử dụng Audio Worklet bao gồm hai phần: AudioWorkletProcessorAudioWorkletNode. Cách này phức tạp hơn so với việc sử dụng ScriptProcessorNode, nhưng cần thiết để cung cấp cho nhà phát triển khả năng cấp thấp để xử lý âm thanh tuỳ chỉnh. AudioWorkletProcessor đại diện cho bộ xử lý âm thanh thực tế được viết bằng mã JavaScript và nằm trong AudioWorkletGlobalScope. AudioWorkletNode là đối tác của AudioWorkletProcessor và chịu trách nhiệm về kết nối đến và từ các AudioNodes khác trong luồng chính. Hàm này được hiển thị trong phạm vi toàn cục chính và hoạt động như một AudioNode thông thường.

Dưới đây là một cặp đoạn mã minh hoạ việc đăng ký và tạo bản sao.

// 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);
});

Để tạo AudioWorkletNode, bạn phải thêm đối tượng AudioContext và tên bộ xử lý dưới dạng chuỗi. Bạn có thể tải và đăng ký định nghĩa trình xử lý bằng lệnh gọi addModule() của đối tượng Audio Worklet mới. Các API Worklet, bao gồm cả Audio Worklet, chỉ có trong ngữ cảnh bảo mật, do đó, trang sử dụng các API này phải được phân phát qua HTTPS, mặc dù http://localhost được coi là an toàn để kiểm thử cục bộ.

Bạn có thể tạo lớp con AudioWorkletNode để xác định một nút tuỳ chỉnh được hỗ trợ bởi bộ xử lý chạy trên worklet.

// 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);

Phương thức registerProcessor() trong AudioWorkletGlobalScope nhận một chuỗi cho tên của trình xử lý cần đăng ký và định nghĩa lớp. Sau khi hoàn tất quá trình đánh giá mã tập lệnh trong phạm vi toàn cục, lời hứa từ AudioWorklet.addModule() sẽ được phân giải để thông báo cho người dùng rằng định nghĩa lớp đã sẵn sàng để sử dụng trong phạm vi toàn cục chính.

Thông số âm thanh tuỳ chỉnh

Một trong những tính năng hữu ích của AudioNodes là tính năng tự động hoá thông số có thể lên lịch bằng AudioParam. AudioWorkletNodes có thể sử dụng các thông số này để nhận các thông số hiển thị có thể được tự động kiểm soát ở tốc độ âm thanh.

Sơ đồ bộ xử lý và nút công việc âm thanh
Hình 2

Bạn có thể khai báo các tham số âm thanh do người dùng xác định trong phần khai báo lớp AudioWorkletProcessor bằng cách thiết lập một tập hợp AudioParamDescriptor. Công cụ WebAudio cơ bản sẽ thu thập thông tin này trong quá trình tạo AudioWorkletNode, sau đó tạo và liên kết các đối tượng AudioParam với nút tương ứng.

/* 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.
    }
  }
}

Phương thức AudioWorkletProcessor.process()

Quá trình xử lý âm thanh thực tế diễn ra trong phương thức gọi lại process() trong AudioWorkletProcessor. Người dùng phải triển khai phương thức này trong định nghĩa lớp. Công cụ WebAudio gọi hàm này theo cách đồng bộ để cung cấp đầu vào và tham số cũng như tìm nạp đầu ra.

/* 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;
}

Ngoài ra, bạn có thể sử dụng giá trị trả về của phương thức process() để kiểm soát thời gian hoạt động của AudioWorkletNode, nhờ đó nhà phát triển có thể quản lý mức sử dụng bộ nhớ. Việc trả về false từ phương thức process() sẽ đánh dấu trình xử lý không hoạt động và công cụ WebAudio không còn gọi phương thức này nữa. Để duy trì hoạt động của bộ xử lý, phương thức này phải trả về true. Nếu không, hệ thống sẽ thu thập rác cho cặp nút và bộ xử lý.

Giao tiếp hai chiều bằng MessagePort

Đôi khi, AudioWorkletNode tuỳ chỉnh muốn hiển thị các thành phần điều khiển không liên kết với AudioParam, chẳng hạn như thuộc tính type dựa trên chuỗi dùng để kiểm soát bộ lọc tuỳ chỉnh. Để thực hiện mục đích này và nhiều mục đích khác, AudioWorkletNodeAudioWorkletProcessor được trang bị MessagePort để giao tiếp hai chiều. Bạn có thể trao đổi mọi loại dữ liệu tuỳ chỉnh thông qua kênh này.

Fig.2
Hình 2

Bạn có thể truy cập vào MessagePort bằng thuộc tính .port trên cả nút và trình xử lý. Phương thức port.postMessage() của nút sẽ gửi một thông báo đến trình xử lý port.onmessage của bộ xử lý liên kết và ngược lại.

/* 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 hỗ trợ tính năng có thể chuyển, cho phép bạn chuyển bộ nhớ dữ liệu hoặc mô-đun WASM qua ranh giới luồng. Điều này mở ra vô số khả năng về cách sử dụng hệ thống Audio Worklet.

Hướng dẫn: Tạo GainNode

Dưới đây là ví dụ đầy đủ về GainNode được xây dựng dựa trên AudioWorkletNodeAudioWorkletProcessor.

Tệp 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>

Tệp 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);

Bài viết này trình bày những kiến thức cơ bản về hệ thống Audio Worklet. Bạn có thể xem các bản minh hoạ trực tiếp tại kho lưu trữ GitHub của nhóm WebAudio của Chrome.

Chuyển đổi tính năng: Thử nghiệm sang Ổn định

Audio Worklet được bật theo mặc định cho Chrome 66 trở lên. Trong Chrome 64 và 65, tính năng này nằm sau cờ thử nghiệm.