LoginSignup
12
6

More than 5 years have passed since last update.

Web Audio API + Faust : ブラウザベースで動くオーディオエフェクターを作る

Posted at

この記事はWanoグループ Advent Calendar 2018の17日目の記事です(定型文).

Web上で音を取り扱うためのJavaScript APIであるWeb Audio APIとFaustというプログラミング言語を組み合わせて,ブラウザ上で動作するエフェクターを作ってみた話です.

今回作ったやつ(音が鳴るので注意)

Web Audio API

 Web Audio APIは,ブラウザ上でただ音を再生したりするだけでなく,オシレータやフィルタといった基本的なDSP (Digital signal processing)モジュールも一通り揃っており,これらを組み合わせるだけで結構本格的なオーディオアプリが作れるので楽しいですよね.JSなのでボリュームノブ等のGUIも簡単に設置できるのも魅力的です.

Faust

 Faust(Functional Audio Stream) Languageは,フランスのGrame研究所が開発したその名の通りオーディオプログラミングに特化した関数型言語です.このFaustの面白いところは,Faustで書かれた単一のソースから,スタンドアローンアプリ,WebAssemblyバイナリ,DAW上で動作するVSTプラグインなど様々なフォーマットへコンパイルすることができる点です.

Faust
├── wasm
├── stand_alone_app
├── vst
└── etc...

とりあえず,何かオーディオエフェクターらしきものでも作ってみます.文法やアルゴリズムの解説をはじめるとキリが無いので,興味のある方は公式チュートリアルがあるのでそちらにトライしてみてください.

DelayEffector.dsp
// javascriptのSyntax hiliteで誤魔化す(割といい感じなのでは?)
import("stdfaust.lib");

DelayG(x) = hgroup("Delay", x);
OtherG(x) = hgroup("SubEffector", x);

// control params
params = environment {
    delayTimeMsec = DelayG(hslider("Delay Time (msec)[style:knob]", 100, 1, 1000, 1));
    feedbackGain = DelayG(hslider("Delay Feedback[style:knob]", 0.5, 0.0, 1.0, 0.01));
    cutoff = OtherG(hslider("Cutoff Frequency (Hz)[style:knob]", 5000, 40, 15000, 1));
    driveGain = OtherG(hslider("Drive[style:knob]", 5.0, 1.0, 20.0, 0.01));
    wetness = DelayG(hslider("Wet[style:knob]", 0.5, 0.0, 1.0, 0.01));
};

// with liner interpolation
delay(u) = @(u, int(tau) + 1) * c, @(u, int(tau)) * (1.0 - c) : +
    with {
        tau = (params.delayTimeMsec * 10^(-3)) * ma.SR; // delay block length
        c = ma.frac(tau);
    };

// simple RC lowpass filter
lowpassFilter(u) = b0*u + b1*@(u, 1) : - ~*(a1)
    with {
        w = 2.0*ma.PI*params.cutoff;
        // filter coefficients
        b0 = w / (w + 2.0*ma.SR);
        b1 = b0;
        a1 = (w - 2.0*ma.SR) / (w + 2.0*ma.SR);
    };

distortion(u) = atan(g*u) / (0.5*ma.PI)
    with {
        g = params.driveGain;
    };

mixer(drySig, wetSig) = (1.0 - k) * drySig, k * wetSig : +
    with {
        k = params.wetness;
    };


feedbackDelay(u) = delay~(distortion : lowpassFilter : *(g), u : +)
    with {
        g = params.feedbackGain;
    };

monoProcess =  _ <: _, feedbackDelay : mixer;

// main process
process = monoProcess, monoProcess;

今回書いたのは,入力音を一定の間隔でエコーさせるものです(Fig. 1).ギターなどやる人にとってはディレイといったほうがなじみ深いかもしれません.

キャプチャ.PNG
Fig 1. エフェクターのブロックダイヤグラム

これをスタンドアローンアプリにコンパイルしてみます.

compile2stand_alone_app
$ faust2alqt DelayEffector.dsp

capture_standalone_app.PNG

コマンド一発でいい感じのGUI付きのアプリができました.ホストのアクティブなオーディオデバイスも自動で認識してくれます.

ちなみに,公式で便利なオンラインエディター+コンパイラが提供されてるので,こちらを使ってもよいと思います.

Web Audio API + Faust

 今度は,先のコードをWebAssemblyバイナリにコンパイルして,Web Audio APIと組み合わせブラウザ上で動かしてみます.

compile2wasm
$ faust2wasm DelayEffector.dsp
# => Compiling with : -ftz 2
# => Compiled with 'wasm' backend
# => DelayEffector.js, DelayEffector.wasm;

WebAssemblyバイナリとそれを使うためのjsファイルが吐き出されました.

適当なwavファイルを読み込んで再生するワンショットサンプラー的なものを作って,その出力をエフェクターに流し込む感じのページを作っていきます.

index.jsx
import React from 'react';
import {render} from 'react-dom';

import {faust} from './DelayEffector.js';

class DelayEffector extends React.Component {
  constructor() {
    super();

    this.state = {
      audioCtx: null,
      audioData: null,
      samplerNode: null,
      delayNode: null,
      effectBypass: false,
      delayTimeMsec: 100,
      feedbackGain: 0.3,
      drive: 1.0,
      cutoffFreqHz: 1000,
      wetness: 0.5,
    }

    this.paramName2faustAdressName = {
      delayTimeMsec: "/DelayEffector/Delay/Delay_Time_(msec)",
      feedbackGain: "/DelayEffector/Delay/Delay_Feedback",
      wetness: "/DelayEffector/Delay/Wet",
      drive: "/DelayEffector/SubEffector/Drive",
      cutoffFreqHz: "/DelayEffector/SubEffector/Cutoff_Frequency_(Hz)",
    }
  }

  componentDidMount() {
    const audioCtx = new AudioContext();
    this._loadAudioFile(audioCtx, './SampleLoop.wav');
    this._setUpRooting(audioCtx);
    this.setState({
      audioCtx: audioCtx
    });
  }

  _loadAudioFile(audioCtx, path) {
    const req = new XMLHttpRequest();
    req.open("GET", path, true);
    req.responseType = "arraybuffer";
    req.onload = () => {
      if(!req.response) return;
      audioCtx.decodeAudioData(req.response).then(decodeBuff => this.setState({audioData: decodeBuff}))
    };
    req.send();
  }

  _setUpRooting(audioCtx) {
    const samplerNode = audioCtx.createBufferSource();
    faust.createDelayEffector(audioCtx, 1024, (delayNode) => {
      console.log(delayNode.getParams());
      samplerNode.connect(delayNode);
      delayNode.connect(audioCtx.destination);
      this.setState({delayNode: delayNode});
    })
    this.setState({samplerNode: samplerNode});
  }


  playAudio() {
    const samplerNode = this.state.samplerNode;
    samplerNode.buffer = this.state.audioData;
    samplerNode.loop = true;
    samplerNode.start();
  }

  onChangeBypass(ev) {
    const effectBypass = ev.target.checked;
    const val = effectBypass ? 0 : this.state.wetness;
    const delayNode = this.state.delayNode;

    delayNode.setParamValue(this.paramName2faustAdressName["wetness"], parseFloat(val));
    this.setState({ effectBypass: effectBypass });
  }

  onChangeWetness(ev) {
    const val = ev.target.value;
    const effectBypass = this.state.effectBypass;

    if (!effectBypass) {
      const delayNode = this.state.delayNode;
      delayNode.setParamValue(this.paramName2faustAdressName["wetness"], parseFloat(val));
    }

    this.setState({ wetness: val });
  }

  onChangeSlider(paramName) {
    const delayNode = this.state.delayNode;

    return (ev) => {
      const val = ev.target.value;
      const adress = this.paramName2faustAdressName[paramName];
      delayNode.setParamValue(adress, parseFloat(val));
      this.setState({ [paramName]: val });
    }
  }

  render () {
    const { delayTimeMsec, feedbackGain, wetness, drive, cutoffFreqHz } = this.state;
    return (
      <div>
        <h1>Sampler</h1>
        <button onClick={this.playAudio.bind(this)}>Play Audio</button>

        <h1>Effector</h1>
        <input type="checkbox" onChange={this.onChangeBypass.bind(this)} />Effect Bypass <br />

        <p>Delay Time (msec)</p>
        <input type="range" value={delayTimeMsec} min="100" max="1000" step="1" onChange={this.onChangeSlider("delayTimeMsec").bind(this)} />
        <span>{delayTimeMsec} msec</span>

        <p>Feedback Gain</p>
        <input type="range" value={feedbackGain} min="0" max="1" step="0.01" onChange={this.onChangeSlider("feedbackGain").bind(this)} />
        <span>{feedbackGain}</span>

        <p>Feedback Distortion</p>
        <input type="range" value={drive} min="1" max="20" step="0.1" onChange={this.onChangeSlider("drive").bind(this)} />
        <span>{drive}</span>

        <p>LPF Cutoff Frequency (Hz)</p>
        <input type="range" value={cutoffFreqHz} min="40" max="10000" step="1" onChange={this.onChangeSlider("cutoffFreqHz").bind(this)} />
        <span>{cutoffFreqHz} Hz</span>

        <p>Wetness</p>
        <input type="range" value={wetness} min="0" max="1" step="0.01" onChange={this.onChangeWetness.bind(this)} />
        <span>{wetness * 100.0} %</span>
      </div>
    )
  }
}

render(<DelayEffector />, document.getElementById('app'));

wasmを吐き出すときにできたDelayEffector.jsに記述されているfaustインスタンスから,Web Audioで使えるAudioNodeインスタンスが作れます.後は,node.setParamValue()でパラメータを更新できます.シンプルですね.

冒頭にも張りましたが,出来上がったものが以下になります(音が鳴るので注意).

WebAudioAPI+Faust_Demo

その他

 実際のところFaustで書かれたコードは一旦C++にトランスパイルされています.だから,wasmが出力できるんですね.また,冒頭にも述べたようにFaustのコンパイラは,コマンド一発で様々なフォーマットを吐き出すことができます."faust2firefox"コマンドなんかは,結構面白くて,システムの構造をブロック線図として出力することができます (Fig. 1のブロック線図はこの機能を使って出力したものになります).他にもVSTをコマンド一発で吐き出すこともできるので,いろいろ試してみたいですね.

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6