using System; using System.Diagnostics; namespace FDK.メディア.サウンド.WASAPI { public unsafe class Device : IDisposable { public float 遅延ms { get { return this.更新間隔ms; } } public CSCore.CoreAudioAPI.AudioClock AudioClock { get { return this.bs_AudioClock; } } public bool Dispose済み { get; protected set; } = true; public void 初期化する( double 希望更新間隔sec = 0.015 ) { int hr = 0; lock( this.スレッド間同期 ) { Trace.Assert( this.Dispose済み ); this.Dispose済み = false; #region " AudioClientをアクティベートする。" //----------------- using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() ) using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) ) { this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス ); } //----------------- #endregion #region " 指定された希望更新間隔とデバイス能力をもとに、更新間隔を決定する。" //----------------- long 共有モードでの間隔100ns = 0; long 排他モードでの最小間隔100ns = 0; // デバイスから間隔値を取得する。 hr = this.AudioClient.GetDevicePeriodNative( out 共有モードでの間隔100ns, out 排他モードでの最小間隔100ns ); if( 0 > hr ) System.Runtime.InteropServices.Marshal.ThrowExceptionForHR( hr ); var 最小間隔ms = (float) 排他モードでの最小間隔100ns / 10000.0f; // 更新間隔ms を「希望更新間隔とデバイスの最小間隔の大きい方 かつ 最大1秒までの値」にする。 this.更新間隔ms = System.Math.Min( 1000.0f, System.Math.Max( (float) ( 希望更新間隔sec * 1000.0 ), 最小間隔ms ) ); //----------------- #endregion #region " デバイスフォーマットを決定する。" //---------------- this.WaveFormat = new CSCore.WaveFormat( 44100, 16, 2, CSCore.AudioEncoding.Pcm ); //---------------- #endregion #region " AudioClient を初期化する。" //----------------- try { this.AudioClient.Initialize( CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive, // 排他モード。 CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, // イベント駆動モード。 (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // バッファサイズ。イベント駆動モードでは、更新間隔と同じ値でなければならない。 (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // 更新間隔。 this.WaveFormat, // バッファのフォーマット。 Guid.Empty ); // この AudioClient = AudioStrem が所属する AudioSession。null ならデフォルトのAudioSessionに登録される。 } catch( CSCore.CoreAudioAPI.CoreAudioAPIException e ) { // 排他&イベント駆動モードの場合、バッファのアライメントエラーが返される場合がある。この場合、サイズを調整してオーディオストリームを作成し直す。 if( AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED == e.ErrorCode ) { int 更新間隔に一番近くてアライメントされているサイズsample = this.AudioClient.GetBufferSize(); this.更新間隔ms = ( 更新間隔に一番近くてアライメントされているサイズsample * 1000.0f / (float) this.WaveFormat.SampleRate ); // AudioClient を一度解放し、もう一度アクティベートし直す。 this.AudioClient.Dispose(); using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() ) using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) ) { this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス ); } // アライメントされたサイズを使って、AudioClient を再初期化する。 this.AudioClient.Initialize( CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive, // 排他モード。 CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, // イベント駆動モード。 (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // バッファサイズ。イベント駆動モードでは、更新間隔と同じ値でなければならない。 (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // 更新間隔。 this.WaveFormat, // バッファのフォーマット。 Guid.Empty ); // この AudioClient = AudioStrem が所属する AudioSession。NULLならデフォルトのAudioSessionに登録される。 // それでもエラーなら例外発生。 } } // 更新間隔を sample, byte 単位で保存する。 this.更新間隔sample = this.AudioClient.GetBufferSize(); // バッファの長さはサンプル単位で返される。 this.更新間隔byte = this.更新間隔sample * ( this.WaveFormat.Channels * this.WaveFormat.BytesPerSample ); //----------------- #endregion #region " AudioRenderClient を取得する。" //----------------- this.AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this.AudioClient ); //----------------- #endregion #region " AudioClock を取得する。" //----------------- this.bs_AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this.AudioClient ); //----------------- #endregion #region " ミキサーを生成し初期化する。" //----------------- this.Mixer = new Mixer(); this.Mixer.初期化する( this.更新間隔sample ); //----------------- #endregion #region " 最初のエンドポイントバッファを無音で埋めておく。" //----------------- var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample ); // 無音を書き込んだことにして、バッファをコミット。(bufferPrtは使わない。) this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent ); //----------------- #endregion #region " 情報表示。" //----------------- FDK.Log.Info( $"WASAPIクライアントを初期化しました。" ); FDK.Log.Info( $" モード: 排他&イベント駆動" ); FDK.Log.Info( $" フォーマット: {this.WaveFormat.BitsPerSample} bits, {this.WaveFormat.SampleRate} Hz" ); FDK.Log.Info( $" エンドポイントバッファ: {( (float) this.更新間隔sample / (double) this.WaveFormat.SampleRate ) * 1000.0f} ミリ秒 ({this.更新間隔sample} samples) × 2枚" ); FDK.Log.Info( $" 希望更新間隔: {希望更新間隔sec * 1000.0} ミリ秒" ); FDK.Log.Info( $" 更新間隔: {this.更新間隔ms} ミリ秒 ({this.更新間隔sample} samples)" ); FDK.Log.Info( $" 最小間隔: {最小間隔ms} ミリ秒" ); //----------------- #endregion #region " ワークキューとイベントを作成し、作業項目を登録する。" //----------------- // MediaFoundation が管理する、プロセス&MMCSSタスクごとに1つずつ作ることができる特別な共有ワークキューを取得、または生成して取得する。 int dwTaskId = 0; SharpDX.MediaFoundation.MediaFactory.LockSharedWorkQueue( ( 11.0 > this.更新間隔ms ) ? "Pro Audio" : "Games", 0, ref dwTaskId, out this.QueueID ); // エンドポイントバッファからの出力要請イベントを作成し、AudioClient に登録する。 this.出力要請イベント = CreateEvent( IntPtr.Zero, false, false, "WASAPI出力要請イベント" ); this.AudioClient.SetEventHandle( this.出力要請イベント ); // コールバックを作成し、ワークキューに最初の作業項目を登録する。 this.出力要請イベントのコールバック = new MFAsyncCallback( this.QueueID, ( ar ) => { this.出力要請イベントへ対応する( ar ); } ); //----------------- #endregion #region " 最初の作業項目を追加する。" //----------------- this.作業項目をキューに格納する(); //----------------- #endregion #region " WASAPI レンダリングを開始。" //----------------- this.AudioClient.Start(); //----------------- #endregion } } public void Dispose() { Trace.Assert( false == this.Dispose済み ); #region " WASAPI作業項目を終了させる。オーディオのレンダリングを止める前に行うこと。" //----------------- { //SharpDX.MediaFoundation.MediaFactory.CancelWorkItem( this.出力要請イベントキャンセル用キー ); --> コールバックの実行中にキャンセルしてしまうと NullReference例外 this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.ON; this.出力終了通知.OFFになるまでブロックする(); FDK.Log.Info( "WASAPI出力処理を終了しました。" ); } //----------------- #endregion lock( this.スレッド間同期 ) { #region " オーディオのレンダリングを停止する。" //----------------- this.AudioClient?.Stop(); //----------------- #endregion #region " ミキサー(とサウンドリスト)は現状を維持する。" //----------------- // 何もしない。 //----------------- #endregion #region " WASAPIオブジェクトを解放する。" //----------------- FDK.Utilities.解放する( ref this.bs_AudioClock ); FDK.Utilities.解放する( ref this.AudioRenderClient ); FDK.Utilities.解放する( ref this.AudioClient ); //----------------- #endregion #region " 共有ワークキューをこのプロセスから解放する。" //----------------- if( int.MaxValue != this.QueueID ) { SharpDX.MediaFoundation.MediaFactory.UnlockWorkQueue( this.QueueID ); this.QueueID = int.MaxValue; } //----------------- #endregion #region " WASAPIイベント駆動用のコールバックとイベントを解放する。" //----------------- FDK.Utilities.解放する( ref this.出力要請イベントのコールバック ); if( IntPtr.Zero != this.出力要請イベント ) CloseHandle( this.出力要請イベント ); //----------------- #endregion this.Dispose済み = true; } FDK.Log.Info( "WASAPIクライアントを終了しました。" ); } public void サウンドをミキサーに追加する( Sound sound ) { this.Mixer.サウンドを追加する( sound ); } public void サウンドをミキサーから削除する( Sound sound ) { this.Mixer.サウンドを削除する( sound ); } // WASAPI オブジェクト protected CSCore.CoreAudioAPI.AudioClient AudioClient = null; protected CSCore.CoreAudioAPI.AudioRenderClient AudioRenderClient = null; // エンドポイントバッファ情報 protected int 更新間隔sample = 0; // デバイスから取得する。 protected float 更新間隔ms; protected int 更新間隔byte; protected CSCore.WaveFormat WaveFormat = null; // ミキサー。サウンドリストもここ。 protected Mixer Mixer = null; // WASAPIバッファ出力用 private int QueueID = int.MaxValue; private IntPtr 出力要請イベント = IntPtr.Zero; private MFAsyncCallback 出力要請イベントのコールバック = null; private long 出力要請イベントキャンセル用キー = 0; private FDK.同期.TriStateEvent 出力終了通知 = new 同期.TriStateEvent(); private readonly object スレッド間同期 = new object(); private void 作業項目をキューに格納する() { var asyncResult = (SharpDX.MediaFoundation.AsyncResult) null; try { // IAsyncCallback を内包した AsyncResult を作成する。 SharpDX.MediaFoundation.MediaFactory.CreateAsyncResult( null, SharpDX.ComObject.ToCallbackPtr( this.出力要請イベントのコールバック ), null, out asyncResult ); // 作成した AsyncResult を、ワークキュー投入イベントの待機状態にする。 SharpDX.MediaFoundation.MediaFactory.PutWaitingWorkItem( hEvent: this.出力要請イベント, priority: 0, resultRef: asyncResult, keyRef: out this.出力要請イベントキャンセル用キー ); } finally { // out 引数に使う変数は using 変数にはできないので、代わりに try-finally を使う。 asyncResult?.Dispose(); } } /// /// このメソッドは、WASAPIイベント発生時にワークキューに投入され作業項目から呼び出される。 /// private void 出力要請イベントへ対応する( SharpDX.MediaFoundation.AsyncResult asyncResult ) { try { // 出力終了通知が来ていれば、応答してすぐに終了する。 if( this.出力終了通知.状態 == 同期.TriStateEvent.状態種別.ON ) { this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.無効; return; } lock( this.スレッド間同期 ) { // エンドポインタの空きバッファへのポインタを取得する。 // このポインタが差すのはネイティブで確保されたメモリなので、GCの対象外である。はず。 var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample ); // イベント駆動なのでサイズ固定。 // ミキサーを使って、エンドポインタへサウンドデータを出力する。 var flags = this.Mixer.エンドポイントへ出力する( (void*) bufferPtr, this.更新間隔sample ); // エンドポインタのバッファを解放する。 this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, flags ); // 後続のイベント待ち作業項目をキューに格納する。 this.作業項目をキューに格納する(); // 以降、WASAPIからイベントが発火されるたび、作業項目を通じて本メソッドが呼び出される。 } } catch { // 例外は無視。 } } #region " バックストア。" //---------------- private CSCore.CoreAudioAPI.AudioClock bs_AudioClock = null; //---------------- #endregion #region " Win32 API " //----------------- private static int AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = unchecked((int) 0x88890019); [System.Runtime.InteropServices.DllImport( "kernel32.dll" )] private static extern IntPtr CreateEvent( IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName ); [System.Runtime.InteropServices.DllImport( "kernel32.dll" )] private static extern bool CloseHandle( IntPtr hObject ); //----------------- #endregion } }