2 using System.Diagnostics;
4 namespace FDK.メディア.サウンド.WASAPI
6 public unsafe class Device : IDisposable
10 get { return ( this.更新間隔sec * 1000.0 ); }
12 public CSCore.CoreAudioAPI.AudioClock AudioClock
14 get { return this.bs_AudioClock; }
22 public void 初期化する( CSCore.CoreAudioAPI.AudioClientShareMode 共有モード, double 希望更新間隔sec = 0.015 )
28 Trace.Assert( this.Dispose済み );
29 this.Dispose済み = false;
33 #region " AudioClientをアクティベートする。"
35 using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() )
36 using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) )
38 this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス );
42 #region " 指定された希望更新間隔とデバイス能力をもとに、更新間隔を決定する。"
44 long 共有モードでの間隔100ns = 0;
45 long 排他モードでの最小間隔100ns = 0;
48 hr = this.AudioClient.GetDevicePeriodNative( out 共有モードでの間隔100ns, out 排他モードでの最小間隔100ns );
50 System.Runtime.InteropServices.Marshal.ThrowExceptionForHR( hr );
52 if( 共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared )
54 this.更新間隔100ns = 共有モードでの間隔100ns;
58 this.更新間隔100ns = Math.Max( FDK.Utilities.変換_sec単位から100ns単位へ( 希望更新間隔sec ), 排他モードでの最小間隔100ns );
62 #region " デバイスフォーマットを決定する。"
64 if( this.共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared )
66 this.WaveFormat = this.AudioClient.GetMixFormat();
70 this.WaveFormat = new CSCore.WaveFormat( 44100, 16, 2, CSCore.AudioEncoding.Pcm );
74 #region " AudioClient を初期化する。"
78 this.AudioClient.Initialize(
80 CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback,
81 // | CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsNoPersist, // 音量とミュートを記憶しない → 無効。してください
85 Guid.Empty ); // この AudioClient (= AudioStrem) が所属する AudioSession。null ならデフォルトのAudioSessionに登録される。
87 catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
89 if( AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED == e.ErrorCode )
91 #region " 排他&イベント駆動モードの場合、バッファサイズアライメントエラーが返される場合がある。この場合、サイズを調整してオーディオストリームを作成し直す。"
93 this.更新間隔100ns = FDK.Utilities.変換_sec単位から100ns単位へ(
94 (double) this.AudioClient.GetBufferSize() / (double) this.WaveFormat.SampleRate ); // GetBufferSize は、更新間隔に一番近い、アライメントされたバッファサイズ(sample単位)を返す。
96 // AudioClient を一度解放し、もう一度アクティベートし直す。
97 this.AudioClient.Dispose();
98 using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() )
99 using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) )
101 this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス );
104 // アライメントされたサイズを使って、AudioClient を再初期化する。それでもエラーなら例外発出。
105 this.AudioClient.Initialize(
107 CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback,
108 // | CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsNoPersist, // 音量とミュートを記憶しない → 無効。してください
118 this.更新間隔sample = this.AudioClient.GetBufferSize();
121 #region " AudioRenderClient を取得する。"
123 this.AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this.AudioClient );
126 #region " AudioClock を取得する。"
128 this.bs_AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this.AudioClient );
132 #region " ミキサーを生成し初期化する。"
134 this.Mixer = new Mixer();
135 this.Mixer.初期化する( this.更新間隔sample );
138 #region " 最初のエンドポイントバッファを無音で埋めておく。"
140 var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample );
142 // 無音を書き込んだことにして、バッファをコミット。(bufferPrtは使わない。)
143 this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent );
149 FDK.Log.Info( $"WASAPIクライアントを初期化しました。" );
150 FDK.Log.Info( ( 共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) ?
151 " モード: 共有 & イベント駆動" :
152 " モード: 排他 & イベント駆動" );
153 FDK.Log.Info( $" フォーマット: {this.WaveFormat.BitsPerSample} bits, {this.WaveFormat.SampleRate} Hz" );
154 FDK.Log.Info( $" エンドポイントバッファ: {( (float) this.更新間隔sample / (double) this.WaveFormat.SampleRate ) * 1000.0f} ミリ秒 ({this.更新間隔sample} samples) × 2枚" );
155 FDK.Log.Info( $" 希望更新間隔: {希望更新間隔sec * 1000.0} ミリ秒" );
156 FDK.Log.Info( $" 更新間隔: {this.更新間隔sec * 1000.0} ミリ秒 ({this.更新間隔sample} samples)" );
157 if( 共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive )
158 FDK.Log.Info( $" 最小間隔: {FDK.Utilities.変換_100ns単位からsec単位へ( 排他モードでの最小間隔100ns )} ミリ秒" );
162 #region " ワークキューとイベントを作成し、作業項目を登録する。"
164 // MediaFoundation が管理する、プロセス&MMCSSタスクごとに1つずつ作ることができる特別な共有ワークキューを取得、または生成して取得する。
166 SharpDX.MediaFoundation.MediaFactory.LockSharedWorkQueue(
167 ( 0.011 > this.更新間隔sec ) ? "Pro Audio" : "Games", 0, ref dwTaskId, out this.QueueID );
169 // エンドポイントバッファからの出力要請イベントを作成し、AudioClient に登録する。
170 this.出力要請イベント = CreateEvent( IntPtr.Zero, false, false, "WASAPI出力要請イベント" );
171 this.AudioClient.SetEventHandle( this.出力要請イベント );
173 // コールバックを作成し、ワークキューに最初の作業項目を登録する。
174 this.出力要請イベントのコールバック = new MFAsyncCallback( this.QueueID, ( ar ) => {
175 this.出力要請イベントへ対応する( ar );
179 #region " 最初の作業項目を追加する。"
181 this.作業項目をキューに格納する();
184 #region " WASAPI レンダリングを開始。"
186 this.AudioClient.Start();
191 public void Dispose()
193 Trace.Assert( false == this.Dispose済み );
195 #region " WASAPI作業項目を終了させる。オーディオのレンダリングを止める前に行うこと。"
198 //SharpDX.MediaFoundation.MediaFactory.CancelWorkItem( this.出力要請イベントキャンセル用キー ); --> コールバックの実行中にキャンセルしてしまうと NullReference例外
199 this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.ON;
200 this.出力終了通知.OFFになるまでブロックする();
201 FDK.Log.Info( "WASAPI出力処理を終了しました。" );
208 #region " オーディオのレンダリングを停止する。"
210 this.AudioClient?.Stop();
213 #region " ミキサー(とサウンドリスト)は現状を維持する。"
220 #region " WASAPIオブジェクトを解放する。"
222 FDK.Utilities.解放する( ref this.bs_AudioClock );
223 FDK.Utilities.解放する( ref this.AudioRenderClient );
224 FDK.Utilities.解放する( ref this.AudioClient );
227 #region " 共有ワークキューをこのプロセスから解放する。"
229 if( int.MaxValue != this.QueueID )
231 SharpDX.MediaFoundation.MediaFactory.UnlockWorkQueue( this.QueueID );
232 this.QueueID = int.MaxValue;
236 #region " WASAPIイベント駆動用のコールバックとイベントを解放する。"
238 FDK.Utilities.解放する( ref this.出力要請イベントのコールバック );
240 if( IntPtr.Zero != this.出力要請イベント )
241 CloseHandle( this.出力要請イベント );
245 this.Dispose済み = true;
248 FDK.Log.Info( "WASAPIクライアントを終了しました。" );
250 public void サウンドをミキサーに追加する( Sound sound )
252 this.Mixer.サウンドを追加する( sound );
254 public void サウンドをミキサーから削除する( Sound sound )
256 this.Mixer.サウンドを削除する( sound );
259 protected CSCore.CoreAudioAPI.AudioClientShareMode 共有モード;
260 protected CSCore.CoreAudioAPI.AudioClient AudioClient = null;
261 protected CSCore.CoreAudioAPI.AudioRenderClient AudioRenderClient = null;
262 protected CSCore.WaveFormat WaveFormat = null;
263 protected long 更新間隔100ns = 0;
264 protected int 更新間隔sample = 0;
265 protected int 更新間隔byte => ( this.更新間隔sample * this.WaveFormat.Channels * this.WaveFormat.BytesPerSample );
266 protected double 更新間隔sec => ( FDK.Utilities.変換_100ns単位からsec単位へ( this.更新間隔100ns ) );
269 protected Mixer Mixer = null;
272 private int QueueID = int.MaxValue;
273 private IntPtr 出力要請イベント = IntPtr.Zero;
274 private MFAsyncCallback 出力要請イベントのコールバック = null;
275 private long 出力要請イベントキャンセル用キー = 0;
276 private FDK.同期.TriStateEvent 出力終了通知 = new 同期.TriStateEvent();
278 private readonly object スレッド間同期 = new object();
280 private void 作業項目をキューに格納する()
282 var asyncResult = (SharpDX.MediaFoundation.AsyncResult) null;
285 // IAsyncCallback を内包した AsyncResult を作成する。
286 SharpDX.MediaFoundation.MediaFactory.CreateAsyncResult(
288 SharpDX.ComObject.ToCallbackPtr<SharpDX.MediaFoundation.IAsyncCallback>( this.出力要請イベントのコールバック ),
292 // 作成した AsyncResult を、ワークキュー投入イベントの待機状態にする。
293 SharpDX.MediaFoundation.MediaFactory.PutWaitingWorkItem(
294 hEvent: this.出力要請イベント,
296 resultRef: asyncResult,
297 keyRef: out this.出力要請イベントキャンセル用キー );
301 // out 引数に使う変数は using 変数にはできないので、代わりに try-finally を使う。
302 asyncResult?.Dispose();
307 /// このメソッドは、WASAPIイベント発生時にワークキューに投入され作業項目から呼び出される。
309 private void 出力要請イベントへ対応する( SharpDX.MediaFoundation.AsyncResult asyncResult )
313 // 出力終了通知が来ていれば、応答してすぐに終了する。
314 if( this.出力終了通知.状態 == 同期.TriStateEvent.状態種別.ON )
316 this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.無効;
322 // エンドポインタの空きバッファへのポインタを取得する。
323 // このポインタが差すのはネイティブで確保されたメモリなので、GCの対象外である。はず。
324 var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample ); // イベント駆動なのでサイズ固定。
326 // ミキサーを使って、エンドポインタへサウンドデータを出力する。
327 var flags = this.Mixer.エンドポイントへ出力する( (void*) bufferPtr, this.更新間隔sample );
329 // エンドポインタのバッファを解放する。
330 this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, flags );
332 // 後続のイベント待ち作業項目をキューに格納する。
333 this.作業項目をキューに格納する();
335 // 以降、WASAPIからイベントが発火されるたび、作業項目を通じて本メソッドが呼び出される。
346 private CSCore.CoreAudioAPI.AudioClock bs_AudioClock = null;
350 #region " Win32 API "
352 private static int AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = unchecked((int) 0x88890019);
354 [System.Runtime.InteropServices.DllImport( "kernel32.dll" )]
355 private static extern IntPtr CreateEvent( IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName );
357 [System.Runtime.InteropServices.DllImport( "kernel32.dll" )]
358 private static extern bool CloseHandle( IntPtr hObject );