アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

C# で音楽再生 3

C# で音楽再生を行ってみるシリーズその 3。前回まで音楽再生には NAudio と Windows Media Format を利用していたが、今回は BASS というライブラリを試す。

  • !!注意!!

    • 2010/1/9 のサンプル プロジェクトは間違ってテスト中のイメージ (プレイヤーが常に NAudio になる) をアップロードしてしまったので 2010/1/10 に更新
    • また、音量調整の対象をシステム全体からストリーム固有のボリューム修正と BASS 版の再生位置のシークを実装し忘れていたので、1/27 にサンプル プロジェクトを修正

サンプル プログラム

プログラムを実行すると以下のようになる。デフォルトのオーディオ操作ライブラリは Bass.Net だが、PlayerViewModel のコンストラクタ引数で NAudio にも切り替えられるように実装したので、ステータスバーにプレイヤーの種類を表示するようにしている。

サンプル プログラム

スクリーンショットの曲は Stray Cats の BLAST OFF から。

個人的に超名盤なのだが記事のために引用したら Amazon レビューがないのでビックリした。やはり日本ではロカビリーはマイナーなのだろうか。

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

BASS とは?

BASS とは、Un4seen Developments が提供しているオーディオ操作ライブラリである。ライセンスは非営利は無料で、商用の場合は有料 ( 詳しくは BASS のページの Licensing を参照の事 ) となる。

このライブラリは非常に豊富な機能を兼ね備えており、個人利用で非営利利用するならば、現時点で最良のオーディオ操作ライブラリと思われる。仕様については BASS のページの Main features を参考の事。BASS そのものはネイティブなライブラリだが、Bass.Net というラッパーライブラリにより、.NET アプリケーションからも利用できるようになっている。

また、BASS は Add-on と呼ばれるプラグインによって機能拡張を行える。Add-on には WMA や AAC のようなコーデックを追加するものだけではなく、音声処理や ID3 などのタグ編集まで、実に様々なものが用意されている。

中でも凄いのが BassWinamp である。この Ad-on は WinAmp のプラグインを BASS 経由で利用可能にする。WinAmp は長い歴史を持つオーディオ プレイヤーで非常に豊富なプラグイン資産を持つ。BASS の Add-on に無い機能でも WinAmp なら存在する可能性が高いから、この Add-on の使い方を覚えておくと有用である。

BASS の使用準備

初めに前述の BASS のページから、以下をダウンロードしておく。

  • Bass.Net
  • BASS 本体
  • BASSWMA などの Add-on

次に Bass.Net をインストールする。ダウンロードした zip を展開すると setup.exe が現れるので、これを起動してセットアップ作業を行う。手順については特に迷う部分はない。

途中でユーザー登録を行うと、送信したメールアドレスへ登録コードのメールが届く。必須の手順ではないが、BASS の初期化時に強制表示されるスプラッシュ スクリーンを抑止する場合は、このコードが必要になる。

Bass.Net をインストールすると、システムにアセンブリが登録される。Visual Studio のソリューション エクスプローラーの参照設定を右クリックして「参照の追加」を選ぶと、ダイアログの .NET タブ内のリストに「BASS.NET API for .Net」というコンポーネントが追加されているので、BASS を使用するプロジェクトに追加しておく。

Bass.Net は BASS の .NET 用ラッパーなので、実行には BASS 本体 ( bass.dll ) が必要となる点に注意する。BASS を使用する場合はプロジェクトの設定で、ビルド時に BASS 本体や Add-on が実行ファイルから参照できる位置にコピーされるようにしておくと安全である。

サンプル プロジェクトの場合は、BASS 本体と Add-on がビルド先の BASS フォルダにコピーされるように設定していて、BASS 初期化処理でも、このフォルダから BASS をロードするように実装している。

BASS による音声再生

サンプル プログラムの設計は MVVM 形式で設計しており、音楽再生機能は Model として位置付けている。音楽再生に必要なインターフェースは殆ど共通なので、これを IAudioPlayer として切り出している。

※前回のサンプルで既にこのインターフェースを使用していたが、今回でようやく実用的な意味を持つ事になる。

これらを実装したプレイヤークラスは PlayerViewModel が使い分けるが、インターフェースの共通性により変更は少なくて済んだ。また、PlayerView については変更自体が発生しなかった。今回のサンプル作成で、MVVM による階層化の現実的なメリットを享受する事となった。

因みにインターフェースの実装は以下のようになる。

/// <summary>
/// 音楽再生を行う為のインターフェースです。
/// </summary>
interface IAudioPlayer : IDisposable
{
    /// <summary>
    /// 再生を一時停止します。
    /// </summary>
    void Pause();

    /// <summary>
    /// 再生を開始します。
    /// </summary>
    void Play();

    /// <summary>
    /// 再生を停止します。
    /// </summary>
    void Stop();

    /// <summary>
    /// 再生位置の変更を行える事を示す値を取得します。
    /// </summary>
    bool CanSeek { get; }

    /// <summary>
    /// 再生位置を時間単位で取得または設定します。
    /// </summary>
    TimeSpan CurrentTime { get; set; }

    /// <summary>
    /// 演奏時間を取得します。
    /// </summary>
    TimeSpan Duration { get; }

    /// <summary>
    /// 音楽再生の状態を取得します。
    /// </summary>
    PlayState PlayState { get; }

    /// <summary>
    /// 音量を取得または設定します。
    /// </summary>
    float Volume { get; set; }
}

/// <summary>
/// 音楽再生の状態を表します。
/// </summary>
enum PlayState
{
    /// <summary>
    /// 再生中。
    /// </summary>
    Playing,

    /// <summary>
    /// 一時停止。
    /// </summary>
    Paused,

    /// <summary>
    /// 停止。
    /// </summary>
    Stopped
}

このインターフェースを実装した Bass.Net のオーディオ プレイヤー クラスの実装は以下のようになる。かなり長いが、削れそうな部分が無かったので、サンプルからそのままコピペした。

using System;
using System.IO;
using Un4seen.Bass;

namespace WpfAudioPlayer.Models.BassLib
{
    /// <summary>
    /// Bass.Net を利用して音楽再生を行います。
    /// </summary>
    sealed class AudioPlayer : IAudioPlayer
    {
        /// <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 ); }

            // ストリーム生成
            this._handle = Bass.BASS_StreamCreateFile( fileName, 0, 0, BASSFlag.BASS_DEFAULT );
            if( this._handle == 0 )
            {
                var error = Bass.BASS_ErrorGetCode();
                throw new Exception( "ストリームの生成に失敗しました。\nError : " + error.ToString() );
            }

            // 演奏時間の算出
            {
                long    length  = Bass.BASS_ChannelGetLength( this._handle );
                double  seconds = Bass.BASS_ChannelBytes2Seconds( this._handle, length );
                this.Duration = TimeSpan.FromSeconds( seconds );
            }

            this.PlayState = PlayState.Stopped;
        }

        /// <summary>
        /// Bass.Net を解放します。
        /// オーディオ再生が実行されている場合は停止されます。
        /// このメソッドを呼び出した場合、再度 BassInitialize メソッドを呼び出すまで Bass.Net は利用できません。
        /// </summary>
        public static void BassFree()
        {
            if( !AudioPlayer.IsBassInitialized ) { return; }

            Bass.BASS_Stop();
            Bass.BASS_PluginFree( 0 );
            Bass.FreeMe();

            AudioPlayer.IsBassInitialized = false;
        }

        /// <summary>
        /// Bass.Net を初期化します。
        /// Bass.Net のライフタイムは、このメソッドから BassFree メソッドの呼び出しまでとなります。
        /// </summary>
        /// <param name="folderPath">BASS ライブラリのモジュールを格納したフォルダを示すパス文字列。</param>
        /// <exception cref="Exception">初期化に失敗した。</exception>
        public static void BassInitialize( string folderPath )
        {
            if( AudioPlayer.IsBassInitialized ) { return; }

            // Bass.Net のスプラッシュ スクリーンを抑止
            //BassNet.Registration( "mail-address", "register-code" );

            // Bass.Net
            if( !Bass.LoadMe( folderPath ) )
            {
                throw new Exception( "Bass.Net の初期化に失敗しました。" );
            }

            // デバイス
            if( !Bass.BASS_Init( -1, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero ) )
            {
                var error = Bass.BASS_ErrorGetCode();
                throw new Exception( "デバイスの初期化に失敗しました。\nError : " + error.ToString() );
            }

            // プラグイン
            {
                var plugins = Bass.BASS_PluginLoadDirectory( folderPath );
                AudioPlayer.FileFilter = Utils.BASSAddOnGetPluginFileFilter( plugins, null );
            }

            AudioPlayer.IsBassInitialized = true;
        }

        /// <summary>
        /// ファイル ダイアログに指定する為のフィルタ文字列を取得します。
        /// </summary>
        public static string FileFilter { get; private set; }

        /// <summary>
        /// Bass.Net の初期化を終えている事を示す値を取得または設定します。
        /// </summary>
        private static bool IsBassInitialized { get; set; }

        #region IAudioPlayer メンバ

        /// <summary>
        /// 再生を一時停止します。
        /// </summary>
        public void Pause()
        {
            if( this.PlayState == PlayState.Playing )
            {
                this.PlayState = PlayState.Paused;
                Bass.BASS_ChannelPause( this._handle );
            }
        }

        /// <summary>
        /// 再生を開始します。
        /// </summary>
        public void Play()
        {
            if( this.PlayState != PlayState.Playing )
            {
                this.PlayState = PlayState.Playing;
                Bass.BASS_ChannelPlay( this._handle, false );
            }
        }

        /// <summary>
        /// 再生を停止します。
        /// </summary>
        public void Stop()
        {
            if( this.PlayState != PlayState.Stopped )
            {
                this.PlayState = PlayState.Stopped;
                Bass.BASS_ChannelStop( this._handle );
                Bass.BASS_ChannelSetPosition( this._handle, 0.0 );
            }
        }

        /// <summary>
        /// 再生位置の変更を行える事を示す値を取得します。
        /// </summary>
        public bool CanSeek { get { return true; } }

        /// <summary>
        /// 再生位置を時間単位で取得または設定します。
        /// </summary>
        public TimeSpan CurrentTime
        {
            get
            {
                var position = Bass.BASS_ChannelGetPosition( this._handle );
                return TimeSpan.FromSeconds( Bass.BASS_ChannelBytes2Seconds( this._handle, position ) );
            }
            set
            {
                var position = Bass.BASS_ChannelSeconds2Bytes( this._handle, value.TotalSeconds );
                Bass.BASS_ChannelSetPosition( this._handle, position );
            }
        }

        /// <summary>
        /// 演奏時間を取得します。
        /// </summary>
        public TimeSpan Duration { get; private set; }

        /// <summary>
        /// 音楽再生の状態を取得します。
        /// </summary>
        public PlayState PlayState { get; private set; }

        /// <summary>
        /// 音量を取得または設定します。
        /// </summary>
        public float Volume
        {
            get
            {
                return this._volume;
            }
            set
            {
                this._volume = value;
                Bass.BASS_ChannelSetAttribute( this._handle, BASSAttribute.BASS_ATTRIB_VOL, value );
            }
        }

        #endregion

        #region IDisposable メンバ

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

        #endregion

        #region フィールド

        /// <summary>
        /// オーディオ再生を行う為のストリームのハンドル。
        /// </summary>
        private int _handle;

        /// <summary>
        /// 音量。
        /// </summary>
        private float _volume = 1.0f;

        #endregion
    }
}

BASS の API は Un4seen.Bass.Bass という静的クラスとして提供されているので、初期化と解放は一元化しておいた方が安全である。

そのため 43 行目の BassFree と 60 行目の BassInitialize を静的メソッドとして定義、プレイヤー クラスを使用する側からの呼び出しを義務づけた上で複数回の実行を回避するようにした。

BASS を初期化するとスプラッシュ スクリーンが強制的に表示されるが BassNet.Registration を呼び出す事で抑止できる。このメソッドを利用するには BASS のユーザー登録が必要となる。BassNet.Registration は登録時のメール アドレスと、そのアドレスに送信されてきたメールにある登録コードを指定する。

.NET アプリのアセンブリは .NET Reflector などを使用する事でソース コードを逆コンパイル可能。よって BassNet.Registration を使用すると、その箇所からメール アドレスと登録コードが漏洩する問題がある。これについては本稿のテーマから逸れるため詳しくは解説しないが、Visual Studio (Express Edition は除く) に付属している Dotfuscator Community Edition などで文字列リテラルを難読化しておくとよいだろう。

音楽ファイル再生時は Bass.BASS_StreamCreateFile でストリームを生成する。BASS 本体または Add-on の対応しているファイルならパスを指定する事でストリームが生成され、識別子として int 型のハンドルが返される。Bass.Net のメソッドではハンドルが必要となるので、フィールドとして記録しておこう。

Bass.Net にはサンプルとかなり詳細なヘルプが付属するので、今回のサンプル以上の機能 (ASIO や WinAmp プラグインの利用など) はそちらを参照の事。

世の中には多くのオーディオ プレイヤーがあり、それぞれが独自にプラグイン機能を実装している。しかし BASS ぐらい高機能なライブラリが存在するなら、よほど特殊な事をしない限りこれを利用した方が資産を共有できてよいと思う。世にあるプレーヤーに好みのデザインやユーザビリティを持つものが見つからないなら、オーディオ機能は BASS に任せて自分専用のニッチなプレイヤーを作ってしまうのもアリだろう。

音量調整について

Bass.Net のオーディオ再生における音量調整はシステム全体またはストリーム固有のボリュームを対象とする。システム全体のボリュームを選ぶと他のアプリケーションの音量にも影響するのでストリーム固有にするのが好ましい。ストリームとシステムのボリュームを変更する処理は以下。

// ストリーム ( handle は Bass.BASS_StreamCreateFile の戻り値 )
Bass.BASS_ChannelSetAttribute( handle, BASSAttribute.BASS_ATTRIB_VOL, 1.0f );

// システム ( ボリュームを取得する場合は Bass.BASS_GetVolume )
Bass.BASS_SetVolume( 1.0f );

設定できる音量の範囲は 0 (無音) ~ 1 (最大) で型は float になる。1 = 100 とした百分率と考えれば理解しやすい。小数点以下を指定することで詳細な音量変更が行える。

Bass.BASS_ChannelSetAttribute は指定されたストリーム固有のパラメータを設定するメソッドで、音量の他にも定位や速度変更などが用意されている。ストリームがサポートしていれば様々な操作をおこなえるようになっている。

Comments from WordPress

  • はるじおー 2011-01-16T15:44:20Z

    はじめまして
    C#でMCIを利用して音ゲーを作っていたのですが、 再生速度を変更した後、再生位置を取得すると正しい値が取得できなかったため、別の方法を探していました。

    そこでこのサイトを見つけました。
    無事、再生や位置取得はできたのですが、速度変更のパラメータを設定できません。

    BASSChannelSetAttributeでBASSATTRIBMUSICSPEEDなどを変更してみたのですが、再生速度は通常のままでした。

    再生速度を設定するためのパラメータを知っていたら、教えていただけないでしょうか?
    よろしくお願いします。

  • はるじおー 2011-01-16T15:59:13Z

    連続投稿すみません
    投稿したあといくつか試していたところ、

    Bass.BASS_ChannelSetAttribute(handle, BASSAttribute.BASS_ATTRIB_FREQ, (float)_Speed / 1000f * 44100f)

    で再生速度が変化しました!
    自己解決したことを報告します。

    申し訳ありませんでした。

  • akabeko akabeko 2011-01-16T22:16:29Z

    オーディオ プレイヤーを作るために Bass.Net を利用していましたが、音ゲーにも応用できるのですね。 音程の変化なしに速度変更できると理想的なのですが、もしそれが実現できるのであれば、オーディオ プレイヤーでも有用そうです。 そういえば、いわゆる「早送り・早戻し」は、はるじおーさんの書かれている方法で実装できそうな気がします。