2017/11/22

Web Audio APIとVue.jsで波形を見るためのオシロスコープをSVGとCanvasでつくる

前回、いろんな音がだせるオシレーターをつくった
音が出せるようになったら、やっぱりどんな波形か見たくなるのが世の常。

ということで、今回はWeb Audio APIとSVG/Canvasをつかってオシロスコープをつくる。
パフォーマンス検証のためSVGとCanvasの双方をつかったが、サンプルが小さかったこともあり違いは見られなかった。(詳細は後述する)

音声波形データを取得する


OscillatorNodeAnalyserNodeに繋ぐことで、音声波形や周波数のデータを取得できる。
// Oscillator → Analyser → Speakerの順に繋ぐ
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
analyser.connect(ctx.destination);

const osc = ctx.createOscillator();
osc.connect(analyser);


// 高速フーリエ変換の周波数領域のサイズ
analyser.fftSize = 128;
// FFTのサイズの半分の値
const bufferLength = analyser.frequencyBinCount;

// 音声波形データをdataにコピーする
let data = new Uint8Array(bufferLength);
analyser.getByteTimeDomainData(data);

// fftSize(時間領域)内の波形データが取得できるので、プロットしてグラフ(オシロスコープ)を表示する
console.log(data);

AnalyserNode.fftSizeプロパティで高速フーリエ変換(FFT)においての周波数領域(データを取得する範囲)を指定する。デフォルトだと2024。32〜32768の範囲で、2のべき乗の値が指定できる。

AnalyserNode.frequencyBinCountプロパティ(readonly)で、可視化に必要なデータ数が取得できるので、この値を使って空のUint8Arrayを作成する。

今回はオシロスコープ(波形の形をみる)ので、AnalyserNode.getByteTimeDomainDataメソッドを使い、先ほど作成したUint8Arrayにコピーする。
このメソッドを実行すると、時系列ごとの波形の値が取得できる。

スペクトラムアナライザ(周波数ごとの音量)を見たいときはAnalyserNode.getByteFrequencyDataメソッドを使う。

あとは取得できたUint8ArrayをSVGやCanvasでプロットし、グラフ化すれば完了!



オシロスコープをつくる


Web Audio API(AnalyserNode)とVue.jsでオシロスコープをつくる。
<div id="app">
  <form>
    <button type="button" @click="start">Start</button>
    <button type="button" @click="stop">Stop</button>
    <div class="scope-type">
      <h3>描画タイプ</h3>
      <label><input type="radio" value="svg" v-model="scopeType">SVG</label>
      <label><input type="radio" value="canvas" v-model="scopeType">Canvas</label>
    </div>
  </form>
  <div class="oscilloscopes">
      <!-- SVGを用いたオシロスコープ -->
      <svg
        v-if="scopeType === 'svg'"
        ref="scope"
        :width="size.width"
        :height="size.height"
        xmlns="http://www.w3.org/2000/svg"
        baseProfile="full"
        >
        <!-- 線の太さ、色はあらかじめセットしておく -->
        <path :d="svg.path" stroke="#00F23E" stroke-width="3" fill="none"></path>
      </svg>

      <!-- Canvasを用いたオシロスコープ -->
      <canvas
        v-if="scopeType === 'canvas'"
        ref="scope"
        :width="size.width"
        :height="size.height"></canvas>
  </div>

  <!-- FPSを表示する -->
  <div class="fps-checker">
    <span>{{ fps }}fps</span>
  </div>
</div>
new Vue({
  el: '#app',
  data() {
    return {
      size: {
        width: 400,
        height: 256
      },
      scopeType: 'svg',
      svg: {
        path: ''
      },
      canvas: {
        ctx: null
      },
      audio: {
        ctx: new AudioContext(),
        osc: null,
        analyser: null,
        running: false
      },
      fps: 0
    }
  },
  methods: {
    start() {
      this.audio.running = true;

      // Oscillator -> Analyser -> Speaker
      this.audio.analyser = this.audio.ctx.createAnalyser();
      this.audio.analyser.connect(this.audio.ctx.destination);
    
      this.audio.osc = this.audio.ctx.createOscillator();
      this.audio.osc.connect(this.audio.analyser);
      
      // 高速フーリエ変換の周波数領域のサイズ(デフォルトは2024)
      this.audio.analyser.fftSize = 128;

      this.audio.osc.start();
      
      // テスト用FPSモニター
      this.monitorFPS();

      switch (this.scopeType) {
        case 'svg':
          this.drawSVG();
          break;
        case 'canvas':
          this.drawCanvas();
          break;
      }
    },
    stop() {
      this.audio.osc.stop();
      this.audio.running = false;
    },
    drawSVG() {
      const bufferLength = this.audio.analyser.frequencyBinCount;
      
      const update = () => {
        // 音声波形データをUint8Arrayにコピー
    let data = new Uint8Array(bufferLength);
        this.audio.analyser.getByteTimeDomainData(data);
        
        // SVGのpathを生成する(例: <path d="Mx0, y0, x1 y1, x2 y2, ..., xn, yn">)
        let d = 'M';
        data.forEach((y, i) => {
          const x = i * (this.size.width / bufferLength);
          d += `${x} ${y},`;
        });
        this.svg.path = d;

        // 音が鳴っていたら再度呼び出し
        if (this.audio.running) {
          window.requestAnimationFrame(update.bind(this));
        }
      };
      
      // update内のthisをvueオブジェクトにするためにbindする
      window.requestAnimationFrame(update.bind(this));
    },
    drawCanvas() {
      // Canvas要素からコンテキストを取得
      const ctx = this.$refs.scope.getContext('2d');
      ctx.lineWidth = 3;            // 線の太さ
      ctx.strokeStyle = '#00F23E';  // 線の色
      
      const bufferLength = this.audio.analyser.frequencyBinCount

      const update = () => {
        // クリア
        ctx.clearRect(0, 0, this.size.width, this.size.height);
        
        ctx.save();
        // 始点(x: 0, y: 中央)に移動
        ctx.moveTo(0, this.size.height / 2);
        ctx.beginPath();
        
        // 音声波形データをUint8Arrayにコピー
        let data = new Uint8Array(bufferLength);
        this.audio.analyser.getByteTimeDomainData(data);
        
        // 線を引く
        data.forEach((p, i) => {
          // x: size.widthをbufferLengthで分割した値を1目盛りにする
          ctx.lineTo(i * (this.size.width / bufferLength), p);
        });
        
        ctx.stroke();
        ctx.restore();
        
        if (this.audio.running) {
          window.requestAnimationFrame(update.bind(this));
        }
      };

      window.requestAnimationFrame(update.bind(this));
    },
    monitorFPS() {
      // テスト用FPSモニター
      let last = 0.0;
      const throttled = _.throttle(frame => {
        this.fps = Math.round((1.0 / frame) * 10000) / 10000;
      }, 300);
      
      const tick = t => {
        const frame = (t - last) / 1000;
    throttled(frame);
        last = t;
      
        if (this.audio.running) {
          window.requestAnimationFrame(tick);
        }
      };
      
      window.requestAnimationFrame(tick)
    }
  }
});


OscillatorNodeAnalyserNodeAudioDestinationNodeと繋ぎ、オシレーターの音声波形データを取得できるようにしている。

SVGによる描画の場合は、1つのパスで<path d="Mx0 y0, x1 y1, x2 y2,... ">のように線を引いている。あとはSVGのd属性とバインドしているだけなので処理としては簡単になっている。

Canvasによる描画の場合は、クリア→描画→クリア→描画→...のサイクルを行っている。
moveTo(0, y)で始点を指定し、取得した音声波形データをもとにctx.lineTo(x, y)で線を引きながら描画している。

今回はサンプルアプリが小さいのでSVG・Canvasとも性能に差異はなかった。
ただコードを見ていただければわかるとおり、SVGの方がかなりシンプルに書ける。

その辺は好みだろうか…。



以上

written by @bc_rikko

0 件のコメント :

コメントを投稿