28
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TreasureAdvent Calendar 2018

Day 20

ブラウザ上で.wavファイルを解析する

Last updated at Posted at 2018-12-24

この記事は、Treasure Advent Calendar 2018の20日目の記事です。(大遅刻しました...)

最近React×Redux×WebAudioAPIを使ってアプリケーションを作っていて、ブラウザ上で音声の解析をした時のメモを書きます。

今回やりたいこと

ブラウザにアップロードした.wavファイルを解析し、サンプリングレートなどの情報と波形データを読み込む。

手順

  1. FileReaderを使ってArrayBufferとしてファイルを読み込む
  2. (audioタグにurlを入れ、再生できるようにする)
  3. .wavのデータを解析
  4. 結果をいい感じに表示する

以下の2つのファイルを拡張しながら説明していきます。

index.html
<!DOCTYPE html>
<html>
  <head>
    <script src="./index.js"></script>
  </head>
  <body>
    <input id="myFile" type="file" />
    <audio id="audio" controls></audio>
    <div id="result"></div>
  </body>
</html>
index.js
window.onload = () => {
  const element = document.getElementById('myFile')
  const audioElement = document.getElementById('audio')
  const resultElement = document.getElementById('result')
}

1. ファイル読み込み

https://lab.syncer.jp/Web/API_Interface/Reference/IDL/FileReader/readAsArrayBuffer/ を参考にinputタグからファイルを読み込みます。

index.js
window.onload = () => {
  const element = document.getElementById('myFile')
  const audioElement = document.getElementById('audio')
  const resultElement = document.getElementById('result')

  element.onchange = () => {
    // ファイルが選択されたか
    if (!element.value) return

    // FileReaderクラスに対応しているか
    if (!window.FileReader) return

    // 0番目を読み込む
    const file = element.files[0]
    if (!file) return

    const fileReader = new FileReader()

    fileReader.onload = () => {
      // fileReader.resultにデータが入る
      console.log(fileReader.result)
    }

    fileReader.readAsArrayBuffer(file)
  }
}

Consoleを見ると、データの読み込み結果が表示されます。

ArrayBuffer(355290) {}
[[Int8Array]]: Int8Array(355290) [82, 73, 70, …]
[[Int16Array]]: Int16Array(177645) [18770, 17990, 27602, …]
[[Uint8Array]]: Uint8Array(355290) [82, 73, 70, …]
byteLength: (...)
__proto__: ArrayBuffer

2. audioタグに入れ音声を再生する

Javascriptでバイナリデータを扱うものの中にBlobがあります。
これとDataViewを使うことでオーディオデータを挿入することができます。

index.js
window.onload = () => {
  const element = document.getElementById('myFile')
  const audioElement = document.getElementById('audio')
  const resultElement = document.getElementById('result')

  element.onchange = () => {
    // ファイルが選択されたか
    if (!element.value) return

    // FileReaderクラスに対応しているか
    if (!window.FileReader) return

    // 0番目を読み込む
    const file = element.files[0]
    if (!file) return

    const fileReader = new FileReader()

    fileReader.onload = () => {
      // fileReader.resultにデータが入る
      console.log(fileReader.result)

      const view = new DataView(fileReader.result)
      const audioBlob = new Blob([view], { type: 'audio/wav' })
      const myURL = window.URL || window.webkitURL
      audioElement.src = myURL.createObjectURL(audioBlob)
    }

    fileReader.readAsArrayBuffer(file)
  }
}

音声読み込み

音声の読み込みと再生ができるようになりました。(ちなみにダウンロードも出来ます)

3. .wavのデータを解析

.wavのデータはRIFFチャンクと呼ばれるブロック構造になっているため、フォーマットに合わせて読み込みを行います。
RIFFチャンクは基本的にfmtチャンクとdataチャンクの2つで構成されています。

wav
(https://qiita.com/syuhei1008/items/0dd07489f58158fb4f83 より引用)

まずは読み込み用のメソッドを作ります。(window.onloadスコープ内)
readWaveData()を呼ぶことで波形データの配列を取得します。

index.js
  // 指定したバイト数分文字列として読み込む
  const readString = (view, offset, length) => {
    let text = ''
    for (let i = 0; i < length; i++) {
      text += String.fromCharCode(view.getUint8(offset + i))
      console.log(text)
    }
    return text
  }

  // ビットレートが16bitのPCMとして読み込む
  const read16bitPCM = (view, offset, length) => {
    let input = []
    let output = []
    for (let i = 0; i < length / 2; i++) {
      input[i] = view.getInt16(offset + i * 2, true)
      output[i] = parseFloat(input[i]) / parseFloat(32768)
      if (output[i] > 1.0) output[i] = 1.0
      else if (output[i] < -1.0) output[i] = -1.0
    }
    return output
  }

  const readWaveData = view => {
    const riffHeader = readString(view, 0, 4) // RIFFヘッダ
    const fileSize = view.getUint32(4, true) // これ以降のファイルサイズ (ファイルサイズ - 8byte)
    const waveHeader = readString(view, 8, 4) // WAVEヘッダ

    const fmt = readString(view, 12, 4) // fmtチャンク
    const fmtChunkSize = view.getUint32(16, true) // fmtチャンクのバイト数(デフォルトは16)
    const fmtID = view.getUint16(20, true) // フォーマットID(非圧縮PCMなら1)
    const channelNum = view.getUint16(22, true) // チャンネル数
    const sampleRate = view.getUint32(24, true) // サンプリングレート
    const dataSpeed = view.getUint32(28, true) // バイト/秒 1秒間の録音に必要なバイト数(サンプリングレート*チャンネル数*ビットレート/8)
    const blockSize = view.getUint16(32, true) // ブロック境界、(ステレオ16bitなら16bit*2=4byte)
    const bitRate = view.getUint16(34, true) // ビットレート

    let exOffset = 0 //拡張パラメータ分のオフセット
    if (fmtChunkSize > 16) {
      const extendedSize = fmtChunkSize - 16 // 拡張パラメータのサイズ
      exOffset = extendedSize
    }
    const data = readString(view, 36 + exOffset, 4) // dataチャンク
    const dataChunkSize = view.getUint32(40 + exOffset, true) // 波形データのバイト数
    const samples = read16bitPCM(view, 44 + exOffset, dataChunkSize + exOffset) // 波形データを受け取る

    return samples
  }

補足ですが、exOffsetをいれることでWAVEファイルにおける拡張パラメータの分だけずらして計算するようにしています。
こちらの記事がわかりやすくて参考になりました。

4. 結果をいい感じに表示する

いい感じにテーブルで表示するメソッドを作ります。

index.js
window.onload = () => {
  /* 省略 */
  const renderTable = (fields, values) => {
    const table = document.createElement('table')
    const tbody = document.createElement('tbody')
    for (i = 0; i < fields.length; i++) {
      const tr = document.createElement('tr')
      for (j = 0; j < 2; j++) {
        const td = document.createElement('td')
        td.innerHTML = j == 0 ? fields[i] : values[i]
        tr.appendChild(td)
      }
      tbody.appendChild(tr)
    }
    table.appendChild(tbody)
    table.border = 1
    table.classList.add('result-table') // クラスを指定しcssを反映
    resultElement.appendChild(table)
  }

  const readWaveData = view => {
    /* 省略 */

    // 左のセルに表示するフィールド名
    const fields = [
      'RIFF',
      'FileSize',
      'WAVE',
      'fmt',
      'fmtChunkSize',
      'fmtID',
      'ChannelNumber',
      'SampleRate',
      'DataSpeed',
      'BlockSize',
      'BitRate',
      'ExtendedSize',
      'data',
      'DataChunkSize',
      'Samples'
    ]

    // 右のセルに表示する値
    const values = [
      riffHeader,
      fileSize,
      waveHeader,
      fmt,
      fmtChunkSize,
      fmtID,
      channelNum,
      sampleRate,
      dataSpeed,
      blockSize,
      bitRate,
      exOffset,
      data,
      dataChunkSize,
      samples
    ]

    renderTable(fields, values)

    return samples
  }

  element.onchange = () => {
    /* 省略 */

    fileReader.onload = () => {
      // fileReader.resultにデータが入る
      console.log(fileReader.result)

      const view = new DataView(fileReader.result)
      const audioBlob = new Blob([view], { type: 'audio/wav' })
      const myURL = window.URL || window.webkitURL
      audioElement.src = myURL.createObjectURL(audioBlob)

      const samples = readWaveData(view) // DataViewから波形データを読み込む
    }

    fileReader.readAsArrayBuffer(file)
  }
}

index.htmlにもstyleを追加

index.html
<!DOCTYPE html>
<html>
  <head>
    <script src="./index.js"></script>
    <style>
      .tables {
        table-layout: fixed;
        width: 800px;
        word-wrap: break-word;
      }
    </style>
  </head>
  <body>
    <input id="myFile" type="file" /> <audio id="audio" controls></audio>
    <div id="result"></div>
  </body>
</html>

最終結果

See the Pen ZVKPbo by konatsu_p (@konatsup) on CodePen.

注意

今回は16bitPCM用の読み込みメソッドしか作っていないため、ビットレートが変わると波形データがうまく読み込めません。
また、fmtチャンクIDがJUNKになっている際も読み込めないので、読み込むデータに合わせて改善する必要があります。

参考

あとがき

波形データからピッチ(音程の高さ)抽出まで書く予定でしたが、時間がなかったので残念...
機会があればまた記事書きます。

(アドカレですが、私だけ遅刻してしまって本当に申し訳ない...)
Treasureで出会えた最高の仲間たちへ、メリークリスマス!
そして、良いお年を!

28
17
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
28
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?