OSDN Git Service

Device クラスのメンバを若干修正。
[strokestylet/CsWin10Desktop3.git] / FDK24 / メディア / サウンド / WASAPI / Device.cs
1 using System;
2 using System.Diagnostics;
3
4 namespace FDK.メディア.サウンド.WASAPI
5 {
6         public unsafe class Device : IDisposable
7         {
8                 public float 遅延ms
9                 {
10                         get { return this.更新間隔ms; }
11                 }
12                 public CSCore.CoreAudioAPI.AudioClock AudioClock
13                 {
14                         get { return this.bs_AudioClock; }
15                 }
16                 public bool Dispose済み
17                 {
18                         get;
19                         protected set;
20                 } = true;
21
22                 public void 初期化する( double 希望更新間隔sec = 0.015 )
23                 {
24                         int hr = 0;
25
26                         lock( this.スレッド間同期 )
27                         {
28                                 Trace.Assert( this.Dispose済み );
29                                 this.Dispose済み = false;
30
31                                 #region " AudioClientをアクティベートする。"
32                                 //-----------------
33                                 using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() )
34                                 using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) )
35                                 {
36                                         this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス );
37                                 }
38                                 //-----------------
39                                 #endregion
40                                 #region " 指定された希望更新間隔とデバイス能力をもとに、更新間隔を決定する。"
41                                 //-----------------
42                                 long 共有モードでの間隔100ns = 0;
43                                 long 排他モードでの最小間隔100ns = 0;
44
45                                 // デバイスから間隔値を取得する。
46                                 hr = this.AudioClient.GetDevicePeriodNative( out 共有モードでの間隔100ns, out 排他モードでの最小間隔100ns );
47                                 if( 0 > hr )
48                                         System.Runtime.InteropServices.Marshal.ThrowExceptionForHR( hr );
49
50                                 var 最小間隔ms = (float) 排他モードでの最小間隔100ns / 10000.0f;
51
52                                 // 更新間隔ms を「希望更新間隔とデバイスの最小間隔の大きい方 かつ 最大1秒までの値」にする。
53                                 this.更新間隔ms = System.Math.Min( 1000.0f, System.Math.Max( (float) ( 希望更新間隔sec * 1000.0 ), 最小間隔ms ) );
54                                 //-----------------
55                                 #endregion
56                                 #region " デバイスフォーマットを決定する。"
57                                 //----------------
58                                 this.WaveFormat = new CSCore.WaveFormat( 44100, 16, 2, CSCore.AudioEncoding.Pcm );
59                                 //----------------
60                                 #endregion
61                                 #region " AudioClient を初期化する。"
62                                 //-----------------
63                                 try
64                                 {
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に登録される。
72                                 }
73                                 catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
74                                 {
75                                         // 排他&イベント駆動モードの場合、バッファのアライメントエラーが返される場合がある。この場合、サイズを調整してオーディオストリームを作成し直す。
76                                         if( AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED == e.ErrorCode )
77                                         {
78                                                 int 更新間隔に一番近くてアライメントされているサイズsample = this.AudioClient.GetBufferSize();
79                                                 this.更新間隔ms = ( 更新間隔に一番近くてアライメントされているサイズsample * 1000.0f / (float) this.WaveFormat.SampleRate );
80
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 ) )
85                                                 {
86                                                         this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス );
87                                                 }
88
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に登録される。
97
98                                                 // それでもエラーなら例外発生。
99                                         }
100                                 }
101
102                                 // 更新間隔を sample, byte 単位で保存する。
103                                 this.更新間隔sample = this.AudioClient.GetBufferSize(); // バッファの長さはサンプル単位で返される。
104                                 this.更新間隔byte = this.更新間隔sample * ( this.WaveFormat.Channels * this.WaveFormat.BytesPerSample );
105                                 //-----------------
106                                 #endregion
107                                 #region " AudioRenderClient を取得する。"
108                                 //-----------------
109                                 this.AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this.AudioClient );
110                                 //-----------------
111                                 #endregion
112                                 #region " AudioClock を取得する。"
113                                 //-----------------
114                                 this.bs_AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this.AudioClient );
115                                 //-----------------
116                                 #endregion
117
118                                 #region " ミキサーを生成し初期化する。"
119                                 //-----------------
120                                 this.Mixer = new Mixer();
121                                 this.Mixer.初期化する( this.更新間隔sample );
122                                 //-----------------
123                                 #endregion
124                                 #region " 最初のエンドポイントバッファを無音で埋めておく。"
125                                 //-----------------
126                                 var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample );
127
128                                 // 無音を書き込んだことにして、バッファをコミット。(bufferPrtは使わない。)
129                                 this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent );
130                                 //-----------------
131                                 #endregion
132
133                                 #region " 情報表示。"
134                                 //-----------------
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} ミリ秒" );
142                                 //-----------------
143                                 #endregion
144
145                                 #region " ワークキューとイベントを作成し、作業項目を登録する。"
146                                 //-----------------
147                                 // MediaFoundation が管理する、プロセス&MMCSSタスクごとに1つずつ作ることができる特別な共有ワークキューを取得、または生成して取得する。
148                                 int dwTaskId = 0;
149                                 SharpDX.MediaFoundation.MediaFactory.LockSharedWorkQueue(
150                                         ( 11.0 > this.更新間隔ms ) ? "Pro Audio" : "Games", 0, ref dwTaskId, out this.QueueID );
151
152                                 // エンドポイントバッファからの出力要請イベントを作成し、AudioClient に登録する。
153                                 this.出力要請イベント = CreateEvent( IntPtr.Zero, false, false, "WASAPI出力要請イベント" );
154                                 this.AudioClient.SetEventHandle( this.出力要請イベント );
155
156                                 // コールバックを作成し、ワークキューに最初の作業項目を登録する。
157                                 this.出力要請イベントのコールバック = new MFAsyncCallback( this.QueueID, ( ar ) => {
158                                         this.出力要請イベントへ対応する( ar );
159                                 } );
160                                 //-----------------
161                                 #endregion
162                                 #region " 最初の作業項目を追加する。"
163                                 //-----------------
164                                 this.作業項目をキューに格納する();
165                                 //-----------------
166                                 #endregion
167                                 #region " WASAPI レンダリングを開始。"
168                                 //-----------------
169                                 this.AudioClient.Start();
170                                 //-----------------
171                                 #endregion
172                         }
173                 }
174                 public void Dispose()
175                 {
176                         Trace.Assert( false == this.Dispose済み );
177
178                         #region " WASAPI作業項目を終了させる。オーディオのレンダリングを止める前に行うこと。"
179                         //-----------------
180                         {
181                                 //SharpDX.MediaFoundation.MediaFactory.CancelWorkItem( this.出力要請イベントキャンセル用キー ); --> コールバックの実行中にキャンセルしてしまうと NullReference例外
182                                 this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.ON;
183                                 this.出力終了通知.OFFになるまでブロックする();
184                                 FDK.Log.Info( "WASAPI出力処理を終了しました。" );
185                         }
186                         //-----------------
187                         #endregion
188
189                         lock( this.スレッド間同期 )
190                         {
191                                 #region " オーディオのレンダリングを停止する。"
192                                 //-----------------
193                                 this.AudioClient?.Stop();
194                                 //-----------------
195                                 #endregion
196                                 #region " ミキサー(とサウンドリスト)は現状を維持する。"
197                                 //-----------------
198
199                                 // 何もしない。
200
201                                 //-----------------
202                                 #endregion
203                                 #region " WASAPIオブジェクトを解放する。"
204                                 //-----------------
205                                 FDK.Utilities.解放する( ref this.bs_AudioClock );
206                                 FDK.Utilities.解放する( ref this.AudioRenderClient );
207                                 FDK.Utilities.解放する( ref this.AudioClient );
208                                 //-----------------
209                                 #endregion
210                                 #region " 共有ワークキューをこのプロセスから解放する。"
211                                 //-----------------
212                                 if( int.MaxValue != this.QueueID )
213                                 {
214                                         SharpDX.MediaFoundation.MediaFactory.UnlockWorkQueue( this.QueueID );
215                                         this.QueueID = int.MaxValue;
216                                 }
217                                 //-----------------
218                                 #endregion
219                                 #region " WASAPIイベント駆動用のコールバックとイベントを解放する。"
220                                 //-----------------
221                                 FDK.Utilities.解放する( ref this.出力要請イベントのコールバック );
222
223                                 if( IntPtr.Zero != this.出力要請イベント )
224                                         CloseHandle( this.出力要請イベント );
225                                 //-----------------
226                                 #endregion
227
228                                 this.Dispose済み = true;
229                         }
230
231                         FDK.Log.Info( "WASAPIクライアントを終了しました。" );
232                 }
233                 public void サウンドをミキサーに追加する( Sound sound )
234                 {
235                         this.Mixer.サウンドを追加する( sound );
236                 }
237                 public void サウンドをミキサーから削除する( Sound sound )
238                 {
239                         this.Mixer.サウンドを削除する( sound );
240                 }
241
242                 // WASAPI オブジェクト
243                 protected CSCore.CoreAudioAPI.AudioClient AudioClient = null;
244                 protected CSCore.CoreAudioAPI.AudioRenderClient AudioRenderClient = null;
245
246                 // エンドポイントバッファ情報
247                 protected int 更新間隔sample = 0;   // デバイスから取得する。
248                 protected float 更新間隔ms;
249                 protected int 更新間隔byte;
250                 protected CSCore.WaveFormat WaveFormat = null;
251
252                 // ミキサー。サウンドリストもここ。
253                 protected Mixer Mixer = null;
254
255                 // WASAPIバッファ出力用
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();
261
262                 private readonly object スレッド間同期 = new object();
263
264                 private void 作業項目をキューに格納する()
265                 {
266                         var asyncResult = (SharpDX.MediaFoundation.AsyncResult) null;
267                         try
268                         {
269                                 // IAsyncCallback を内包した AsyncResult を作成する。
270                                 SharpDX.MediaFoundation.MediaFactory.CreateAsyncResult(
271                                         null,
272                                         SharpDX.ComObject.ToCallbackPtr<SharpDX.MediaFoundation.IAsyncCallback>( this.出力要請イベントのコールバック ),
273                                         null,
274                                         out asyncResult );
275
276                                 // 作成した AsyncResult を、ワークキュー投入イベントの待機状態にする。
277                                 SharpDX.MediaFoundation.MediaFactory.PutWaitingWorkItem(
278                                         hEvent: this.出力要請イベント,
279                                         priority: 0,
280                                         resultRef: asyncResult,
281                                         keyRef: out this.出力要請イベントキャンセル用キー );
282                         }
283                         finally
284                         {
285                                 // out 引数に使う変数は using 変数にはできないので、代わりに try-finally を使う。
286                                 asyncResult?.Dispose();
287                         }
288                 }
289
290                 /// <summary>
291                 /// このメソッドは、WASAPIイベント発生時にワークキューに投入され作業項目から呼び出される。
292                 /// </summary>
293                 private void 出力要請イベントへ対応する( SharpDX.MediaFoundation.AsyncResult asyncResult )
294                 {
295                         try
296                         {
297                                 // 出力終了通知が来ていれば、応答してすぐに終了する。
298                                 if( this.出力終了通知.状態 == 同期.TriStateEvent.状態種別.ON )
299                                 {
300                                         this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.無効;
301                                         return;
302                                 }
303
304                                 lock( this.スレッド間同期 )
305                                 {
306                                         // エンドポインタの空きバッファへのポインタを取得する。
307                                         // このポインタが差すのはネイティブで確保されたメモリなので、GCの対象外である。はず。
308                                         var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample );    // イベント駆動なのでサイズ固定。
309
310                                         // ミキサーを使って、エンドポインタへサウンドデータを出力する。
311                                         var flags = this.Mixer.エンドポイントへ出力する( (void*) bufferPtr, this.更新間隔sample );
312
313                                         // エンドポインタのバッファを解放する。
314                                         this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, flags );
315
316                                         // 後続のイベント待ち作業項目をキューに格納する。
317                                         this.作業項目をキューに格納する();
318
319                                         // 以降、WASAPIからイベントが発火されるたび、作業項目を通じて本メソッドが呼び出される。
320                                 }
321                         }
322                         catch
323                         {
324                                 // 例外は無視。
325                         }
326                 }
327
328                 #region " バックストア。"
329                 //----------------
330                 private CSCore.CoreAudioAPI.AudioClock bs_AudioClock = null;
331                 //----------------
332                 #endregion
333
334                 #region " Win32 API "
335                 //-----------------
336                 private static int AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = unchecked((int) 0x88890019);
337
338                 [System.Runtime.InteropServices.DllImport( "kernel32.dll" )]
339                 private static extern IntPtr CreateEvent( IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName );
340
341                 [System.Runtime.InteropServices.DllImport( "kernel32.dll" )]
342                 private static extern bool CloseHandle( IntPtr hObject );
343                 //-----------------
344                 #endregion
345         }
346 }