OSDN Git Service

デバイスの初期化と同時にレンダリングを開始するように修正。
[strokestylet/CsWin10Desktop3.git] / FDK24 / メディア / サウンド / WASAPI / Device.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Diagnostics;
4 using System.Linq;
5 using System.Runtime.InteropServices;
6 using CSCore;
7
8 namespace FDK.メディア.サウンド.WASAPI
9 {
10         public class Device : IDisposable
11         {
12                 public CSCore.SoundOut.PlaybackState レンダリング状態
13                 {
14                         get { return this._レンダリング状態; }
15                 }
16
17                 public double 遅延sec
18                 {
19                         get { return FDK.Utilities.変換_100ns単位からsec単位へ( this._遅延100ns ); }
20                         protected set { this._遅延100ns = FDK.Utilities.変換_sec単位から100ns単位へ( value ); }
21                 }
22
23                 public long 遅延100ns
24                 {
25                         get { return this._遅延100ns; }
26                         protected set { this._遅延100ns = value; }
27                 }
28
29                 public CSCore.WaveFormat フォーマット
30                 {
31                         get { return this._WaveFormat; }
32                 }
33
34                 /// <summary>
35                 ///             レンダリングボリューム。
36                 ///             0.0 (0%) ~ 1.0 (100%) 。
37                 /// </summary>
38                 public float 音量
39                 {
40                         get
41                         {
42                                 return ( null != this._レンダリング先 ) ? this._レンダリング先.Volume : 1.0f;
43                         }
44                         set
45                         {
46                                 if( ( 0.0f > value ) || ( 1.0f < value ) )
47                                         throw new ArgumentOutOfRangeException();
48
49                                 this._レンダリング先.Volume = value;
50                         }
51                 }
52
53                 public Device( CSCore.CoreAudioAPI.AudioClientShareMode 共有モード, double 遅延sec = 0.010, CSCore.WaveFormat 希望フォーマット = null )
54                 {
55                         this._共有モード = 共有モード;
56                         this.遅延sec = 遅延sec;
57                         this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
58
59                         this._初期化する( 希望フォーマット );
60
61                         this.PlayRendering();
62                 }
63
64                 /// <summary>
65                 ///             メディアファイル(動画、音声)からサウンドインスタンスを生成して返す。
66                 /// </summary>
67                 public Sound CreateSound( string path )
68                 {
69                         return new Sound( path, this._Mixer );
70                 }
71
72                 /// <summary>
73                 ///             現在のデバイス位置を取得する。
74                 /// </summary>
75                 public void GetClock( out long Pu64Position, out long QPCPosition )
76                 {
77                         lock( this._スレッド間同期 )
78                         {
79                                 this._AudioClock.GetPositionNative( out Pu64Position, out QPCPosition );
80                         }
81                 }
82
83                 /// <summary>
84                 ///             ミキサーの出力を開始する。
85                 ///             以降、ミキサーに Sound を追加すれば、自動的に再生される。
86                 /// </summary>
87                 public void PlayRendering()
88                 {
89                         lock( this._スレッド間同期 )
90                         {
91                                 if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused )
92                                 {
93                                         // Pause 中なら Resume する。
94                                         this.ResumeRendering();
95                                 }
96                                 else if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Stopped )
97                                 {
98                                         using( var 起動完了通知 = new System.Threading.AutoResetEvent( false ) )
99                                         {
100                                                 // スレッドがすでに終了していることを確認する。
101                                                 this._レンダリングスレッド?.Join();
102
103                                                 // レンダリングスレッドを起動する。
104                                                 this._レンダリングスレッド = new System.Threading.Thread( this._レンダリングスレッドエントリ ) {
105                                                         Name = "WASAPI Playback",
106                                                         Priority = System.Threading.ThreadPriority.AboveNormal, // 標準よりやや上
107                                                 };
108                                                 this._レンダリングスレッド.Start( 起動完了通知 );
109
110                                                 // スレッドからの起動完了通知を待つ。
111                                                 起動完了通知.WaitOne();
112                                         }
113                                 }
114                         }
115                 }
116
117                 /// <summary>
118                 ///             ミキサーの出力を停止する。
119                 ///             ミキサーに登録されているすべての Sound の再生が停止する。
120                 /// </summary>
121                 public void StopRendering()
122                 {
123                         lock( this._スレッド間同期 )
124                         {
125                                 if( ( this._レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped ) && ( null != this._レンダリングスレッド ) )
126                                 {
127                                         // レンダリングスレッドに終了を通知し、その終了を待つ。
128                                         this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
129                                         this._レンダリングスレッド.Join();
130                                         this._レンダリングスレッド = null;
131                                         Debug.WriteLine( "WASAPIのレンダリングを停止しました。" );
132                                 }
133                                 else
134                                 {
135                                         Debug.WriteLine( "WASAPIのレンダリングを停止しようとしましたが、すでに停止しています。" );
136                                 }
137                         }
138                 }
139
140                 /// <summary>
141                 ///             ミキサーの出力を一時停止する。
142                 ///             ミキサーに登録されているすべての Sound の再生が一時停止する。
143                 ///             ResumeRendering()で出力を再開できる。
144                 /// </summary>
145                 public void PauseRendering()
146                 {
147                         lock( this._スレッド間同期 )
148                         {
149                                 if( this.レンダリング状態 == CSCore.SoundOut.PlaybackState.Playing )
150                                 {
151                                         this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Paused;
152                                         Debug.WriteLine( "WASAPIのレンダリングを一時停止しました。" );
153                                 }
154                                 else
155                                 {
156                                         Debug.WriteLine( "WASAPIのレンダリングを一時停止しようとしましたが、すでに一時停止しています。" );
157                                 }
158                         }
159                 }
160
161                 /// <summary>
162                 ///             ミキサーの出力を再開する。
163                 ///             PauseRendering() で一時停止状態にあるときのみ有効。
164                 /// </summary>
165                 public void ResumeRendering()
166                 {
167                         lock( this._スレッド間同期 )
168                         {
169                                 if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused )
170                                 {
171                                         this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Playing;
172                                         Debug.WriteLine( "WASAPIのレンダリングを再開しました。" );
173                                 }
174                                 else
175                                 {
176                                         Debug.WriteLine( "WASAPIのレンダリングを再開しようとしましたが、すでに再開されています。" );
177                                 }
178                         }
179                 }
180
181                 #region " 解放; Dispose-Finallize パターン "
182                 //----------------
183                 ~Device()
184                 {
185                         this.Dispose( false );
186                 }
187
188                 public void Dispose()
189                 {
190                         this.Dispose( true );
191                         GC.SuppressFinalize( this );
192                 }
193
194                 protected virtual void Dispose( bool bDisposeManaged )
195                 {
196                         if( !this._dispose済み )
197                         {
198                                 lock( this._スレッド間同期 )
199                                 {
200                                         if( bDisposeManaged )
201                                         {
202                                                 // (A) ここでマネージリソースを解放する。
203                                                 this.StopRendering();
204                                                 this._解放する();
205                                         }
206
207                                         // (B) ここでネイティブリソースを解放する。
208
209                                         // ...特にない。
210
211                                 }
212
213                                 this._dispose済み = true;
214                         }
215                 }
216                 //----------------
217                 #endregion
218
219
220                 private volatile CSCore.SoundOut.PlaybackState _レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
221
222                 private CSCore.CoreAudioAPI.AudioClientShareMode _共有モード;
223
224                 private long _遅延100ns = 0;
225
226                 private CSCore.WaveFormat _WaveFormat = null;
227
228                 private CSCore.CoreAudioAPI.AudioClock _AudioClock = null;
229
230                 private CSCore.CoreAudioAPI.AudioRenderClient _AudioRenderClient = null;
231
232                 private CSCore.CoreAudioAPI.AudioClient _AudioClient = null;
233
234                 private CSCore.CoreAudioAPI.MMDevice _MMDevice = null;
235
236                 private System.Threading.Thread _レンダリングスレッド = null;
237
238                 private System.Threading.EventWaitHandle _レンダリングイベント = null;
239
240                 private CSCore.Streams.VolumeSource _レンダリング先 = null;
241
242                 private CSCore.IWaveSource _生レンダリング先 = null;
243
244                 private Mixer _Mixer = null;
245
246                 private readonly object _スレッド間同期 = new object();
247
248                 private bool _dispose済み = false;
249
250
251                 private void _初期化する( CSCore.WaveFormat 希望フォーマット = null )
252                 {
253                         lock( this._スレッド間同期 )
254                         {
255                                 if( this._レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped )
256                                         throw new InvalidOperationException( "WASAPI のレンダリングを停止しないまま初期化することはできません。" );
257
258                                 this._レンダリングスレッド?.Join();
259
260                                 this._解放する();
261
262                                 // MMDevice を取得する。
263                                 this._MMDevice = CSCore.CoreAudioAPI.MMDeviceEnumerator.DefaultAudioEndpoint(
264                                         CSCore.CoreAudioAPI.DataFlow.Render,    // 方向:再生
265                                         CSCore.CoreAudioAPI.Role.Console );     // 用途:ゲーム、システム通知音、音声命令
266
267                                 // AudioClient を取得する。
268                                 this._AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( this._MMDevice );
269
270                                 // フォーマットを決定する。
271                                 var defaultFormat = ( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) ?
272                                         this._AudioClient.GetMixFormat() :
273                                         new CSCore.WaveFormat( 48000, 32, 2, AudioEncoding.IeeeFloat );
274
275                                 if( null == ( this._WaveFormat = this._適切なフォーマットを調べて返す( 希望フォーマット ?? defaultFormat ) ) )
276                                 {
277                                         throw new NotSupportedException( "サポート可能な WaveFormat が見つかりませんでした。" );
278                                 }
279
280                                 // 遅延を既定値にする(共有モードの場合のみ)。
281                                 if( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared )
282                                         this._遅延100ns = this._AudioClient.DefaultDevicePeriod;
283
284                                 // AudioClient を初期化する。
285                                 Action AudioClientを初期化する = () => {
286                                         this._AudioClient.Initialize(
287                                                 this._共有モード,
288                                                 CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback,    // イベント駆動で固定。
289                                                 this._遅延100ns,
290                                                 this._遅延100ns,
291                                                 this._WaveFormat,
292                                                 Guid.Empty );
293                                 };
294                                 try
295                                 {
296                                         AudioClientを初期化する();
297                                 }
298                                 catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
299                                 {
300                                         // 排他モードかつイベント駆動 の場合、この例外が返されることがある。
301                                         // この場合、バッファサイズを調整して再度初期化する。
302                                         if( e.ErrorCode == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED )
303                                         {
304                                                 int サイズframe = this._AudioClient.GetBufferSize();
305                                                 this._遅延100ns = (long) ( 10.0 * 1000.0 * 1000.0 * サイズframe / this._WaveFormat.SampleRate + 0.5 );   // +0.5 は四捨五入
306
307                                                 AudioClientを初期化する();    // それでも例外なら知らん。
308                                         }
309                                 }
310
311                                 // イベント駆動用に使うイベントを生成し、AudioClient へ登録する。
312                                 this._レンダリングイベント = new System.Threading.EventWaitHandle( false, System.Threading.EventResetMode.AutoReset );
313                                 this._AudioClient.SetEventHandle( this._レンダリングイベント.SafeWaitHandle.DangerousGetHandle() );
314
315                                 // その他の WASAPI インターフェースを取得する。
316                                 this._AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this._AudioClient );
317                                 this._AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this._AudioClient );
318
319                                 // ミキサーを生成し、デバイスのソース(DirectSound でいうところのプライマリバッファ)として登録する。
320                                 this._Mixer = new Mixer( this._WaveFormat ) {
321                                         DivideResult = false,
322                                 };
323                                 this._SetSource( this._Mixer );
324                         }
325                 }
326
327                 private void _解放する()
328                 {
329                         FDK.Utilities.解放する( ref this._Mixer );
330                         FDK.Utilities.解放する( ref this._AudioClock );
331                         FDK.Utilities.解放する( ref this._AudioRenderClient );
332
333                         if( ( null != this._AudioClient ) && ( this._AudioClient.BasePtr != IntPtr.Zero ) )
334                         {
335                                 try
336                                 {
337                                         this._AudioClient.StopNative();
338                                         this._AudioClient.Reset();
339                                 }
340                                 catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
341                                 {
342                                         if( e.ErrorCode != AUDCLNT_E_NOT_INITIALIZED )
343                                                 throw;
344                                 }
345                         }
346
347                         FDK.Utilities.解放する( ref this._AudioClient );
348                         FDK.Utilities.解放する( ref this._レンダリングイベント );
349                         FDK.Utilities.解放する( ref this._MMDevice );
350                 }
351
352                 private void _SetSource( CSCore.ISampleSource targetSource )
353                 {
354                         if( null != this._レンダリング先 )
355                                 throw new InvalidOperationException( "レンダリングターゲットはすでに設定済みです。" );
356
357                         this._レンダリング先 = new CSCore.Streams.VolumeSource( targetSource );  // サンプル(float)単位のレンダリング先と、
358                         this._生レンダリング先 = targetSource.ToWaveSource();                                           // データ(byte)単位のレンダリング先とを持っておく。
359                 }
360
361                 /// <summary>
362                 ///             希望したフォーマットをもとに、適切なフォーマットを調べて返す。
363                 /// </summary>
364                 /// <param name="waveFormat">希望するフォーマット</param>
365                 /// <param name="audioClient">AudioClient インスタンス。Initialize 前でも可。</param>
366                 /// <returns>適切なフォーマット。見つからなかったら null。</returns>
367                 private CSCore.WaveFormat _適切なフォーマットを調べて返す( CSCore.WaveFormat waveFormat )
368                 {
369                         Trace.Assert( null != this._AudioClient );
370
371                         var 最も近いフォーマット = (CSCore.WaveFormat) null;
372                         var 最終的に決定されたフォーマット = (CSCore.WaveFormat) null;
373
374                         if( this._AudioClient.IsFormatSupported( this._共有モード, waveFormat, out 最も近いフォーマット ) )
375                         {
376                                 // (A) そのまま使える。
377                                 最終的に決定されたフォーマット = waveFormat;
378                         }
379                         else if( null != 最も近いフォーマット )
380                         {
381                                 // (B) AudioClient が推奨フォーマットを返してきたなら、それを採択する。
382                                 最終的に決定されたフォーマット = 最も近いフォーマット;
383                         }
384                         else
385                         {
386                                 // (C) AudioClient からの提案がなかった場合は、共有モードのフォーマットを採択する。
387
388                                 var 共有モードのフォーマット = this._AudioClient.GetMixFormat();
389
390                                 if( ( null != 共有モードのフォーマット ) && this._AudioClient.IsFormatSupported( this._共有モード, 共有モードのフォーマット ) )
391                                 {
392                                         最終的に決定されたフォーマット = 共有モードのフォーマット;
393                                 }
394                                 else
395                                 {
396                                         // (D) AudioClient が共有モードのフォーマットすらNGと言ってきた場合は、以下から探す。
397
398                                         最終的に決定されたフォーマット = new[]
399                                         {
400                                                         new CSCore.WaveFormatExtensible( waveFormat.SampleRate, 32, waveFormat.Channels, CSCore.AudioSubTypes.IeeeFloat ),
401                                                         new CSCore.WaveFormatExtensible( waveFormat.SampleRate, 24, waveFormat.Channels, CSCore.AudioSubTypes.Pcm ),
402                                                         new CSCore.WaveFormatExtensible( waveFormat.SampleRate, 16, waveFormat.Channels, CSCore.AudioSubTypes.Pcm ),
403                                                         new CSCore.WaveFormatExtensible( waveFormat.SampleRate,  8, waveFormat.Channels, CSCore.AudioSubTypes.Pcm ),
404                                                 }
405                                         .FirstOrDefault( ( format ) => ( this._AudioClient.IsFormatSupported( this._共有モード, format ) ) );
406
407                                         // (E) それでも見つからなかったら null 。
408                                 }
409                         }
410
411                         return 最終的に決定されたフォーマット;
412                 }
413
414                 /// <summary>
415                 ///             WASAPIイベント駆動スレッドのエントリ。
416                 /// </summary>
417                 /// <param name="起動完了通知">無事に起動できたら、これを Set して(スレッドの生成元に)知らせる。</param>
418                 private void _レンダリングスレッドエントリ( object 起動完了通知 )
419                 {
420                         var 例外 = (Exception) null;
421                         var avrtHandle = IntPtr.Zero;
422
423                         try
424                         {
425                                 int バッファサイズframe = this._AudioClient.BufferSize;
426                                 int フレームサイズbyte = this._WaveFormat.Channels * this._WaveFormat.BytesPerSample;
427                                 var バッファ = new byte[ バッファサイズframe * フレームサイズbyte ];
428
429                                 // このスレッドの MMCSS 型を登録する。
430                                 int taskIndex;
431                                 string mmcssType = new[] {
432                                         new { 最大遅延 = 0.010, 型名 = "Pro Audio" },             // 優先度の高いものから。
433                                         new { 最大遅延 = 0.015, 型名 = "Games" },
434                                 }
435                                 .FirstOrDefault( ( i ) => ( i.最大遅延 > this.遅延sec ) )?.型名 ?? "Audio";
436                                 avrtHandle = Device.AvSetMmThreadCharacteristics( mmcssType, out taskIndex );
437
438                                 // AudioClient を開始する。
439                                 this._AudioClient.Start();
440                                 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Playing;
441
442                                 // 起動完了を通知する。
443                                 ( 起動完了通知 as System.Threading.EventWaitHandle )?.Set();
444                                 起動完了通知 = null;
445
446                                 // 以下、メインループ。
447
448                                 var イベントs = new System.Threading.WaitHandle[] { this._レンダリングイベント };
449                                 while( this.レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped )
450                                 {
451                                         int イベント番号 = System.Threading.WaitHandle.WaitAny(
452                                                 waitHandles: イベントs,
453                                                 millisecondsTimeout: (int) ( 3000.0 * this.遅延sec ), // 適正値は レイテンシ×3 [ms] (MSDN)
454                                                 exitContext: false );
455
456                                         if( イベント番号 == System.Threading.WaitHandle.WaitTimeout )
457                                                 continue;
458
459                                         if( this.レンダリング状態 == CSCore.SoundOut.PlaybackState.Playing )
460                                         {
461                                                 int 未再生数frame = ( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive ) ? 0 : this._AudioClient.GetCurrentPadding();
462                                                 int 空きframe = バッファサイズframe - 未再生数frame;
463
464                                                 if( 空きframe > 5 )   // あまりに空きが小さいならスキップする。
465                                                 {
466                                                         if( !this._バッファを埋める( バッファ, 空きframe, フレームサイズbyte ) )
467                                                                 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
468                                                 }
469                                         }
470                                 }
471
472                                 // 以下、終了処理。
473
474                                 // このスレッドの MMCSS 特性を元に戻す。
475                                 Device.AvRevertMmThreadCharacteristics( avrtHandle );
476                                 avrtHandle = IntPtr.Zero;
477
478                                 // ハードウェアの再生が終わるくらいまで、少し待つ。
479                                 System.Threading.Thread.Sleep( (int) ( this.遅延sec * 1000 / 2 ) );
480
481                                 // AudioClient を停止する。
482                                 this._AudioClient.Stop();
483                                 this._AudioClient.Reset();
484                         }
485                         catch( Exception e )
486                         {
487                                 例外 = e;
488                         }
489                         finally
490                         {
491                                 if( avrtHandle != IntPtr.Zero )
492                                         Device.AvRevertMmThreadCharacteristics( avrtHandle );
493
494                                 ( 起動完了通知 as System.Threading.EventWaitHandle )?.Set();      // 失敗時を想定して。
495                         }
496                 }
497
498                 private bool _バッファを埋める( byte[] バッファ, int フレーム数, int フレームサイズbyte )
499                 {
500                         int 読み込むサイズbyte = フレーム数 * フレームサイズbyte;
501                         読み込むサイズbyte -= ( 読み込むサイズbyte % this._生レンダリング先.WaveFormat.BlockAlign );  // BlockAlign の倍数にする。
502
503                         if( 読み込むサイズbyte <= 0 )
504                                 return true;
505
506                         int 読み込んだサイズbyte = this._生レンダリング先.Read( バッファ, 0, 読み込むサイズbyte );
507
508                         IntPtr ptr = this._AudioRenderClient.GetBuffer( フレーム数 );
509                         Marshal.Copy( バッファ, 0, ptr, 読み込んだサイズbyte );
510                         this._AudioRenderClient.ReleaseBuffer( 読み込んだサイズbyte / フレームサイズbyte, CSCore.CoreAudioAPI.AudioClientBufferFlags.None );
511
512                         return ( 0 < 読み込んだサイズbyte );
513                 }
514
515                 #region " Win32 "
516                 //----------------
517                 private const int AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = unchecked((int) 0x88890019);
518                 private const int AUDCLNT_E_INVALID_DEVICE_PERIOD = unchecked((int) 0x88890020);
519                 private const int AUDCLNT_E_NOT_INITIALIZED = unchecked((int) 0x88890001);
520
521                 [DllImport( "Avrt.dll", CharSet = CharSet.Unicode )]
522                 private static extern IntPtr AvSetMmThreadCharacteristics( [MarshalAs( UnmanagedType.LPWStr )] string proAudio, out int taskIndex );
523
524                 [DllImport( "Avrt.dll" )]
525                 private static extern bool AvRevertMmThreadCharacteristics( IntPtr avrtHandle );
526                 //----------------
527                 #endregion
528         }
529 }