using System;
+using System.Collections.Generic;
using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices;
+using CSCore;
namespace FDK.メディア.サウンド.WASAPI
{
- public unsafe class Device : IDisposable
+ public class Device : IDisposable
{
- public float 遅延ms
+ public CSCore.SoundOut.PlaybackState レンダリング状態
{
- get { return this.更新間隔ms; }
+ get { return this._レンダリング状態; }
}
- public CSCore.CoreAudioAPI.AudioClock AudioClock
+
+ public double 遅延sec
{
- get { return this.bs_AudioClock; }
+ get { return FDK.Utilities.変換_100ns単位からsec単位へ( this._遅延100ns ); }
+ protected set { this._遅延100ns = FDK.Utilities.変換_sec単位から100ns単位へ( value ); }
}
- public bool Dispose済み
+
+ public long 遅延100ns
{
- get;
- protected set;
- } = true;
+ get { return this._遅延100ns; }
+ protected set { this._遅延100ns = value; }
+ }
- public void 初期化する( float 希望更新間隔ms )
+ public CSCore.WaveFormat フォーマット
{
- int hr = 0;
+ get { return this._WaveFormat; }
+ }
- lock( this.スレッド間同期 )
+ /// <summary>
+ /// レンダリングボリューム。
+ /// 0.0 (0%) ~ 1.0 (100%) 。
+ /// </summary>
+ public float 音量
+ {
+ get
+ {
+ return ( null != this._レンダリング先 ) ? this._レンダリング先.Volume : 1.0f;
+ }
+ set
{
- Trace.Assert( this.Dispose済み );
- this.Dispose済み = false;
+ if( ( 0.0f > value ) || ( 1.0f < value ) )
+ throw new ArgumentOutOfRangeException();
- this.希望更新間隔ms = 希望更新間隔ms;
+ this._レンダリング先.Volume = value;
+ }
+ }
- #region " AudioClientをアクティベートする。"
- //-----------------
- using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() )
- using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) )
- {
- this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス );
- }
- //-----------------
- #endregion
- #region " 指定された希望更新間隔とデバイス能力をもとに、更新間隔を決定する。"
- //-----------------
- long 共有モードでの間隔in100ns = 0;
- long 排他モードでの最小間隔in100ns = 0;
-
- // 最小間隔を取得する。
- hr = this.AudioClient.GetDevicePeriodNative( out 共有モードでの間隔in100ns, out 排他モードでの最小間隔in100ns );
- if( 0 > hr )
- System.Runtime.InteropServices.Marshal.ThrowExceptionForHR( hr );
-
- // 取得できたらms単位に変換。
- this.最小間隔ms = (float) 排他モードでの最小間隔in100ns / 10000.0f;
-
- // 更新間隔ms を「希望更新間隔とデバイスの最小間隔の大きい方以上 かつ 1秒以下で丸められた値」にする。
- this.更新間隔ms = System.Math.Min( 1000.0f, System.Math.Max( this.希望更新間隔ms, this.最小間隔ms ) );
- //-----------------
- #endregion
- #region " AudioClient を初期化する。"
- //-----------------
- var waveFormat = new CSCore.WaveFormat( 44100, 16, 2, CSCore.AudioEncoding.Pcm );
+ public CSCore.CoreAudioAPI.AudioClock AudioClock
+ {
+ get { return this._AudioClock; }
+ }
- try
+ public Device( CSCore.CoreAudioAPI.AudioClientShareMode 共有モード, double 遅延sec = 0.010, CSCore.WaveFormat 希望フォーマット = null )
+ {
+ this._共有モード = 共有モード;
+ this.遅延sec = 遅延sec;
+ this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
+
+ this._初期化する( 希望フォーマット );
+ }
+
+ /// <summary>
+ /// メディアファイル(動画、音声)からサウンドインスタンスを生成して返す。
+ /// </summary>
+ public Sound CreateSound( string path )
+ {
+ return new Sound( path, this._Mixer );
+ }
+
+ /// <summary>
+ /// ミキサーの出力を開始する。
+ /// 以降、ミキサーに Sound を追加すれば、自動的に再生される。
+ /// </summary>
+ public void PlayRendering()
+ {
+ lock( this._スレッド間同期 )
+ {
+ if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused )
{
- this.AudioClient.Initialize(
- CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive, // 排他モード。
- CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, // イベント駆動モード。
- (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // バッファサイズ。イベント駆動モードでは、更新間隔と同じ値でなければならない。
- (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // 更新間隔。
- waveFormat, // バッファのフォーマット。
- Guid.Empty ); // この AudioClient = AudioStrem が所属する AudioSession。null ならデフォルトのAudioSessionに登録される。
+ // Pause 中なら Resume する。
+ this.ResumeRendering();
}
- catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
+ else if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Stopped )
{
- // 排他&イベント駆動モードの場合、バッファのアライメントエラーが返される場合がある。この場合、サイズを調整してオーディオストリームを作成し直す。
- if( AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED == e.ErrorCode )
+ using( var 起動完了通知 = new System.Threading.AutoResetEvent( false ) )
{
- int 更新間隔に一番近くてアライメントされているサイズsample = this.AudioClient.GetBufferSize();
- this.更新間隔ms = ( 更新間隔に一番近くてアライメントされているサイズsample * 1000.0f / (float) waveFormat.SampleRate );
-
- // AudioClient を一度解放し、もう一度アクティベートし直す。
- this.AudioClient.Dispose();
- using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() )
- using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) )
- {
- this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス );
- }
-
- // アライメントされたサイズを使って、AudioClient を再初期化する。
- this.AudioClient.Initialize(
- CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive, // 排他モード。
- CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, // イベント駆動モード。
- (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // バッファサイズ。イベント駆動モードでは、更新間隔と同じ値でなければならない。
- (long) ( this.更新間隔ms * 10000.0f + 0.5f ), // 更新間隔。
- waveFormat, // バッファのフォーマット。
- Guid.Empty ); // この AudioClient = AudioStrem が所属する AudioSession。NULLならデフォルトのAudioSessionに登録される。
-
- // それでもエラーなら例外発生。
+ // スレッドがすでに終了していることを確認する。
+ this._レンダリングスレッド?.Join();
+
+ // レンダリングスレッドを起動する。
+ this._レンダリングスレッド = new System.Threading.Thread( this._レンダリングスレッドエントリ ) {
+ Name = "WASAPI Playback",
+ Priority = System.Threading.ThreadPriority.AboveNormal, // 標準よりやや上
+ };
+ this._レンダリングスレッド.Start( 起動完了通知 );
+
+ // スレッドからの起動完了通知を待つ。
+ 起動完了通知.WaitOne();
}
}
+ }
+ }
-
- // 更新間隔を sample, byte 単位で保存する。
- this.更新間隔sample = this.AudioClient.GetBufferSize(); // バッファの長さはサンプル単位で返される。
- this.更新間隔byte = this.更新間隔sample * ( waveFormat.Channels * waveFormat.BitsPerSample / 8 );
- //-----------------
- #endregion
- #region " AudioClient から AudioRenderClient を取得する。"
- //-----------------
- this.AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this.AudioClient );
- //-----------------
- #endregion
- #region " AudioClient から AudioClock を取得する。"
- //-----------------
- this.bs_AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this.AudioClient );
- //-----------------
- #endregion
-
- #region " ミキサーを生成し初期化する。"
- //-----------------
- this.Mixer = new Mixer();
- this.Mixer.初期化する( this.更新間隔sample );
- //-----------------
- #endregion
- #region " 最初のエンドポイントバッファを無音で埋めておく。"
- //-----------------
- var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample );
-
- // 無音を書き込んだことにして、バッファをコミット。(bufferPrtは使わない。)
- this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent );
- //-----------------
- #endregion
-
- #region " 情報表示。"
- //-----------------
- FDK.Log.Info( $"WASAPIクライアントを初期化しました。" );
- FDK.Log.Info( $" モード: 排他&イベント駆動" );
- FDK.Log.Info( $" フォーマット: {waveFormat.BitsPerSample} bits, {waveFormat.SampleRate} Hz" );
- FDK.Log.Info( $" エンドポイントバッファ: {( (float) this.更新間隔sample / (double) waveFormat.SampleRate ) * 1000.0f} ミリ秒 ({this.更新間隔sample} samples) × 2枚" );
- FDK.Log.Info( $" 希望更新間隔: {this.希望更新間隔ms} ミリ秒" );
- FDK.Log.Info( $" 更新間隔: {this.更新間隔ms} ミリ秒 ({this.更新間隔sample} samples)" );
- FDK.Log.Info( $" 最小間隔: {this.最小間隔ms} ミリ秒" );
- //-----------------
- #endregion
-
- #region " ワークキューとイベントを作成し、作業項目を登録する。"
- //-----------------
+ /// <summary>
+ /// ミキサーの出力を停止する。
+ /// ミキサーに登録されているすべての Sound の再生が停止する。
+ /// </summary>
+ public void StopRendering()
+ {
+ lock( this._スレッド間同期 )
+ {
+ if( ( this._レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped ) && ( null != this._レンダリングスレッド ) )
+ {
+ // レンダリングスレッドに終了を通知し、その終了を待つ。
+ this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
+ this._レンダリングスレッド.Join();
+ this._レンダリングスレッド = null;
+ Debug.WriteLine( "WASAPIのレンダリングを停止しました。" );
+ }
+ else
{
- // MediaFoundation が管理する、プロセス&MMCSSタスクごとに1つずつ作ることができる特別な共有ワークキューを取得、または生成して取得する。
- int dwTaskId = 0;
- SharpDX.MediaFoundation.MediaFactory.LockSharedWorkQueue(
- ( 11.0 > this.更新間隔ms ) ? "Pro Audio" : "Games", 0, ref dwTaskId, out this.QueueID );
-
- // エンドポイントバッファからの出力要請イベントを作成し、AudioClient に登録する。
- this.出力要請イベント = CreateEvent( IntPtr.Zero, false, false, "WASAPI出力要請イベント" );
- this.AudioClient.SetEventHandle( this.出力要請イベント );
-
- // コールバックを作成し、ワークキューに最初の作業項目を登録する。
- this.出力要請イベントのコールバック = new MFAsyncCallback( this.QueueID, ( ar ) => {
- this.出力要請イベントへ対応する( ar );
- } );
+ Debug.WriteLine( "WASAPIのレンダリングを停止しようとしましたが、すでに停止しています。" );
}
- //-----------------
- #endregion
- #region " 最初の作業項目を追加する。"
- //-----------------
- this.作業項目をキューに格納する();
- //-----------------
- #endregion
- #region " WASAPI レンダリングを開始。"
- //-----------------
- this.AudioClient.Start();
- //-----------------
- #endregion
}
}
- public void Dispose()
- {
- Trace.Assert( false == this.Dispose済み );
- #region " WASAPI作業項目を終了させる。オーディオのレンダリングを止める前に行うこと。"
- //-----------------
+ /// <summary>
+ /// ミキサーの出力を一時停止する。
+ /// ミキサーに登録されているすべての Sound の再生が一時停止する。
+ /// ResumeRendering()で出力を再開できる。
+ /// </summary>
+ public void PauseRendering()
+ {
+ lock( this._スレッド間同期 )
{
- //SharpDX.MediaFoundation.MediaFactory.CancelWorkItem( this.出力要請イベントキャンセル用キー ); --> コールバックの実行中にキャンセルしてしまうと NullReference例外
- this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.ON;
- this.出力終了通知.OFFになるまでブロックする();
- FDK.Log.Info( "WASAPI出力処理を終了しました。" );
+ if( this.レンダリング状態 == CSCore.SoundOut.PlaybackState.Playing )
+ {
+ this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Paused;
+ Debug.WriteLine( "WASAPIのレンダリングを一時停止しました。" );
+ }
+ else
+ {
+ Debug.WriteLine( "WASAPIのレンダリングを一時停止しようとしましたが、すでに一時停止しています。" );
+ }
}
- //-----------------
- #endregion
+ }
- lock( this.スレッド間同期 )
+ /// <summary>
+ /// ミキサーの出力を再開する。
+ /// PauseRendering() で一時停止状態にあるときのみ有効。
+ /// </summary>
+ public void ResumeRendering()
+ {
+ lock( this._スレッド間同期 )
{
- #region " オーディオのレンダリングを停止する。"
- //-----------------
- this.AudioClient?.Stop();
- //-----------------
- #endregion
- #region " ミキサー(とサウンドリスト)は現状を維持する。"
- //-----------------
-
- // 何もしない。
-
- //-----------------
- #endregion
- #region " WASAPIオブジェクトを解放する。"
- //-----------------
- FDK.Utilities.解放する( ref this.bs_AudioClock );
- FDK.Utilities.解放する( ref this.AudioRenderClient );
- FDK.Utilities.解放する( ref this.AudioClient );
- //-----------------
- #endregion
- #region " 共有ワークキューをこのプロセスから解放する。"
- //-----------------
- if( int.MaxValue != this.QueueID )
+ if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused )
+ {
+ this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Playing;
+ Debug.WriteLine( "WASAPIのレンダリングを再開しました。" );
+ }
+ else
{
- SharpDX.MediaFoundation.MediaFactory.UnlockWorkQueue( this.QueueID );
- this.QueueID = int.MaxValue;
+ Debug.WriteLine( "WASAPIのレンダリングを再開しようとしましたが、すでに再開されています。" );
}
- //-----------------
- #endregion
- #region " WASAPIイベント駆動用のコールバックとイベントを解放する。"
- //-----------------
- FDK.Utilities.解放する( ref this.出力要請イベントのコールバック );
-
- if( IntPtr.Zero != this.出力要請イベント )
- CloseHandle( this.出力要請イベント );
- //-----------------
- #endregion
-
- this.Dispose済み = true;
}
+ }
- FDK.Log.Info( "WASAPIクライアントを終了しました。" );
+ #region " 解放; Dispose-Finallize パターン "
+ //----------------
+ ~Device()
+ {
+ this.Dispose( false );
}
- public void サウンドをミキサーに追加する( Sound sound )
+
+ public void Dispose()
{
- this.Mixer.サウンドを追加する( sound );
+ this.Dispose( true );
+ GC.SuppressFinalize( this );
}
- public void サウンドをミキサーから削除する( Sound sound )
+
+ protected virtual void Dispose( bool bDisposeManaged )
+ {
+ if( !this._dispose済み )
+ {
+ lock( this._スレッド間同期 )
+ {
+ if( bDisposeManaged )
+ {
+ // (A) ここでマネージリソースを解放する。
+ this.StopRendering();
+ this._解放する();
+ }
+
+ // (B) ここでネイティブリソースを解放する。
+
+ // ...特にない。
+
+ }
+
+ this._dispose済み = true;
+ }
+ }
+ //----------------
+ #endregion
+
+
+ private volatile CSCore.SoundOut.PlaybackState _レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
+
+ private CSCore.CoreAudioAPI.AudioClientShareMode _共有モード;
+
+ private long _遅延100ns = 0;
+
+ private CSCore.WaveFormat _WaveFormat = null;
+
+ private CSCore.CoreAudioAPI.AudioClock _AudioClock = null;
+
+ private CSCore.CoreAudioAPI.AudioRenderClient _AudioRenderClient = null;
+
+ private CSCore.CoreAudioAPI.AudioClient _AudioClient = null;
+
+ private CSCore.CoreAudioAPI.MMDevice _MMDevice = null;
+
+ private System.Threading.Thread _レンダリングスレッド = null;
+
+ private System.Threading.EventWaitHandle _レンダリングイベント = null;
+
+ private CSCore.Streams.VolumeSource _レンダリング先 = null;
+
+ private CSCore.IWaveSource _生レンダリング先 = null;
+
+ private Mixer _Mixer = null;
+
+ private readonly object _スレッド間同期 = new object();
+
+ private bool _dispose済み = false;
+
+
+ private void _初期化する( CSCore.WaveFormat 希望フォーマット = null )
{
- this.Mixer.サウンドを削除する( sound );
+ lock( this._スレッド間同期 )
+ {
+ if( this._レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped )
+ throw new InvalidOperationException( "WASAPI のレンダリングを停止しないまま初期化することはできません。" );
+
+ this._レンダリングスレッド?.Join();
+
+ this._解放する();
+
+ // MMDevice を取得する。
+ this._MMDevice = CSCore.CoreAudioAPI.MMDeviceEnumerator.DefaultAudioEndpoint(
+ CSCore.CoreAudioAPI.DataFlow.Render, // 方向:再生
+ CSCore.CoreAudioAPI.Role.Console ); // 用途:ゲーム、システム通知音、音声命令
+
+ // AudioClient を取得する。
+ this._AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( this._MMDevice );
+
+ // フォーマットを決定する。
+ var defaultFormat = ( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) ?
+ this._AudioClient.GetMixFormat() :
+ new CSCore.WaveFormat( 48000, 32, 2, AudioEncoding.IeeeFloat );
+
+ if( null == ( this._WaveFormat = this._適切なフォーマットを調べて返す( 希望フォーマット ?? defaultFormat ) ) )
+ {
+ throw new NotSupportedException( "サポート可能な WaveFormat が見つかりませんでした。" );
+ }
+
+ // 遅延を既定値にする(共有モードの場合のみ)。
+ if( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared )
+ this._遅延100ns = this._AudioClient.DefaultDevicePeriod;
+
+ // AudioClient を初期化する。
+ Action AudioClientを初期化する = () => {
+ this._AudioClient.Initialize(
+ this._共有モード,
+ CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, // イベント駆動で固定。
+ this._遅延100ns,
+ this._遅延100ns,
+ this._WaveFormat,
+ Guid.Empty );
+ };
+ try
+ {
+ AudioClientを初期化する();
+ }
+ catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
+ {
+ // 排他モードかつイベント駆動 の場合、この例外が返されることがある。
+ // この場合、バッファサイズを調整して再度初期化する。
+ if( e.ErrorCode == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED )
+ {
+ int サイズframe = this._AudioClient.GetBufferSize();
+ this._遅延100ns = (long) ( 10.0 * 1000.0 * 1000.0 * サイズframe / this._WaveFormat.SampleRate + 0.5 ); // +0.5 は四捨五入
+
+ AudioClientを初期化する(); // それでも例外なら知らん。
+ }
+ }
+
+ // イベント駆動用に使うイベントを生成し、AudioClient へ登録する。
+ this._レンダリングイベント = new System.Threading.EventWaitHandle( false, System.Threading.EventResetMode.AutoReset );
+ this._AudioClient.SetEventHandle( this._レンダリングイベント.SafeWaitHandle.DangerousGetHandle() );
+
+ // その他の WASAPI インターフェースを取得する。
+ this._AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this._AudioClient );
+ this._AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this._AudioClient );
+
+ // ミキサーを生成し、デバイスのソース(DirectSound でいうところのプライマリバッファ)として登録する。
+ this._Mixer = new Mixer( this._WaveFormat ) {
+ DivideResult = false,
+ };
+ this._SetSource( this._Mixer );
+ }
}
- // WASAPI オブジェクト
- protected CSCore.CoreAudioAPI.AudioClient AudioClient = null;
- protected CSCore.CoreAudioAPI.AudioRenderClient AudioRenderClient = null;
+ private void _解放する()
+ {
+ FDK.Utilities.解放する( ref this._Mixer );
+ FDK.Utilities.解放する( ref this._AudioClock );
+ FDK.Utilities.解放する( ref this._AudioRenderClient );
- // エンドポイントバッファ情報
- protected float 希望更新間隔ms;
- protected float 最小間隔ms;
- protected float 更新間隔ms;
- protected int 更新間隔sample;
- protected int 更新間隔byte;
+ if( ( null != this._AudioClient ) && ( this._AudioClient.BasePtr != IntPtr.Zero ) )
+ {
+ try
+ {
+ this._AudioClient.StopNative();
+ this._AudioClient.Reset();
+ }
+ catch( CSCore.CoreAudioAPI.CoreAudioAPIException e )
+ {
+ if( e.ErrorCode != AUDCLNT_E_NOT_INITIALIZED )
+ throw;
+ }
+ }
- // ミキサー。サウンドリストもここ。
- protected Mixer Mixer = null;
+ FDK.Utilities.解放する( ref this._AudioClient );
+ FDK.Utilities.解放する( ref this._レンダリングイベント );
+ FDK.Utilities.解放する( ref this._MMDevice );
+ }
- // WASAPIバッファ出力用
- private int QueueID = int.MaxValue;
- private IntPtr 出力要請イベント = IntPtr.Zero;
- private MFAsyncCallback 出力要請イベントのコールバック = null;
- private long 出力要請イベントキャンセル用キー = 0;
- private FDK.同期.TriStateEvent 出力終了通知 = new 同期.TriStateEvent();
+ private void _SetSource( CSCore.ISampleSource targetSource )
+ {
+ if( null != this._レンダリング先 )
+ throw new InvalidOperationException( "レンダリングターゲットはすでに設定済みです。" );
- private readonly object スレッド間同期 = new object();
+ this._レンダリング先 = new CSCore.Streams.VolumeSource( targetSource ); // サンプル(float)単位のレンダリング先と、
+ this._生レンダリング先 = targetSource.ToWaveSource(); // データ(byte)単位のレンダリング先とを持っておく。
+ }
- private void 作業項目をキューに格納する()
+ /// <summary>
+ /// 希望したフォーマットをもとに、適切なフォーマットを調べて返す。
+ /// </summary>
+ /// <param name="waveFormat">希望するフォーマット</param>
+ /// <param name="audioClient">AudioClient インスタンス。Initialize 前でも可。</param>
+ /// <returns>適切なフォーマット。見つからなかったら null。</returns>
+ private CSCore.WaveFormat _適切なフォーマットを調べて返す( CSCore.WaveFormat waveFormat )
{
- var asyncResult = (SharpDX.MediaFoundation.AsyncResult) null;
- try
+ Trace.Assert( null != this._AudioClient );
+
+ var 最も近いフォーマット = (CSCore.WaveFormat) null;
+ var 最終的に決定されたフォーマット = (CSCore.WaveFormat) null;
+
+ if( this._AudioClient.IsFormatSupported( this._共有モード, waveFormat, out 最も近いフォーマット ) )
{
- // IAsyncCallback を内包した AsyncResult を作成する。
- SharpDX.MediaFoundation.MediaFactory.CreateAsyncResult(
- null,
- SharpDX.ComObject.ToCallbackPtr<SharpDX.MediaFoundation.IAsyncCallback>( this.出力要請イベントのコールバック ),
- null,
- out asyncResult );
-
- // 作成した AsyncResult を、ワークキュー投入イベントの待機状態にする。
- SharpDX.MediaFoundation.MediaFactory.PutWaitingWorkItem(
- hEvent: this.出力要請イベント,
- priority: 0,
- resultRef: asyncResult,
- keyRef: out this.出力要請イベントキャンセル用キー );
+ // (A) そのまま使える。
+ 最終的に決定されたフォーマット = waveFormat;
}
- finally
+ else if( null != 最も近いフォーマット )
{
- // out 引数に使う変数は using 変数にはできないので、代わりに try-finally を使う。
- asyncResult?.Dispose();
+ // (B) AudioClient が推奨フォーマットを返してきたなら、それを採択する。
+ 最終的に決定されたフォーマット = 最も近いフォーマット;
}
+ else
+ {
+ // (C) AudioClient からの提案がなかった場合は、共有モードのフォーマットを採択する。
+
+ var 共有モードのフォーマット = this._AudioClient.GetMixFormat();
+
+ if( ( null != 共有モードのフォーマット ) && this._AudioClient.IsFormatSupported( this._共有モード, 共有モードのフォーマット ) )
+ {
+ 最終的に決定されたフォーマット = 共有モードのフォーマット;
+ }
+ else
+ {
+ // (D) AudioClient が共有モードのフォーマットすらNGと言ってきた場合は、以下から探す。
+
+ 最終的に決定されたフォーマット = new[]
+ {
+ new CSCore.WaveFormatExtensible( waveFormat.SampleRate, 32, waveFormat.Channels, CSCore.AudioSubTypes.IeeeFloat ),
+ new CSCore.WaveFormatExtensible( waveFormat.SampleRate, 24, waveFormat.Channels, CSCore.AudioSubTypes.Pcm ),
+ new CSCore.WaveFormatExtensible( waveFormat.SampleRate, 16, waveFormat.Channels, CSCore.AudioSubTypes.Pcm ),
+ new CSCore.WaveFormatExtensible( waveFormat.SampleRate, 8, waveFormat.Channels, CSCore.AudioSubTypes.Pcm ),
+ }
+ .FirstOrDefault( ( format ) => ( this._AudioClient.IsFormatSupported( this._共有モード, format ) ) );
+
+ // (E) それでも見つからなかったら null 。
+ }
+ }
+
+ return 最終的に決定されたフォーマット;
}
/// <summary>
- /// このメソッドは、WASAPIイベント発生時にワークキューに投入され作業項目から呼び出される。
+ /// WASAPIイベント駆動スレッドのエントリ。
/// </summary>
- private void 出力要請イベントへ対応する( SharpDX.MediaFoundation.AsyncResult asyncResult )
+ /// <param name="起動完了通知">無事に起動できたら、これを Set して(スレッドの生成元に)知らせる。</param>
+ private void _レンダリングスレッドエントリ( object 起動完了通知 )
{
+ var 例外 = (Exception) null;
+ var avrtHandle = IntPtr.Zero;
+
try
{
- // 出力終了通知が来ていれば、応答してすぐに終了する。
- if( this.出力終了通知.状態 == 同期.TriStateEvent.状態種別.ON )
- {
- this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.無効;
- return;
+ int バッファサイズframe = this._AudioClient.BufferSize;
+ int フレームサイズbyte = this._WaveFormat.Channels * this._WaveFormat.BytesPerSample;
+ var バッファ = new byte[ バッファサイズframe * フレームサイズbyte ];
+
+ // このスレッドの MMCSS 型を登録する。
+ int taskIndex;
+ string mmcssType = new[] {
+ new { 最大遅延 = 0.010, 型名 = "Pro Audio" }, // 優先度の高いものから。
+ new { 最大遅延 = 0.015, 型名 = "Games" },
}
+ .FirstOrDefault( ( i ) => ( i.最大遅延 > this.遅延sec ) )?.型名 ?? "Audio";
+ avrtHandle = Device.AvSetMmThreadCharacteristics( mmcssType, out taskIndex );
- lock( this.スレッド間同期 )
- {
- // エンドポインタの空きバッファへのポインタを取得する。
- // このポインタが差すのはネイティブで確保されたメモリなので、GCの対象外である。はず。
- var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample ); // イベント駆動なのでサイズ固定。
+ // AudioClient を開始する。
+ this._AudioClient.Start();
+ this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Playing;
+
+ // 起動完了を通知する。
+ ( 起動完了通知 as System.Threading.EventWaitHandle )?.Set();
+ 起動完了通知 = null;
- // ミキサーを使って、エンドポインタへサウンドデータを出力する。
- var flags = this.Mixer.エンドポイントへ出力する( (void*) bufferPtr, this.更新間隔sample );
+ // 以下、メインループ。
- // エンドポインタのバッファを解放する。
- this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, flags );
+ var イベントs = new System.Threading.WaitHandle[] { this._レンダリングイベント };
+ while( this.レンダリング状態 != CSCore.SoundOut.PlaybackState.Stopped )
+ {
+ int イベント番号 = System.Threading.WaitHandle.WaitAny(
+ waitHandles: イベントs,
+ millisecondsTimeout: (int) ( 3000.0 * this.遅延sec ), // 適正値は レイテンシ×3 [ms] (MSDN)
+ exitContext: false );
- // 後続のイベント待ち作業項目をキューに格納する。
- this.作業項目をキューに格納する();
+ if( イベント番号 == System.Threading.WaitHandle.WaitTimeout )
+ continue;
+
+ if( this.レンダリング状態 == CSCore.SoundOut.PlaybackState.Playing )
+ {
+ int 未再生数frame = ( this._共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive ) ? 0 : this._AudioClient.GetCurrentPadding();
+ int 空きframe = バッファサイズframe - 未再生数frame;
- // 以降、WASAPIからイベントが発火されるたび、作業項目を通じて本メソッドが呼び出される。
+ if( 空きframe > 5 ) // あまりに空きが小さいならスキップする。
+ {
+ if( !this._バッファを埋める( バッファ, 空きframe, フレームサイズbyte ) )
+ this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped;
+ }
+ }
}
+
+ // 以下、終了処理。
+
+ // このスレッドの MMCSS 特性を元に戻す。
+ Device.AvRevertMmThreadCharacteristics( avrtHandle );
+ avrtHandle = IntPtr.Zero;
+
+ // ハードウェアの再生が終わるくらいまで、少し待つ。
+ System.Threading.Thread.Sleep( (int) ( this.遅延sec * 1000 / 2 ) );
+
+ // AudioClient を停止する。
+ this._AudioClient.Stop();
+ this._AudioClient.Reset();
}
- catch
+ catch( Exception e )
{
- // 例外は無視。
+ 例外 = e;
+ }
+ finally
+ {
+ if( avrtHandle != IntPtr.Zero )
+ Device.AvRevertMmThreadCharacteristics( avrtHandle );
+
+ ( 起動完了通知 as System.Threading.EventWaitHandle )?.Set(); // 失敗時を想定して。
}
}
- #region " バックストア。"
- //----------------
- private CSCore.CoreAudioAPI.AudioClock bs_AudioClock = null;
- //----------------
- #endregion
+ private bool _バッファを埋める( byte[] バッファ, int フレーム数, int フレームサイズbyte )
+ {
+ int 読み込むサイズbyte = フレーム数 * フレームサイズbyte;
+ 読み込むサイズbyte -= ( 読み込むサイズbyte % this._生レンダリング先.WaveFormat.BlockAlign ); // BlockAlign の倍数にする。
+
+ if( 読み込むサイズbyte <= 0 )
+ return true;
- #region " Win32 API "
- //-----------------
- private static int AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = unchecked((int) 0x88890019);
+ int 読み込んだサイズbyte = this._生レンダリング先.Read( バッファ, 0, 読み込むサイズbyte );
- [System.Runtime.InteropServices.DllImport( "kernel32.dll" )]
- private static extern IntPtr CreateEvent( IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName );
+ IntPtr ptr = this._AudioRenderClient.GetBuffer( フレーム数 );
+ Marshal.Copy( バッファ, 0, ptr, 読み込んだサイズbyte );
+ this._AudioRenderClient.ReleaseBuffer( 読み込んだサイズbyte / フレームサイズbyte, CSCore.CoreAudioAPI.AudioClientBufferFlags.None );
- [System.Runtime.InteropServices.DllImport( "kernel32.dll" )]
- private static extern bool CloseHandle( IntPtr hObject );
- //-----------------
+ return ( 0 < 読み込んだサイズbyte );
+ }
+
+ #region " Win32 "
+ //----------------
+ private const int AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = unchecked((int) 0x88890019);
+ private const int AUDCLNT_E_INVALID_DEVICE_PERIOD = unchecked((int) 0x88890020);
+ private const int AUDCLNT_E_NOT_INITIALIZED = unchecked((int) 0x88890001);
+
+ [DllImport( "Avrt.dll", CharSet = CharSet.Unicode )]
+ private static extern IntPtr AvSetMmThreadCharacteristics( [MarshalAs( UnmanagedType.LPWStr )] string proAudio, out int taskIndex );
+
+ [DllImport( "Avrt.dll" )]
+ private static extern bool AvRevertMmThreadCharacteristics( IntPtr avrtHandle );
+ //----------------
#endregion
}
}