C# で音楽再生 2

2010年1月4日 0 開発 , , , , ,

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

2010 年の初記事は帰省中に読んだ FREE の書評を書くつもりだったが、意外と時間がとれずに読了できていないので、年末に書きかけてた記事を仕上げることにした。

というわけで C# で音楽再生するシリーズその 2。

もくじ

サンプル プログラム

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

サンプル プログラム

スクリーンショットに表示されている曲は TOKYO No.1 SOUL SET の名盤 TRIPLE BARREL から。

今回のテストでひさしぶりに聴いたが、やはり素晴らしい。オクラホマ・スタンピートで聞こえるハープのフレーズはケミカル・ブラザーズのアルバムにも登場するが、何か有名な元ネタがあるのだろうか?→後で調べたら Willie Hutch の Brother’s Gonna Work It Out という曲だった。

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

サンプル プロジェクトのビルドには 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 標準の Mp3FileReaderWaveFileReader と同様に 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 をサポートしている。

データの読み取りには IWMMetadataEditor2IWMHeaderInfo を使用する。WMCreateEditor 関数の返す IWMMetadataEditor2 を IWMHeaderInfo にキャストするとメタデータ操作オブジェクトを得られる。

メタデータは IWMHeaderInfo.GetAttributeByName から取得できるのだが、返されるデータ型はバイト配列となるため、適宜 String や int に変換すること。

メタデータが ASF なら文字列は常に Unicode になるのだが、ID3 の値の文字コードが ANSI でも読み取りは Unicode になるようだ。文字コード判定と変換も実行してくれるのだろうか?

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

C# で音楽再生 1

2009年12月30日 0 開発 , , , , ,

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」に収録されている。