2 using System.Diagnostics;
4 namespace FDK.メディア.サウンド.WASAPI
6 public unsafe class Device : IDisposable
10 get { return this.更新間隔ms; }
12 public CSCore.CoreAudioAPI.AudioClock AudioClock
14 get { return this.bs_AudioClock; }
22 public void 初期化する( double 希望更新間隔sec = 0.015 )
28 Trace.Assert( this.Dispose済み );
29 this.Dispose済み = false;
31 #region " AudioClientをアクティベートする。"
33 using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() )
34 using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) )
36 this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス );
40 #region " 指定された希望更新間隔とデバイス能力をもとに、更新間隔を決定する。"
42 long 共有モードでの間隔100ns = 0;
43 long 排他モードでの最小間隔100ns = 0;
46 hr = this.AudioClient.GetDevicePeriodNative( out 共有モードでの間隔100ns, out 排他モードでの最小間隔100ns );
48 System.Runtime.InteropServices.Marshal.ThrowExceptionForHR( hr );
50 var 最小間隔ms = (float) 排他モードでの最小間隔100ns / 10000.0f;
52 // 更新間隔ms を「希望更新間隔とデバイスの最小間隔の大きい方 かつ 最大1秒までの値」にする。
53 this.更新間隔ms = System.Math.Min( 1000.0f, System.Math.Max( (float) ( 希望更新間隔sec * 1000.0 ), 最小間隔ms ) );
56 #region " デバイスフォーマットを決定する。"
58 this.WaveFormat = new CSCore.WaveFormat( 44100, 16, 2, CSCore.AudioEncoding.Pcm );
61 #region " AudioClient を初期化する。"
65 this.AudioClient.Initialize(
66 CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive, // 排他モード。
67 CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, // イベント駆動モード。
68 (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // バッファサイズ。イベント駆動モードでは、更新間隔と同じ値でなければならない。
69 (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // 更新間隔。
70 this.WaveFormat, // バッファのフォーマット。
71 Guid.Empty ); // この AudioClient = AudioStrem が所属する AudioSession。null ならデフォルトのAudioSessionに登録される。
73 catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
75 // 排他&イベント駆動モードの場合、バッファのアライメントエラーが返される場合がある。この場合、サイズを調整してオーディオストリームを作成し直す。
76 if( AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED == e.ErrorCode )
78 int 更新間隔に一番近くてアライメントされているサイズsample = this.AudioClient.GetBufferSize();
79 this.更新間隔ms = ( 更新間隔に一番近くてアライメントされているサイズsample * 1000.0f / (float) this.WaveFormat.SampleRate );
81 // AudioClient を一度解放し、もう一度アクティベートし直す。
82 this.AudioClient.Dispose();
83 using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() )
84 using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) )
86 this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス );
89 // アライメントされたサイズを使って、AudioClient を再初期化する。
90 this.AudioClient.Initialize(
91 CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive, // 排他モード。
92 CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, // イベント駆動モード。
93 (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // バッファサイズ。イベント駆動モードでは、更新間隔と同じ値でなければならない。
94 (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // 更新間隔。
95 this.WaveFormat, // バッファのフォーマット。
96 Guid.Empty ); // この AudioClient = AudioStrem が所属する AudioSession。NULLならデフォルトのAudioSessionに登録される。
102 // 更新間隔を sample, byte 単位で保存する。
103 this.更新間隔sample = this.AudioClient.GetBufferSize(); // バッファの長さはサンプル単位で返される。
104 this.更新間隔byte = this.更新間隔sample * ( this.WaveFormat.Channels * this.WaveFormat.BytesPerSample );
107 #region " AudioRenderClient を取得する。"
109 this.AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this.AudioClient );
112 #region " AudioClock を取得する。"
114 this.bs_AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this.AudioClient );
118 #region " ミキサーを生成し初期化する。"
120 this.Mixer = new Mixer();
121 this.Mixer.初期化する( this.更新間隔sample );
124 #region " 最初のエンドポイントバッファを無音で埋めておく。"
126 var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample );
128 // 無音を書き込んだことにして、バッファをコミット。(bufferPrtは使わない。)
129 this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent );
135 FDK.Log.Info( $"WASAPIクライアントを初期化しました。" );
136 FDK.Log.Info( $" モード: 排他&イベント駆動" );
137 FDK.Log.Info( $" フォーマット: {this.WaveFormat.BitsPerSample} bits, {this.WaveFormat.SampleRate} Hz" );
138 FDK.Log.Info( $" エンドポイントバッファ: {( (float) this.更新間隔sample / (double) this.WaveFormat.SampleRate ) * 1000.0f} ミリ秒 ({this.更新間隔sample} samples) × 2枚" );
139 FDK.Log.Info( $" 希望更新間隔: {希望更新間隔sec * 1000.0} ミリ秒" );
140 FDK.Log.Info( $" 更新間隔: {this.更新間隔ms} ミリ秒 ({this.更新間隔sample} samples)" );
141 FDK.Log.Info( $" 最小間隔: {最小間隔ms} ミリ秒" );
145 #region " ワークキューとイベントを作成し、作業項目を登録する。"
147 // MediaFoundation が管理する、プロセス&MMCSSタスクごとに1つずつ作ることができる特別な共有ワークキューを取得、または生成して取得する。
149 SharpDX.MediaFoundation.MediaFactory.LockSharedWorkQueue(
150 ( 11.0 > this.更新間隔ms ) ? "Pro Audio" : "Games", 0, ref dwTaskId, out this.QueueID );
152 // エンドポイントバッファからの出力要請イベントを作成し、AudioClient に登録する。
153 this.出力要請イベント = CreateEvent( IntPtr.Zero, false, false, "WASAPI出力要請イベント" );
154 this.AudioClient.SetEventHandle( this.出力要請イベント );
156 // コールバックを作成し、ワークキューに最初の作業項目を登録する。
157 this.出力要請イベントのコールバック = new MFAsyncCallback( this.QueueID, ( ar ) => {
158 this.出力要請イベントへ対応する( ar );
162 #region " 最初の作業項目を追加する。"
164 this.作業項目をキューに格納する();
167 #region " WASAPI レンダリングを開始。"
169 this.AudioClient.Start();
174 public void Dispose()
176 Trace.Assert( false == this.Dispose済み );
178 #region " WASAPI作業項目を終了させる。オーディオのレンダリングを止める前に行うこと。"
181 //SharpDX.MediaFoundation.MediaFactory.CancelWorkItem( this.出力要請イベントキャンセル用キー ); --> コールバックの実行中にキャンセルしてしまうと NullReference例外
182 this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.ON;
183 this.出力終了通知.OFFになるまでブロックする();
184 FDK.Log.Info( "WASAPI出力処理を終了しました。" );
191 #region " オーディオのレンダリングを停止する。"
193 this.AudioClient?.Stop();
196 #region " ミキサー(とサウンドリスト)は現状を維持する。"
203 #region " WASAPIオブジェクトを解放する。"
205 FDK.Utilities.解放する( ref this.bs_AudioClock );
206 FDK.Utilities.解放する( ref this.AudioRenderClient );
207 FDK.Utilities.解放する( ref this.AudioClient );
210 #region " 共有ワークキューをこのプロセスから解放する。"
212 if( int.MaxValue != this.QueueID )
214 SharpDX.MediaFoundation.MediaFactory.UnlockWorkQueue( this.QueueID );
215 this.QueueID = int.MaxValue;
219 #region " WASAPIイベント駆動用のコールバックとイベントを解放する。"
221 FDK.Utilities.解放する( ref this.出力要請イベントのコールバック );
223 if( IntPtr.Zero != this.出力要請イベント )
224 CloseHandle( this.出力要請イベント );
228 this.Dispose済み = true;
231 FDK.Log.Info( "WASAPIクライアントを終了しました。" );
233 public void サウンドをミキサーに追加する( Sound sound )
235 this.Mixer.サウンドを追加する( sound );
237 public void サウンドをミキサーから削除する( Sound sound )
239 this.Mixer.サウンドを削除する( sound );
243 protected CSCore.CoreAudioAPI.AudioClient AudioClient = null;
244 protected CSCore.CoreAudioAPI.AudioRenderClient AudioRenderClient = null;
247 protected int 更新間隔sample = 0; // デバイスから取得する。
248 protected float 更新間隔ms;
249 protected int 更新間隔byte;
250 protected CSCore.WaveFormat WaveFormat = null;
253 protected Mixer Mixer = null;
256 private int QueueID = int.MaxValue;
257 private IntPtr 出力要請イベント = IntPtr.Zero;
258 private MFAsyncCallback 出力要請イベントのコールバック = null;
259 private long 出力要請イベントキャンセル用キー = 0;
260 private FDK.同期.TriStateEvent 出力終了通知 = new 同期.TriStateEvent();
262 private readonly object スレッド間同期 = new object();
264 private void 作業項目をキューに格納する()
266 var asyncResult = (SharpDX.MediaFoundation.AsyncResult) null;
269 // IAsyncCallback を内包した AsyncResult を作成する。
270 SharpDX.MediaFoundation.MediaFactory.CreateAsyncResult(
272 SharpDX.ComObject.ToCallbackPtr<SharpDX.MediaFoundation.IAsyncCallback>( this.出力要請イベントのコールバック ),
276 // 作成した AsyncResult を、ワークキュー投入イベントの待機状態にする。
277 SharpDX.MediaFoundation.MediaFactory.PutWaitingWorkItem(
278 hEvent: this.出力要請イベント,
280 resultRef: asyncResult,
281 keyRef: out this.出力要請イベントキャンセル用キー );
285 // out 引数に使う変数は using 変数にはできないので、代わりに try-finally を使う。
286 asyncResult?.Dispose();
291 /// このメソッドは、WASAPIイベント発生時にワークキューに投入され作業項目から呼び出される。
293 private void 出力要請イベントへ対応する( SharpDX.MediaFoundation.AsyncResult asyncResult )
297 // 出力終了通知が来ていれば、応答してすぐに終了する。
298 if( this.出力終了通知.状態 == 同期.TriStateEvent.状態種別.ON )
300 this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.無効;
306 // エンドポインタの空きバッファへのポインタを取得する。
307 // このポインタが差すのはネイティブで確保されたメモリなので、GCの対象外である。はず。
308 var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample ); // イベント駆動なのでサイズ固定。
310 // ミキサーを使って、エンドポインタへサウンドデータを出力する。
311 var flags = this.Mixer.エンドポイントへ出力する( (void*) bufferPtr, this.更新間隔sample );
313 // エンドポインタのバッファを解放する。
314 this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, flags );
316 // 後続のイベント待ち作業項目をキューに格納する。
317 this.作業項目をキューに格納する();
319 // 以降、WASAPIからイベントが発火されるたび、作業項目を通じて本メソッドが呼び出される。
330 private CSCore.CoreAudioAPI.AudioClock bs_AudioClock = null;
334 #region " Win32 API "
336 private static int AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = unchecked((int) 0x88890019);
338 [System.Runtime.InteropServices.DllImport( "kernel32.dll" )]
339 private static extern IntPtr CreateEvent( IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName );
341 [System.Runtime.InteropServices.DllImport( "kernel32.dll" )]
342 private static extern bool CloseHandle( IntPtr hObject );