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 WaveFormat
31 get { return this._WaveFormat; }
36 /// 0.0 (0%) ~ 1.0 (100%) 。
42 return ( null != this._Mixer ) ? this._Mixer.Volume : 1.0f;
46 if( ( 0.0f > value ) || ( 1.0f < value ) )
47 throw new ArgumentOutOfRangeException();
49 this._Mixer.Volume = value;
53 public Device( CSCore.CoreAudioAPI.AudioClientShareMode 共有モード, double バッファサイズsec = 0.010, CSCore.WaveFormat 希望フォーマット = null )
56 this.遅延sec = バッファサイズsec;
57 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
59 this._初期化する( 希望フォーマット );
63 FDK.Log.Info( $"WASAPIデバイスを初期化しました。" );
64 FDK.Log.Info( $" Mode: {this._共有モード}" );
65 FDK.Log.Info( $" Laytency: {this.遅延sec * 1000.0} ms" );
66 var wfx = this.WaveFormat as CSCore.WaveFormatExtensible;
68 FDK.Log.Info( $" Format: {this.WaveFormat.WaveFormatTag}, {this.WaveFormat.SampleRate}Hz, {this.WaveFormat.Channels}ch, {this.WaveFormat.BitsPerSample}bits" );
70 FDK.Log.Info( $" Format: {wfx.WaveFormatTag}[{CSCore.AudioSubTypes.EncodingFromSubType( wfx.SubFormat )}], {wfx.SampleRate}Hz, {wfx.Channels}ch, {wfx.BitsPerSample}bits" );
74 /// メディアファイル(動画、音声)からサウンドインスタンスを生成して返す。
76 public Sound CreateSound( string path )
78 return new Sound( path, this._Mixer );
82 /// IWaveSource からサウンドインスタンスを生成して返す。
84 public Sound CreateSound( CSCore.IWaveSource source )
86 return new Sound( source, this._Mixer );
91 /// 以降、ミキサーに Sound を追加すれば、自動的に再生される。
93 public void PlayRendering()
97 if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused )
99 // Pause 中なら Resume する。
100 this.ResumeRendering();
102 else if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Stopped )
104 using( var 起動完了通知 = new System.Threading.AutoResetEvent( false ) )
106 // スレッドがすでに終了していることを確認する。
107 this._レンダリングスレッド?.Join();
110 this._レンダリングスレッド = new System.Threading.Thread( this._レンダリングスレッドエントリ ) {
111 Name = "WASAPI Playback",
112 Priority = System.Threading.ThreadPriority.AboveNormal, // 標準よりやや上
114 this._レンダリングスレッド.Start( 起動完了通知 );
125 /// ミキサーに登録されているすべての Sound の再生が停止する。
127 public void StopRendering()
129 lock( this._スレッド間同期 )
131 if( ( this._レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped ) && ( null != this._レンダリングスレッド ) )
133 // レンダリングスレッドに終了を通知し、その終了を待つ。
134 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
135 this._レンダリングスレッド.Join();
136 this._レンダリングスレッド = null;
137 FDK.Log.Info( "WASAPIのレンダリングを停止しました。" );
141 FDK.Log.WARNING( "WASAPIのレンダリングを停止しようとしましたが、すでに停止しています。" );
148 /// ミキサーに登録されているすべての Sound の再生が一時停止する。
149 /// ResumeRendering()で出力を再開できる。
151 public void PauseRendering()
153 lock( this._スレッド間同期 )
155 if( this.レンダリング状態 == CSCore.SoundOut.PlaybackState.Playing )
157 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Paused;
158 FDK.Log.Info( "WASAPIのレンダリングを一時停止しました。" );
162 FDK.Log.WARNING( "WASAPIのレンダリングを一時停止しようとしましたが、すでに一時停止しています。" );
169 /// PauseRendering() で一時停止状態にあるときのみ有効。
171 public void ResumeRendering()
173 lock( this._スレッド間同期 )
175 if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused )
177 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Playing;
178 FDK.Log.Info( "WASAPIのレンダリングを再開しました。" );
182 FDK.Log.WARNING( "WASAPIのレンダリングを再開しようとしましたが、すでに再開されています。" );
190 /// <returns>エラー時は double.NaN を返す。</returns>
191 public double GetDevicePosition()
193 //lock( this._スレッド間同期 )
195 long position, qpcPosition, frequency;
196 this.GetClock( out position, out qpcPosition, out frequency );
198 if( 0.0 >= frequency )
201 return (double) position / frequency;
205 #region " 解放; Dispose-Finallize パターン "
209 this.Dispose( false );
212 public void Dispose()
214 this.Dispose( true );
215 GC.SuppressFinalize( this );
218 protected virtual void Dispose( bool bDisposeManaged )
220 if( !this._dispose済み )
222 lock( this._スレッド間同期 )
224 if( bDisposeManaged )
226 // (A) ここでマネージリソースを解放する。
227 this.StopRendering();
231 // (B) ここでネイティブリソースを解放する。
237 this._dispose済み = true;
244 private volatile CSCore.SoundOut.PlaybackState _レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
246 private CSCore.CoreAudioAPI.AudioClientShareMode _共有モード;
248 private long _遅延100ns = 0;
250 private CSCore.WaveFormat _WaveFormat = null;
252 private CSCore.CoreAudioAPI.AudioClock _AudioClock = null;
254 private CSCore.CoreAudioAPI.AudioRenderClient _AudioRenderClient = null;
256 private CSCore.CoreAudioAPI.AudioClient _AudioClient = null;
258 private CSCore.CoreAudioAPI.MMDevice _MMDevice = null;
260 private System.Threading.Thread _レンダリングスレッド = null;
262 private System.Threading.EventWaitHandle _レンダリングイベント = null;
264 private Mixer _Mixer = null;
266 private readonly object _スレッド間同期 = new object();
268 private bool _dispose済み = false;
271 private void _初期化する( CSCore.WaveFormat 希望フォーマット )
273 lock( this._スレッド間同期 )
275 if( this._レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped )
276 throw new InvalidOperationException( "WASAPI のレンダリングを停止しないまま初期化することはできません。" );
278 this._レンダリングスレッド?.Join();
283 this._MMDevice = CSCore.CoreAudioAPI.MMDeviceEnumerator.DefaultAudioEndpoint(
284 CSCore.CoreAudioAPI.DataFlow.Render, // 方向:再生
285 CSCore.CoreAudioAPI.Role.Console ); // 用途:ゲーム、システム通知音、音声命令
287 // AudioClient を取得する。
288 this._AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( this._MMDevice );
291 if( null == ( this._WaveFormat = this._適切なフォーマットを調べて返す( 希望フォーマット ) ) )
292 throw new NotSupportedException( "サポート可能な WaveFormat が見つかりませんでした。" );
294 // 遅延を既定値にする(共有モードの場合のみ)。
295 if( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared )
296 this._遅延100ns = this._AudioClient.DefaultDevicePeriod;
298 // AudioClient を初期化する。
299 Action AudioClientを初期化する = () => {
300 this._AudioClient.Initialize(
302 CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, // イベント駆動で固定。
304 this._遅延100ns, // イベント駆動の場合、Periodicity は BufferDuration と同じ値でなければならない。
312 catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
314 // 排他モードかつイベント駆動 の場合、この例外が返されることがある。
315 // この場合、バッファサイズを調整して再度初期化する。
316 if( e.ErrorCode == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED )
318 int サイズframe = this._AudioClient.GetBufferSize(); // アライメント済みサイズが取得できる。
319 this._遅延100ns = (long) ( 10.0 * 1000.0 * 1000.0 * サイズframe / this._WaveFormat.SampleRate + 0.5 ); // +0.5 は四捨五入
321 AudioClientを初期化する(); // それでも例外なら知らん。
329 // イベント駆動用に使うイベントを生成し、AudioClient へ登録する。
330 this._レンダリングイベント = new System.Threading.EventWaitHandle( false, System.Threading.EventResetMode.AutoReset );
331 this._AudioClient.SetEventHandle( this._レンダリングイベント.SafeWaitHandle.DangerousGetHandle() );
333 // その他の WASAPI インターフェースを取得する。
334 this._AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this._AudioClient );
335 this._AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this._AudioClient );
338 this._Mixer = new Mixer( this._WaveFormat );
344 FDK.Utilities.解放する( ref this._Mixer );
345 FDK.Utilities.解放する( ref this._AudioClock );
346 FDK.Utilities.解放する( ref this._AudioRenderClient );
348 if( ( null != this._AudioClient ) && ( this._AudioClient.BasePtr != IntPtr.Zero ) )
352 this._AudioClient.StopNative();
353 this._AudioClient.Reset();
355 catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
357 if( e.ErrorCode != AUDCLNT_E_NOT_INITIALIZED )
362 FDK.Utilities.解放する( ref this._AudioClient );
363 FDK.Utilities.解放する( ref this._レンダリングイベント );
364 FDK.Utilities.解放する( ref this._MMDevice );
368 /// 希望したフォーマットをもとに、適切なフォーマットを調べて返す。
370 /// <param name="waveFormat">希望するフォーマット</param>
371 /// <param name="audioClient">AudioClient インスタンス。Initialize 前でも可。</param>
372 /// <returns>適切なフォーマット。見つからなかったら null。</returns>
373 private CSCore.WaveFormat _適切なフォーマットを調べて返す( CSCore.WaveFormat waveFormat )
375 Trace.Assert( null != this._AudioClient );
377 var 最も近いフォーマット = (CSCore.WaveFormat) null;
378 var 最終的に決定されたフォーマット = (CSCore.WaveFormat) null;
380 if( ( null != waveFormat ) && this._AudioClient.IsFormatSupported( this._共有モード, waveFormat, out 最も近いフォーマット ) )
383 最終的に決定されたフォーマット = waveFormat;
385 else if( null != 最も近いフォーマット )
387 // (B) AudioClient が推奨フォーマットを返してきたなら、それを採択する。
388 最終的に決定されたフォーマット = 最も近いフォーマット;
392 // (C) AudioClient からの提案がなかった場合は、共有モードのフォーマットを採択する。
394 var 共有モードのフォーマット = this._AudioClient.GetMixFormat();
396 if( ( null != 共有モードのフォーマット ) && this._AudioClient.IsFormatSupported( this._共有モード, 共有モードのフォーマット ) )
398 最終的に決定されたフォーマット = 共有モードのフォーマット;
402 // (D) AudioClient が共有モードのフォーマットもNGである場合は、以下から探す。
404 CSCore.WaveFormat closest = null;
406 bool found = this._AudioClient.IsFormatSupported( CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive,
407 new WaveFormat( 48000, 24, 2, AudioEncoding.Pcm ) {
412 最終的に決定されたフォーマット = new[] {
413 new CSCore.WaveFormat( 48000, 32, 2, AudioEncoding.IeeeFloat ),
414 new CSCore.WaveFormat( 44100, 32, 2, AudioEncoding.IeeeFloat ),
418 * > wFormatTag が WAVE_FORMAT_PCM の場合、wBitsPerSample は 8 または 16 でなければならない。
419 * > wFormatTag が WAVE_FORMAT_EXTENSIBLE の場合、この値は、任意の 8 の倍数を指定できる。
420 * https://msdn.microsoft.com/ja-jp/library/cc371566.aspx
422 * また、Realtek HD Audio の場合、IAudioClient.IsSupportedFormat() は 24bit PCM でも true を返してくるが、
423 * 単純に 1sample = 3byte で書き込んでも正常に再生できない。
424 * おそらく 32bit で包む必要があると思われるが、その方法は不明。
426 //new CSCore.WaveFormat( 48000, 24, 2, AudioEncoding.Pcm ),
427 //new CSCore.WaveFormat( 44100, 24, 2, AudioEncoding.Pcm ),
428 new CSCore.WaveFormat( 48000, 16, 2, AudioEncoding.Pcm ),
429 new CSCore.WaveFormat( 44100, 16, 2, AudioEncoding.Pcm ),
430 new CSCore.WaveFormat( 48000, 8, 2, AudioEncoding.Pcm ),
431 new CSCore.WaveFormat( 44100, 8, 2, AudioEncoding.Pcm ),
432 new CSCore.WaveFormat( 48000, 32, 1, AudioEncoding.IeeeFloat ),
433 new CSCore.WaveFormat( 44100, 32, 1, AudioEncoding.IeeeFloat ),
434 //new CSCore.WaveFormat( 48000, 24, 1, AudioEncoding.Pcm ),
435 //new CSCore.WaveFormat( 44100, 24, 1, AudioEncoding.Pcm ),
436 new CSCore.WaveFormat( 48000, 16, 1, AudioEncoding.Pcm ),
437 new CSCore.WaveFormat( 44100, 16, 1, AudioEncoding.Pcm ),
438 new CSCore.WaveFormat( 48000, 8, 1, AudioEncoding.Pcm ),
439 new CSCore.WaveFormat( 44100, 8, 1, AudioEncoding.Pcm ),
441 .FirstOrDefault( ( format ) => ( this._AudioClient.IsFormatSupported( this._共有モード, format ) ) );
443 // (E) それでも見つからなかったら null 。
447 return 最終的に決定されたフォーマット;
451 /// WASAPIイベント駆動スレッドのエントリ。
453 /// <param name="起動完了通知">無事に起動できたら、これを Set して(スレッドの生成元に)知らせる。</param>
454 private void _レンダリングスレッドエントリ( object 起動完了通知 )
456 var 例外 = (Exception) null;
457 var avrtHandle = IntPtr.Zero;
463 int バッファサイズframe = this._AudioClient.BufferSize;
464 var バッファ = new float[ バッファサイズframe * this.WaveFormat.Channels ]; // 前提1・this._レンダリング先(ミキサー)の出力は 32bit-float で固定。
466 // このスレッドの MMCSS 型を登録する。
468 string mmcssType = new[] {
469 new { 最大遅延 = 0.0105, 型名 = "Pro Audio" }, // 優先度の高いものから。
470 new { 最大遅延 = 0.0150, 型名 = "Games" },
472 .FirstOrDefault( ( i ) => ( i.最大遅延 > this.遅延sec ) )?.型名 ?? "Audio";
473 avrtHandle = Device.AvSetMmThreadCharacteristics( mmcssType, out taskIndex );
475 // AudioClient を開始する。
476 this._AudioClient.Start();
477 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Playing;
480 ( 起動完了通知 as System.Threading.EventWaitHandle )?.Set();
487 var イベントs = new System.Threading.WaitHandle[] { this._レンダリングイベント };
488 while( this.レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped )
490 int イベント番号 = System.Threading.WaitHandle.WaitAny(
492 millisecondsTimeout: (int) ( 3000.0 * this.遅延sec ), // 適正値は レイテンシ×3 [ms] (MSDN)
493 exitContext: false );
495 if( イベント番号 == System.Threading.WaitHandle.WaitTimeout )
498 if( this.レンダリング状態 == CSCore.SoundOut.PlaybackState.Playing )
500 int 未再生数frame = ( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive ) ? 0 : this._AudioClient.GetCurrentPadding();
501 int 空きframe = バッファサイズframe - 未再生数frame;
503 if( 空きframe > 5 ) // あまりに空きが小さいならスキップする。
505 // レンダリング先からデータを取得して AudioRenderClient へ出力する。
507 int 読み込むサイズsample = 空きframe * this.WaveFormat.Channels; // 前提2・レンダリング先.WaveFormat と this.WaveFormat は同一。
508 読み込むサイズsample -= ( 読み込むサイズsample % ( this.WaveFormat.BlockAlign / this.WaveFormat.BytesPerSample ) ); // BlockAlign 境界にそろえる。
510 if( 0 < 読み込むサイズsample )
512 // ミキサーからの出力をバッファに取得する。
513 int 読み込んだサイズsample = this._Mixer.Read( バッファ, 0, 読み込むサイズsample );
515 // バッファのデータを変換しつつ、AudioRenderClient へ出力する。
516 IntPtr bufferPtr = this._AudioRenderClient.GetBuffer( 空きframe );
519 var encoding = CSCore.AudioSubTypes.EncodingFromSubType( CSCore.WaveFormatExtensible.SubTypeFromWaveFormat( this.WaveFormat ) );
521 if( encoding == AudioEncoding.Pcm )
523 if( 24 == this.WaveFormat.BitsPerSample )
525 #region " (A) Mixer:32bit-float → AudioRenderClient:24bit-PCM の場合 "
529 byte* ptr = (byte*) bufferPtr.ToPointer(); // AudioRenderClient のバッファは GC 対象外なのでピン止め不要。
531 for( int i = 0; i < 読み込んだサイズsample; i++ )
533 float data = バッファ[ i ];
534 if( -1.0f > data ) data = -1.0f;
535 if( +1.0f < data ) data = +1.0f;
537 uint sample32 = (uint) ( data * 8388608f - 1f ); // 24bit PCM の値域は -8388608~+8388607
538 byte* psample32 = (byte*) &sample32;
539 *ptr++ = *psample32++;
540 *ptr++ = *psample32++;
541 *ptr++ = *psample32++;
547 else if( 16 == this.WaveFormat.BitsPerSample )
549 #region " (B) Mixer:32bit-float → AudioRenderClient:16bit-PCM の場合 "
553 byte* ptr = (byte*) bufferPtr.ToPointer(); // AudioRenderClient のバッファは GC 対象外なのでピン止め不要。
555 for( int i = 0; i < 読み込んだサイズsample; i++ )
557 float data = バッファ[ i ];
558 if( -1.0f > data ) data = -1.0f;
559 if( +1.0f < data ) data = +1.0f;
561 short sample16 = (short) ( data * short.MaxValue );
562 byte* psample16 = (byte*) &sample16;
563 *ptr++ = *psample16++;
564 *ptr++ = *psample16++;
570 else if( 8 == this.WaveFormat.BitsPerSample )
572 #region " (C) Mixer:32bit-float → AudioRenderClient:8bit-PCM の場合 "
576 byte* ptr = (byte*) bufferPtr.ToPointer(); // AudioRenderClient のバッファは GC 対象外なのでピン止め不要。
578 for( int i = 0; i < 読み込んだサイズsample; i++ )
580 float data = バッファ[ i ];
581 if( -1.0f > data ) data = -1.0f;
582 if( +1.0f < data ) data = +1.0f;
584 byte value = (byte) ( ( data + 1 ) * 128f );
585 *ptr++ = unchecked(value);
592 else if( encoding == AudioEncoding.IeeeFloat )
594 #region " (D) Mixer:32bit-float → AudioRenderClient:32bit-float の場合 "
596 Marshal.Copy( バッファ, 0, bufferPtr, 読み込んだサイズsample );
603 int 出力したフレーム数 = 読み込んだサイズsample / this.WaveFormat.Channels;
604 this._AudioRenderClient.ReleaseBuffer(
606 ( 0 < 出力したフレーム数 ) ? CSCore.CoreAudioAPI.AudioClientBufferFlags.None : CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent );
609 // レンダリング先からの出力がなくなったらおしまい。
610 if( 0 == 読み込んだサイズsample )
611 this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
621 // このスレッドの MMCSS 特性を元に戻す。
622 Device.AvRevertMmThreadCharacteristics( avrtHandle );
623 avrtHandle = IntPtr.Zero;
625 // ハードウェアの再生が終わるくらいまで、少し待つ。
626 System.Threading.Thread.Sleep( (int) ( this.遅延sec * 1000 / 2 ) );
628 // AudioClient を停止する。
629 this._AudioClient.Stop();
630 this._AudioClient.Reset();
636 FDK.Log.ERROR( $"例外が発生しました。レンダリングスレッドを中断します。[{e.Message}]" );
641 if( avrtHandle != IntPtr.Zero )
642 Device.AvRevertMmThreadCharacteristics( avrtHandle );
644 ( 起動完了通知 as System.Threading.EventWaitHandle )?.Set(); // 失敗時を想定して。
651 private void GetClock( out long Pu64Position, out long QPCPosition, out long Pu64Frequency )
653 //lock( this._スレッド間同期 ) なくてもいいっぽい。
655 this._AudioClock.GetFrequencyNative( out Pu64Frequency );
657 // IAudioClock::GetPosition() は、S_FALSE を返すことがある。
658 // これは、WASAPI排他モードにおいて、GetPosition 時に優先度の高いイベントが発生しており
659 // 既定時間内にデバイス位置を取得できなかった場合に返される。(MSDNより)
664 for( int リトライ回数 = 0; リトライ回数 < 10; リトライ回数++ ) // 最大10回までリトライ。
666 hr = this._AudioClock.GetPositionNative( out pos, out qpcPos );
668 if( ( (int) CSCore.Win32.HResult.S_OK ) == hr )
672 else if( ( (int) CSCore.Win32.HResult.S_FALSE ) == hr )
678 throw new CSCore.Win32.Win32ComException( hr, "IAudioClock", "GetPosition" );
683 QPCPosition = qpcPos;
689 private const int AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = unchecked((int) 0x88890019);
690 private const int AUDCLNT_E_INVALID_DEVICE_PERIOD = unchecked((int) 0x88890020);
691 private const int AUDCLNT_E_NOT_INITIALIZED = unchecked((int) 0x88890001);
693 [DllImport( "Avrt.dll", CharSet = CharSet.Unicode )]
694 private static extern IntPtr AvSetMmThreadCharacteristics( [MarshalAs( UnmanagedType.LPWStr )] string proAudio, out int taskIndex );
696 [DllImport( "Avrt.dll" )]
697 private static extern bool AvRevertMmThreadCharacteristics( IntPtr avrtHandle );