Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事はNuxt.js #2 Advent Calendar 2018 13日目の記事です。

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

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

つくったもの

スクリーンショット (189).png

ブラウザ上で音楽を切り貼りできるアプリ、MusicCutterを作りました。
紹介動画はこちら

特徴

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

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

$ yarn add nuxt-vue-material
nuxt.config.js
modules: [
    ['nuxt-vue-material', {
      theme: 'default-dark'
    }],
]

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

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には色を指定します。
指定する名前や数値はこちらで確認できます。

PWA対応

$ yarn add @nuxtjs/pwa

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

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

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

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

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

Google Analytics の実装

$ yarn add @nuxtjs/google-analytics

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

nuxt.config.js
  modules: [
  //...
    ['@nuxtjs/google-analytics', {
      id: 'UA-12345-6'
    }]
  ]

また、ページ内でユーザーがどのような行動をしたかも収集可能です。
Google Analyticsではイベントトラッキングを用いることで実現できます。
以下のコードでイベントを送信できます。

this.$ga.event('category', 'action', 'label', 123)

各パラメータの意味はこちらから確認できます。

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

Vue.Draggableの導入

タイムライン上での要素の並び替えのためにVue.Draggableを導入しました。

$ yarn add vuedraggable

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

<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://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API

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

簡単な使い方

単純に音声を再生する例を以下に示します。

    //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を作成します。
  2. fetchAPIで音声ファイルを取得し、バイナリデータ(ArrayBuffer)を取得します
  3. AudioContext.decodeAudioData(ArrayBuffer)を使用してオーディオデータをデコードします。
  4. AudioContext.createBufferSourceを使用してAudioSourceBufferNodeを作成します。
  5. AudioSourceBufferNodeにオーディオデータをセットし、AudioContextのDestinationに繋ぎます。
  6. 再生します。

フィルタを使う

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

    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について詳しく知りたければ、こちらの記事がとても参考になります。

曲のテンポを解析する

曲のテンポを解析するアルゴリズムは、Beat Detection Using JavaScript and the Web Audio APIを参考にしました。
また、このアルゴリズムの改良版であるCalculating BPM using Javascript and the Spotify Web APIも参考にしました。

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

1.フィルターを掛けて音楽を解析しやすくする

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

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

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で紹介されていた
WebAudioRecorder.jsに使用されているMP3エンコーダMp3LameEncoder.jsを使用しました。
使い方は驚くほど簡単で、WebAudioAPIで使用した音声バッファをエンコーダに投げるだけで、処理をしてくれる手軽さです。
ただ、Mp3LameEncoder.jsはモジュールに対応していないのでスクリプトタグで読み込むか、WebWorker上ならimportScriptsを使って読み込む必要があります。
以下にWaveファイルをMP3にエンコードする流れを示します。

    //バッファを準備
    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でクライアントにファイルを保存させています。

Firebase Hosting

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

デプロイの準備

firebase-toolsをインストールします。

$ yarn global add firebase-tools

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

$ firebase init

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

静的ファイルの生成

$ yarn generate

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

デプロイ

$ firebase deploy

と入力するとデプロイが行われます。

おわりに

いかがだったでしょうか?

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

siy1121
情報系の学部に通う大学3年生 / プログラミングしたり、CG(動画)作ったり、作曲したり色々してます / 動画や音声を扱うプログラムを書くのが好き / Kotlinが好きですが、js/tsの偉大さも感じている今日このごろです
https://siy.space
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away