Nhập Worklet âm thanh

Hongchan Choi

Chrome 64 đi kèm với một tính năng mới rất được mong đợi trong API Web âm thanh – AudioWorklet. Bài viết này giới thiệu khái niệm và cách sử dụng dành cho những người muốn tạo trình xử lý âm thanh tuỳ chỉnh bằng mã JavaScript. Vui lòng xem bản minh hoạ trực tiếp trên GitHub. Ngoài ra, bài viết tiếp theo trong loạt bài viết Mẫu thiết kế Worklet âm thanh có thể là một bài viết thú vị về cách tạo ứng dụng âm thanh nâng cao.

Nền: ScriptProcessorNode

Tính năng xử lý âm thanh trong API Web âm thanh chạy trong một luồng riêng biệt với luồng giao diện người dùng chính nên hoạt động trơn tru. Để 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 các trình xử lý sự kiện để gọi tập lệnh người dùng trong luồng giao diện người dùng chính.

Có 2 vấn đề trong thiết kế này: quá trình 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. Giao diện người dùng gây ra độ trễ, còn giao thức thứ hai gây áp lực cho luồng chính, vốn thường có nhiều tác vụ liên quan đến giao diện người dùng và DOM, khiến giao diện người dùng bị "giật" hoặc âm thanh "sự cố". Do lỗi thiết kế cơ bản này, ScriptProcessorNode không còn được dùng trong phần thông số kỹ thuật và thay thế bằng AudioWorklet.

Khái niệm

Audio Worklet lưu giữ toàn bộ mã JavaScript do người dùng cung cấp trong luồng xử lý âm thanh – tức là 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 AudioNode tích hợp khác, giúp đảm bảo không có độ trễ bổ sung và kết xuất đồng bộ.

Phạm vi toàn cục chính và biểu đồ phạm vi của Audio Worklet
Hình.1

Đăng ký và tạo thực thể

Quy trình sử dụng Audio Worklet bao gồm 2 phần: AudioWorkletProcessor và AudioWorkletNode. Việc này phức tạp hơn so với việc sử dụng ScriptProcessorNode nhưng việc này là cần thiết để cung cấp cho các nhà phát triển khả năng xử lý âm thanh tuỳ chỉnh ở cấp thấp. AudioWorkletProcessor đại diện cho trình xử lý âm thanh thực tế được viết bằng mã JavaScript và nằm trong AudioWorkletGlobalScope. AudioWorkletNode là phiên bản tương ứng của AudioWorkletProcessor và đảm nhận việc kết nối đến và đi từ các AudioNode khác trong luồng chính. Lớp này hiển thị trong phạm vi toàn cục chính và có các chức năng như 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 thực thể.

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

Việc tạo AudioWorkletNode cần ít nhất 2 thứ: đối tượng AudioContext và tên bộ xử lý dưới dạng một chuỗi. Định nghĩa trình xử lý có thể được tải và đăng ký 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ỉ hoạt động trong ngữ cảnh bảo mật. Do đó, một 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à phương thức bảo mật để kiểm thử cục bộ.

Ngoài ra, bạn cũng có thể lưu ý rằng bạn có thể phân lớp con AudioWorkletNode để xác định một nút tuỳ chỉnh được hỗ trợ bởi bộ xử lý đang chạy trên 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);

Phương thức registerProcessor() trong AudioWorkletGlobalScope lấy một chuỗi tên bộ xử lý cần đăng ký và phần định nghĩa lớp. Sau khi hoàn tất quá trình đánh giá mã tập lệnh ở phạm vi toàn cầu, lời hứa từ AudioWorklet.addModule() sẽ được giải quyết để 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.

AudioParam tuỳ chỉnh

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

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

Bạn có thể khai báo AudioParams do người dùng xác định trong định nghĩa lớp AudioWorkletProcessor bằng cách thiết lập một tập hợp các AudioParamDescriptors. Công cụ WebAudio cơ sở sẽ nhận thông tin này trong quá trình xây dựng 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 và người dùng phải triển khai phương thức này trong phần định nghĩa lớp. Công cụ WebAudio sẽ gọi hàm này theo chế độ đẳng thời gian để cấp dữ liệu cho dữ liệu đầu vào, tham số và đầu ra tìm nạp.

/* 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 vòng đời của AudioWorkletNode sao cho 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 bộ xử lý là không hoạt động và công cụ WebAudio sẽ không gọi phương thức này nữa. Để bộ xử lý tiếp tục hoạt động, phương thức này phải trả về true. Nếu không, cặp nút/bộ xử lý sẽ bị hệ thống thu gom rác.

Giao tiếp hai chiều với MessagePort

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

Fig.2
Hình.2

Bạn có thể truy cập MessagePort thông qua thuộc tính .port trên cả nút và bộ xử lý. Phương thức port.postMessage() của nút này 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!');
});
/* "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);

Ngoài ra, xin lưu ý rằng MessagePort hỗ trợ Transferable, 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ừng bước: tạo GainNode

Sau đây là một ví dụ hoàn chỉnh về GainNode được tạo dựa trên AudioWorkletNode và AudioWorkletProcessor.

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>

đạt được-vi xử lý.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);

Đây là nội dung cơ bản của hệ thống Audio Worklet. Các bản minh hoạ trực tiếp có trên kho lưu trữ GitHub của nhóm Chrome WebAudio.

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

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