ブラウザ上で音楽編集ができるソフトをNuxt.js+WebAudioAPIで作った話

この記事は[Nuxt.js #2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nuxt-js-2) 13日目の記事です。

近年のWebアプリの発展は凄まじく、多くのブラウザベースのアプリが誕生しました。
従来はデスクトップアプリが担当するような高度な処理も、どんどんブラウザ上で実装できる時代になってきました。
友人も、最近はブラウザベースのアプリしか使っていない、とぼやいています。

このビッグウェーブに乗るべく、もともとデスクトップアプリで作る予定だったものをWebアプリで作ることにしました。
その名も**MusicCutter**です。

# つくったもの
![スクリーンショット (189).png](https://qiita-image-store.s3.amazonaws.com/0/89220/495f9dfc-da4c-5841-7ac5-cb5205438307.png)

ブラウザ上で音楽を切り貼りできるアプリ、[MusicCutter](https://music-cutter-siy.firebaseapp.com/)を作りました。
[紹介動画はこちら](https://www.youtube.com/watch?v=FlBU4IX2-_o)

## 特徴
- 音楽の切り貼りに特化したアプリ
- 曲のテンポを自動解析し、拍単位での編集を可能にする
- ブラウザ上で処理を完結させるため、サーバーに音声ファイルをアップロードする必要がない
- 上記の理由から、オンラインである必要がないため、オフラインで動作可能なPWAに対応する

初めて音楽を切り貼りする人が手を出すのは、Audacityといった波形編集ソフトだと思います。
波形編集ソフトでは、秒単位で編集を行うため、曲を切り貼りするのに耳でタイミングを聞き取るしかありませんでした。このソフトでは、曲のテンポを解析し、拍ベースで編集が行えるため、細かいタイミングを気にせず切り貼りが出来ます。

# 使った技術
## Nuxt.js
みんなご存知Nuxtです。自分が所属している組織で使っていたのがきっかけで出会いました。

### なぜVue CLIでなくNuxtを選んだか
Vueでアプリを作る時のデファクトスタンダードはVue CLIだと思います。
NuxtはVue CLIよりも規約で縛る思想にあるので、プロジェクトの秩序が保たれやすいと考えたのが大きな理由です。


## Web Audio API
Web上でオーディオを高度に制御するためのAPIです。
音声にフィルターを掛けたり、視覚化したり、再生のタイミングを正確にスケジューリングしたりできます。

## Firebase Hosting
Nxutで生成した静的ファイルをホスティングするのに使いました。

# Nuxtでやったこと
## プラグインの導入
### Vue Materialの導入
UIを作るのに[Vue Material](https://vuematerial.io/)を採用しました。

`nuxt-vue-material`というNuxtプラグインとして導入できるパッケージがあったのでそちらを使いました。

```shell
$ yarn add nuxt-vue-material
```

```js:nuxt.config.js

modules: [
    ['nuxt-vue-material', {
      theme: 'default-dark'
    }],
]

```
コンフィグでテーマが変えられるのが手軽で良いです。
`Vue Material`では予め`Light` `Dark` `Light Green` `Dark Green`の4種類のテーマがありますが、独自にカスタマイズしたい場合はテーマにnullを指定します。
その後、layoutのスタイルタグに以下を記述します。

```sass:default.vue
<style lang="scss">
@import "~vue-material/dist/theme/engine"; // Import the theme engine

@include md-register-theme("default", (
  primary: md-get-palette-color(amber, A700), // The primary color of your application
  accent: md-get-palette-color(red, A200) // The accent or secondary color
));

@import "~vue-material/dist/theme/all"; // Apply the theme
</style>
```
`md-get-palette-color`には色を指定します。
指定する名前や数値は[こちら](https://material.io/design/color/the-color-system.html#tools-for-picking-colors)で確認できます。

###PWA対応

```shell
$ yarn add @nuxtjs/pwa
```

`@nuxtjs/pwa`という公式パッケージを使いました。
nuxt.config.jsに以下を追加します。

```js:nuxt.config.js
  modules: [
    //...
    '@nuxtjs/pwa'
  ],

  manifest: {
    name: "アプリ名",
    lang: 'ja'
  },

  workbox: {
    offlineAssets:["オフラインで使うファイル","",""]
  },
```

`manifest`を追加するだけでPWAとしてブラウザに認識させられるようになりますが、今回はオフライン動作に対応するため、`workbox`にオフラインで使うファイルを指定します。

### Google Analytics の実装

```shell
$ yarn add @nuxtjs/google-analytics
```

`@nuxtjs/google-analytics`パッケージを使用しました。
以下のようにトラッキングIDを指定するだけで、基本的な機能が使えるようになります。

```js:nuxt.config.js
  modules: [
  //...
    ['@nuxtjs/google-analytics', {
      id: 'UA-12345-6'
    }]
  ]
```
また、ページ内でユーザーがどのような行動をしたかも収集可能です。
Google Analyticsではイベントトラッキングを用いることで実現できます。
以下のコードでイベントを送信できます。

```js
this.$ga.event('category', 'action', 'label', 123)
```
各パラメータの意味は[こちら](https://developers.google.com/analytics/devguides/collection/analyticsjs/events)から確認できます。

MusicCutterを例に取ると、どれくらいのユーザーが編集をして保存までたどり着いたかを調べるために、Saveボタンを押すとイベントが送信される仕組みになっています。

### Vue.Draggableの導入
タイムライン上での要素の並び替えのために[Vue.Draggable](https://github.com/SortableJS/Vue.Draggable)を導入しました。

```shell
$ yarn add vuedraggable
```

MusicCutterでは以下のような使い方をしています。

```vue
<draggable v-model="SoundBlocks">
   <div v-for="block in SoundBlocks" :key="block.id">
    <SoundBlock :data="block"/>
  </div>
</draggable>
```

# Web Audio API
## 概要
> ![web-audio-api-flowchart[1].png](https://qiita-image-store.s3.amazonaws.com/0/89220/79a2bf18-3a32-45aa-2360-2c2195e91491.png)
https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API

上の画像はWeb Audio APIの概要を示しています。
複数のノードをチェインして最終的に`Destination`に接続することで、音を出すことができます。


## 簡単な使い方
単純に音声を再生する例を以下に示します。

```js
    //1. AudioContextの準備
    const context = new AudioContext();
    
    //再生するバッファを準備
    const prepareBuffer = async path => {
      //2. fetch APIで音声ファイルを取得
      const res = await fetch(path);
      //ArrayBufferを取得
      const arr = await res.arrayBuffer();
      //3. 音声ファイルをデコード
      const buf = await context.decodeAudioData(arr);

      return buf;
    }

    const play = async () => {
      const source = context.createBufferSource(); //4. Sourceノードを作成
      source.buffer = await prepareBuffer("./audio.mp3"); //5. 再生するバッファを指定
      source.connect(context.destination); // SourceノードをDestinationにつなぐ
      source.start(0);//6. 再生開始
    }

    play();

```
1. 音声処理グラフを表す[`AudioContext`](https://developer.mozilla.org/ja/docs/Web/API/AudioContext)を作成します。
2. fetchAPIで音声ファイルを取得し、バイナリデータ(`ArrayBuffer`)を取得します
3. `AudioContext.decodeAudioData(ArrayBuffer)`を使用してオーディオデータをデコードします。
4. `AudioContext.createBufferSource`を使用して[`AudioSourceBufferNode`](https://developer.mozilla.org/ja/docs/Web/API/AudioBufferSourceNode)を作成します。
5. `AudioSourceBufferNode`にオーディオデータをセットし、`AudioContext`のDestinationに繋ぎます。
6. 再生します。

## フィルタを使う
音量を変更するフィルタを挟んでみましょう。`play()`を以下のように変更します。

```js
    const play = async () => {
      const source = context.createBufferSource(); // Sourceノードを作成
      source.buffer = await prepareBuffer("./audio.mp3"); // 再生するバッファを指定
      const gainNode = context.createGain();// ゲインノードを作成
      gainNode.gain.value = 0.5;// 音量を半分に設定
      source.connect(gainNode);// Sourceをゲインノードに接続
      gainNode.connect(context.destination);// ゲインノードをDestinationに接続

      source.start(0); // 再生開始
    }
```


Web Audio APIについて詳しく知りたければ、[こちらの記事](https://www.html5rocks.com/ja/tutorials/webaudio/intro/)がとても参考になります。
## 曲のテンポを解析する
曲のテンポを解析するアルゴリズムは、[Beat Detection Using JavaScript and the Web Audio API](http://joesul.li/van/beat-detection-using-web-audio/)を参考にしました。
また、このアルゴリズムの改良版である[Calculating BPM using Javascript and the Spotify Web API](https://github.com/JMPerez/beats-audio-api)も参考にしました。

テンポ解析についてはこれだけで記事が1本かけそうなので後で書くかもしれません。
ここでは軽く流れを説明します。

### 1.フィルターを掛けて音楽を解析しやすくする
ドラムの音がテンポを解析する手がかりになるので、ドラムの音を目立たせて他の音はカットします。
具体的には、ハイパスフィルターとローパスフィルターを掛けて音の中域部分をバッサリ切り落とします。

今回はフィルタを掛けながら再生するのではなく、フィルタを掛けたバッファを取得したいので以下のように`OfflineContext`を使います。

```js
const filter = async (buffer) => {
  // レンダリング用のオフラインコンテキストを生成
  const offlineContext = new OfflineAudioContext(1, buffer.length, buffer.sampleRate);

  // Sourceを作成
  const source = offlineContext.createBufferSource();
  source.buffer = buffer;

  // フィルタを作成する
  const lowpass = offlineContext.createBiquadFilter();
  lowpass.type = "lowpass";
  lowpass.frequency.value = 150;
  lowpass.Q.value = 1;

  const highpass = offlineContext.createBiquadFilter();
  highpass.type = "highpass";
  highpass.frequency.value = 100;
  highpass.Q.value = 1;

  // フィルタをチェインしてコンテキストにつなぐ
  source.connect(lowpass);
  lowpass.connect(highpass);
  highpass.connect(offlineContext.destination);

  // 開始
  source.start(0);

  // レンダリングをする
  return offlineContext.startRendering()
}
```

### 2.音のピークを取得する
0.5秒(120bpm相当)間隔で音声を区切り、ブロック内で最も音量が大きいサンプルを集計していきます。

### 3.ピークの間隔からテンポを推測する
ピークの間隔の統計を取り、テンポを推測します。
MusicCutterでは、誤差を考慮していくつかテンポの候補を出すようにしています。

## ブラウザ上でMP3エンコード
エンコード用のサーバーを準備すると、アップロード/ダウンロードが発生する上、維持費がかかってしまうので
クライアントで完結させるためにブラウザ上でMP3エンコードするという**暴挙**にでました。

[Using WebAudioRecorder.js to Record MP3, Vorbis and WAV Audio on Your Website](https://addpipe.com/blog/using-webaudiorecorder-js-to-record-audio-on-your-website/)で紹介されていた
[WebAudioRecorder.js](https://github.com/higuma/web-audio-recorder-js)に使用されているMP3エンコーダ[Mp3LameEncoder.js](https://github.com/higuma/mp3-lame-encoder-js)を使用しました。
使い方は驚くほど簡単で、`WebAudioAPI`で使用した音声バッファをエンコーダに投げるだけで、処理をしてくれる手軽さです。
ただ、`Mp3LameEncoder.js`はモジュールに対応していないのでスクリプトタグで読み込むか、`WebWorker`上なら`importScripts`を使って読み込む必要があります。
以下にWaveファイルをMP3にエンコードする流れを示します。


```js
    //バッファを準備
    const prepareBuffer = async path => {
      //fetch APIで音声ファイルを取得
      const res = await fetch(path);
      //ArrayBufferを取得
      const arr = await res.arrayBuffer();
      //音声ファイルをデコード
      const buf = await context.decodeAudioData(arr);

      return buf;
    }
    
    const encode = async () => {
      //waveファイルを読み込む
      const buf = await prepareBuffer("./audio.wav");
      //左右のチャンネルの生データ(Float32Array)を取得
      const dataL = buf.getChannelData(0);
      const dataR = buf.getChannelData(1);
      //エンコーダの初期化
      const encoder = new Mp3LameEncoder(buf.sampleRate, 192);
      //エンコードする。これは複数回呼び出すことが可能
      encoder.encode([dataL,dataR]);
      //blobが返される
      const blob = encoder.finish();
      //blobを保存したりサーバーにアップロードしたりする
    }

    encode();
```

MusicCutterでは`WebWorker`上でエンコードを行い、[FileSaver.js](https://www.npmjs.com/package/file-saver)でクライアントにファイルを保存させています。

# Firebase Hosting
静的ファイルをホストしてくれるサービスです。
Nuxtで静的ファイルを生成できるのでそれをデプロイします。

## デプロイの準備
`firebase-tools`をインストールします。

```shell
$ yarn global add firebase-tools
```

次にプロジェクトフォルダで

```shell
$ firebase init
```
を実行します。種類は`Hosting`を選択します
質問に答えていくと
`What do you want to use as your public directory?`
と聞かれるので`dist`と入力しておきます。

## 静的ファイルの生成
```shell
$ yarn generate
```
を実行すると、Nuxtが`./dist`フォルダに静的ファイルを生成してくれます。

## デプロイ
```shell
$ firebase deploy
```
と入力するとデプロイが行われます。

#おわりに
いかがだったでしょうか？

NuxtとWebAudioAPIを使うだけで、ブラウザ上で動作する音楽編集ソフトを作ることができました。
Nuxtを始め、ブラウザアプリにさらなる可能性を感じていただけたら幸いです。