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

この記事は[Wanoグループ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/wano-group)の17日目の記事です（定型文）．

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

[今回作ったやつ（音が鳴るので注意）](https://ta-daiki.github.io/wano-ac-2018/web/dst/index.html)

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

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

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

とりあえず，何かオーディオエフェクターらしきものでも作ってみます．文法やアルゴリズムの解説をはじめるとキリが無いので，興味のある方は[公式チュートリアル](https://faust.grame.fr/doc/manual/index.html#quick-start)があるのでそちらにトライしてみてください．

```javascript: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](https://qiita-image-store.s3.amazonaws.com/0/322265/0cd783f8-f0aa-de6a-31f3-ea794e061d84.png)
Fig 1. エフェクターのブロックダイヤグラム

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

```sh:compile2stand_alone_app
$ faust2alqt DelayEffector.dsp
```

![capture_standalone_app.PNG](https://qiita-image-store.s3.amazonaws.com/0/322265/58f26d67-d589-fdbe-5a93-39983999fb8e.png)

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

ちなみに，公式で便利な[オンラインエディター＋コンパイラ](![image.png](https://qiita-image-store.s3.amazonaws.com/0/322265/e515d49b-b589-ddbd-71fe-2c08362fdf58.png)
)が提供されてるので，こちらを使ってもよいと思います．

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

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

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

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

```jsx: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](https://ta-daiki.github.io/wano-ac-2018/web/dst/index.html)

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