2 using System.Collections.Generic;
3 using System.Diagnostics;
5 using System.Runtime.InteropServices;
8 namespace FDK.メディア.サウンド.WASAPI
10 public class Device : IDisposable
12 public CSCore.SoundOut.PlaybackState レンダリング状態
14 get { return this._レンダリング状態; }
19 get { return FDK.Utilities.変換_100ns単位からsec単位へ( this._遅延100ns ); }
20 protected set { this._遅延100ns = FDK.Utilities.変換_sec単位から100ns単位へ( value ); }
25 get { return this._遅延100ns; }
26 protected set { this._遅延100ns = value; }
29 public CSCore.WaveFormat フォーマット
31 get { return this._WaveFormat; }
36 /// 0.0 (0%) ~ 1.0 (100%) 。
42 return ( null != this._レンダリング先 ) ? this._レンダリング先.Volume : 1.0f;
46 if( ( 0.0f > value ) || ( 1.0f < value ) )
47 throw new ArgumentOutOfRangeException();
49 this._レンダリング先.Volume = value;
53 public Device( CSCore.CoreAudioAPI.AudioClientShareMode 共有モード, double 遅延sec = 0.010, CSCore.WaveFormat 希望フォーマット = null )
57 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
59 this._初期化する( 希望フォーマット );
65 /// メディアファイル(動画、音声)からサウンドインスタンスを生成して返す。
67 public Sound CreateSound( string path )
69 return new Sound( path, this._Mixer );
75 public void GetClock( out long Pu64Position, out long QPCPosition )
79 this._AudioClock.GetPositionNative( out Pu64Position, out QPCPosition );
85 /// 以降、ミキサーに Sound を追加すれば、自動的に再生される。
87 public void PlayRendering()
91 if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused )
93 // Pause 中なら Resume する。
94 this.ResumeRendering();
96 else if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Stopped )
98 using( var 起動完了通知 = new System.Threading.AutoResetEvent( false ) )
100 // スレッドがすでに終了していることを確認する。
101 this._レンダリングスレッド?.Join();
104 this._レンダリングスレッド = new System.Threading.Thread( this._レンダリングスレッドエントリ ) {
105 Name = "WASAPI Playback",
106 Priority = System.Threading.ThreadPriority.AboveNormal, // 標準よりやや上
108 this._レンダリングスレッド.Start( 起動完了通知 );
119 /// ミキサーに登録されているすべての Sound の再生が停止する。
121 public void StopRendering()
123 lock( this._スレッド間同期 )
125 if( ( this._レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped ) && ( null != this._レンダリングスレッド ) )
127 // レンダリングスレッドに終了を通知し、その終了を待つ。
128 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
129 this._レンダリングスレッド.Join();
130 this._レンダリングスレッド = null;
131 Debug.WriteLine( "WASAPIのレンダリングを停止しました。" );
135 Debug.WriteLine( "WASAPIのレンダリングを停止しようとしましたが、すでに停止しています。" );
142 /// ミキサーに登録されているすべての Sound の再生が一時停止する。
143 /// ResumeRendering()で出力を再開できる。
145 public void PauseRendering()
147 lock( this._スレッド間同期 )
149 if( this.レンダリング状態 == CSCore.SoundOut.PlaybackState.Playing )
151 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Paused;
152 Debug.WriteLine( "WASAPIのレンダリングを一時停止しました。" );
156 Debug.WriteLine( "WASAPIのレンダリングを一時停止しようとしましたが、すでに一時停止しています。" );
163 /// PauseRendering() で一時停止状態にあるときのみ有効。
165 public void ResumeRendering()
167 lock( this._スレッド間同期 )
169 if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused )
171 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Playing;
172 Debug.WriteLine( "WASAPIのレンダリングを再開しました。" );
176 Debug.WriteLine( "WASAPIのレンダリングを再開しようとしましたが、すでに再開されています。" );
181 #region " 解放; Dispose-Finallize パターン "
185 this.Dispose( false );
188 public void Dispose()
190 this.Dispose( true );
191 GC.SuppressFinalize( this );
194 protected virtual void Dispose( bool bDisposeManaged )
196 if( !this._dispose済み )
198 lock( this._スレッド間同期 )
200 if( bDisposeManaged )
202 // (A) ここでマネージリソースを解放する。
203 this.StopRendering();
207 // (B) ここでネイティブリソースを解放する。
213 this._dispose済み = true;
220 private volatile CSCore.SoundOut.PlaybackState _レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
222 private CSCore.CoreAudioAPI.AudioClientShareMode _共有モード;
224 private long _遅延100ns = 0;
226 private CSCore.WaveFormat _WaveFormat = null;
228 private CSCore.CoreAudioAPI.AudioClock _AudioClock = null;
230 private CSCore.CoreAudioAPI.AudioRenderClient _AudioRenderClient = null;
232 private CSCore.CoreAudioAPI.AudioClient _AudioClient = null;
234 private CSCore.CoreAudioAPI.MMDevice _MMDevice = null;
236 private System.Threading.Thread _レンダリングスレッド = null;
238 private System.Threading.EventWaitHandle _レンダリングイベント = null;
240 private CSCore.Streams.VolumeSource _レンダリング先 = null;
242 private CSCore.IWaveSource _生レンダリング先 = null;
244 private Mixer _Mixer = null;
246 private readonly object _スレッド間同期 = new object();
248 private bool _dispose済み = false;
251 private void _初期化する( CSCore.WaveFormat 希望フォーマット = null )
253 lock( this._スレッド間同期 )
255 if( this._レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped )
256 throw new InvalidOperationException( "WASAPI のレンダリングを停止しないまま初期化することはできません。" );
258 this._レンダリングスレッド?.Join();
263 this._MMDevice = CSCore.CoreAudioAPI.MMDeviceEnumerator.DefaultAudioEndpoint(
264 CSCore.CoreAudioAPI.DataFlow.Render, // 方向:再生
265 CSCore.CoreAudioAPI.Role.Console ); // 用途:ゲーム、システム通知音、音声命令
267 // AudioClient を取得する。
268 this._AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( this._MMDevice );
271 var defaultFormat = ( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) ?
272 this._AudioClient.GetMixFormat() :
273 new CSCore.WaveFormat( 48000, 32, 2, AudioEncoding.IeeeFloat );
275 if( null == ( this._WaveFormat = this._適切なフォーマットを調べて返す( 希望フォーマット ?? defaultFormat ) ) )
277 throw new NotSupportedException( "サポート可能な WaveFormat が見つかりませんでした。" );
280 // 遅延を既定値にする(共有モードの場合のみ)。
281 if( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared )
282 this._遅延100ns = this._AudioClient.DefaultDevicePeriod;
284 // AudioClient を初期化する。
285 Action AudioClientを初期化する = () => {
286 this._AudioClient.Initialize(
288 CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, // イベント駆動で固定。
298 catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
300 // 排他モードかつイベント駆動 の場合、この例外が返されることがある。
301 // この場合、バッファサイズを調整して再度初期化する。
302 if( e.ErrorCode == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED )
304 int サイズframe = this._AudioClient.GetBufferSize();
305 this._遅延100ns = (long) ( 10.0 * 1000.0 * 1000.0 * サイズframe / this._WaveFormat.SampleRate + 0.5 ); // +0.5 は四捨五入
307 AudioClientを初期化する(); // それでも例外なら知らん。
311 // イベント駆動用に使うイベントを生成し、AudioClient へ登録する。
312 this._レンダリングイベント = new System.Threading.EventWaitHandle( false, System.Threading.EventResetMode.AutoReset );
313 this._AudioClient.SetEventHandle( this._レンダリングイベント.SafeWaitHandle.DangerousGetHandle() );
315 // その他の WASAPI インターフェースを取得する。
316 this._AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this._AudioClient );
317 this._AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this._AudioClient );
319 // ミキサーを生成し、デバイスのソース(DirectSound でいうところのプライマリバッファ)として登録する。
320 this._Mixer = new Mixer( this._WaveFormat ) {
321 DivideResult = false,
323 this._SetSource( this._Mixer );
329 FDK.Utilities.解放する( ref this._Mixer );
330 FDK.Utilities.解放する( ref this._AudioClock );
331 FDK.Utilities.解放する( ref this._AudioRenderClient );
333 if( ( null != this._AudioClient ) && ( this._AudioClient.BasePtr != IntPtr.Zero ) )
337 this._AudioClient.StopNative();
338 this._AudioClient.Reset();
340 catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
342 if( e.ErrorCode != AUDCLNT_E_NOT_INITIALIZED )
347 FDK.Utilities.解放する( ref this._AudioClient );
348 FDK.Utilities.解放する( ref this._レンダリングイベント );
349 FDK.Utilities.解放する( ref this._MMDevice );
352 private void _SetSource( CSCore.ISampleSource targetSource )
354 if( null != this._レンダリング先 )
355 throw new InvalidOperationException( "レンダリングターゲットはすでに設定済みです。" );
357 this._レンダリング先 = new CSCore.Streams.VolumeSource( targetSource ); // サンプル(float)単位のレンダリング先と、
358 this._生レンダリング先 = targetSource.ToWaveSource(); // データ(byte)単位のレンダリング先とを持っておく。
362 /// 希望したフォーマットをもとに、適切なフォーマットを調べて返す。
364 /// <param name="waveFormat">希望するフォーマット</param>
365 /// <param name="audioClient">AudioClient インスタンス。Initialize 前でも可。</param>
366 /// <returns>適切なフォーマット。見つからなかったら null。</returns>
367 private CSCore.WaveFormat _適切なフォーマットを調べて返す( CSCore.WaveFormat waveFormat )
369 Trace.Assert( null != this._AudioClient );
371 var 最も近いフォーマット = (CSCore.WaveFormat) null;
372 var 最終的に決定されたフォーマット = (CSCore.WaveFormat) null;
374 if( this._AudioClient.IsFormatSupported( this._共有モード, waveFormat, out 最も近いフォーマット ) )
377 最終的に決定されたフォーマット = waveFormat;
379 else if( null != 最も近いフォーマット )
381 // (B) AudioClient が推奨フォーマットを返してきたなら、それを採択する。
382 最終的に決定されたフォーマット = 最も近いフォーマット;
386 // (C) AudioClient からの提案がなかった場合は、共有モードのフォーマットを採択する。
388 var 共有モードのフォーマット = this._AudioClient.GetMixFormat();
390 if( ( null != 共有モードのフォーマット ) && this._AudioClient.IsFormatSupported( this._共有モード, 共有モードのフォーマット ) )
392 最終的に決定されたフォーマット = 共有モードのフォーマット;
396 // (D) AudioClient が共有モードのフォーマットすらNGと言ってきた場合は、以下から探す。
398 最終的に決定されたフォーマット = new[]
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 ),
405 .FirstOrDefault( ( format ) => ( this._AudioClient.IsFormatSupported( this._共有モード, format ) ) );
407 // (E) それでも見つからなかったら null 。
411 return 最終的に決定されたフォーマット;
415 /// WASAPIイベント駆動スレッドのエントリ。
417 /// <param name="起動完了通知">無事に起動できたら、これを Set して(スレッドの生成元に)知らせる。</param>
418 private void _レンダリングスレッドエントリ( object 起動完了通知 )
420 var 例外 = (Exception) null;
421 var avrtHandle = IntPtr.Zero;
425 int バッファサイズframe = this._AudioClient.BufferSize;
426 int フレームサイズbyte = this._WaveFormat.Channels * this._WaveFormat.BytesPerSample;
427 var バッファ = new byte[ バッファサイズframe * フレームサイズbyte ];
429 // このスレッドの MMCSS 型を登録する。
431 string mmcssType = new[] {
432 new { 最大遅延 = 0.010, 型名 = "Pro Audio" }, // 優先度の高いものから。
433 new { 最大遅延 = 0.015, 型名 = "Games" },
435 .FirstOrDefault( ( i ) => ( i.最大遅延 > this.遅延sec ) )?.型名 ?? "Audio";
436 avrtHandle = Device.AvSetMmThreadCharacteristics( mmcssType, out taskIndex );
438 // AudioClient を開始する。
439 this._AudioClient.Start();
440 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Playing;
443 ( 起動完了通知 as System.Threading.EventWaitHandle )?.Set();
448 var イベントs = new System.Threading.WaitHandle[] { this._レンダリングイベント };
449 while( this.レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped )
451 int イベント番号 = System.Threading.WaitHandle.WaitAny(
453 millisecondsTimeout: (int) ( 3000.0 * this.遅延sec ), // 適正値は レイテンシ×3 [ms] (MSDN)
454 exitContext: false );
456 if( イベント番号 == System.Threading.WaitHandle.WaitTimeout )
459 if( this.レンダリング状態 == CSCore.SoundOut.PlaybackState.Playing )
461 int 未再生数frame = ( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive ) ? 0 : this._AudioClient.GetCurrentPadding();
462 int 空きframe = バッファサイズframe - 未再生数frame;
464 if( 空きframe > 5 ) // あまりに空きが小さいならスキップする。
466 if( !this._バッファを埋める( バッファ, 空きframe, フレームサイズbyte ) )
467 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
474 // このスレッドの MMCSS 特性を元に戻す。
475 Device.AvRevertMmThreadCharacteristics( avrtHandle );
476 avrtHandle = IntPtr.Zero;
478 // ハードウェアの再生が終わるくらいまで、少し待つ。
479 System.Threading.Thread.Sleep( (int) ( this.遅延sec * 1000 / 2 ) );
481 // AudioClient を停止する。
482 this._AudioClient.Stop();
483 this._AudioClient.Reset();
491 if( avrtHandle != IntPtr.Zero )
492 Device.AvRevertMmThreadCharacteristics( avrtHandle );
494 ( 起動完了通知 as System.Threading.EventWaitHandle )?.Set(); // 失敗時を想定して。
498 private bool _バッファを埋める( byte[] バッファ, int フレーム数, int フレームサイズbyte )
500 int 読み込むサイズbyte = フレーム数 * フレームサイズbyte;
501 読み込むサイズbyte -= ( 読み込むサイズbyte % this._生レンダリング先.WaveFormat.BlockAlign ); // BlockAlign の倍数にする。
503 if( 読み込むサイズbyte <= 0 )
506 int 読み込んだサイズbyte = this._生レンダリング先.Read( バッファ, 0, 読み込むサイズbyte );
508 IntPtr ptr = this._AudioRenderClient.GetBuffer( フレーム数 );
509 Marshal.Copy( バッファ, 0, ptr, 読み込んだサイズbyte );
510 this._AudioRenderClient.ReleaseBuffer( 読み込んだサイズbyte / フレームサイズbyte, CSCore.CoreAudioAPI.AudioClientBufferFlags.None );
512 return ( 0 < 読み込んだサイズbyte );
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);
521 [DllImport( "Avrt.dll", CharSet = CharSet.Unicode )]
522 private static extern IntPtr AvSetMmThreadCharacteristics( [MarshalAs( UnmanagedType.LPWStr )] string proAudio, out int taskIndex );
524 [DllImport( "Avrt.dll" )]
525 private static extern bool AvRevertMmThreadCharacteristics( IntPtr avrtHandle );