C#から簡易的にWaveファイルを書き出す

## 仕様

　シンプルなWAVEファイル(任意のチャンネル数、任意のサンプリングレート、16bit)を書き出す。
　データは`IEnumerable<short>`で与える(配列を渡すとか、LINQを渡すとか)。
　データが2^32バイト未満の場合はデータチャンクに大きさを記録する。
　データが2^32バイト以上の場合はデータチャンクの大きさをゼロにする。これにより4GiB制限を超えた音声データを記録することができる。ただし正常なファイルではないので、例えばWindows Media Playerでは開くことができない。一方でEdgeのようなChromium系のブラウザでは開くことができ、もちろん、自作のプログラム等での取り扱いにも支障はない。(本来この用途にはRF64のような64bitファイルフォーマットを使うべきだが、今回は簡易的な用途を想定しているので「そこそこ動く」程度で妥協する)
　処理速度は気にしない。

## 使用例

### 1ch

```C#:シングルトーン
var sps = 44.1e3;
var f = 0.8e3;
WriteWave16("log.wav",
    Enumerable.Range(0, (int)sps)
    .Select(i => (short)(Math.Sin(f * Math.PI * 2 / sps * i) * short.MaxValue)),
    1, (int)sps);
```

```C#:デュアルトーン
var sps = 44.1e3;
var f1 = 697.0;
var f2 = 1209.0;
WriteWave16("log.wav",
    Enumerable.Range(0, (int)sps)
    .Select(i => (short)((
        Math.Sin(f1 * Math.PI * 2 / sps * i) +
        Math.Sin(f2 * Math.PI * 2 / sps * i)) * 0.5 * short.MaxValue)),
    1, (int)sps);
```

```C#:チャープ信号
var sps = 44.1e3;
var f0 = 1e3;
var f1 = 2e3;
var T = 10.0;
var k = (f1 - f0) / T;

WriteWave16("log.wav",
    Enumerable.Range(0, (int)(sps * T))
    .Select(i => i / sps)
    .Select(t => (short)(Math.Sin(2 * Math.PI * (f0 * t + k / 2 * t * t)) * short.MaxValue)),
    1, (int)sps);
```

### 2ch

```C#:シングルトーン
var sps = 44.1e3;
var f1 = 697.0;
var f2 = 1209.0;
WriteWave16("log.wav",
    Enumerable.Range(0, (int)sps * 2)
    .Select(i => (short)((Math.Sin((i % 2 == 0 ? f1 : f2) * Math.PI * 2 / sps * (i / 2))) * short.MaxValue)),
    2, (int)sps);
```

```C#:シングルトーン
var sps = 44.1e3;
var f1 = 697.0;
var f2 = 1209.0;

var tmp = new short[(int)sps][];
for (int i = 0; i < tmp.Length; i++)
{
    tmp[i] = new[]
    {
        (short)(Math.Sin(f1 * Math.PI * 2 / sps * i) * short.MaxValue),
        (short)(Math.Sin(f2 * Math.PI * 2 / sps * i) * short.MaxValue),
    };
}

WriteWave16("log.wav", tmp.SelectMany(a => a), 2, (int)sps);
```

## ソース

```C#
static void WriteWave16(string path, IEnumerable<short> src, int channel, int samplingrate)
    => WriteWave16(new FileStream(path, FileMode.Create, FileAccess.Write), src, channel, samplingrate);

static void WriteWave16(Stream stream, IEnumerable<short> src, int channel, int samplingrate, bool leaveOpen = false)
{
    try
    {
        stream.Write(GenerateWaveHeader(channel, samplingrate, 16, channel * 2));

        var start = stream.Position;
        foreach (var a in src) { stream.Write(BitConverter.GetBytes(a)); }

        var pos = stream.Position;
        var size = pos - start;
        if (size <= uint.MaxValue)
        {
            stream.Position = start - 4;
            stream.Write(BitConverter.GetBytes((uint)size));
            stream.Position = pos;
        }
    }
    finally
    {
        if (!leaveOpen) { stream.Dispose(); }
    }
}

static byte[] GenerateWaveHeader(int channels, int samplingrate, int bits, int blocksize = 4)
{
    var tmp = new byte[44];

    Encoding.ASCII.GetBytes("RIFF\0\0\0\0WAVEfmt ").CopyTo(tmp, 0);
    Encoding.ASCII.GetBytes("data").CopyTo(tmp, 36);

    BitConverter.GetBytes((uint)16).CopyTo(tmp, 16);
    BitConverter.GetBytes((ushort)1).CopyTo(tmp, 20);
    BitConverter.GetBytes((ushort)channels).CopyTo(tmp, 22);
    BitConverter.GetBytes((uint)samplingrate).CopyTo(tmp, 24);
    BitConverter.GetBytes((uint)(samplingrate * blocksize)).CopyTo(tmp, 28);
    BitConverter.GetBytes((ushort)blocksize).CopyTo(tmp, 32);
    BitConverter.GetBytes((ushort)bits).CopyTo(tmp, 34);

    return tmp;
}
```