C# で音楽再生 2

あけましておめでとうございます。本年もよろしくおねがいいたします。

2010 年最初の記事は、帰省中に FREE を読んでその書評を書くつもりだったが、意外と時間がとれずに読了できていない ( 興味深くて読み進めるのが勿体ないという気持ちもある ) ので、年末に書きかけてた記事を仕上げる事にした。

というわけで、C# で音楽再生を行ってみるシリーズその 2。

シリーズまとめ
C# で音楽再生

目次

サンプル プログラム

プログラムを実行すると以下のようになる。画像のファイルパスの一部については、個人情報保護の為にモザイクを掛けている。

サンプル プログラム

サンプル プログラム

スクリーンショットの曲は TOKYO No.1 SOUL SET の名盤 TRIPLE BARREL から。

今回のテストで久しぶりに聴いたが、やはり素晴らしい。オクラホマ・スタンピートで聞こえるハープのフレーズはケミカル・ブラザーズのアルバムにも登場するが、何か有名な元ネタがあるのだろうか?

プロジェクト一式は以下となる。

サンプルプロジェクト
WpfAudioPlayer2.zip

サンプル プロジェクトのビルドには Visual Studio 2008 SP1、プログラムの実行には NET Framework 3.5 SP1 が必要となる。

今回の変更点

前回のサンプルでは NAudio 標準の機能とコーデックだけを使用していたが、今回は WMA の再生と ID3/ASF のメタデータ取得に対応する。

Windows Media Format Runtime

WMA 再生とメタデータ取得には、Windows Media Format Runtime ( 長いので以降は WMF と呼ぶ ) を使用する。

WMF のインターフェースは COM として公開されているので、C# から利用する場合は ComImport でラッパーを書く事になる。MSDN を眺めつつ、自分でインターフェースを書いてゆくのも良いが、以下のサイトで既にラッパーが公開されているので、ありがたく土台にさせていただく。

この記事のサンプルでは、上記サイトで公開されている実装の内、必要なものだけを抜粋・改変して使用している。

オーディオ データの読み取り

まずは WMA からオーディオ データを読み取る為のクラスを作成する。音声の出力は NAudio を利用する事になるので、NAudio 標準の Mp3FileReader や WaveFileReader と同様に、WaveStream の派生クラスとして実装を行う。

ただし、WMA 再生に利用する IWMSyncReader インターフェースには重要な制限事項がある。前述した CodeProject の記事にも但し書きされているが、このインターフェースは MTAThread で使用しなければならない。

NAudio を利用する場合、オーディオ データの読み取りと音声出力デバイスへの出力は複数のスレッドをまたぐ事になるので、STAThread のまま IWMSyncReader による読み込みを実行すると、途中で例外が発生してデータが読めなくなる。

.NET アプリケーションの標準は STAThread なので、この対応を行う場合は属性を MTAThread に書き換える必要があるが、これは非常に影響が大きく避けたいので、以下のような対策を行う。

  1. IWMSyncReader を使用するクラス、WmfReader を実装
  2. WmfReader の生成とデータ読み取りを単一のスレッドで行うための WmfSyncReader を実装
  3. NAudio に渡すインスタンスは WmfSyncReader にする

ソースコードは長いので引用しないが、この対策を念頭に置いてサンプル プロジェクトの WmfReader/WmfSyncReader の実装を見ると理解し易いと思う。

メタデータの読み取り

WMF のメタデータ読み取りは ASF と ID3v1 ~ v2.4 をサポートしている。

データの読み取りには IWMMetadataEditor2 と IWMHeaderInfo を使用する。WMCreateEditor 関数から IWMMetadataEditor2 が得られ、これを IWMHeaderInfo にキャストするとメタデータ編集用のオブジェクトが得られる。

メタデータの取得は IWMHeaderInfo.GetAttributeByName で行うが、返されるデータの型はバイト配列となるので、適宜、.NET の型に変換する。

メタデータが ASF の場合、文字列は常に Unicode で読み書きされるが、ID3 に ANSI で書き込んでいた場合も読み取りは Unicode になるようだ。

ジャケット画像や歌詞データの場合は構造体となる。今回のサンプルでは文字列と整数、真偽値、GUID だけに対応した。

C# で音楽再生 1

C# で音楽再生を行う方法について考えてみる。

ただし .NET Framework 標準の SoundPlayer クラスや WPF の MediaPlayer クラスは用いず、代りに NAudio と BASS ライブラリを使用する。今回は NAudio を使用した WAV/MP3 プレイヤーを作成してみる。

シリーズまとめ
C# で音楽再生

目次

サンプル プログラム

プログラムを実行すると以下のようになる。画像のファイルパスの一部については、個人情報保護の為にモザイクを掛けている。

サンプル プログラム

サンプル プログラム

音符マークのボタンから音楽ファイルを取り込むと、ファイルが ListView に表示される。ListView から曲を選択してプレイヤー部分の再生ボタンを押すと、音楽の再生が開始される。

GUI は一般的なプレイヤーに倣っているので、操作方法は何となく掴めると思う。メタデータを読み込んでいないので、ListView の表示が寂しいが、これは次回以降に対応したい。

プロジェクト一式は以下となる。

サンプルプロジェクト
WpfAudioPlayer.zip

サンプル プロジェクトのビルドには Visual Studio 2008 SP1、プログラムの実行には NET Framework 3.5 SP1 が必要となる。

NAudio とは?

NAudio とは、CodePlex で開発されている .NET 向けの音声操作ライブラリである。

CodePlex
NAudio

このライブラリでは以下の機能をサポートしている。

機能 対応 説明
音声出力 WaveOut Windows 標準の音声出力。
DirectSound DirectX による音声出力。
ASIO Steinberg によるオーディオデバイスの規格。
Windows Audio Session API Windows Vista から追加されたオーディオデバイスの規格。
対応ファイル WAVE Windows 標準の音声ファイル。
MP3 広く利用されている圧縮音声ファイル。
SoundFont DTM などで使用される音色ファイル。
MIDI DTM などで使用される演奏ファイル。
SFZ DTM などで使用される、演奏と音色を組み合わせたファイル。

今回はこれらの機能の内、WaveOut と WAVE/MP3 再生を利用する。

NAudio の使用準備

まず、NAudio の公式ページから NAudio をダウンロードする。トップページ右側の Download ボタンを押すと、ライセンス確認の後に zip ファイルを入手できる。

入手した zip ファイルを展開して、以下のフォルダが格納されている事を確認する。

フォルダ 内容
Binaries NAudio.dll。これが NAudio の本体となる。
Sample Apps サンプル アプリケーション。
Source Code NAudio 本体とサンプル アプリケーションのソース コード一式。

NAudio を使用する場合、Visual Studio のソリューション エクスプローラーから、プロジェクトに NAudio.dll への参照を追加する。

参照を追加した場合、そのプログラムの実行には NAudio.dll が必要となるので、ソリューション エクスプローラーの参照設定上にある NAudio のプロパティを開き、ローカル コピーを True にしておくと、ビルド時にコピーを自動実行してくれるので便利だ。

NAudio による音声再生

音声を再生する場合、ファイルから音声データを読み取る WaveStream、音声の出力を行う IWavePlayer を継承したオブジェクトの 2 種類を使用する。

NAudio に付属するサンプルの内、WinForms を使用した NAudioDemo のソースを読むと理解し易いだろう。今回の記事では WPF を使用しており、サンプルのには NAudioWpfDemo という WPF 用のデモも用意されているが、NAudioDemo の方が簡単だと思う。

以下は、音声再生を行うクラスのサンプルとなる。MeteringStream というクラスは NAudioDemo のものを流用している。

using System;
using System.IO;
using NAudio.Wave;

namespace WpfAudioPlayer
{
	/// <summary>
	/// オーディオ再生を行います。
	/// </summary>
	sealed class AudioPlayer : IDisposable
	{
		private WaveStream    _audioStream;
		private WaveChannel32 _volumeStream;
		private IWavePlayer   _waveOut;

		/// <summary>
		/// インスタンスを初期化します。
		/// </summary>
		/// <param name="fileName">ファイルへのパス。</param>
		/// <exception cref="FileNotFoundException">ファイルが存在しない。</exception>
		/// <exception cref="Exception">ストリームの生成に失敗した。</exception>
		public AudioPlayer( string fileName )
		{
			if( !File.Exists( fileName ) ) { throw new FileNotFoundException( fileName ); }

			try
			{
				this.InitializeStream( fileName );
			}
			catch( Exception exp )
			{
				this.Dispose();
				throw exp;
			}
		}

		/// <summary>
		/// ファイルへのストリームを生成します。
		/// </summary>
		/// <param name="fileName">ファイルへのパス。</param>
		/// <exception cref="InvalidOperationException">ストリームの生成に失敗した。</exception>
		private void InitializeStream( string fileName )
		{
			WaveChannel32 stream;
			if( fileName.EndsWith( ".wav" ) )
			{
				WaveStream reader = new WaveFileReader( fileName );
				if( reader.WaveFormat.Encoding != WaveFormatEncoding.Pcm )
				{
					reader = WaveFormatConversionStream.CreatePcmStream( reader );
					reader = new BlockAlignReductionStream( reader );
				}
				
				if( reader.WaveFormat.BitsPerSample != 16 )
				{
					var format = new WaveFormat( reader.WaveFormat.SampleRate, 16, reader.WaveFormat.Channels );
					reader = new WaveFormatConversionStream( format, reader );
				}
				
				stream = new WaveChannel32( reader );
			}
			else if( fileName.EndsWith( ".mp3" ) )
			{
				var reader             = new Mp3FileReader( fileName );
				var pcmStream          = WaveFormatConversionStream.CreatePcmStream( reader );
				var blockAlignedStream = new BlockAlignReductionStream( pcmStream );

				stream = new WaveChannel32( blockAlignedStream );
			}
			else
			{
				throw new InvalidOperationException( "Unsupported extension" );
			}

			this._volumeStream = stream;
			this._audioStream  = new MeteringStream( stream, stream.WaveFormat.SampleRate / 10 );

			this._waveOut = new WaveOut() { DesiredLatency = 200 };
			this._waveOut.Init( this._audioStream );
		}

		/// <summary>
		/// 再生を一時停止します。
		/// </summary>
		public void Pause()
		{
			this._waveOut.Pause();
		}

		/// <summary>
		/// 再生を開始します。
		/// </summary>
		public void Play()
		{
			switch( this._waveOut.PlaybackState )
			{
			case PlaybackState.Playing:
				break;
			
			case PlaybackState.Paused:
			case PlaybackState.Stopped:
				this._waveOut.Play();
				break;
			}
		}

		/// <summary>
		/// 再生を停止します。
		/// </summary>
		public void Stop()
		{
			this._waveOut.Stop();
			this._audioStream.Position = 0;
		}

		/// <summary>
		/// ボリュームを取得または設定します。
		/// </summary>
		public float Volume
		{
			get
			{
				return this._volumeStream.Volume;
			}
			set
			{
				this._volumeStream.Volume = value;
			}
		}

		/// <summary>
		/// リソースの解放を行います。
		/// </summary>
		public void Dispose()
		{
			if( this._waveOut != null )
			{
				this._waveOut.Stop();
			}

			if( this._audioStream != null )
			{
				this._volumeStream.Close();
				this._volumeStream = null;

				this._audioStream.Close();
				this._audioStream = null;
			}

			if( this._waveOut != null )
			{
				this._waveOut.Dispose();
				this._waveOut = null;
			}
		}
	}
}

InitializeStream 内では、指定されたファイルの拡張子によって WaveStream の生成を分岐している。もし WMA などに対応する場合は、WaveStream を継承した音声データの読み取りクラスを実装し、ここに追加する事になる。

75、76 行目の処理では、ファイルからの音声データ読み取りとボリューム調整用の 2 種類のストリームを生成している。

前者から音声データが読み取られると、音声出力デバイスに渡される前にボリューム調整用ストリームを経由する仕組みとなっているので、経由時に設定されたボリュームに合わせて音声データを加工し、音量調整を行う。

78 行目の処理では、音声出力を行う為のオブジェクトを生成している。このオブジェクトによって、音声の再生・一時停止・停止を行う。初期化子で DesiredLatency に 200 を指定しており、これは音声の遅延対策の為の処理で利用されるのだが、詳細は長くなるので割愛する。だいたい 200 ~ 300 程度を指定しておけば良いだろう。

init メソッドに音声データを読み取るストリームを指定する事で、再生中に読み取りとデバイス出力が行われる。この処理は非同期に実行されるので、ストリームとオブジェクトの生成に成功したら、後は再生などの操作と Dispose による寿命管理を行うだけでオーディオ プレイヤー部分は完成である。

スクリーンショットに表示していた楽曲は、筋肉少女帯の「仏陀L」に収録されている。

AUTHOR

アカベコ

HN:アカベコ。プログラマ。写真の赤べこは善光寺の土産。

CATEGORIES