Ses İş Akışına Girin

Hongchan Choi

Chrome 64, Web Audio API'sında heyecanla beklenen yeni bir özellik olan AudioWorklet'i sunuyor. Bu makalede, JavaScript kodu ile özel ses işlemcisi oluşturmak isteyenler için kavramı ve kullanımı tanıtılmaktadır. Lütfen GitHub'daki canlı demolara göz atın. Ayrıca, dizide yer alan bir sonraki makale olan Ses İş Akışı Tasarım Modeli, gelişmiş bir ses uygulaması oluşturmak için ilgi çekici bir kaynak olabilir.

Arka plan: ScriptProcessorNode

Web Audio API'de ses işleme, ana kullanıcı arayüzü iş parçacığından ayrı bir iş parçacığında çalıştığından sorunsuz şekilde çalışır. Web Audio API'si, JavaScript'te özel ses işlemeyi etkinleştirmek için ana kullanıcı arayüzü iş parçacığında kullanıcı komut dosyasını çağırmak üzere etkinlik işleyicileri kullanan bir ScriptProcessorNode önerisinde bulundu.

Bu tasarımda iki sorun vardır: Etkinlik işleme tasarım gereği eşzamansız ve kod yürütme ana iş parçacığında gerçekleşir. Birincisi gecikmeye neden olur. İkincisi ise çeşitli kullanıcı arayüzü ve DOM ile ilgili görevlerle genellikle dolu olan ana iş parçacığına baskı yapar. Bu durum, kullanıcı arayüzünün "donmasına" veya sesin "aksaklığa" neden olmasına neden olur. Bu temel tasarım hatası nedeniyle ScriptProcessorNode spesifikasyonu kullanımdan kaldırıldı ve AudioWorklet ile değiştirildi.

Kavramlar

Audio Worklet, kullanıcı tarafından sağlanan JavaScript kodunu ses işleme iş parçacığı içinde iyi bir şekilde tutar, yani sesi işlemek için ana iş parçacığına atlamasına gerek yoktur. Bu, kullanıcı tarafından sağlanan komut dosyası kodunun, diğer yerleşik AudioNode'larla birlikte ses işleme iş parçacığında (AudioWorkletGlobalScope) çalıştırılacağı anlamına gelir. Böylece, ek gecikme ve eşzamanlı oluşturma işlemi yapılmaz.

Ana global kapsam ve Ses İş Akışı kapsamı şeması
Şekil 1

Kayıt ve Destekleme

Audio Worklet'in kullanımı iki bölümden oluşur: AudioWorkletProcessor ve AudioWorkletNode. Bu, ScriptProcessorNode'un kullanılmasından daha karmaşıktır ancak geliştiricilere özel ses işleme için düşük düzeyli bir özellik sunmak için gereklidir. AudioWorkletProcessor, JavaScript kodunda yazılmış gerçek ses işlemcisini temsil eder ve AudioWorkletGlobalScope'ta bulunur. AudioWorkletNode, AudioWorkletProcessor'ın eşdeğeridir ve ana iş parçacığındaki diğer AudioNode'larla aralarındaki bağlantıyı üstlenir. Ana küresel kapsamda gösterilir ve normal bir AudioNode gibi çalışır.

Burada kayıt işlemini ve örneği gösteren bir çift kod snippet'i verilmiştir.

// 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 oluşturmak için en az iki şey gerekir: AudioContext nesnesi ve dize olarak işlemci adı. Yeni Ses İş Akışı nesnesinin addModule() çağrısıyla bir işlemci tanımı yüklenebilir ve kaydedilebilir. Audio Worklet içeren Worklet API'leri yalnızca güvenli bağlamda kullanılabilir. Bu nedenle, http://localhost yerel test için güvenli kabul edilse de API'leri kullanan bir sayfanın HTTPS üzerinden sunulması gerekir.

İş uygulamasında çalışan işlemci tarafından desteklenen özel bir düğüm tanımlamak için AudioWorkletNode alt sınıfını oluşturabilirsiniz.

// 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'taki registerProcessor() yöntemi, kaydedilecek işlemci adı ve sınıf tanımı için bir dize alır. Global kapsamda komut dosyası kod değerlendirmesi tamamlandıktan sonra, kullanıcılara sınıf tanımının ana küresel kapsamda kullanılmaya hazır olduğunu bildirmek için AudioWorklet.addModule() tarafından verilen vadetilecektir.

Özel AudioParam

AudioNodes ile ilgili faydalı şeylerden biri, AudioParams ile programlanabilir parametre otomasyonudur. AudioWorkletNodes bunları kullanarak ses hızında otomatik olarak kontrol edilebilen açık parametreleri alabilir.

Ses iş akışı düğümü ve işlemcisi şeması
Şekil 2

Kullanıcı tanımlı AudioParams, bir AudioParamDescriptors grubu ayarlanarak AudioWorkletProcessor sınıf tanımında bildirilebilir. Temel WebAudio motoru, bir AudioWorkletNode oluşturulduğunda bu bilgileri alır ve ardından AudioParam nesnelerini oluşturup buna uygun olarak düğüme bağlar.

/* 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() yöntemi

Gerçek ses işleme, AudioWorkletProcessor'daki process() geri çağırma yönteminde gerçekleşir ve bu işlemin kullanıcı tarafından sınıf tanımında uygulanması gerekir. WebAudio motoru, girişler ile parametreleri beslemek ve çıkışları getirmek için bu işlevi eş zamanlı bir şekilde çağırır.

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

Buna ek olarak, geliştiricilerin bellek ayak izini yönetebilmesi amacıyla AudioWorkletNode'un kullanım ömrünü kontrol etmek için process() yönteminin döndürülen değeri kullanılabilir. process() yönteminden false döndürülmesi, işlemciyi etkin değil olarak işaretler ve WebAudio motoru artık bu yöntemi çağırmaz. İşlemciyi canlı tutmak için yöntemin true döndürmesi gerekir. Aksi takdirde düğüm/işlemci çifti, bir süre sonra sistem tarafından çöp olarak toplanır.

MessagePort ile Çift Yönlü İletişim

Bazen özel AudioWorkletNodes, AudioParam ile eşlenmeyen denetimleri göstermek ister. Örneğin, özel bir filtreyi kontrol etmek için dizeye dayalı bir type özelliği kullanılabilir. Bu amaç doğrultusunda AudioWorkletNode ve AudioWorkletProcessor, iki yönlü iletişim için bir MessagePort ile donatılmıştır. Bu kanal üzerinden her türlü özel veri takas edilebilir.

Fig.2
Şekil 2

MessagePort'a hem düğümde hem de işlemcide .port özelliği aracılığıyla erişilebilir. Düğümün port.postMessage() yöntemi, ilişkili işlemcinin port.onmessage işleyicisine bir mesaj gönderir ve bunun tersi de geçerlidir.

/* 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'un, veri depolama alanını veya WASM modülünü iş parçacığı sınırının üzerinden aktarmanıza olanak tanıyan Aktarılabilir özelliğini desteklediğini de unutmayın. Bu, Ses İş Akışı sisteminin nasıl kullanılabileceğine ilişkin sayısız olasılık ortaya çıkarır.

Adım adım açıklamalı kılavuz: GainNode oluşturma

Her şeyi bir araya getiren, AudioWorkletNode ve AudioWorkletProcessor üzerine kurulu GainNode örneği aşağıda verilmiştir.

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>

kazanç-işlemci.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);

Bu, Ses İş Akışı sisteminin temellerini kapsar. Canlı demoları Chrome WebAudio ekibinin GitHub deposunda bulabilirsiniz.

Özellik Geçişi: Deneysel'den Mevcut Ürüne Geçiş

Audio İş Akışı, Chrome 66 veya sonraki sürümlerde varsayılan olarak etkindir. Bu özellik, Chrome 64 ve 65 sürümlerinde deneme işaretinin arkasındaydı.