LoginSignup
11
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-11

概要

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

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

:warning: GitHub Link Card Creator を利用させて頂きました。

環境

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();  // ループ再生
11
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9