OSDN Git Service

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