MemoryStream で音を再生する


「System.Media.SoundPlayer で音を再生する」の記事で、SoundPlayer クラスは wav ファイルをただ再生するくらいしか用途がないよね、と書きましたね。でもあとで考えてみると、Stream から音を再生することもできるのでした。ですから、それについて説明しておこうと思うのですよ。

 

とはいっても、基本的にはファイルから読みだして音を再生するのと同じです。ですが、ファイルを使わずにできるわけですから、たとえば動的に音のデータを生成して出すような用途には使えますよね。Stream って何かっていうと、要するにwavファイルを読みこむデータのバッファみたいなものです。実際、SoundPlayer クラスでファイル名を指定して再生するときも、陰でこっそり使われているのです。そこに手を突っ込んじゃうというわけ。

wavファイルのヘッダ構造をクラスにする

まず何が必要かというと、wavファイルってどんな構造なのかって情報。そこで、検索してみると、近藤正芳さんが「WAV ファイルフォーマット」というページを作ってくれてます。いいですね。分かりやすい。そこで、まずこのデータ構造をそのまま再現するクラス WAVHDR というのを書きます。このクラスは、実体としては懐かしの構造体ですが、C# ではちょっとめんどくさくて、いろいろ周りにややこしいことが書いてあります。これは、データの並び順が変わらないようにするためのおまじないです。説明しないけど、まあこんなもんだと思ってください。

[StructLayout(LayoutKind.Sequential)]
public class WAVHDR
{
    [MarshalAs(UnmanagedType.I4)]
    public UInt32 riff = 0x46464952; /* RIFF */
    [MarshalAs(UnmanagedType.I4)]
    public UInt32 fileSize;
    [MarshalAs(UnmanagedType.I4)]
    public UInt32 wave = 0x45564157; /* WAVE */
    [MarshalAs(UnmanagedType.I4)]
    public UInt32 fmt = 0x20746D66; /* fmt  */
    [MarshalAs(UnmanagedType.I4)]
    public UInt32 fmtbytes = 16;
    [MarshalAs(UnmanagedType.I2)]
    public UInt16 formatid;
    [MarshalAs(UnmanagedType.I2)]
    public UInt16 channel;
    [MarshalAs(UnmanagedType.I4)]
    public UInt32 fs;
    [MarshalAs(UnmanagedType.I4)]
    public UInt32 bytespersec;
    [MarshalAs(UnmanagedType.I2)]
    public UInt16 blocksize;
    [MarshalAs(UnmanagedType.I2)]
    public UInt16 bitspersample;
    [MarshalAs(UnmanagedType.I4)]
    public UInt32 data = 0x61746164; /* data */
    [MarshalAs(UnmanagedType.I4)]
    public UInt32 size;

    //convert the struct to byte array
    public byte[] getByteArray()
    {
        int len = Marshal.SizeOf(this);
        byte[] arr = new byte[len];
        IntPtr ptr = Marshal.AllocHGlobal(len);
        Marshal.StructureToPtr(this, ptr, true/*or false*/);
        Marshal.Copy(ptr, arr, 0, len /*or arr.Length*/);
        Marshal.FreeHGlobal(ptr);
        return arr;
    }
}

基本的なデータ並びに加えてひとつだけ関数が定義してあります。getByteArray() です。こいつは、このデータ並びを byte 単位で読みだす関数です。

波形データを生成する

今回は、44.1K、モノラルの波形データを10秒ほど作ってみましょう。

    uint fs = 44100; // 44.1K
    Int16[] data = PrepareWave(10 /*sec*/, 1000/*Hz*/, fs);

こんな感じです。PrepareWave() は秒数、波形の周波数、サンプリング周波数を渡すと、なにがしかの波形を16ビットのデータ配列で返す関数です。

    private Int16[] PrepareWave(uint dur, double freq, uint fs)
    {
        uint size = dur * fs; // 再生秒数×サンプリング周波数が配列サイズ
        Int16[] data = new Int16[size];

        for (uint i = 0; i < size; i++)
        {
            // full は別のところで double full = Math.Pow(2.0,15.0); で計算済み
            // full = 32768 のはず
            double d = full*generateWave(2 * Math.PI * freq * (double)i / fs);
            // 浮動小数点計算誤差でInt16を越えることがあるので、切り捨てておく
            // もっと厳密に計算したほうがいい音になるはず
            d = Math.Floor(d);
            // Int16 に変換
            data[i] = Convert.ToInt16(d);
        }
        return data;
    }

generateWave() 関数は Math.Sin() とかでいいのですが、改造しやすいように別途次のように用意しておきました。

    private double generateWave(double x)
    {
        return(Math.Sin(x));
    }

さてここまでで 16ビットモノラルの正弦波がバッファに用意できました。

ヘッダを整える

次に、wavファイルのヘッダ情報を設定します。

    uint fs = 44100; // 44.1K

    wavHdr.formatid = 0x0001; //PCM 非圧縮
    wavHdr.channel = 1; // ch=1 モノラル
    wavHdr.fs = fs;    //
    wavHdr.bytespersec = fs * 2; // 16bit 44.1K
    wavHdr.blocksize = 2; // ブロックサイズ (Byte/sample×チャンネル数)->→16ビットモノラルなので 2
    wavHdr.bitspersample = 16; // サンプルあたりのビット数 (bit/sample)
    wavHdr.size = 10 * fs * 2; // 波形データのバイト数
    wavHdr.fileSize = wavHdr.size + (uint)Marshal.SizeOf(wavHdr); // 全体のバイト数

これでwavヘッダも波形データも準備ができましたので、Stream を用意して、SoundPlayer に渡します。

	wavStream = PrepareStream(wavHdr, data);
	player.Stream = wavStream;

	private MemoryStream PrepareStream(WAVHDR hdr,Int16[] databuf)
	{
		MemoryStream memoryStream = new MemoryStream((int)wavHdr.fileSize);
		BinaryWriter bWriter = new BinaryWriter(memoryStream);
		// ヘッダ書きだし
		foreach (byte b in hdr.getByteArray())
		{
			bWriter.Write(b);
		}
		// 波形書きだし
		foreach(Int16 data in databuf)
		{
			bWriter.Write(data);
		}
		bWriter.Flush();
		memoryStream.Seek(0, SeekOrigin.Begin);
		return memoryStream;
	}

BinaryWriter クラスを使って MemoryStream にヘッダ、波形データの順で書きだしています。あとは再生すればいいだけです。今回は、PlayLooping() で繰り返し再生してみましょう。すると、1KHzの発振器の出来上がりというわけです。

private void buttonPlay_Click(object sender, EventArgs e)
{
	player.PlayLooping();
}

generateWave() を書きかえれば、いろんな波形が出せますよ。

ただね、どうせだったら、生の波形データをそのまま再生できる仕組みがあれば wavファイルフォーマットとか意識しなくて済むんですよね。それはまた今度の宿題っぽいですね。

ではでは。

load_downloadSoundPlayStreamTest  ソースをダウンロード

参考

  • C# WAV file class, audio mixing, and some light audio manipulation
    By Nightfox, 21 Apr 2009
    http://www.codeproject.com/Articles/35725/C-WAV-file-class-audio-mixing-and-some-light-audio
  • [C#] Loading WAV formatted beep to MemoryStream & played by system audio
    AceInfinity さん 06-23-2012, 06:22 PM
    http://tech.reboot.pro/showthread.php?tid=2866
  • WAV ファイルフォーマット
    近藤正芳氏
    http://www.kk.iij4u.or.jp/~kondo/wave/
  • WAV ファイルへの書き込み
    MSDN
    http://msdn.microsoft.com/ja-jp/library/cc371628.aspx
  • C#でWAVE音源から音を鳴らす
    2009年6月20日土曜日
    http://mureta6.blogspot.jp/2009/06/cwave.html
印刷
You can skip to the end and leave a response. Pinging is currently not allowed.
Subscribe to RSS Feed Twitter は Ume108 だよ