アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

C# で音楽再生 1

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

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

サンプル プログラム

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

サンプル プログラム

音符マークのボタンから音楽ファイルを取り込むとファイルが ListView に表示される。ここから曲を選択してプレイヤー部分の再生ボタンを押すと音楽が再生される。GUI は一般的なプレイヤーに倣っているので操作方法はなんとなく掴めるだろう。メタデータを読み込んでいないので表示が寂しいが、これは改めて対応したい。

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

NAudio とは?

NAudio とは、CodePlex で開発されている (いた。2018/11/27 現在は Archive となり GitHub で運用されている) .NET 向けの音声操作ライブラリである。

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

音声出力

出力形式 説明
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 を入手する。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 行目の処理では音声出力を行う為のオブジェクトを生成している。このオブジェクトにより音声を再生・一時停止・停止する。初期化子で DesiredLatency200 を指定している。これは音声の遅延対策の為の処理で利用されるのだが詳細は長くなるので割愛する。だいたい 200300 程度を指定しておけば良いだろう。

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

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