C#のNAudioを使ってwaveのループ対応ストリーム再生をする

# 概要
C#のNAudioライブラリを使ってwaveファイルの再生を行い、その際、smplチャンクを見てループに対応したストリーム再生をします。

ソースファイルはgithubに置いてあります。
https://github.com/unknown-ds/sndstrm_play

<a href="https://github.com/unknown-ds/sndstrm_play"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/unknown-ds/sndstrm_play.png" width="460px"></a>

:warning: [GitHub Link Card Creator](https://ghlinkcard.com/) を利用させて頂きました。


## 環境
Windiws 10
Visual Studio Express 2017 for Windows Desktop

### Nuget
NAudio ver.1.8.5


# waveファイルフォーマット
waveフォーマットのまとめ。
必須のfmtとdataとループ情報のsmplチャンクについて。

### Riffチャンク
| 名前|サイズ|説明|
|:---:|:---:|:--:|
|ChunkID|4|"RIFF"|
|ChunkSize|4|全体サイズ|
|Format|4|"WAVE"|

### fmtチャンク
データフォーマットが記載されています。

| 名前|サイズ|説明|
|:---:|:---:|:--:|
|ChunkID|4|"fmt "|
|ChunkSize|4|fmtチャンクデータサイズ、16|
|AudioFormatTag|2|フォーマットタグ、無圧縮なら1|
|wChannels|2|チャンネル数|
|dwSamplesPerSec|4|サンプリングレート[Hz]|
|dwAvgBytesPerSec|4|データ速度[byte/sec]|
|wBlockAlign|2|ブロックサイズ[Byte/sample×チャンネル数]|
|wBitsPerSample|2|サンプルあたりのbit数[bit/sample] 8or16|

### dataチャンク
オーディオデータの実体部分です。

| 名前|サイズ|説明|
|:---:|:---:|:--:|
|ChunkID|4|"data"|
|ChunkSize|4|dataチャンクデータサイズ|
|Data|不定|オーディオデータ|

### smplチャンク
よくわからないデータが多いですが、ループ数cSampleLoopsとループ情報の開始dwStart, 終了dwEndあたりだけ着目します。

|名前|サイズ|説明|
|:---:|:---:|:--:|
|chunkID|4|"smpl"|
|chunkSize|4|チャンクサイズ(IDとSize除く)|
|dwManufacturer|4|製造者コード(WAVEは0)|
|dwProduct|4|プロダクトコード（WAVEは0）|
|dwSamplePeriod|4|1サンプルの時間[nano sec]|
|dwMIDIUnityNote|4|MIDI時のノート|
|dwMIDIPitchFraction|4|MIDI時のピッチチューニング用|
|dwSMPTEFormat|4|SMPTEフォーマットの設定|
|dwSMPTEOffset|4|SMPTEの設定|
|cSampleLoops|4|SampleLoop構造体の数|
|cbSamplerData|4|追加情報用サイズ（通常は0）|

後ろにループ情報が付きます。

|名前|サイズ|説明|
|:---:|:---:|:--:|
|dwIdentifier|4|固有ID|
|dwType|4|ループタイプ(通常は0)|
|dwStart|4|ループ開始位置[sample]|
|dwEnd|4|ループ終了位置[sample]|
|dwFraction|4|ループチューニング用変数|
|dwPlayCount|4|ループ回数(0の時は無限ループ)|


# 実装
## テスト用waveファイル
テスト用ファイル[test.wav](https://github.com/unknown-ds/sndstrm_play/blob/master/test.wav)です。以下のような音源で左チャンネル再生、右チャンネル再生、ステレオ再生を行った後、7秒の位置(ステレオ再生位置)へループ指定しています。

### 波形データ
![test_waveform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/493736c2-caa6-6c89-3f44-0e39a222724a.png)

### 数値データ
|項目|値|説明|
|:---:|:---:|:--:|
|チャンネル数|2|ステレオ|
|量子化ビット数|16|16bit|
|サンプリングレート|44100|44.1kHz|
|データ開始位置|44||
|データ終了位置|1894444||
|ループ開始位置|1234844|7秒の位置|
|ループ終了位置|1894444|データ終了と同じ|

## ソース
ほとんど BufferedWaveProvider に投げているだけです、現在の再生位置が欲しい場合は、自前で計算する必要があります。
また、ループ情報は１つのみで回数無限としています。

### ファイル読み込みとプレイヤー準備処理
チャンクデータの確認とbufferedWaveProviderやwavePlayerの用意を行っています。バッファは別で監視を行っているためPlay()した後は何もしていません。

```Program.cs
using (var _Fs = new FileStream(wavFilePath, FileMode.Open, FileAccess.Read))
{
	// wavファイルのチャンク読み込み
	wavChunkReader.ReadChunk(_Fs);
	int _rate = wavChunkReader.SamplingRate;
	int _bits = wavChunkReader.SamplingBit;
	int _channels = wavChunkReader.Channels;
	
	// wavフォーマット
	var _wavFormat = new WaveFormat(_rate, _bits, _channels);
	
	// wavプロバイダーを生成
	bufferedWaveProvider = new BufferedWaveProvider(_wavFormat);
	
	// ボリューム調整用
	var wavProvider = new VolumeWaveProvider16(bufferedWaveProvider) { Volume = 1.0f };
	
	// 再生デバイスと出力先を設定(NAudioの用語でRender は出力、Capture は入力)
	var mmDevice = new MMDeviceEnumerator().GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
	
	// タスクキャンセル用
	var tokenSource = new CancellationTokenSource();
	var token = tokenSource.Token;
	
	// バッファ監視
	Task _Task = TaskReadBuffer(_Fs, wavChunkReader, bufferedWaveProvider, token);
	
	// 再生処理
	using (var wavPlayer = new WasapiOut(mmDevice, AudioClientShareMode.Shared, false, 0))
	{
	    // 出力に入力を接続して再生開始
	    wavPlayer.Init(wavProvider);
	    wavPlayer.Volume = 0.2f;
	    wavPlayer.Play();
	
	    Console.WriteLine("\nPress Button Exit.");
	    Console.ReadLine();
	
	    wavPlayer.Stop();
	}
	
	// タスクキャンセル
	tokenSource.Cancel();
}

```

### バッファ監視処理
await でディレイをかけて一定時間置きにバッファを監視して空いた部分に音源のバイトデータを追加しています。

```Program.cs
static async Task TaskReadBuffer(
    FileStream          _Fs,        // ファイルストリーム
    CWaveChunkReader    _wavChunk,  // wavチャンク
    BufferedWaveProvider _Provider, // バッファプロバイダー
    CancellationToken   _token      // タスクキャンセル通知用
    )
{
    // 終了位置
    int _StreamEnd = (_wavChunk.IsLoop())? _wavChunk.LoopEnd: _wavChunk.DataEnd;

    // データ開始位置へシーク
    _Fs.Seek(_wavChunk.DataStart, SeekOrigin.Begin);

    var _IsLoop = true;
    while (_IsLoop)
    {
        // 空きバッファサイズ（データを追加するサイズ）
        int _EmptySize = _Provider.BufferLength - _Provider.BufferedBytes;
        
        // ストリーム位置チェック
        if (_Fs.Position + _EmptySize > _StreamEnd)
        {
            _EmptySize = _StreamEnd - (int)_Fs.Position;
            
            if (_wavChunk.IsLoop())
            {
                // ループ開始位置へ
                _Fs.Seek(_wavChunk.LoopStart, SeekOrigin.Begin);
            }
            else
            {
                _IsLoop = false;
            }
        }

        var _tmp = new byte[_EmptySize];
        if (_EmptySize > 0)
        {
            // ファイルから読み込み
            _Fs.Read(_tmp, 0, _EmptySize);
            // サンプルをバッファへ追加
            _Provider.AddSamples(_tmp, 0, _EmptySize);
        }
        
        // タスクキャンセル要求チェック
        if (_token.IsCancellationRequested) break;

        await Task.Delay(100);
    }

}
```

# まとめ
ループ処理部分を工夫すればイントロ->ループ->アウトロのようなデータも再生できます。（綺麗につなぐためのデータの作成が大変だと思います）

## 追記
単純にループで再生させる場合以下のようなコードでいけます。
ただしループ時にわずかにノイズが乗る感じがします。（waveファイルデータ次第？）

```Program.cs
string wavFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test.wav");
var _music = new SoundPlayer(wavFilePath);
_music.PlayLooping();  // ループ再生
```