NAudioで波形を生成しながら再生する(っぽいやつ)

探してもあんまり見つからなかったので。

## 手段

NAudioのWaveOutは`WaveFileReader`を経由してファイルを読み込みながら再生できる。
`WaveFileReader`に偽装してやれば好きなデータをWaveOutに食わせられる。

### ソース

`WaveStream`を継承してテキトーに`NotImplementedException`の部分を穴埋めしただけ。

```C#
class DummyWaveStream : WaveStream
{
    private const int samplingRate = 44100;

    private Int64 pos = 0;

    public override Int64 Length
    {
        get
        {
            return (Int64.MaxValue);
        }
    }

    public override Int64 Position
    {
        get
        {
            return (pos);
        }
        set
        {
            pos = value;
        }
    }

    public override WaveFormat WaveFormat
    {
        get
        {
            return (new WaveFormat(samplingRate, 16, 2));
        }
    }

    public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
    {
        if (offset != 0 || buffer.Length != count) { throw new Exception(); }

        const double freq = 500;
        const int bytePerSample = 4;

        for (int i = 0; i < count; i += bytePerSample)
        {
            double sin = Math.Sin(freq * (pos + i) / samplingRate * Math.PI * 2 / bytePerSample);
            short wav = (short)(sin * 30000);

            buffer[i + 1] = (byte)(wav >> 8);
            buffer[i + 0] = (byte)wav;
        }

        pos += count;

        return (count);
    }
}
```

`DummyWaveStream`を`WaveOut`に与えると、500Hzの正弦波を再生する。
このサンプルでは左側しか音声を与えていないので、右のスピーカーは無音になる。

`Read`の中でイベントを蹴るなりdelegateを呼ぶなりして外部で音声を作れば、任意の波形を再生できる。

Posを真面目に扱うと面倒なので、setは`NotImplementedException`にしてしまうと楽かも。

## その他

### デバイス一覧

```C#
void updateAudioDeviceList()
{
    audioDeviceComboBox.Items.Clear();
    audioDeviceComboBox.Items.AddRange(
            Enumerable.Range(0, WaveOut.DeviceCount)
        .Select(i => i + " " + WaveOut.GetCapabilities(i).ProductName)
        .ToArray());

    if (audioDeviceComboBox.Items.Count > 0)
    {
        audioDeviceComboBox.SelectedIndex = 0;
    }
}
```

みたいな感じでデバイス番号とデバイス名を列挙できる。
WaveOutでデバイスを指定する際にデバイス番号が必要になる。必ずしも必要ではないが、名前に併記したほうが楽。

デバイス名は変なところで切られてしまう。NAudio内部処理の問題な気がする。長い名前が全部必要なら別の手段を講じる必要がある。

### Play/Stop

```C#
private WaveOut waveOut = null;
private DummyWaveStream waveSrc = null;

private void playCheckBox_Click(Object sender, EventArgs e)
{
    if (waveOut == null)
    {
        string deviceIndexAndNameText = audioDeviceComboBox.SelectedItem as string;

        if (!string.IsNullOrEmpty(deviceIndexAndNameText))
        {
            int devIndex = int.Parse(deviceIndexAndNameText.Split(' ')[0]);

            waveSrc = new DummyWaveStream();

            waveOut = new WaveOut();
            waveOut.DeviceNumber = devIndex;
            waveOut.Init(waveSrc);
            waveOut.Play();
        }
    }
    else
    {
        waveOut.Stop();
        waveOut.Dispose();
        waveOut = null;

        waveSrc.Dispose();
        waveSrc = null;
    }

    playCheckBox.Checked = waveOut != null;
    audioDeviceComboBox.Enabled = waveOut == null;
}
```

CheckBoxの`AutoCheck`は`false`にしておく。

waveOutがnullならPlay、そうでなければStop、という動作。

Playの場合は、デバイス名の最初の文字列を取り出して`int.Parse`に与えてデバイス番号を得る。
`waveOut.Init()`に`DummyWaveStream`のインスタンスを与える。

StopはStopしてDisposeしてnullを入れるだけ。

### 再生時間

データ数が$2^{63} - 1$バイト、1サンプルあたり4バイト、44.1kHzなので、160万年くらいの長さ。まぁ、たぶん、大抵の用途では足りるだろう。
特殊な用途だと1サンプル64バイト/2MHzみたいなデータもあるけど、それでも2000年程度は再生できるし、その用途だとNAudioを使ったりはしないだろうから問題ないはず。

## 追記

`Length`, `Position`はNAudio周りからはアクセスされないっぽい。`NotImplementedException`のままでも動いた(`WaveFormat`は実装する必要がある)。

また、Readの戻り値はチェックされないらしく、少ないサンプル数を返した場合は正常に動作しない。