この記事は、Treasure Advent Calendar 2018の20日目の記事です。(大遅刻しました...)
最近React×Redux×WebAudioAPIを使ってアプリケーションを作っていて、ブラウザ上で音声の解析をした時のメモを書きます。
今回やりたいこと
ブラウザにアップロードした.wavファイルを解析し、サンプリングレートなどの情報と波形データを読み込む。
手順
- FileReaderを使ってArrayBufferとしてファイルを読み込む
- (audioタグにurlを入れ、再生できるようにする)
- .wavのデータを解析
- 結果をいい感じに表示する
- 使用したサンプル音声(ダウンロードできます)
以下の2つのファイルを拡張しながら説明していきます。
<!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>
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タグからファイルを読み込みます。
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
を使うことでオーディオデータを挿入することができます。
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つで構成されています。
(https://qiita.com/syuhei1008/items/0dd07489f58158fb4f83 より引用)
まずは読み込み用のメソッドを作ります。(window.onloadスコープ内)
readWaveData()
を呼ぶことで波形データの配列を取得します。
// 指定したバイト数分文字列として読み込む
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. 結果をいい感じに表示する
いい感じにテーブルで表示するメソッドを作ります。
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を追加
<!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
になっている際も読み込めないので、読み込むデータに合わせて改善する必要があります。
参考
- https://www.html5rocks.com/ja/tutorials/file/dndfiles/
- https://qiita.com/syuhei1008/items/0dd07489f58158fb4f83
- https://numb86-tech.hatenablog.com/entry/2018/01/22/203406
- http://var.blog.jp/archives/62330155.html
- https://qiita.com/edo_m18/items/612d2b31498d22d13b7b
- https://lab.syncer.jp/Web/API_Interface/Reference/IDL/FileReader/readAsArrayBuffer/
- https://hakuhin.jp/js/file_reader.html#FILE_READER_RESULT
- http://d.hatena.ne.jp/uppudding/20071223/1198420222
あとがき
波形データからピッチ(音程の高さ)抽出まで書く予定でしたが、時間がなかったので残念...
機会があればまた記事書きます。
(アドカレですが、私だけ遅刻してしまって本当に申し訳ない...)
Treasureで出会えた最高の仲間たちへ、メリークリスマス!
そして、良いお年を!