Edited at

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


概要

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

ソースファイルはgithubに置いてあります。

https://github.com/unknown-ds/sndstrm_play


環境

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です。以下のような音源で左チャンネル再生、右チャンネル再生、ステレオ再生を行った後、7秒の位置(ステレオ再生位置)へループ指定しています。


波形データ

test_waveform.png


数値データ

項目

説明

チャンネル数
2
ステレオ

量子化ビット数
16
16bit

サンプリングレート
44100
44.1kHz

データ開始位置
44

データ終了位置
1894444

ループ開始位置
1234844
7秒の位置

ループ終了位置
1894444
データ終了と同じ


ソース

ほとんど BufferedWaveProvider に投げているだけです、現在の再生位置が欲しい場合は、自前で計算する必要があります。

また、ループ情報は1つのみで回数無限としています。


ファイル読み込みとプレイヤー準備処理

チャンクデータの確認と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(); // ループ再生