AudioBuffer?
JSではWebAudioAPIを使って音を出すことができる
WebAudioAPIで音源を定義する方法は大体4通り
- OscillatorNode
- 正弦波とか三角波とか矩形波とか
- 任意の周波数で行ける
- ビープ音とか出したいならこれ
- 意外と音質良い
- AudioBufferSourceNode
- AudioBufferの再生
- 実態はバイナリデータ
- 入手方法
- 音源ファイルのArrayBufferを
.decodeAudioData()
- JSで頑張って波形生成
- 音源ファイルのArrayBufferを
- 短めの音声が得意
- 効果音とかによく使う
- AudioBufferの再生
- MediaElementAudioSourceNode
-
<audio>
タグとかを再生 - 長めの音声が得意
- iOSだと端末の再生中判定になったりしてちょっと不便
-
- MediaStreamAudioSourceNode
- マイクのリアルタイム再生
- 詳しくは https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API#音源の定義
つまりAudioBufferは生の波形データを保持できるオブジェクト
Float32Array
を各チャンネル毎に持っている
ネット上の音源ファイルから波形の型付き配列を得るサンプル
const
actx=new(window.AudioContext||webkitAudioContext)(),
abuf=await new Promise(async(ok,err)=>actx.decodeAudioData(
await(
await fetch('https://example.com/audio/nyan.mp3')
).arrayBuffer(),
ok,
err
));
console.log(
[...Array(abuf.numberOfChannels)].map((_,i)=>abuf.getChannelData(i))
);
wav?
非圧縮で音声データを詰め込むときに便利なファイル形式
実はコンテナでmp3のデータを詰めることもできるらしい
構造
理解するにあたってお世話になったサイト様
wav自体がRIFFという汎用ファイル形式に沿ってできている模様
値を入れる際はリトルエンディアン
オフセット | サイズ | 例 | 説明 |
---|---|---|---|
0 | 4 | 52,49,46,46 | チャンク識別子 常に"RIFF"(0x52494646) |
4 | 4 | xx,xx,xx,xx | チャンクサイズ これ以降のサイズ =ファイルサイズ-8=波形データサイズ+36 |
8 | 4 | 57,41,56,45 | ファイルフォーマット wavファイルは常に"WAVE"(0x57415645) |
12 | 4 | 66,6d,74,20 | チャンク識別子 常に"fmt%20"(0x666d7420) |
16 | 4 | 10,00,00,00 | チャンクサイズ 例はPCM |
20 | 2 | 01,00 | 音声フォーマット 例はPCM |
22 | 2 | 02,00 | チャンネル数 例は2ch |
24 | 4 | 44,ac,00,00 | サンプリング周波数 例は44.1k(=0x44ac0000)Hz |
28 | 4 | 10,b1,02,00 | byte/sec =サンプリング周波数*ブロックサイズ 例は44.1kHz、16bit、2ch(4410022=0x2b110) |
32 | 2 | 04,00 | ブロックサイズ =チャンネル数*ビット数/8 例は16bit、2ch(2*16/8=0x4) |
34 | 2 | 10,00 | ビット数 例は16bit |
(2) | 拡張パラメタサイズ PCMでは未使用 | ||
(*) | 拡張パラメタ PCMでは未使用 | ||
36 | 4 | 64,61,74,61 | チャンク識別子 常に"data"(0x64617461) |
40 | 4 | xx,xx,xx,xx | チャンクサイズ =波形データサイズ |
44 | * | xx,…… | 波形データ PCMでは時間順に格納 |
ビット数に関して一般的に
8bit,16bit,24bit,32bit float, 64bit float
があるようで一般的に使われているのは
8bit,16bit の2つで
float系は音声フォーマットが3になる
今回はファイルサイズの肥大化を防ぐ目的から16bitを選択
波形データは
8bit: 0~255
16bit: -32768~32767
float: -1~1
で格納するらしい
複数チャンネルある場合はインターリブ配置
つまり
1ch,2ch,3ch,1ch,2ch,3ch,……
ちなみにステレオで1chは左
先駆者様
とりあえずjs audiobuffer to wav
で調べるとこれが出てくる
先にArrayBufferのサイズを計算してDataViewを使ってデータを入れている
32bit floatにも対応している
あまりに長いので以前に人力minifyしたものがこれ
16bitで固定して計算できる定数は置き換えている
toWav=x=>{//https://github.com/Jam3/audiobuffer-to-wav
let ch=x.numberOfChannels,rate=x.sampleRate;
if(ch==1)x=x.getChannelData(0);else{const l=x.getChannelData(0),r=x.getChannelData(1);x=new Float32Array(l.length+r.length);for(let i=0;i<x.length;i++){x[i*2]=l[i];x[i*2+1]=r[i];}};
const b=new ArrayBuffer(44+x.length*2),v=new DataView(b),str=(c,s)=>{for(let i=0;i<s.length;i++)v.setUint8(c+i,s.charCodeAt(i))};
str(0,'RIFF');v.setUint32(4,36+x.length*2,true);str(8,'WAVE');
str(12,'fmt ');v.setUint32(16,16,true);v.setUint16(20,1,true);v.setUint16(22,ch,true);v.setUint32(24,rate,true);ch*=2;v.setUint32(28,rate*ch,true);v.setUint16(32,ch,true);v.setUint16(34,16,true);
str(36,'data');v.setUint32(40,x.length*2,true);for(let i=0;i<x.length;i++){const s=Math.max(-1,Math.min(1,x[i]));v.setInt16(44+i*2,s<0?s*0x8000:s*0x7FFF,true);}
return b;
}
実装
筆者は以前にzipファイル生成を1kBで実装している
同じノリで書いた結果完成したものがこちら
toWav=w=>((
{numberOfChannels:c,sampleRate:r},
l4=x=>[x,x>>>8,x>>>16,x>>>24],
l2=x=>[x,x>>>8],
x=(
x=>[...Array(x[0].length)].flatMap((_,i)=>x.flatMap(y=>l2(y[i]*0x7fff)))
)([...Array(c)].map((_,i)=>w.getChannelData(i)))
)=>new Uint8Array([
82,73,70,70,// RIFF
l4(36+x.length),
87,65,86,69,
102,109,116,32,// fmt
16,0,0,0,
1,0,
l2(c),
l4(r),
l4(r*(c*=2)),
l2(c),
16,0,
100,97,116,97,// data
l4(x.length),
x
].flat()).buffer)(w),
数をリトルエンディアンにする関数l4
l2
はもともとzip生成から借りてきた
le=(x,l=4)=>new Uint8Array(l).map((_,i)=>x>>>(i*8))
だったがパフォーマンスの関係で直書きした
x&0xff
を付けずともUint8Arrayにする際に下位8bitだけになるので省いてある
x=(x=>[...Array(x[0].length)].flatMap((_,i)=>x.flatMap(y=>l2(y[i]*0x7fff))))([...Array(c)].map((_,i)=>w.getChannelData(i)))
は
AudioBufferから波形を取得してインターリブ配置にして16bit化するワンライナーである
二次元配列はmapを二つ組み合わせることで転置することができる
今回は一次元に並べたいのでflatMapを用いている
同時に16bit化するにあたってUint8Arrayに詰めるために16bitを8*2bitにする必要があるのでここでもflatMapを用いている
16bit化について
2の補数表現で負数を表すには絶対値の二進数を論理反転するだけで良い
つまり符号がついている正規化された数から変換するにはそのまま0x7fff
を掛けて下位8bitをとるだけで良い
下位8bit問題はUint8Arrayに渡すので何も考えないで良い
これをそのままl2
に渡している
変換後の値域は-32767~32767になる
許せない場合は先駆者さんと同じように三項演算子で符号に応じて0x7fffと0x8000を切り替えれば解決する
が、-1~1のデータを変換しているからこれは-32768を含めないほうが正しいのかも
コメントを消して折りたたむと
toWav=w=>((
{numberOfChannels:c,sampleRate:r},l4=x=>[x,x>>>8,x>>>16,x>>>24],l2=x=>[x,x>>>8],
x=(x=>[...Array(x[0].length)].flatMap((_,i)=>x.flatMap(y=>l2(y[i]*0x7fff))))([...Array(c)].map((_,i)=>w.getChannelData(i)))
)=>new Uint8Array([82,73,70,70,l4(36+x.length),87,65,86,69,102,109,116,32,16,0,0,0,1,0,l2(c),l4(r),l4(r*(c*=2)),l2(c),16,0,100,97,116,97,l4(x.length),x].flat()).buffer)(w),
先駆者様の人力minify版とこれの50秒程度のデータに対する10回の実行時間の比較
あんまり速くない?
タシカニ
使い道
恐らく一番便利なのはMediaRecorderの保存をwavにする用途
mr.ondataavailable=e=>save(e.data);
だとwebmなどで保存されるものが
mr.ondataavailable=async e=>save(new Blob([
toWav(await new Promise(async(f,r)=>actx.decodeAudioData(
await new Response(e.data).arrayBuffer(),f,r
)))
]));
でwavにすることができる
あんまり使い道ない?
タシカニ