AudioBuffer(WebAudioAPI)をwavファイルに変換(0.4kB)

# AudioBuffer?
JSではWebAudioAPIを使って音を出すことができる
WebAudioAPIで音源を定義する方法は大体4通り

- OscillatorNode
    - 正弦波とか三角波とか矩形波とか
    - 任意の周波数で行ける
    - ビープ音とか出したいならこれ
    - 意外と音質良い
- AudioBufferSourceNode
    - AudioBufferの再生
        - 実態はバイナリデータ
        - 入手方法
            - 音源ファイルのArrayBufferを`.decodeAudioData()`
            - JSで頑張って波形生成
    - 短めの音声が得意
    - 効果音とかによく使う
- MediaElementAudioSourceNode
    - `<audio>`タグとかを再生
    - 長めの音声が得意
    - iOSだと端末の再生中判定になったりしてちょっと不便
- MediaStreamAudioSourceNode
    - マイクのリアルタイム再生
- 詳しくは https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API#音源の定義

つまりAudioBufferは生の波形データを保持できるオブジェクト
`Float32Array`を各チャンネル毎に持っている

ネット上の音源ファイルから波形の型付き配列を得るサンプル

```js
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))
);
```



https://developer.mozilla.org/ja/docs/Web/API/AudioBuffer

# wav?
https://ja.wikipedia.org/wiki/WAV

非圧縮で音声データを詰め込むときに便利なファイル形式
実はコンテナでmp3のデータを詰めることもできるらしい

## 構造
理解するにあたってお世話になったサイト様

https://www.youfit.co.jp/archives/1418

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(44100*2*2=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`で調べるとこれが出てくる

https://github.com/Jam3/audiobuffer-to-wav

先にArrayBufferのサイズを計算してDataViewを使ってデータを入れている
32bit floatにも対応している

あまりに長いので以前に人力minifyしたものがこれ
16bitで固定して計算できる定数は置き換えている
```js
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で実装](https://qiita.com/McbeEringi/items/5acc1f940ff7504f7e16)している
同じノリで書いた結果完成したものがこちら
```js
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生成から借りてきた
```js
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を含めないほうが正しいのかも




コメントを消して折りたたむと
```js
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回の実行時間の比較

https://twitter.com/McbeEringi/status/1617952953217781780?s=20

あんまり速くない?
ﾀｼｶﾆ
# 使い道
恐らく一番便利なのはMediaRecorderの保存をwavにする用途
```js
mr.ondataavailable=e=>save(e.data);
```
だとwebmなどで保存されるものが
```js
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にすることができる

あんまり使い道ない?
ﾀｼｶﾆ