2018年8月9日木曜日

C#でWAVEを生成して再生する

 C#で音を出したかったので、自分で生成して再生してみた。
 同時に1chしか出せないこと、たぶん生成しながら再生することができないこと、たぶんリングバッファ状に延々と再生することができないこと、といった制限がある。そのかわり簡単に再生できる。

 手段としては、SoundPlayerにStreamを渡し、そのStreamは自分で作ったWAVEファイルになっている。あとはBackgroundWorkerで再生する。
 SoundPlayerで非同期に再生すると、再生終了を知ることができないらしい。ということでBackgroundWorkerで同期的に再生し、PlaySyncメソッドから戻った時点でSoundPlayerとStreamをDisposeしている。
 BackgroundWorkerは同時にひとつのスレッドしか持てないが、SoundPlayerも同時再生はできないらしいので特に問題ないはず。

 今回は16bit1chの波形のみを扱った。あとサンプリングレートは44.1kHzに固定している。
 波形を生成するときに10000の係数をかけている。本来は32767をかけるべきで、実際Streamをファイルに書き出すならそれで良いのだが、なぜかSoundPlayerでは数倍に増幅されて再生される。そうすると正弦波の上下がクリップされて矩形波状になり、高調波がかなりでる。この係数は環境依存かもしれない。

 正弦波1つを再生するならConsole.Beepで任意周波数を任意時間再生できるが、もうちょっと複雑な音を出そうとするとConsole.Beepでは出せないはず。
 ゲームでBGMを鳴らしながらSEも鳴らして、という用途には向かないだろうが、まぁそこまでする予定はないのでこれでいいのだ。リングバッファ的なモノで再生しようとするとかなり面倒らしいんだよねぇ。


// RunWorkerAsyncの引数に再生するWAVEファイルのストリームを指定する。
// 再生が終了した時点でストリームをDisposeする。
private readonly BackgroundWorker SoundPlayerBackgroundWorker;

public Form1()
{
    InitializeComponent();

    SoundPlayerBackgroundWorker = new BackgroundWorker();
    SoundPlayerBackgroundWorker.DoWork += SoundPlayerBackgroundWorker_DoWork;
}

private void button1_Click(Object sender, EventArgs e)
{
    playSinwave(500, 1);
}

private void button2_Click(Object sender, EventArgs e)
{
    playSinwave(800, 0.5);
}

private void playSinwave(double waveFreq, double seconds)
{
    Int32 amp = 10000;

    if (!SoundPlayerBackgroundWorker.IsBusy)
    {
        const UInt32 samplingRate = 44100;
        Int16[] wave = new Int16[(int)(samplingRate * seconds)];

        for (int i = 0; i < wave.Length; i++)
        {
            double phase = (double)i / samplingRate * waveFreq * Math.PI * 2;
            double sin = Math.Sin(phase);
            wave[i] = (Int16)(sin * amp);
        }

        SoundPlayerBackgroundWorker.RunWorkerAsync(generateWaveData(wave, samplingRate));
    }
}

private void SoundPlayerBackgroundWorker_DoWork(Object sender, DoWorkEventArgs e)
{
    Stream ms = (Stream)e.Argument;

    using (SoundPlayer sp = new SoundPlayer(ms))
    {
        sp.PlaySync();
    }

    ms.Dispose();
}

private MemoryStream generateWaveData(Int16[] data, UInt32 samplingRate)
{
    MemoryStream ms = new MemoryStream();

    ms.Write(Encoding.ASCII.GetBytes("RIFF"), 0, 4); // RIFF hdr
    ms.Write(BitConverter.GetBytes((UInt32)data.Length * 2 + 44 - 8), 0, 4); // file size - 8
    ms.Write(Encoding.ASCII.GetBytes("WAVE"), 0, 4); // RIFF type is "WAVE"
    ms.Write(Encoding.ASCII.GetBytes("fmt "), 0, 4); // "fmt " chunk
    ms.Write(BitConverter.GetBytes((UInt32)16), 0, 4); // fmt chunk size
    ms.Write(BitConverter.GetBytes((UInt16)1), 0, 2); // format ID
    ms.Write(BitConverter.GetBytes((UInt16)1), 0, 2); // number of channels
    ms.Write(BitConverter.GetBytes((UInt32)samplingRate), 0, 4); // sampling rate
    ms.Write(BitConverter.GetBytes((UInt32)samplingRate * 2), 0, 4); // data speed: bytes per sec
    ms.Write(BitConverter.GetBytes((UInt16)2), 0, 2); // block size: bytes per 1 sample (channels * bit depth / 8)
    ms.Write(BitConverter.GetBytes((UInt16)16), 0, 2); // bit depth (bits)
    ms.Write(Encoding.ASCII.GetBytes("data"), 0, 4); // "data" chunk
    ms.Write(BitConverter.GetBytes((UInt32)data.Length * 2), 0, 4); // data chunk size

    foreach (Int16 hoge in data)
    {
        ms.Write(BitConverter.GetBytes(hoge), 0, 2);
    }

    ms.Position = 0;

    return (ms);
}

0 件のコメント:

コメントを投稿