Реализация JavaScript AudioNode

Возможно ли реализовать пользовательский AudioNode с помощью API веб-аудио?

Я хотел бы создать node, который будет содержать несколько других узлов (ChannelSplitters и AnalyserNodes). В идеале я смогу подключиться к этому пользовательскому node, как и любой другой AudioNode. Например,

var customNode = new CustomNode();
mediaStreamSource = context.createMediaStreamSource(userMedia);

// This will not work, as I need to know what to implement in CustomNode
mediaStreamSource.connect(customNode);
customNode.connect(context.destination);

Согласно документации MDN, AudioNode реализует интерфейс EventTarget. Это все, что используется для перетасовки аудио? И если да, то как реализовать этот интерфейс для обработки звука?

Ответ 1

Файл класса AudioNode

"use strict";

var AudioNode = global.AudioNode;
var AudioNode$connect;
var AudioNode$disconnect;

function connect() {
  var args = [].slice.call(arguments);

  if (args.length && typeof args[0].__connectFrom === "function") {
    args[0].__connectFrom.apply(args[0], [ this ].concat(args.slice(1)));
  } else {
    AudioNode$connect.apply(this, args);
  }
}

function disconnect() {
  var args = [].slice.call(arguments);

  if (args.length && typeof args[0].__disconnectFrom === "function") {
    args[0].__disconnectFrom.apply(args[0], [ this ].concat(args.slice(1)));
  } else {
    AudioNode$disconnect.apply(this, args);
  }
}

function use() {
  if (typeof AudioNode !== "undefined" && AudioNode.prototype.connect !== connect) {
    AudioNode$connect = AudioNode.prototype.connect;
    AudioNode$disconnect = AudioNode.prototype.disconnect;

    AudioNode.prototype.connect = connect;
    AudioNode.prototype.disconnect = disconnect;
  }
}

function unuse() {
  if (typeof AudioNode !== "undefined" && AudioNode.prototype.connect === connect) {
    AudioNode.prototype.connect = AudioNode$connect;
    AudioNode.prototype.disconnect = AudioNode$disconnect;
  }
}

module.exports = {
  use: use,
  unuse: unuse,
};

Файл теста AudioNode

"use strict";

var assert = require("power-assert");
var PowerAudioNode = require("../");

function CustomAudioNode(audioContext) {
  this.audioContext = audioContext;
  this.gain1 = audioContext.createGain();
  this.gain2 = audioContext.createGain();
  this.inlet = this.gain1;
  this.outlet = this.gain2;
}

CustomAudioNode.prototype.connect = function() {
  this.gain1.connect(this.gain2);
  this.gain2.connect.apply(this.gain2, arguments);
};

CustomAudioNode.prototype.disconnect = function() {
  this.gain1.disconnect();
  this.gain2.disconnect.apply(this.gain2, arguments);
};

CustomAudioNode.prototype.__connectFrom = function(source) {
  source.connect(this.gain1);
};

CustomAudioNode.prototype.__disconnectFrom = function(source) {
  source.disconnect();
};

describe("PowerAudioNode", function() {
  describe("use(): void", function() {
    before(PowerAudioNode.use);
    before(PowerAudioNode.use);
    it("works", function() {
      var audioContext = new global.AudioContext();
      var oscillator = audioContext.createOscillator();
      var customAudioNode = new CustomAudioNode(audioContext);
      var compressor = audioContext.createDynamicsCompressor();

      oscillator.connect(customAudioNode);
      customAudioNode.connect(compressor);
      compressor.connect(audioContext.destination);

      assert(audioContext.destination.$isConnectedFrom(compressor));
      assert(compressor.$isConnectedFrom(customAudioNode.outlet));
      assert(customAudioNode.inlet.$isConnectedFrom(oscillator));

      oscillator.disconnect(customAudioNode);
      customAudioNode.disconnect();
      compressor.disconnect();

      assert(!audioContext.destination.$isConnectedFrom(compressor));
      assert(!compressor.$isConnectedFrom(customAudioNode.outlet));
      assert(!customAudioNode.inlet.$isConnectedFrom(oscillator));
    });
  });
  describe("unuse(): void", function() {
    before(PowerAudioNode.unuse);
    it("works", function() {
      var audioContext = new global.AudioContext();
      var oscillator = audioContext.createOscillator();
      var customAudioNode = new CustomAudioNode(audioContext);
      var compressor = audioContext.createDynamicsCompressor();

      assert.throws(function() {
        oscillator.connect(customAudioNode);
      });
      customAudioNode.connect(compressor);
      compressor.connect(audioContext.destination);

      assert(audioContext.destination.$isConnectedFrom(compressor));
      assert(compressor.$isConnectedFrom(customAudioNode.outlet));
      assert(!customAudioNode.inlet.$isConnectedFrom(oscillator));

      oscillator.disconnect();
      customAudioNode.disconnect();
      compressor.disconnect();

      assert(!audioContext.destination.$isConnectedFrom(compressor));
      assert(!compressor.$isConnectedFrom(customAudioNode.outlet));
      assert(!customAudioNode.inlet.$isConnectedFrom(oscillator));
    });
  });
});

Ответ 2

Эта статья, похоже, имеет способ делать то, что вы ищете.

Основная предпосылка:

function MyCustomNode(){
  this.input = audioContext.createGainNode();
  var output = audioContext.createGainNode();
  this.connect = function(target){
     output.connect(target);
  };
}

Пример:

function AudioBus(){
  this.input = audioContext.createGainNode();
  var output = audioContext.createGainNode();
  var custom = new MyCustomNode();

  this.input.connect(custom);
  custom.connect(output);

  this.connect = function(target){
     output.connect(target);
  };
}

//create some native oscillators and our custom audio bus
var bus = new AudioBus(),
    instrument1 = audioContext.createOscillator(),
    instrument2 = audioContext.createOscillator(),
    instrument3 = audioContext.createOscillator();

//connect our instruments to the same bus
instrument1.connect(bus.input);
instrument2.connect(bus.input);
instrument3.connect(bus.input);
bus.connect(audioContext.destination);

Изменить: Вопрос может быть возможным дублированием создания пользовательского эха node с веб-аудио, но я считаю, что ответ, который вы ищете, тот, что у @MattDiamond. Это не совсем красивое решение, но, похоже, оно выполняет свою работу:

function FeedbackDelayNode(context, delay, feedback){
  this.delayTime.value = delay;
  this.gainNode = context.createGainNode();
  this.gainNode.gain.value = feedback;
  this.connect(this.gainNode);
  this.gainNode.connect(this);
}

function FeedbackDelayFactory(context, delayTime, feedback){
  var delay = context.createDelayNode(delayTime + 1);
  FeedbackDelayNode.call(delay, context, delayTime, feedback);
  return delay;
}

AudioContext.prototype.createFeedbackDelay = function(delay, feedback){
  return FeedbackDelayFactory(this, delay, feedback);
};