From 9fc8f64c0cfb72c478385c7a973bf999ef951386 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E3=81=8F=E3=81=BE=E3=81=8B=E3=81=BF=E5=B7=A5=E6=88=BF?= Date: Wed, 16 Nov 2016 16:31:33 +0900 Subject: [PATCH] =?utf8?q?WASAPI=20=E5=90=8D=E5=89=8D=E7=A9=BA=E9=96=93?= =?utf8?q?=E3=82=92=20WASAPIold=20=E3=81=AB=E6=94=B9=E5=90=8D=E3=81=97?= =?utf8?q?=E3=80=81=E6=96=B0=E3=81=97=E3=81=84=20WASAPI=20=E5=90=8D?= =?utf8?q?=E5=89=8D=E7=A9=BA=E9=96=93=E3=81=AB=E6=96=B0=E3=81=97=E3=81=84?= =?utf8?q?=E5=AE=9F=E8=A3=85=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- FDK24/Extensions.cs | 46 ++ FDK24/FDK24.csproj | 15 +- FDK24/メディア/サウンド/WASAPI/Decoder.cs | 236 +++++++ FDK24/メディア/サウンド/WASAPI/Device.cs | 713 +++++++++++++-------- FDK24/メディア/サウンド/WASAPI/Mixer.cs | 264 +++++--- FDK24/メディア/サウンド/WASAPI/Sound.cs | 394 ++---------- .../メディア/サウンド/WASAPI/SoundTimer.cs | 25 +- .../メディア/サウンド/WASAPIold/Device.cs | 360 +++++++++++ .../{WASAPI => WASAPIold}/MFAsyncCallback.cs | 2 +- FDK24/メディア/サウンド/WASAPIold/Mixer.cs | 128 ++++ FDK24/メディア/サウンド/WASAPIold/Sound.cs | 376 +++++++++++ .../サウンド/WASAPIold/SoundTimer.cs | 68 ++ StrokeStyleT/StrokeStyleT.cs | 6 +- .../ステージ/演奏/ドラムサウンド.cs | 6 +- .../ステージ/演奏/演奏ステージ.cs | 6 +- 15 files changed, 1917 insertions(+), 728 deletions(-) create mode 100644 FDK24/Extensions.cs create mode 100644 FDK24/メディア/サウンド/WASAPI/Decoder.cs create mode 100644 FDK24/メディア/サウンド/WASAPIold/Device.cs rename FDK24/メディア/サウンド/{WASAPI => WASAPIold}/MFAsyncCallback.cs (95%) create mode 100644 FDK24/メディア/サウンド/WASAPIold/Mixer.cs create mode 100644 FDK24/メディア/サウンド/WASAPIold/Sound.cs create mode 100644 FDK24/メディア/サウンド/WASAPIold/SoundTimer.cs diff --git a/FDK24/Extensions.cs b/FDK24/Extensions.cs new file mode 100644 index 0000000..5743118 --- /dev/null +++ b/FDK24/Extensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace FDK +{ + public static class Extensions + { + /// + /// COM オブジェクトの参照カウントを取得して返す。 + /// + /// COMオブジェクト。 + /// 現在の参照カウントの値。 + public static int GetRefferenceCount( this SharpDX.IUnknown unknownObject ) + { + try + { + unknownObject.AddReference(); + } + catch( InvalidOperationException ) + { + // すでに Dispose されている。 + return 0; + } + + return unknownObject.Release(); + } + + /// + /// 文字列が Null でも空でもないなら true を返す。 + /// + public static bool Nullでも空でもない( this string 検査対象 ) + { + return !string.IsNullOrEmpty( 検査対象 ); + } + + /// + /// 文字列が Null または空なら true を返す。 + /// + public static bool Nullまたは空である( this string 検査対象 ) + { + return string.IsNullOrEmpty( 検査対象 ); + } + } +} diff --git a/FDK24/FDK24.csproj b/FDK24/FDK24.csproj index e17d277..ada5180 100644 --- a/FDK24/FDK24.csproj +++ b/FDK24/FDK24.csproj @@ -105,6 +105,11 @@ + + + + + @@ -114,11 +119,11 @@ - - - - - + + + + + diff --git a/FDK24/メディア/サウンド/WASAPI/Decoder.cs b/FDK24/メディア/サウンド/WASAPI/Decoder.cs new file mode 100644 index 0000000..8db8b27 --- /dev/null +++ b/FDK24/メディア/サウンド/WASAPI/Decoder.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using CSCore; + +namespace FDK.メディア.サウンド.WASAPI +{ + /// + /// 指定されたメディアファイル(動画, 音楽)をデコードして、CSCore.IWaveStream オブジェクトを生成する。 + /// + internal class Decoder : CSCore.IWaveSource + { + /// + /// シークは常にサポートする。 + /// + public bool CanSeek => ( true ); + + /// + /// デコード後のオーディオデータの長さ[byte]。 + /// + public long Length + { + get { return this._EncodedWaveData.Length; } + } + + /// + /// 現在の位置。 + /// 先頭からのオフセット[byte]で表す。 + /// + public long Position + { + get { return this._Position; } + set + { + if( ( 0 > value ) || ( this.Length <= value ) ) + throw new ArgumentOutOfRangeException(); + + this._Position = value; + } + } + + /// + /// デコード後のオーディオデータのフォーマット。 + /// + public CSCore.WaveFormat WaveFormat + { + get; + protected set; + } + + /// + /// メディアファイル(動画、音楽)をデコードする。 + /// + /// メディアファイル(MediaFoundation でデコードできるもの) + /// デコード先のフォーマット。 + public Decoder( string path, CSCore.WaveFormat waveFormat ) + { + // デコード先フォーマットの形式は、IEEE FLOAT しかサポートしない。 + if( ( waveFormat.WaveFormatTag != AudioEncoding.IeeeFloat ) && + ( !CSCore.WaveFormatExtensible.SubTypeFromWaveFormat( waveFormat ).Equals( SharpDX.MediaFoundation.AudioFormatGuids.Float ) ) ) + { + throw new NotSupportedException( "IEEE Float 以外の形式のフォーマットはサポートしません。" ); + } + this.WaveFormat = waveFormat; + + this._初期化する( path ); + } + + public void Dispose() + { + this._解放する(); + } + + /// + /// 連続した要素を読み込み、this.Position を読み込んだ要素の数だけ進める。 + /// + /// + /// 読み込んだ要素を格納するための配列。 + /// このメソッドから戻ると、buffer には offset ~ (offset + count - 1) の数の要素が格納されている。 + /// + /// + /// buffer に格納を始める位置。 + /// + /// + /// 読み込む最大の要素数。 + /// + /// + /// buffer に読み込んだ要素の総数。 + /// + public int Read( byte[] buffer, int offset, int count ) + { + // 音がめちゃくちゃになるとうざいので、このメソッド内では例外を出さないこと。 + + if( ( null == this._EncodedWaveData ) && ( null == buffer ) ) + return 0; + + // offset は、0~buffer.Length-1 に収める。 + offset = Math.Max( 0, Math.Min( buffer.Length - 1, offset ) ); + + // count は、_EncodeWaveData.Length, buffer.Length-offset, count のうちの最小値とする。 + count = Math.Min( Math.Min( this._EncodedWaveData.Length, count ), buffer.Length - offset ); + + if( 0 < count ) + { + Array.Copy( + sourceArray: this._EncodedWaveData, + sourceIndex: this._Position, + destinationArray: buffer, + destinationIndex: offset, + length: count ); + + this._Position += count; + } + + return count; + } + + private SharpDX.MediaFoundation.MediaType _MediaType = null; + private byte[] _EncodedWaveData = null; + private long _Position = 0; + + private void _初期化する( string path ) + { + try + { + using( var sourceReader = new SharpDX.MediaFoundation.SourceReader( path ) ) + using( var waveStream = new System.IO.MemoryStream() ) + { + #region " 最初のオーディオストリームを選択し、その他のすべてのストリームを非選択にする。" + //---------------- + sourceReader.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.AllStreams, false ); + sourceReader.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, true ); + //---------------- + #endregion + #region " デコードフォーマットを持つ MediaType を作成し、SourceReader に登録する。" + //---------------- + using( var partialMediaType = new SharpDX.MediaFoundation.MediaType() ) + { + // 部分メディアタイプを作成し、オーディオフォーマットを設定する。 (オーディオフォーマットは IEEE FLOAT で固定。) + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.MajorType, SharpDX.MediaFoundation.MediaTypeGuids.Audio ); + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.Subtype, SharpDX.MediaFoundation.AudioFormatGuids.Float ); + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioNumChannels, this.WaveFormat.Channels ); + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioSamplesPerSecond, this.WaveFormat.SampleRate ); + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBlockAlignment, this.WaveFormat.BlockAlign ); + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioAvgBytesPerSecond, this.WaveFormat.BytesPerSecond ); + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBitsPerSample, this.WaveFormat.BitsPerSample ); + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AllSamplesIndependent, 1 ); // TRUE + + if( this.WaveFormat.WaveFormatTag == AudioEncoding.Extensible ) + { + var wfmEx = this.WaveFormat as CSCore.WaveFormatExtensible; + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioChannelMask, (int) wfmEx.ChannelMask ); + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioSamplesPerBlock, wfmEx.SamplesPerBlock ); + partialMediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioValidBitsPerSample, wfmEx.ValidBitsPerSample ); + } + + // 作成したメディアタイプを sourceReader にセットする。必要なデコーダが見つからなかったら、ここで例外が発生する。 + sourceReader.SetCurrentMediaType( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, partialMediaType ); + + // 完成されたメディアタイプを取得する。 + this._MediaType = sourceReader.GetCurrentMediaType( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream ); + + // 最初のオーディオストリームが選択されていることを保証する。 + sourceReader.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, true ); + } + //---------------- + #endregion + #region " sourceReader からサンプルを取得してデコードし、waveStream へ書き込んだのち、byte[] _EncodedWaveData へ出力する。" + //----------------- + using( var pcmWriter = new System.IO.BinaryWriter( waveStream ) ) + { + while( true ) + { + // 次のサンプルを読み込む。 + int dwActualStreamIndexRef = 0; + var dwStreamFlagsRef = SharpDX.MediaFoundation.SourceReaderFlags.None; + Int64 llTimestampRef = 0; + using( var sample = sourceReader.ReadSample( + SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, + SharpDX.MediaFoundation.SourceReaderControlFlags.None, + out dwActualStreamIndexRef, + out dwStreamFlagsRef, + out llTimestampRef ) ) + { + if( null == sample ) + break; // EndOfStream やエラーのときも null になる。 + + // sample をロックし、オーディオデータへのポインタを取得する。 + int cbMaxLengthRef = 0; + int cbCurrentLengthRef = 0; + using( var mediaBuffer = sample.ConvertToContiguousBuffer() ) + { + // オーディオデータをメモリストリームに書き込む。 + var audioData = mediaBuffer.Lock( out cbMaxLengthRef, out cbCurrentLengthRef ); + try + { + byte[] dstData = new byte[ cbCurrentLengthRef ]; + System.Runtime.InteropServices.Marshal.Copy( audioData, dstData, 0, cbCurrentLengthRef ); + pcmWriter.Write( dstData, 0, cbCurrentLengthRef ); + } + finally + { + mediaBuffer.Unlock(); + } + } + } + } + + // ストリームの内容を byte 配列に出力。 + this._EncodedWaveData = waveStream.ToArray(); + } + //----------------- + #endregion + } + } + catch + { + this._EncodedWaveData = new byte[] { }; + } + + this._Position = 0; + } + + private void _解放する() + { + FDK.Utilities.解放する( ref this._MediaType ); + } + + #region " Win32 " + //---------------- + private const int MF_E_INVALIDMEDIATYPE = unchecked((int) 0xC00D36B4); + //---------------- + #endregion + } +} diff --git a/FDK24/メディア/サウンド/WASAPI/Device.cs b/FDK24/メディア/サウンド/WASAPI/Device.cs index 984923c..0495c97 100644 --- a/FDK24/メディア/サウンド/WASAPI/Device.cs +++ b/FDK24/メディア/サウンド/WASAPI/Device.cs @@ -1,360 +1,521 @@ 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 CSCore.SoundOut.PlaybackState レンダリング状態 + { + get { return this._レンダリング状態; } + } + public double 遅延sec { - get { return ( this.更新間隔sec * 1000.0 ); } + get { return FDK.Utilities.変換_100ns単位からsec単位へ( this._遅延100ns ); } + protected set { this._遅延100ns = FDK.Utilities.変換_sec単位から100ns単位へ( value ); } } - public CSCore.CoreAudioAPI.AudioClock AudioClock + + public long 遅延100ns { - get { return this.bs_AudioClock; } + get { return this._遅延100ns; } + protected set { this._遅延100ns = value; } } - public bool Dispose済み + + public CSCore.WaveFormat フォーマット { - get; - protected set; - } = true; + get { return this._WaveFormat; } + } - public void 初期化する( CSCore.CoreAudioAPI.AudioClientShareMode 共有モード, double 希望更新間隔sec = 0.015 ) + /// + /// レンダリングボリューム。 + /// 0.0 (0%) ~ 1.0 (100%) 。 + /// + public float 音量 { - FDK.Log.BeginInfo( $"{FDK.Utilities.現在のメソッド名}" ); + get + { + return ( null != this._レンダリング先 ) ? this._レンダリング先.Volume : 1.0f; + } + set + { + if( ( 0.0f > value ) || ( 1.0f < value ) ) + throw new ArgumentOutOfRangeException(); - int hr = 0; + this._レンダリング先.Volume = value; + } + } - lock( this.スレッド間同期 ) - { - Trace.Assert( this.Dispose済み ); - this.Dispose済み = false; + public CSCore.CoreAudioAPI.AudioClock AudioClock + { + get { return this._AudioClock; } + } - this.共有モード = 共有モード; + public Device( CSCore.CoreAudioAPI.AudioClientShareMode 共有モード, double 遅延sec = 0.010, CSCore.WaveFormat 希望フォーマット = null ) + { + this._共有モード = 共有モード; + this.遅延sec = 遅延sec; + this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Stopped; + + this._初期化する( 希望フォーマット ); + } + + /// + /// メディアファイル(動画、音声)からサウンドインスタンスを生成して返す。 + /// + public Sound CreateSound( string path ) + { + return new Sound( path, this._Mixer ); + } - #region " AudioClientをアクティベートする。" - //----------------- - using( var devices = new CSCore.CoreAudioAPI.MMDeviceEnumerator() ) - using( var 既定のデバイス = devices.GetDefaultAudioEndpoint( CSCore.CoreAudioAPI.DataFlow.Render, CSCore.CoreAudioAPI.Role.Console ) ) + /// + /// ミキサーの出力を開始する。 + /// 以降、ミキサーに Sound を追加すれば、自動的に再生される。 + /// + public void PlayRendering() + { + lock( this._スレッド間同期 ) + { + if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused ) { - this.AudioClient = CSCore.CoreAudioAPI.AudioClient.FromMMDevice( 既定のデバイス ); + // Pause 中なら Resume する。 + this.ResumeRendering(); } - //----------------- - #endregion - #region " 指定された希望更新間隔とデバイス能力をもとに、更新間隔を決定する。" - //----------------- - long 共有モードでの間隔100ns = 0; - long 排他モードでの最小間隔100ns = 0; - - // デバイスから間隔値を取得する。 - hr = this.AudioClient.GetDevicePeriodNative( out 共有モードでの間隔100ns, out 排他モードでの最小間隔100ns ); - if( 0 > hr ) - System.Runtime.InteropServices.Marshal.ThrowExceptionForHR( hr ); - - if( 共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) + else if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Stopped ) { - this.更新間隔100ns = 共有モードでの間隔100ns; + using( var 起動完了通知 = new System.Threading.AutoResetEvent( false ) ) + { + // スレッドがすでに終了していることを確認する。 + this._レンダリングスレッド?.Join(); + + // レンダリングスレッドを起動する。 + this._レンダリングスレッド = new System.Threading.Thread( this._レンダリングスレッドエントリ ) { + Name = "WASAPI Playback", + Priority = System.Threading.ThreadPriority.AboveNormal, // 標準よりやや上 + }; + this._レンダリングスレッド.Start( 起動完了通知 ); + + // スレッドからの起動完了通知を待つ。 + 起動完了通知.WaitOne(); + } + } + } + } + + /// + /// ミキサーの出力を停止する。 + /// ミキサーに登録されているすべての Sound の再生が停止する。 + /// + 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 { - this.更新間隔100ns = Math.Max( FDK.Utilities.変換_sec単位から100ns単位へ( 希望更新間隔sec ), 排他モードでの最小間隔100ns ); + Debug.WriteLine( "WASAPIのレンダリングを停止しようとしましたが、すでに停止しています。" ); } - //----------------- - #endregion - #region " デバイスフォーマットを決定する。" - //---------------- - if( this.共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) + } + } + + /// + /// ミキサーの出力を一時停止する。 + /// ミキサーに登録されているすべての Sound の再生が一時停止する。 + /// ResumeRendering()で出力を再開できる。 + /// + public void PauseRendering() + { + lock( this._スレッド間同期 ) + { + if( this.レンダリング状態 == CSCore.SoundOut.PlaybackState.Playing ) { - this.WaveFormat = this.AudioClient.GetMixFormat(); + this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Paused; + Debug.WriteLine( "WASAPIのレンダリングを一時停止しました。" ); } else { - this.WaveFormat = new CSCore.WaveFormat( 44100, 16, 2, CSCore.AudioEncoding.Pcm ); + Debug.WriteLine( "WASAPIのレンダリングを一時停止しようとしましたが、すでに一時停止しています。" ); } - //---------------- - #endregion - #region " AudioClient を初期化する。" - //----------------- - try + } + } + + /// + /// ミキサーの出力を再開する。 + /// PauseRendering() で一時停止状態にあるときのみ有効。 + /// + public void ResumeRendering() + { + lock( this._スレッド間同期 ) + { + if( this._レンダリング状態 == CSCore.SoundOut.PlaybackState.Paused ) { - this.AudioClient.Initialize( - this.共有モード, - CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, - // | CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsNoPersist, // 音量とミュートを記憶しない → 無効。してください - this.更新間隔100ns, - this.更新間隔100ns, - this.WaveFormat, - Guid.Empty ); // この AudioClient (= AudioStrem) が所属する AudioSession。null ならデフォルトのAudioSessionに登録される。 + this._レンダリング状態 = CSCore.SoundOut.PlaybackState.Playing; + Debug.WriteLine( "WASAPIのレンダリングを再開しました。" ); } - catch( CSCore.CoreAudioAPI.CoreAudioAPIException e ) + else { - if( AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED == e.ErrorCode ) - { - #region " 排他&イベント駆動モードの場合、バッファサイズアライメントエラーが返される場合がある。この場合、サイズを調整してオーディオストリームを作成し直す。" - //---------------- - this.更新間隔100ns = FDK.Utilities.変換_sec単位から100ns単位へ( - (double) this.AudioClient.GetBufferSize() / (double) this.WaveFormat.SampleRate ); // GetBufferSize は、更新間隔に一番近い、アライメントされたバッファサイズ(sample単位)を返す。 - - // 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( - this.共有モード, - CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, - // | CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsNoPersist, // 音量とミュートを記憶しない → 無効。してください - this.更新間隔100ns, - this.更新間隔100ns, - this.WaveFormat, - Guid.Empty ); - //---------------- - #endregion - } + Debug.WriteLine( "WASAPIのレンダリングを再開しようとしましたが、すでに再開されています。" ); } - - this.更新間隔sample = this.AudioClient.GetBufferSize(); - //----------------- - #endregion - #region " AudioRenderClient を取得する。" - //----------------- - this.AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this.AudioClient ); - //----------------- - #endregion - #region " AudioClock を取得する。" - //----------------- - this.bs_AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this.AudioClient ); - //----------------- - #endregion - #region " エンドポイントバッファを無音で埋めておく。" - //----------------- - this.AudioRenderClient.GetBuffer( this.更新間隔sample ); - - // 無音を書き込んだことにして、バッファをコミット。(GetBuffer の戻り値は使わない。) - this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent ); - //----------------- - #endregion - - #region " ミキサーを生成し初期化する。" - //----------------- - this.Mixer = new Mixer( this.更新間隔sample ); - //----------------- - #endregion - - #region " 情報表示。" - //----------------- - FDK.Log.Info( $"WASAPIクライアントを初期化しました。" ); - FDK.Log.Info( ( 共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) ? - " モード: 共有 & イベント駆動" : - " モード: 排他 & イベント駆動" ); - FDK.Log.Info( $" フォーマット: {this.WaveFormat.BitsPerSample} bits, {this.WaveFormat.SampleRate} Hz" ); - FDK.Log.Info( $" エンドポイントバッファ: {( (float) this.更新間隔sample / (double) this.WaveFormat.SampleRate ) * 1000.0f} ミリ秒 ({this.更新間隔sample} samples) × 2枚" ); - FDK.Log.Info( $" 希望更新間隔: {希望更新間隔sec * 1000.0} ミリ秒" ); - FDK.Log.Info( $" 更新間隔: {this.更新間隔sec * 1000.0} ミリ秒 ({this.更新間隔sample} samples)" ); - if( 共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive ) - FDK.Log.Info( $" 最小間隔: {FDK.Utilities.変換_100ns単位からsec単位へ( 排他モードでの最小間隔100ns )} ミリ秒" ); - //----------------- - #endregion - - #region " ワークキューとイベントを作成し、作業項目を登録する。" - //----------------- - // MediaFoundation が管理する、プロセス&MMCSSタスクごとに1つずつ作ることができる特別な共有ワークキューを取得(または生成して取得)する。 - int dwTaskId = 0; - SharpDX.MediaFoundation.MediaFactory.LockSharedWorkQueue( ( 0.011 > this.更新間隔sec ) ? "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, this.出力要請イベントへ対応する ); - this.作業項目をキューに格納する(); - //----------------- - #endregion - #region " WASAPI レンダリングを開始。" - //----------------- - this.AudioClient.Start(); - //----------------- - #endregion } + } - FDK.Log.EndInfo( $"{FDK.Utilities.現在のメソッド名}" ); + #region " 解放; Dispose-Finallize パターン " + //---------------- + ~Device() + { + this.Dispose( false ); } + public void Dispose() { - FDK.Log.BeginInfo( $"{FDK.Utilities.現在のメソッド名}" ); - - Trace.Assert( false == this.Dispose済み ); + this.Dispose( true ); + GC.SuppressFinalize( this ); + } - #region " WASAPI作業項目を終了させる。オーディオのレンダリングを止める前に行うこと。" - //----------------- + protected virtual void Dispose( bool bDisposeManaged ) + { + if( !this._dispose済み ) { - //SharpDX.MediaFoundation.MediaFactory.CancelWorkItem( this.出力要請イベントキャンセル用キー ); --> コールバックの実行中にキャンセルしてしまうと NullReference例外 - this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.ON; - this.出力終了通知.OFFになるまでブロックする(); - FDK.Log.Info( "WASAPI出力処理を終了しました。" ); + lock( this._スレッド間同期 ) + { + if( bDisposeManaged ) + { + // (A) ここでマネージリソースを解放する。 + this.StopRendering(); + this._解放する(); + } + + // (B) ここでネイティブリソースを解放する。 + + // ...特にない。 + + } + + this._dispose済み = true; } - //----------------- - #endregion + } + //---------------- + #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; - lock( this.スレッド間同期 ) + 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 ) + { + 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.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 ) ) ) { - SharpDX.MediaFoundation.MediaFactory.UnlockWorkQueue( this.QueueID ); - this.QueueID = int.MaxValue; + throw new NotSupportedException( "サポート可能な WaveFormat が見つかりませんでした。" ); } - //----------------- - #endregion - #region " WASAPIイベント駆動用のコールバックとイベントを解放する。" - //----------------- - FDK.Utilities.解放する( ref this.出力要請イベントのコールバック ); - - if( IntPtr.Zero != this.出力要請イベント ) - Device.CloseHandle( this.出力要請イベント ); - //----------------- - #endregion - - this.Dispose済み = true; - } - FDK.Log.EndInfo( $"{FDK.Utilities.現在のメソッド名}" ); - } - public void サウンドをミキサーに追加する( Sound sound ) - { - this.Mixer.サウンドを追加する( sound ); + // 遅延を既定値にする(共有モードの場合のみ)。 + 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 ); + } } - public void サウンドをミキサーから削除する( Sound sound ) + + private void _解放する() { - this.Mixer.サウンドを削除する( sound ); - } + FDK.Utilities.解放する( ref this._Mixer ); + FDK.Utilities.解放する( ref this._AudioClock ); + FDK.Utilities.解放する( ref this._AudioRenderClient ); - protected CSCore.CoreAudioAPI.AudioClientShareMode 共有モード; - protected CSCore.CoreAudioAPI.AudioClient AudioClient = null; - protected CSCore.CoreAudioAPI.AudioRenderClient AudioRenderClient = null; - protected CSCore.WaveFormat WaveFormat = null; - protected long 更新間隔100ns = 0; - protected int 更新間隔sample = 0; - protected int 更新間隔byte => ( this.更新間隔sample * this.WaveFormat.Channels * this.WaveFormat.BytesPerSample ); - protected double 更新間隔sec => ( FDK.Utilities.変換_100ns単位からsec単位へ( this.更新間隔100ns ) ); + 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 作業項目をキューに格納する() + /// + /// 希望したフォーマットをもとに、適切なフォーマットを調べて返す。 + /// + /// 希望するフォーマット + /// AudioClient インスタンス。Initialize 前でも可。 + /// 適切なフォーマット。見つからなかったら null。 + 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( 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 最終的に決定されたフォーマット; } /// - /// このメソッドは、WASAPIイベント発生時にワークキューに投入され作業項目から呼び出される。 + /// WASAPIイベント駆動スレッドのエントリ。 /// - private void 出力要請イベントへ対応する( SharpDX.MediaFoundation.AsyncResult asyncResult ) + /// 無事に起動できたら、これを Set して(スレッドの生成元に)知らせる。 + 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 ); + // 以下、メインループ。 + + 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.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, flags ); + if( イベント番号 == System.Threading.WaitHandle.WaitTimeout ) + continue; - // 後続のイベント待ち作業項目をキューに格納する。 - this.作業項目をキューに格納する(); + 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( Exception e ) + { + 例外 = e; } - catch + 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 } } diff --git a/FDK24/メディア/サウンド/WASAPI/Mixer.cs b/FDK24/メディア/サウンド/WASAPI/Mixer.cs index c9c9966..d0e6331 100644 --- a/FDK24/メディア/サウンド/WASAPI/Mixer.cs +++ b/FDK24/メディア/サウンド/WASAPI/Mixer.cs @@ -1,128 +1,222 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using CSCore; namespace FDK.メディア.サウンド.WASAPI { /// - /// Sound のリストを持ち、そのサウンドデータを合成してWASAPIデバイスへ出力するミキサー。 + /// オーディオミキサー。 + /// 自身が ISampleSource であり、そのまま AudioClient のレンダリングターゲットに指定する。 /// - public unsafe class Mixer : IDisposable + internal class Mixer : CSCore.ISampleSource { - public Mixer() + /// + /// true なら、サンプルの値を、合成したソースの数で割る。 + /// (例えば、ソースを3つ合成した場合、合成した結果のサンプル値を3で割る。各サウンドはそれぞれ小さく聞こえる。) + /// + public bool DivideResult { + get; + set; + } = false; + + /// + /// 音量。0.0(無音)~1.0(原音)。 + /// + public float Volume + { + get { return this._Volume; } + set + { + if( ( 0.0f > value ) || ( 1.0f < value ) ) + throw new ArgumentOutOfRangeException(); + + this._Volume = value; + } } - public Mixer( int エンドポイントバッファサイズsample ) : this() + + /// + /// ミキサーのフォーマット。 + /// + public CSCore.WaveFormat WaveFormat { - this.初期化する( エンドポイントバッファサイズsample ); + get { return _WaveFormat; } } - public void 初期化する( int エンドポイントバッファサイズsample ) + + /// + /// ミキサーはループするので、Position には 非対応。 + /// + public long Position { - lock( this.スレッド間同期 ) - { - if( エンドポイントバッファサイズsample == this.エンドポイントバッファサイズsample ) - return; // サイズに変更があったときのみ初期化する。 + get { return 0; } + set { throw new NotSupportedException(); } + } - this.エンドポイントバッファサイズsample = エンドポイントバッファサイズsample; - this.サウンドリスト.Clear(); + /// + /// ミキサーはシークできない。 + /// + public bool CanSeek => ( false ); - if( null != this.合成用バッファ ) - FDK.Memory.Free( this.合成用バッファ ); - this.合成用バッファ = FDK.Memory.Alloc( this.エンドポイントバッファサイズsample * ( 4 * 2 ) ); // 1sample = 32bit×2ch - } + /// + /// ミキサーはループするので、長さの概念はない。 + /// + public long Length => ( 0 ); + + /// + /// 指定したフォーマットを持つミキサーを生成する。 + /// + public Mixer( CSCore.WaveFormat waveFormat ) + { + this._WaveFormat = waveFormat; } + public void Dispose() { - lock( this.スレッド間同期 ) + lock( this._スレッド間同期 ) { - this.サウンドリスト.Clear(); - if( null != this.合成用バッファ ) - { - FDK.Memory.Free( this.合成用バッファ ); - this.合成用バッファ = null; - } - this.エンドポイントバッファサイズsample = -1; + // すべての Sound を解放する。 + foreach( var sampleSource in this._Sounds ) + sampleSource.Dispose(); + + // Sound リストをクリアする。 + this._Sounds.Clear(); } } - public void サウンドリストをクリアする() + + /// + /// Sound をミキサーに追加する。 + /// 追加されると同時に、Sound の再生が開始される。 + /// + public void AddSound( Sound sound ) { - lock( this.スレッド間同期 ) + if( null == sound ) + throw new ArgumentNullException(); + + if( ( sound.WaveFormat.Channels != this._WaveFormat.Channels ) || + ( sound.WaveFormat.SampleRate != this._WaveFormat.SampleRate ) ) { - this.サウンドリスト.Clear(); + throw new ArgumentException(); } - } - public void サウンドを追加する( Sound sound ) - { - lock( this.スレッド間同期 ) + + lock( this._スレッド間同期 ) { - this.サウンドリスト.Add( sound ); + if( !this.Contains( sound ) ) + this._Sounds.Add( sound ); } } - public void サウンドを削除する( Sound sound ) + + /// + /// Sound をミキサーから除外する。 + /// 除外されると同時に、Sound の再生は終了する。 + /// + public void RemoveSound( Sound sound ) { - lock( this.スレッド間同期 ) + lock( this._スレッド間同期 ) { - this.サウンドリスト.Remove( sound ); + if( this.Contains( sound ) ) + this._Sounds.Remove( sound ); } } - public CSCore.CoreAudioAPI.AudioClientBufferFlags エンドポイントへ出力する( void* エンドポイントの出力先, int 出力数sample ) + + /// + /// Sound がミキサーに登録されているかを調べる。 + /// + /// Sound がミキサーに追加済みなら true 。 + public bool Contains( Sound sound ) { - lock( this.スレッド間同期 ) - { - if( null == this.合成用バッファ ) - return CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent; + if( null == sound ) + return false; - #region " すべてのサウンドについて、合成バッファへ出力する。" - //----------------- - bool 最初の出力である = true; + return this._Sounds.Contains( sound ); + } - foreach( var sound in this.サウンドリスト ) + /// + /// バッファにサウンドデータを出力する。 + /// + /// 出力したサンプル数。 + public int Read( float[] バッファ, int バッファの出力開始位置, int 出力サンプル数 ) + { + if( 0 < 出力サンプル数 ) + { + lock( this._スレッド間同期 ) { - var flag = sound.次のサウンドデータを出力する( this.合成用バッファ, 出力数sample, 最初の出力である ); + // 中間バッファが十分あることを確認する。足りなければ新しく確保して戻ってくる。 + this._中間バッファ = this._中間バッファ.CheckBuffer( 出力サンプル数 ); - if( false == flag.HasFlag( CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent ) ) - 最初の出力である = false; // sound が何らかのデータを出力した(戻り値がSILENTじゃなかった) - } + // 無音を出力する。 + Array.Clear( バッファ, 0, 出力サンプル数 ); - // 全サウンドが SILENT だったなら、エンドポイントには何も書き込まずに SILENT フラグを返す。 - if( 最初の出力である ) - return CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent; - //----------------- - #endregion - #region " 合成バッファのデータ値(32bit;オーバーサンプル)を16bitに丸めてエンドポイントに出力する。" - //----------------- - Int32* 出力元 = (Int32*) ( this.合成用バッファ ); - Int16* 出力先 = (Int16*) エンドポイントの出力先; - for( int i = 0; i < 出力数sample; i++ ) - { - Int32 src; - - // 音量やミュートの処理は不要。(WASAPI が自動でマスタ音量・ミュート状態に合わせてくれる) - - // 左ch - src = *出力元++; - if( -32768 > src ) - src = -32768; - else if( 32767 < src ) - src = 32767; - *出力先++ = (Int16) src; - - // 右ch - src = *出力元++; - if( -32768 > src ) - src = -32768; - else if( 32767 < src ) - src = 32767; - *出力先++ = (Int16) src; + // 登録されたサンプルソースを出力する。 + if( 0 < this._Sounds.Count ) + { + // DiviveResult 用。 + var サウンド別読み出し数 = new List( this._Sounds.Count ); + + // ミキサに登録されているすべての Sound について……(逆順) + for( int m = this._Sounds.Count - 1; m >= 0; m-- ) + { + var sound = this._Sounds[ m ]; + + // 中間バッファにサウンドデータを受け取る。 + int 受け取ったサンプル数 = sound.SampleSource.Read( this._中間バッファ, 0, 出力サンプル数 ); + + // 中間バッファから出力バッファへ転送する。 + for( int i = バッファの出力開始位置, n = 0; n < 受け取ったサンプル数; i++, n++ ) + { + float data = this._中間バッファ[ n ] // 原音 + * sound.Volume // 個別音量 + * this._Volume; // ミキサ音量 + + バッファ[ i ] += data; // ベースに無音を出力済みなので、上書きじゃなく常に加算。 + } + + if( 0 < 受け取ったサンプル数 ) + { + // DiviveResult 用。 + サウンド別読み出し数.Add( 受け取ったサンプル数 ); + } + else + { + // 再生終了。リストから削除。 + this.RemoveSound( sound ); + } + } + + if( DivideResult ) + { + サウンド別読み出し数.Sort(); + + int 出力位置 = バッファの出力開始位置; + int 残りのSound数 = サウンド別読み出し数.Count; + + foreach( var 読み出し数 in サウンド別読み出し数 ) + { + if( 0 == 残りのSound数 ) + break; + + while( 出力位置 < ( バッファの出力開始位置 + 読み出し数 ) ) + { + バッファ[ 出力位置 ] /= 残りのSound数; + バッファ[ 出力位置 ] = Math.Max( -1, Math.Min( 1, バッファ[ 出力位置 ] ) ); + 出力位置++; + } + + 残りのSound数--; + } + } + } } - //----------------- - #endregion } - return CSCore.CoreAudioAPI.AudioClientBufferFlags.None; + + return 出力サンプル数; } - private int エンドポイントバッファサイズsample = -1; - private readonly List サウンドリスト = new List(); - private void* 合成用バッファ = null; - private readonly object スレッド間同期 = new object(); + private float _Volume = 1.0f; + private CSCore.WaveFormat _WaveFormat = null; + private readonly List _Sounds = new List(); + private float[] _中間バッファ = null; + private readonly object _スレッド間同期 = new object(); } } diff --git a/FDK24/メディア/サウンド/WASAPI/Sound.cs b/FDK24/メディア/サウンド/WASAPI/Sound.cs index d2e9723..189a44e 100644 --- a/FDK24/メディア/サウンド/WASAPI/Sound.cs +++ b/FDK24/メディア/サウンド/WASAPI/Sound.cs @@ -1,376 +1,86 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using CSCore; namespace FDK.メディア.サウンド.WASAPI { - public unsafe class Sound : IDisposable + public class Sound { - public enum E再生状態 + public long Length { - 停止中, // 初期状態 - 再生中, - 一時停止中, - 再生終了, - }; - public E再生状態 再生状態 = E再生状態.停止中; - - /// - /// 0.0(最小)~1.0(原音) の範囲で指定する。再生中でも反映される。 - /// - public float 音量 - { - set - { - float 設定値 = Math.Min( Math.Max( value, 0.0f ), 1.0f ); // 0.0未満は0.0へ、1.0超は1.0へ。 - lock( this.排他利用 ) - { - this.bs_音量 = 設定値; - } - } - get - { - lock( this.排他利用 ) - { - return this.bs_音量; - } - } - } - public double 長さsec - { - get - { - lock( this.排他利用 ) - { - return ( this.サウンドデータサイズsample / this.WAVEフォーマット.SampleRate ); - } - } + get { return this._SampleSource.Length; } } - public Sound() - { - } - public Sound( string サウンドファイルuri ) : this() + public long Position { - this.ファイルから作成する( サウンドファイルuri ); + get { return this._SampleSource.Position; } + set { this._SampleSource.Position = value; } } - public void ファイルから作成する( string サウンドファイルuri ) - { - lock( this.排他利用 ) - { - #region " 作成済みなら先にDisposeする。" - //----------------- - if( this.作成済み ) - this.Dispose(); - - this.作成済み = false; - //----------------- - #endregion - - byte[] encodedPcm = null; - using( var sourceReader = new SharpDX.MediaFoundation.SourceReader( サウンドファイルuri ) ) - using( var pcmStream = new System.IO.MemoryStream() ) - { - #region " サウンドファイル名から SourceReader を作成する。" - //----------------- - - // 先述の using で作成済み。 - - // 最初のオーディオストリームを選択し、その他のすべてのストリームを非選択にする。 - sourceReader.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.AllStreams, false ); - sourceReader.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, true ); - - // メディアタイプを作成し、オーディオフォーマットを設定する。(固定フォーマットとする。) - using( var mediaType = new SharpDX.MediaFoundation.MediaType() ) - { - mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.MajorType, SharpDX.MediaFoundation.MediaTypeGuids.Audio ); - mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.Subtype, SharpDX.MediaFoundation.AudioFormatGuids.Pcm ); - mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioNumChannels, this.WAVEフォーマット.Channels ); - mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioSamplesPerSecond, this.WAVEフォーマット.SampleRate ); - mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBlockAlignment, this.WAVEフォーマット.BlockAlign ); - mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioAvgBytesPerSecond, this.WAVEフォーマット.AverageBytesPerSecond ); - mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBitsPerSample, this.WAVEフォーマット.BitsPerSample ); - mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AllSamplesIndependent, 1 ); // TRUE - - // 作成したメディアタイプを sourceReader にセットする。sourceReader は、必要なデコーダをロードするだろう。 - sourceReader.SetCurrentMediaType( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, mediaType ); - } - - // 最初のオーディオストリームが選択されていることを保証する。 - sourceReader.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, true ); - //----------------- - #endregion - #region " sourceReader からサンプルを取得してデコードし、メモリストリーム pcmStream へ書き込んだのち、encodedPcm へ変換する。" - //----------------- - using( var pcmWriter = new System.IO.BinaryWriter( pcmStream ) ) - { - while( true ) - { - // 次のサンプルを読み込む。 - int dwActualStreamIndexRef = 0; - var dwStreamFlagsRef = SharpDX.MediaFoundation.SourceReaderFlags.None; - Int64 llTimestampRef = 0; - - using( var sample = sourceReader.ReadSample( - SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, - SharpDX.MediaFoundation.SourceReaderControlFlags.None, - out dwActualStreamIndexRef, - out dwStreamFlagsRef, - out llTimestampRef ) ) - { - if( null == sample ) - break; // EndOfStream やエラーも含まれる。 - - // サンプルをロックし、オーディオデータへのポインタを取得する。 - int cbMaxLengthRef = 0; - int cbCurrentLengthRef = 0; - using( var mediaBuffer = sample.ConvertToContiguousBuffer() ) - { - // オーディオデータをメモリストリームに書き込む。 - var audioData = mediaBuffer.Lock( out cbMaxLengthRef, out cbCurrentLengthRef ); - - byte[] dstData = new byte[ cbCurrentLengthRef ]; - byte* psrcData = (byte*) audioData.ToPointer(); // fixed - fixed ( byte* pdstData = dstData ) - { - CopyMemory( pdstData, psrcData, cbCurrentLengthRef ); - } - pcmWriter.Write( dstData, 0, cbCurrentLengthRef ); - - // サンプルのロックを解除する。 - mediaBuffer.Unlock(); - } - } - } - - // ストリームの内容を byte 配列に出力。(Position に関係なく全部出力される。) - encodedPcm = pcmStream.ToArray(); - } - //----------------- - #endregion - } - #region " オーバーサンプリングサウンドデータバッファを確保し、encodedPcm からサンプルを転送する。" - //----------------- - using( var pcmReader = new System.IO.BinaryReader( new System.IO.MemoryStream( encodedPcm ) ) ) - { - // PCMサイズを計算する。(16bit → 32bit でオーバーサンプリングする。) - this.サウンドデータサイズbyte = encodedPcm.Length * 2; // 32bit は 16bit の2倍。 - this.サウンドデータサイズsample = this.サウンドデータサイズbyte / 8; // 1sample = 32bit×2h = 64bit = 8bytes - - // オーバーサンプリングサウンドデータ用メモリを確保する。 - this.サウンドデータ = (byte*) FDK.Memory.Alloc( this.サウンドデータサイズbyte ); - - // ストリームからオーバーサンプリングサウンドデータへ転送する。 - var p = (Int32*) this.サウンドデータ; - for( int i = 0; i < this.サウンドデータサイズsample; i++ ) - { - // 1サンプル = 2ch×INT16 を 2ch×INT32 に変換しながら格納。 - *p++ = (Int32) pcmReader.ReadInt16(); // 左ch - *p++ = (Int32) pcmReader.ReadInt16(); // 右ch - } - } - //----------------- - #endregion - - this.再生位置sample = 0; - this.作成済み = true; - } - } - public void 再生を開始する( double 再生開始位置sec = 0.0 ) + public WaveFormat WaveFormat { - lock( this.排他利用 ) - { - if( false == this.作成済み ) - return; // エラーにはしない。サウンド作成失敗時には、何も再生しないようにするだけ。 - - int 開始位置sample = (int) ( 再生開始位置sec * this.WAVEフォーマット.SampleRate ); - if( 開始位置sample < this.サウンドデータサイズsample ) - { - this.再生状態 = E再生状態.再生中; - this.再生位置sample = 開始位置sample; - } - } + get { return this._SampleSource.WaveFormat; } } - public void 再生を一時停止する() - { - lock( this.排他利用 ) - { - if( false == this.作成済み ) - return; // エラーにはしない。サウンド作成失敗時には、何も再生しないようにするだけ。 - this.再生状態 = E再生状態.一時停止中; - } - } - public void 再生を再開する() + public CSCore.ISampleSource SampleSource { - lock( this.排他利用 ) - { - if( false == this.作成済み ) - return; // エラーにはしない。サウンド作成失敗時には、何も再生しないようにするだけ。 - - if( E再生状態.一時停止中 != this.再生状態 ) - this.再生位置sample = 0; - - this.再生状態 = E再生状態.再生中; - } + get { return this._SampleSource; } } - public void 再生を停止する() - { - lock( this.排他利用 ) - { - if( false == this.作成済み ) - return; // エラーにはしない。サウンド作成失敗時には、何も再生しないようにするだけ。 - this.再生状態 = E再生状態.停止中; - this.再生位置sample = 0; - } - } - public CSCore.CoreAudioAPI.AudioClientBufferFlags 次のサウンドデータを出力する( void* 出力先, int 出力サンプル数, bool 最初の出力である ) + /// + /// 音量。0.0(無音)~1.0(原音)。 + /// + public float Volume { - lock( this.排他利用 ) + get { return this._Volume; } + set { - #region " 未作成、または再生中でないなら無音フラグをもって帰還。" - //----------------- - if( ( false == this.作成済み ) || ( E再生状態.再生中 != this.再生状態 ) ) - return CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent; - //----------------- - #endregion - - int オーバーサンプルサイズbyte = 4 * 2; // 32bit×2ch - Int32* 出力元 = (Int32*) ( this.サウンドデータ + ( this.再生位置sample * オーバーサンプルサイズbyte ) ); - Int32* _出力先 = (Int32*) 出力先; // この実装ではサンプルは Int32 単位 - int 出力できるサンプル数 = System.Math.Min( 出力サンプル数, ( this.サウンドデータサイズsample - this.再生位置sample ) ); - int 出力できないサンプル数 = 出力サンプル数 - 出力できるサンプル数; - - if( 出力できるサンプル数 <= 0 ) - this.再生状態 = E再生状態.再生終了; // 念のため - - if( 最初の出力である ) - { - #region " (A) 上書き。余った部分にもデータ(無音またはループ)を出力する。" - //----------------- - if( 1.0f == this.bs_音量 ) - { - // 原音(最大音量)。 - CopyMemory( _出力先, 出力元, ( 出力できるサンプル数 * オーバーサンプルサイズbyte ) ); - } - else - { - // 音量を反映。 - for( int i = 0; i < 出力できるサンプル数; i++ ) - { - // 1サンプル = 2ch×INT32 - *_出力先++ = (Int32) ( ( *出力元++ ) * this.bs_音量 ); - *_出力先++ = (Int32) ( ( *出力元++ ) * this.bs_音量 ); - } - } + if( ( 0.0f > value ) || ( 1.0f < value ) ) + throw new ArgumentOutOfRangeException(); - if( 0 < 出力できないサンプル数 ) // サウンドデータの末尾に達した - { - // 残りの部分は、とりあえず今は無音。(ループ再生未対応) - ZeroMemory( - (void*) ( ( (byte*) _出力先 ) + ( 出力できるサンプル数 * オーバーサンプルサイズbyte ) ), - 出力できないサンプル数 * オーバーサンプルサイズbyte ); - } - //----------------- - #endregion - } - else - { - #region " (B) 加算合成。余った部分は放置してもいいし、ループしてデータ加算を続けてもいい。" - //----------------- - for( int i = 0; i < 出力できるサンプル数; i++ ) - { - // 1サンプル = 2ch×INT32 - *_出力先++ += (Int32) ( ( *出力元++ ) * this.bs_音量 ); - *_出力先++ += (Int32) ( ( *出力元++ ) * this.bs_音量 ); - } - - if( 0 < 出力できないサンプル数 ) - { - // 残りの部分は、今回の実装では無視。(ループ再生未対応。) - } - //----------------- - #endregion - } - - #region " 再生位置を移動。" - //--------------------------------------------------- - this.再生位置sample += 出力できるサンプル数; - - if( this.サウンドデータサイズsample <= this.再生位置sample ) // サウンドデータの末尾に達した - { - this.再生位置sample = this.サウンドデータサイズsample; - this.再生状態 = E再生状態.再生終了; // 再生終了に伴う自動終了なので、"停止中" ではない。 - } - //--------------------------------------------------- - #endregion + this._Volume = value; } - - return CSCore.CoreAudioAPI.AudioClientBufferFlags.None; } - #region " Dispose-Finalizeパターン " - //---------------- - ~Sound() + /// + /// Sound の生成は、コンストラクタではなく Device.CreateSound() で行うこと。 + /// (Device 内部で持っている Mixer への参照が必要なため。) + /// + /// サウンドファイルパス + /// 使用する Mixer。 + internal Sound( string path, Mixer mixer ) { - this.Dispose( false ); + this._MixerRef = new WeakReference( mixer ); + this._WaveSource = new Decoder( path, mixer.WaveFormat ); + this._SampleSource = this._WaveSource.ToSampleSource(); } + public void Dispose() { - this.Dispose( true ); - GC.SuppressFinalize( this ); + FDK.Utilities.解放する( ref this._SampleSource ); + FDK.Utilities.解放する( ref this._WaveSource ); + this._MixerRef = null; } - protected void Dispose( bool Managedも解放する ) - { - Action サウンドデータを解放する = () => { - if( null != this.サウンドデータ ) - { - FDK.Memory.Free( (void*) this.サウンドデータ ); - this.サウンドデータ = null; - } - }; - - if( Managedも解放する ) - { - // C#オブジェクトの解放があればここで。 - // this.排他利用を使った Unmanaged の解放。 - lock( this.排他利用 ) - { - サウンドデータを解放する(); - } - } - else - { - // (使える保証がないので)this.排他利用 を使わないUnmanaged の解放。 - サウンドデータを解放する(); - } + public void Play() + { + Mixer mixer; + if( this._MixerRef.TryGetTarget( out mixer ) ) + mixer.AddSound( this ); } - //---------------- - #endregion - private bool 作成済み = false; - private byte* サウンドデータ = null; - private int サウンドデータサイズbyte = 0; - private int サウンドデータサイズsample = 0; - private int 再生位置sample = 0; - private readonly SharpDX.Multimedia.WaveFormat WAVEフォーマット = new SharpDX.Multimedia.WaveFormat( 44100, 16, 2 ); // 固定 - private readonly object 排他利用 = new object(); - - #region " バックストア " - //---------------- - private float bs_音量 = 1.0f; - //---------------- - #endregion - - #region " Win32 API " - //----------------- - [System.Runtime.InteropServices.DllImport( "kernel32.dll", SetLastError = true )] - private static extern unsafe void CopyMemory( void* dst, void* src, int size ); + public void Stop() + { + Mixer mixer; + if( this._MixerRef.TryGetTarget( out mixer ) ) + mixer.RemoveSound( this ); + } - [System.Runtime.InteropServices.DllImport( "kernel32.dll", SetLastError = true )] - private static extern unsafe void ZeroMemory( void* dst, int length ); - //----------------- - #endregion + private CSCore.IWaveSource _WaveSource = null; + private CSCore.ISampleSource _SampleSource = null; + private System.WeakReference _MixerRef = null; + private float _Volume = 1.0f; } } diff --git a/FDK24/メディア/サウンド/WASAPI/SoundTimer.cs b/FDK24/メディア/サウンド/WASAPI/SoundTimer.cs index bc62ad4..24c2230 100644 --- a/FDK24/メディア/サウンド/WASAPI/SoundTimer.cs +++ b/FDK24/メディア/サウンド/WASAPI/SoundTimer.cs @@ -1,25 +1,30 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using CSCore; namespace FDK.メディア.サウンド.WASAPI { /// - /// WASAPIデバイス(のIAudioClock)をソースとするタイマー。 + /// WASAPIデバイス(のIAudioClock)をソースとするタイマー。 /// /// - /// WASAPIデバイスと厳密に同期をとる場合には、QPCTimer ではなく、このクラスを使用する。 + /// WASAPIデバイスと厳密に同期をとる場合には、QPCTimer ではなく、このクラスを使用する。 /// public class SoundTimer { - /// - /// エラー時は double.NaN を返す。 - /// + /// エラー時は double.NaN を返す。 public double 現在のデバイス位置secを取得する( CSCore.CoreAudioAPI.AudioClock audioClock ) { - lock( this.スレッド間同期 ) + lock( this._スレッド間同期 ) { int hr = 0; - long デバイス周波数 = 0;// audioClock.Pu64Frequency; + + long デバイス周波数 = 0; + //デバイス周波数 = audioClock.Pu64Frequency; --> たまにおかしくなるので、使わない。 audioClock.GetFrequencyNative( out デバイス周波数 ); + long QPC周波数 = FDK.カウンタ.QPCTimer.周波数; long デバイス位置 = 0; long デバイス位置取得時のパフォーマンスカウンタを100ns単位に変換した時間 = 0; @@ -57,12 +62,12 @@ namespace FDK.メディア.サウンド.WASAPI // そこで、この時点で最新のパフォーマンスカウンタを取得し、時間の増加分をデバイス位置に加えて精度を上げる。(MSDNより) double QPCから調べた差分sec = ( (double) FDK.カウンタ.QPCTimer.生カウント / QPC周波数 ) - - ( (double) デバイス位置取得時のパフォーマンスカウンタを100ns単位に変換した時間 ) / 10000000.0; + FDK.Utilities.変換_100ns単位からsec単位へ( デバイス位置取得時のパフォーマンスカウンタを100ns単位に変換した時間 ); - return ( (double) デバイス位置 ) / デバイス周波数 + QPCから調べた差分sec; + return ( ( (double) デバイス位置 ) / デバイス周波数 ) + QPCから調べた差分sec; } } - private readonly object スレッド間同期 = new object(); + private readonly object _スレッド間同期 = new object(); } } diff --git a/FDK24/メディア/サウンド/WASAPIold/Device.cs b/FDK24/メディア/サウンド/WASAPIold/Device.cs new file mode 100644 index 0000000..88f8823 --- /dev/null +++ b/FDK24/メディア/サウンド/WASAPIold/Device.cs @@ -0,0 +1,360 @@ +using System; +using System.Diagnostics; + +namespace FDK.メディア.サウンド.WASAPIold +{ + public unsafe class Device : IDisposable + { + public double 遅延sec + { + get { return ( this.更新間隔sec * 1000.0 ); } + } + public CSCore.CoreAudioAPI.AudioClock AudioClock + { + get { return this.bs_AudioClock; } + } + public bool Dispose済み + { + get; + protected set; + } = true; + + public void 初期化する( CSCore.CoreAudioAPI.AudioClientShareMode 共有モード, double 希望更新間隔sec = 0.015 ) + { + FDK.Log.BeginInfo( $"{FDK.Utilities.現在のメソッド名}" ); + + int hr = 0; + + lock( this.スレッド間同期 ) + { + Trace.Assert( this.Dispose済み ); + this.Dispose済み = false; + + this.共有モード = 共有モード; + + #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 共有モードでの間隔100ns = 0; + long 排他モードでの最小間隔100ns = 0; + + // デバイスから間隔値を取得する。 + hr = this.AudioClient.GetDevicePeriodNative( out 共有モードでの間隔100ns, out 排他モードでの最小間隔100ns ); + if( 0 > hr ) + System.Runtime.InteropServices.Marshal.ThrowExceptionForHR( hr ); + + if( 共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) + { + this.更新間隔100ns = 共有モードでの間隔100ns; + } + else + { + this.更新間隔100ns = Math.Max( FDK.Utilities.変換_sec単位から100ns単位へ( 希望更新間隔sec ), 排他モードでの最小間隔100ns ); + } + //----------------- + #endregion + #region " デバイスフォーマットを決定する。" + //---------------- + if( this.共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) + { + this.WaveFormat = this.AudioClient.GetMixFormat(); + } + else + { + this.WaveFormat = new CSCore.WaveFormat( 44100, 16, 2, CSCore.AudioEncoding.Pcm ); + } + //---------------- + #endregion + #region " AudioClient を初期化する。" + //----------------- + try + { + this.AudioClient.Initialize( + this.共有モード, + CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, + // | CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsNoPersist, // 音量とミュートを記憶しない → 無効。してください + this.更新間隔100ns, + this.更新間隔100ns, + this.WaveFormat, + Guid.Empty ); // この AudioClient (= AudioStrem) が所属する AudioSession。null ならデフォルトのAudioSessionに登録される。 + } + catch( CSCore.CoreAudioAPI.CoreAudioAPIException e ) + { + if( AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED == e.ErrorCode ) + { + #region " 排他&イベント駆動モードの場合、バッファサイズアライメントエラーが返される場合がある。この場合、サイズを調整してオーディオストリームを作成し直す。" + //---------------- + this.更新間隔100ns = FDK.Utilities.変換_sec単位から100ns単位へ( + (double) this.AudioClient.GetBufferSize() / (double) this.WaveFormat.SampleRate ); // GetBufferSize は、更新間隔に一番近い、アライメントされたバッファサイズ(sample単位)を返す。 + + // 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( + this.共有モード, + CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsEventCallback, + // | CSCore.CoreAudioAPI.AudioClientStreamFlags.StreamFlagsNoPersist, // 音量とミュートを記憶しない → 無効。してください + this.更新間隔100ns, + this.更新間隔100ns, + this.WaveFormat, + Guid.Empty ); + //---------------- + #endregion + } + } + + this.更新間隔sample = this.AudioClient.GetBufferSize(); + //----------------- + #endregion + #region " AudioRenderClient を取得する。" + //----------------- + this.AudioRenderClient = CSCore.CoreAudioAPI.AudioRenderClient.FromAudioClient( this.AudioClient ); + //----------------- + #endregion + #region " AudioClock を取得する。" + //----------------- + this.bs_AudioClock = CSCore.CoreAudioAPI.AudioClock.FromAudioClient( this.AudioClient ); + //----------------- + #endregion + #region " エンドポイントバッファを無音で埋めておく。" + //----------------- + this.AudioRenderClient.GetBuffer( this.更新間隔sample ); + + // 無音を書き込んだことにして、バッファをコミット。(GetBuffer の戻り値は使わない。) + this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent ); + //----------------- + #endregion + + #region " ミキサーを生成し初期化する。" + //----------------- + this.Mixer = new Mixer( this.更新間隔sample ); + //----------------- + #endregion + + #region " 情報表示。" + //----------------- + FDK.Log.Info( $"WASAPIクライアントを初期化しました。" ); + FDK.Log.Info( ( 共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared ) ? + " モード: 共有 & イベント駆動" : + " モード: 排他 & イベント駆動" ); + FDK.Log.Info( $" フォーマット: {this.WaveFormat.BitsPerSample} bits, {this.WaveFormat.SampleRate} Hz" ); + FDK.Log.Info( $" エンドポイントバッファ: {( (float) this.更新間隔sample / (double) this.WaveFormat.SampleRate ) * 1000.0f} ミリ秒 ({this.更新間隔sample} samples) × 2枚" ); + FDK.Log.Info( $" 希望更新間隔: {希望更新間隔sec * 1000.0} ミリ秒" ); + FDK.Log.Info( $" 更新間隔: {this.更新間隔sec * 1000.0} ミリ秒 ({this.更新間隔sample} samples)" ); + if( 共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive ) + FDK.Log.Info( $" 最小間隔: {FDK.Utilities.変換_100ns単位からsec単位へ( 排他モードでの最小間隔100ns )} ミリ秒" ); + //----------------- + #endregion + + #region " ワークキューとイベントを作成し、作業項目を登録する。" + //----------------- + // MediaFoundation が管理する、プロセス&MMCSSタスクごとに1つずつ作ることができる特別な共有ワークキューを取得(または生成して取得)する。 + int dwTaskId = 0; + SharpDX.MediaFoundation.MediaFactory.LockSharedWorkQueue( ( 0.011 > this.更新間隔sec ) ? "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, this.出力要請イベントへ対応する ); + this.作業項目をキューに格納する(); + //----------------- + #endregion + #region " WASAPI レンダリングを開始。" + //----------------- + this.AudioClient.Start(); + //----------------- + #endregion + } + + FDK.Log.EndInfo( $"{FDK.Utilities.現在のメソッド名}" ); + } + public void Dispose() + { + FDK.Log.BeginInfo( $"{FDK.Utilities.現在のメソッド名}" ); + + Trace.Assert( false == this.Dispose済み ); + + #region " WASAPI作業項目を終了させる。オーディオのレンダリングを止める前に行うこと。" + //----------------- + { + //SharpDX.MediaFoundation.MediaFactory.CancelWorkItem( this.出力要請イベントキャンセル用キー ); --> コールバックの実行中にキャンセルしてしまうと NullReference例外 + this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.ON; + this.出力終了通知.OFFになるまでブロックする(); + FDK.Log.Info( "WASAPI出力処理を終了しました。" ); + } + //----------------- + #endregion + + 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 ) + { + SharpDX.MediaFoundation.MediaFactory.UnlockWorkQueue( this.QueueID ); + this.QueueID = int.MaxValue; + } + //----------------- + #endregion + #region " WASAPIイベント駆動用のコールバックとイベントを解放する。" + //----------------- + FDK.Utilities.解放する( ref this.出力要請イベントのコールバック ); + + if( IntPtr.Zero != this.出力要請イベント ) + Device.CloseHandle( this.出力要請イベント ); + //----------------- + #endregion + + this.Dispose済み = true; + } + + FDK.Log.EndInfo( $"{FDK.Utilities.現在のメソッド名}" ); + } + public void サウンドをミキサーに追加する( Sound sound ) + { + this.Mixer.サウンドを追加する( sound ); + } + public void サウンドをミキサーから削除する( Sound sound ) + { + this.Mixer.サウンドを削除する( sound ); + } + + protected CSCore.CoreAudioAPI.AudioClientShareMode 共有モード; + protected CSCore.CoreAudioAPI.AudioClient AudioClient = null; + protected CSCore.CoreAudioAPI.AudioRenderClient AudioRenderClient = null; + protected CSCore.WaveFormat WaveFormat = null; + protected long 更新間隔100ns = 0; + protected int 更新間隔sample = 0; + protected int 更新間隔byte => ( this.更新間隔sample * this.WaveFormat.Channels * this.WaveFormat.BytesPerSample ); + protected double 更新間隔sec => ( FDK.Utilities.変換_100ns単位からsec単位へ( this.更新間隔100ns ) ); + + // ミキサー。サウンドリストもここ。 + protected Mixer Mixer = null; + + // WASAPIバッファ出力用 + private int QueueID = int.MaxValue; + private IntPtr 出力要請イベント = IntPtr.Zero; + private MFAsyncCallback 出力要請イベントのコールバック = null; + private long 出力要請イベントキャンセル用キー = 0; + private FDK.同期.TriStateEvent 出力終了通知 = new 同期.TriStateEvent(); + + private readonly object スレッド間同期 = new object(); + + private void 作業項目をキューに格納する() + { + var asyncResult = (SharpDX.MediaFoundation.AsyncResult) null; + try + { + // IAsyncCallback を内包した AsyncResult を作成する。 + SharpDX.MediaFoundation.MediaFactory.CreateAsyncResult( + null, + SharpDX.ComObject.ToCallbackPtr( this.出力要請イベントのコールバック ), + null, + out asyncResult ); + + // 作成した AsyncResult を、ワークキュー投入イベントの待機状態にする。 + SharpDX.MediaFoundation.MediaFactory.PutWaitingWorkItem( + hEvent: this.出力要請イベント, + priority: 0, + resultRef: asyncResult, + keyRef: out this.出力要請イベントキャンセル用キー ); + } + finally + { + // out 引数に使う変数は using 変数にはできないので、代わりに try-finally を使う。 + asyncResult?.Dispose(); + } + } + + /// + /// このメソッドは、WASAPIイベント発生時にワークキューに投入され作業項目から呼び出される。 + /// + private void 出力要請イベントへ対応する( SharpDX.MediaFoundation.AsyncResult asyncResult ) + { + try + { + // 出力終了通知が来ていれば、応答してすぐに終了する。 + if( this.出力終了通知.状態 == 同期.TriStateEvent.状態種別.ON ) + { + this.出力終了通知.状態 = 同期.TriStateEvent.状態種別.無効; + return; + } + + lock( this.スレッド間同期 ) + { + // エンドポインタの空きバッファへのポインタを取得する。 + // このポインタが差すのはネイティブで確保されたメモリなので、GCの対象外である。はず。 + var bufferPtr = this.AudioRenderClient.GetBuffer( this.更新間隔sample ); // イベント駆動なのでサイズ固定。 + + // ミキサーを使って、エンドポインタへサウンドデータを出力する。 + var flags = this.Mixer.エンドポイントへ出力する( (void*) bufferPtr, this.更新間隔sample ); + + // エンドポインタのバッファを解放する。 + this.AudioRenderClient.ReleaseBuffer( this.更新間隔sample, flags ); + + // 後続のイベント待ち作業項目をキューに格納する。 + this.作業項目をキューに格納する(); + + // 以降、WASAPIからイベントが発火されるたび、作業項目を通じて本メソッドが呼び出される。 + } + } + catch + { + // 例外は無視。 + } + } + + #region " バックストア。" + //---------------- + private CSCore.CoreAudioAPI.AudioClock bs_AudioClock = null; + //---------------- + #endregion + + #region " Win32 API " + //----------------- + private static int AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED = unchecked((int) 0x88890019); + + [System.Runtime.InteropServices.DllImport( "kernel32.dll" )] + private static extern IntPtr CreateEvent( IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName ); + + [System.Runtime.InteropServices.DllImport( "kernel32.dll" )] + private static extern bool CloseHandle( IntPtr hObject ); + //----------------- + #endregion + } +} diff --git a/FDK24/メディア/サウンド/WASAPI/MFAsyncCallback.cs b/FDK24/メディア/サウンド/WASAPIold/MFAsyncCallback.cs similarity index 95% rename from FDK24/メディア/サウンド/WASAPI/MFAsyncCallback.cs rename to FDK24/メディア/サウンド/WASAPIold/MFAsyncCallback.cs index 4f37fbf..dd76175 100644 --- a/FDK24/メディア/サウンド/WASAPI/MFAsyncCallback.cs +++ b/FDK24/メディア/サウンド/WASAPIold/MFAsyncCallback.cs @@ -1,6 +1,6 @@ using System; -namespace FDK.メディア.サウンド.WASAPI +namespace FDK.メディア.サウンド.WASAPIold { /// /// IMFAsyncCallback の汎用的な実装。 diff --git a/FDK24/メディア/サウンド/WASAPIold/Mixer.cs b/FDK24/メディア/サウンド/WASAPIold/Mixer.cs new file mode 100644 index 0000000..4e2dba7 --- /dev/null +++ b/FDK24/メディア/サウンド/WASAPIold/Mixer.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; + +namespace FDK.メディア.サウンド.WASAPIold +{ + /// + /// Sound のリストを持ち、そのサウンドデータを合成してWASAPIデバイスへ出力するミキサー。 + /// + public unsafe class Mixer : IDisposable + { + public Mixer() + { + } + public Mixer( int エンドポイントバッファサイズsample ) : this() + { + this.初期化する( エンドポイントバッファサイズsample ); + } + public void 初期化する( int エンドポイントバッファサイズsample ) + { + lock( this.スレッド間同期 ) + { + if( エンドポイントバッファサイズsample == this.エンドポイントバッファサイズsample ) + return; // サイズに変更があったときのみ初期化する。 + + this.エンドポイントバッファサイズsample = エンドポイントバッファサイズsample; + this.サウンドリスト.Clear(); + + if( null != this.合成用バッファ ) + FDK.Memory.Free( this.合成用バッファ ); + this.合成用バッファ = FDK.Memory.Alloc( this.エンドポイントバッファサイズsample * ( 4 * 2 ) ); // 1sample = 32bit×2ch + } + } + public void Dispose() + { + lock( this.スレッド間同期 ) + { + this.サウンドリスト.Clear(); + if( null != this.合成用バッファ ) + { + FDK.Memory.Free( this.合成用バッファ ); + this.合成用バッファ = null; + } + this.エンドポイントバッファサイズsample = -1; + } + } + public void サウンドリストをクリアする() + { + lock( this.スレッド間同期 ) + { + this.サウンドリスト.Clear(); + } + } + public void サウンドを追加する( Sound sound ) + { + lock( this.スレッド間同期 ) + { + this.サウンドリスト.Add( sound ); + } + } + public void サウンドを削除する( Sound sound ) + { + lock( this.スレッド間同期 ) + { + this.サウンドリスト.Remove( sound ); + } + } + public CSCore.CoreAudioAPI.AudioClientBufferFlags エンドポイントへ出力する( void* エンドポイントの出力先, int 出力数sample ) + { + lock( this.スレッド間同期 ) + { + if( null == this.合成用バッファ ) + return CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent; + + #region " すべてのサウンドについて、合成バッファへ出力する。" + //----------------- + bool 最初の出力である = true; + + foreach( var sound in this.サウンドリスト ) + { + var flag = sound.次のサウンドデータを出力する( this.合成用バッファ, 出力数sample, 最初の出力である ); + + if( false == flag.HasFlag( CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent ) ) + 最初の出力である = false; // sound が何らかのデータを出力した(戻り値がSILENTじゃなかった) + } + + // 全サウンドが SILENT だったなら、エンドポイントには何も書き込まずに SILENT フラグを返す。 + if( 最初の出力である ) + return CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent; + //----------------- + #endregion + #region " 合成バッファのデータ値(32bit;オーバーサンプル)を16bitに丸めてエンドポイントに出力する。" + //----------------- + Int32* 出力元 = (Int32*) ( this.合成用バッファ ); + Int16* 出力先 = (Int16*) エンドポイントの出力先; + for( int i = 0; i < 出力数sample; i++ ) + { + Int32 src; + + // 音量やミュートの処理は不要。(WASAPI が自動でマスタ音量・ミュート状態に合わせてくれる) + + // 左ch + src = *出力元++; + if( -32768 > src ) + src = -32768; + else if( 32767 < src ) + src = 32767; + *出力先++ = (Int16) src; + + // 右ch + src = *出力元++; + if( -32768 > src ) + src = -32768; + else if( 32767 < src ) + src = 32767; + *出力先++ = (Int16) src; + } + //----------------- + #endregion + } + return CSCore.CoreAudioAPI.AudioClientBufferFlags.None; + } + + private int エンドポイントバッファサイズsample = -1; + private readonly List サウンドリスト = new List(); + private void* 合成用バッファ = null; + private readonly object スレッド間同期 = new object(); + } +} diff --git a/FDK24/メディア/サウンド/WASAPIold/Sound.cs b/FDK24/メディア/サウンド/WASAPIold/Sound.cs new file mode 100644 index 0000000..d7afcbe --- /dev/null +++ b/FDK24/メディア/サウンド/WASAPIold/Sound.cs @@ -0,0 +1,376 @@ +using System; + +namespace FDK.メディア.サウンド.WASAPIold +{ + public unsafe class Sound : IDisposable + { + public enum E再生状態 + { + 停止中, // 初期状態 + 再生中, + 一時停止中, + 再生終了, + }; + public E再生状態 再生状態 = E再生状態.停止中; + + /// + /// 0.0(最小)~1.0(原音) の範囲で指定する。再生中でも反映される。 + /// + public float 音量 + { + set + { + float 設定値 = Math.Min( Math.Max( value, 0.0f ), 1.0f ); // 0.0未満は0.0へ、1.0超は1.0へ。 + lock( this.排他利用 ) + { + this.bs_音量 = 設定値; + } + } + get + { + lock( this.排他利用 ) + { + return this.bs_音量; + } + } + } + public double 長さsec + { + get + { + lock( this.排他利用 ) + { + return ( this.サウンドデータサイズsample / this.WAVEフォーマット.SampleRate ); + } + } + } + + public Sound() + { + } + public Sound( string サウンドファイルuri ) : this() + { + this.ファイルから作成する( サウンドファイルuri ); + } + public void ファイルから作成する( string サウンドファイルuri ) + { + lock( this.排他利用 ) + { + #region " 作成済みなら先にDisposeする。" + //----------------- + if( this.作成済み ) + this.Dispose(); + + this.作成済み = false; + //----------------- + #endregion + + byte[] encodedPcm = null; + + using( var sourceReader = new SharpDX.MediaFoundation.SourceReader( サウンドファイルuri ) ) + using( var pcmStream = new System.IO.MemoryStream() ) + { + #region " サウンドファイル名から SourceReader を作成する。" + //----------------- + + // 先述の using で作成済み。 + + // 最初のオーディオストリームを選択し、その他のすべてのストリームを非選択にする。 + sourceReader.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.AllStreams, false ); + sourceReader.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, true ); + + // メディアタイプを作成し、オーディオフォーマットを設定する。(固定フォーマットとする。) + using( var mediaType = new SharpDX.MediaFoundation.MediaType() ) + { + mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.MajorType, SharpDX.MediaFoundation.MediaTypeGuids.Audio ); + mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.Subtype, SharpDX.MediaFoundation.AudioFormatGuids.Pcm ); + mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioNumChannels, this.WAVEフォーマット.Channels ); + mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioSamplesPerSecond, this.WAVEフォーマット.SampleRate ); + mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBlockAlignment, this.WAVEフォーマット.BlockAlign ); + mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioAvgBytesPerSecond, this.WAVEフォーマット.AverageBytesPerSecond ); + mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBitsPerSample, this.WAVEフォーマット.BitsPerSample ); + mediaType.Set( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AllSamplesIndependent, 1 ); // TRUE + + // 作成したメディアタイプを sourceReader にセットする。sourceReader は、必要なデコーダをロードするだろう。 + sourceReader.SetCurrentMediaType( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, mediaType ); + } + + // 最初のオーディオストリームが選択されていることを保証する。 + sourceReader.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, true ); + //----------------- + #endregion + #region " sourceReader からサンプルを取得してデコードし、メモリストリーム pcmStream へ書き込んだのち、encodedPcm へ変換する。" + //----------------- + using( var pcmWriter = new System.IO.BinaryWriter( pcmStream ) ) + { + while( true ) + { + // 次のサンプルを読み込む。 + int dwActualStreamIndexRef = 0; + var dwStreamFlagsRef = SharpDX.MediaFoundation.SourceReaderFlags.None; + Int64 llTimestampRef = 0; + + using( var sample = sourceReader.ReadSample( + SharpDX.MediaFoundation.SourceReaderIndex.FirstAudioStream, + SharpDX.MediaFoundation.SourceReaderControlFlags.None, + out dwActualStreamIndexRef, + out dwStreamFlagsRef, + out llTimestampRef ) ) + { + if( null == sample ) + break; // EndOfStream やエラーも含まれる。 + + // サンプルをロックし、オーディオデータへのポインタを取得する。 + int cbMaxLengthRef = 0; + int cbCurrentLengthRef = 0; + using( var mediaBuffer = sample.ConvertToContiguousBuffer() ) + { + // オーディオデータをメモリストリームに書き込む。 + var audioData = mediaBuffer.Lock( out cbMaxLengthRef, out cbCurrentLengthRef ); + + byte[] dstData = new byte[ cbCurrentLengthRef ]; + byte* psrcData = (byte*) audioData.ToPointer(); // fixed + fixed ( byte* pdstData = dstData ) + { + CopyMemory( pdstData, psrcData, cbCurrentLengthRef ); + } + pcmWriter.Write( dstData, 0, cbCurrentLengthRef ); + + // サンプルのロックを解除する。 + mediaBuffer.Unlock(); + } + } + } + + // ストリームの内容を byte 配列に出力。(Position に関係なく全部出力される。) + encodedPcm = pcmStream.ToArray(); + } + //----------------- + #endregion + } + #region " オーバーサンプリングサウンドデータバッファを確保し、encodedPcm からサンプルを転送する。" + //----------------- + using( var pcmReader = new System.IO.BinaryReader( new System.IO.MemoryStream( encodedPcm ) ) ) + { + // PCMサイズを計算する。(16bit → 32bit でオーバーサンプリングする。) + this.サウンドデータサイズbyte = encodedPcm.Length * 2; // 32bit は 16bit の2倍。 + this.サウンドデータサイズsample = this.サウンドデータサイズbyte / 8; // 1sample = 32bit×2h = 64bit = 8bytes + + // オーバーサンプリングサウンドデータ用メモリを確保する。 + this.サウンドデータ = (byte*) FDK.Memory.Alloc( this.サウンドデータサイズbyte ); + + // ストリームからオーバーサンプリングサウンドデータへ転送する。 + var p = (Int32*) this.サウンドデータ; + for( int i = 0; i < this.サウンドデータサイズsample; i++ ) + { + // 1サンプル = 2ch×INT16 を 2ch×INT32 に変換しながら格納。 + *p++ = (Int32) pcmReader.ReadInt16(); // 左ch + *p++ = (Int32) pcmReader.ReadInt16(); // 右ch + } + } + //----------------- + #endregion + + this.再生位置sample = 0; + this.作成済み = true; + } + } + public void 再生を開始する( double 再生開始位置sec = 0.0 ) + { + lock( this.排他利用 ) + { + if( false == this.作成済み ) + return; // エラーにはしない。サウンド作成失敗時には、何も再生しないようにするだけ。 + + int 開始位置sample = (int) ( 再生開始位置sec * this.WAVEフォーマット.SampleRate ); + if( 開始位置sample < this.サウンドデータサイズsample ) + { + this.再生状態 = E再生状態.再生中; + this.再生位置sample = 開始位置sample; + } + } + } + public void 再生を一時停止する() + { + lock( this.排他利用 ) + { + if( false == this.作成済み ) + return; // エラーにはしない。サウンド作成失敗時には、何も再生しないようにするだけ。 + + this.再生状態 = E再生状態.一時停止中; + } + } + public void 再生を再開する() + { + lock( this.排他利用 ) + { + if( false == this.作成済み ) + return; // エラーにはしない。サウンド作成失敗時には、何も再生しないようにするだけ。 + + if( E再生状態.一時停止中 != this.再生状態 ) + this.再生位置sample = 0; + + this.再生状態 = E再生状態.再生中; + } + } + public void 再生を停止する() + { + lock( this.排他利用 ) + { + if( false == this.作成済み ) + return; // エラーにはしない。サウンド作成失敗時には、何も再生しないようにするだけ。 + + this.再生状態 = E再生状態.停止中; + this.再生位置sample = 0; + } + } + public CSCore.CoreAudioAPI.AudioClientBufferFlags 次のサウンドデータを出力する( void* 出力先, int 出力サンプル数, bool 最初の出力である ) + { + lock( this.排他利用 ) + { + #region " 未作成、または再生中でないなら無音フラグをもって帰還。" + //----------------- + if( ( false == this.作成済み ) || ( E再生状態.再生中 != this.再生状態 ) ) + return CSCore.CoreAudioAPI.AudioClientBufferFlags.Silent; + //----------------- + #endregion + + int オーバーサンプルサイズbyte = 4 * 2; // 32bit×2ch + Int32* 出力元 = (Int32*) ( this.サウンドデータ + ( this.再生位置sample * オーバーサンプルサイズbyte ) ); + Int32* _出力先 = (Int32*) 出力先; // この実装ではサンプルは Int32 単位 + int 出力できるサンプル数 = System.Math.Min( 出力サンプル数, ( this.サウンドデータサイズsample - this.再生位置sample ) ); + int 出力できないサンプル数 = 出力サンプル数 - 出力できるサンプル数; + + if( 出力できるサンプル数 <= 0 ) + this.再生状態 = E再生状態.再生終了; // 念のため + + if( 最初の出力である ) + { + #region " (A) 上書き。余った部分にもデータ(無音またはループ)を出力する。" + //----------------- + if( 1.0f == this.bs_音量 ) + { + // 原音(最大音量)。 + CopyMemory( _出力先, 出力元, ( 出力できるサンプル数 * オーバーサンプルサイズbyte ) ); + } + else + { + // 音量を反映。 + for( int i = 0; i < 出力できるサンプル数; i++ ) + { + // 1サンプル = 2ch×INT32 + *_出力先++ = (Int32) ( ( *出力元++ ) * this.bs_音量 ); + *_出力先++ = (Int32) ( ( *出力元++ ) * this.bs_音量 ); + } + } + + if( 0 < 出力できないサンプル数 ) // サウンドデータの末尾に達した + { + // 残りの部分は、とりあえず今は無音。(ループ再生未対応) + ZeroMemory( + (void*) ( ( (byte*) _出力先 ) + ( 出力できるサンプル数 * オーバーサンプルサイズbyte ) ), + 出力できないサンプル数 * オーバーサンプルサイズbyte ); + } + //----------------- + #endregion + } + else + { + #region " (B) 加算合成。余った部分は放置してもいいし、ループしてデータ加算を続けてもいい。" + //----------------- + for( int i = 0; i < 出力できるサンプル数; i++ ) + { + // 1サンプル = 2ch×INT32 + *_出力先++ += (Int32) ( ( *出力元++ ) * this.bs_音量 ); + *_出力先++ += (Int32) ( ( *出力元++ ) * this.bs_音量 ); + } + + if( 0 < 出力できないサンプル数 ) + { + // 残りの部分は、今回の実装では無視。(ループ再生未対応。) + } + //----------------- + #endregion + } + + #region " 再生位置を移動。" + //--------------------------------------------------- + this.再生位置sample += 出力できるサンプル数; + + if( this.サウンドデータサイズsample <= this.再生位置sample ) // サウンドデータの末尾に達した + { + this.再生位置sample = this.サウンドデータサイズsample; + this.再生状態 = E再生状態.再生終了; // 再生終了に伴う自動終了なので、"停止中" ではない。 + } + //--------------------------------------------------- + #endregion + } + + return CSCore.CoreAudioAPI.AudioClientBufferFlags.None; + } + + #region " Dispose-Finalizeパターン " + //---------------- + ~Sound() + { + this.Dispose( false ); + } + public void Dispose() + { + this.Dispose( true ); + GC.SuppressFinalize( this ); + } + protected void Dispose( bool Managedも解放する ) + { + Action サウンドデータを解放する = () => { + if( null != this.サウンドデータ ) + { + FDK.Memory.Free( (void*) this.サウンドデータ ); + this.サウンドデータ = null; + } + }; + + if( Managedも解放する ) + { + // C#オブジェクトの解放があればここで。 + + // this.排他利用を使った Unmanaged の解放。 + lock( this.排他利用 ) + { + サウンドデータを解放する(); + } + } + else + { + // (使える保証がないので)this.排他利用 を使わないUnmanaged の解放。 + サウンドデータを解放する(); + } + } + //---------------- + #endregion + + private bool 作成済み = false; + private byte* サウンドデータ = null; + private int サウンドデータサイズbyte = 0; + private int サウンドデータサイズsample = 0; + private int 再生位置sample = 0; + private readonly SharpDX.Multimedia.WaveFormat WAVEフォーマット = new SharpDX.Multimedia.WaveFormat( 44100, 16, 2 ); // 固定 + private readonly object 排他利用 = new object(); + + #region " バックストア " + //---------------- + private float bs_音量 = 1.0f; + //---------------- + #endregion + + #region " Win32 API " + //----------------- + [System.Runtime.InteropServices.DllImport( "kernel32.dll", SetLastError = true )] + private static extern unsafe void CopyMemory( void* dst, void* src, int size ); + + [System.Runtime.InteropServices.DllImport( "kernel32.dll", SetLastError = true )] + private static extern unsafe void ZeroMemory( void* dst, int length ); + //----------------- + #endregion + } +} diff --git a/FDK24/メディア/サウンド/WASAPIold/SoundTimer.cs b/FDK24/メディア/サウンド/WASAPIold/SoundTimer.cs new file mode 100644 index 0000000..309407b --- /dev/null +++ b/FDK24/メディア/サウンド/WASAPIold/SoundTimer.cs @@ -0,0 +1,68 @@ +using System; + +namespace FDK.メディア.サウンド.WASAPIold +{ + /// + /// WASAPIデバイス(のIAudioClock)をソースとするタイマー。 + /// + /// + /// WASAPIデバイスと厳密に同期をとる場合には、QPCTimer ではなく、このクラスを使用する。 + /// + public class SoundTimer + { + /// + /// エラー時は double.NaN を返す。 + /// + public double 現在のデバイス位置secを取得する( CSCore.CoreAudioAPI.AudioClock audioClock ) + { + lock( this.スレッド間同期 ) + { + int hr = 0; + long デバイス周波数 = 0;// audioClock.Pu64Frequency; + audioClock.GetFrequencyNative( out デバイス周波数 ); + long QPC周波数 = FDK.カウンタ.QPCTimer.周波数; + long デバイス位置 = 0; + long デバイス位置取得時のパフォーマンスカウンタを100ns単位に変換した時間 = 0; + + if( 0.0 >= デバイス周波数 ) + return double.NaN; + + // IAudioClock::GetPosition() は、S_FALSE を返すことがある。 + // これは、WASAPI排他モードにおいて、GetPosition 時に優先度の高いイベントが発生しており + // 既定時間内にデバイス位置を取得できなかった場合に返される。(MSDNより) + for( int リトライ回数 = 0; リトライ回数 < 10; リトライ回数++ ) // 最大10回までリトライ。 + { + hr = audioClock.GetPositionNative( out デバイス位置, out デバイス位置取得時のパフォーマンスカウンタを100ns単位に変換した時間 ); + + if( ( (int) CSCore.Win32.HResult.S_OK ) == hr ) + { + break; // OK + } + else if( ( (int) CSCore.Win32.HResult.S_FALSE ) == hr ) + { + continue; // リトライ + } + else + { + throw new CSCore.Win32.Win32ComException( hr, "IAudioClock", "GetPosition" ); + } + } + if( ( (int) CSCore.Win32.HResult.S_FALSE ) == hr ) + { + // 全部リトライしてもまだダメならエラー。 + return double.NaN; + } + + // デバイス位置は、GetPosition() で取得した瞬間からここに至るまでに、既にいくらか進んでいる。 + // そこで、この時点で最新のパフォーマンスカウンタを取得し、時間の増加分をデバイス位置に加えて精度を上げる。(MSDNより) + double QPCから調べた差分sec = + ( (double) FDK.カウンタ.QPCTimer.生カウント / QPC周波数 ) - + ( (double) デバイス位置取得時のパフォーマンスカウンタを100ns単位に変換した時間 ) / 10000000.0; + + return ( (double) デバイス位置 ) / デバイス周波数 + QPCから調べた差分sec; + } + } + + private readonly object スレッド間同期 = new object(); + } +} diff --git a/StrokeStyleT/StrokeStyleT.cs b/StrokeStyleT/StrokeStyleT.cs index e047a70..bf463eb 100644 --- a/StrokeStyleT/StrokeStyleT.cs +++ b/StrokeStyleT/StrokeStyleT.cs @@ -18,7 +18,7 @@ namespace SST { get { return StrokeStyleT.bs_フォルダ; } } - public static FDK.メディア.サウンド.WASAPI.Device Wasapiデバイス + public static FDK.メディア.サウンド.WASAPIold.Device Wasapiデバイス { get { return StrokeStyleT.bs_Wasapiデバイス; } } @@ -308,7 +308,7 @@ namespace SST #endregion #region " WASAPI デバイスを初期化する。" //---------------- - StrokeStyleT.bs_Wasapiデバイス = new FDK.メディア.サウンド.WASAPI.Device(); + StrokeStyleT.bs_Wasapiデバイス = new FDK.メディア.サウンド.WASAPIold.Device(); StrokeStyleT.bs_Wasapiデバイス.初期化する( CSCore.CoreAudioAPI.AudioClientShareMode.Exclusive, 0.015 ); //---------------- #endregion @@ -845,7 +845,7 @@ namespace SST private static SST.フォルダ bs_フォルダ = null; private static FDK.入力.Keyboard bs_キーボード入力 = null; private static FDK.入力.MidiIn bs_MIDI入力 = null; - private static FDK.メディア.サウンド.WASAPI.Device bs_Wasapiデバイス = null; + private static FDK.メディア.サウンド.WASAPIold.Device bs_Wasapiデバイス = null; private static readonly System.Random bs_乱数 = new Random( DateTime.Now.Millisecond ); private static SST.ユーザ.ユーザ管理 bs_ユーザ管理 = null; private static SST.曲.曲ツリー管理 bs_曲ツリー管理 = null; diff --git a/StrokeStyleT/ステージ/演奏/ドラムサウンド.cs b/StrokeStyleT/ステージ/演奏/ドラムサウンド.cs index 636ecbc..18ed76a 100644 --- a/StrokeStyleT/ステージ/演奏/ドラムサウンド.cs +++ b/StrokeStyleT/ステージ/演奏/ドラムサウンド.cs @@ -62,7 +62,7 @@ namespace SST.ステージ.演奏 protected readonly string KitXmlファイルパス = @"$(Static)\sounds\Kit.xml"; protected class Cコンテキスト : IDisposable { - public FDK.メディア.サウンド.WASAPI.Sound[] Sounds = new FDK.メディア.サウンド.WASAPI.Sound[ ドラムサウンド.多重度 ]; + public FDK.メディア.サウンド.WASAPIold.Sound[] Sounds = new FDK.メディア.サウンド.WASAPIold.Sound[ ドラムサウンド.多重度 ]; public int 次に再生するSound番号 = 0; public void Dispose() @@ -141,13 +141,13 @@ namespace SST.ステージ.演奏 // コンテキストを作成する。 var context = new Cコンテキスト() { - Sounds = new FDK.メディア.サウンド.WASAPI.Sound[ ドラムサウンド.多重度 ], + Sounds = new FDK.メディア.サウンド.WASAPIold.Sound[ ドラムサウンド.多重度 ], 次に再生するSound番号 = 0, }; for( int i = 0; i < context.Sounds.Length; i++ ) { // 多重度分のサウンドを生成しつつ、ミキサーにも登録。 - context.Sounds[ i ] = new FDK.メディア.サウンド.WASAPI.Sound( サウンドファイルパス ); + context.Sounds[ i ] = new FDK.メディア.サウンド.WASAPIold.Sound( サウンドファイルパス ); StrokeStyleT.Wasapiデバイス.サウンドをミキサーに追加する( context.Sounds[ i ] ); } diff --git a/StrokeStyleT/ステージ/演奏/演奏ステージ.cs b/StrokeStyleT/ステージ/演奏/演奏ステージ.cs index 15a6ff5..5563b26 100644 --- a/StrokeStyleT/ステージ/演奏/演奏ステージ.cs +++ b/StrokeStyleT/ステージ/演奏/演奏ステージ.cs @@ -103,7 +103,7 @@ namespace SST.ステージ.演奏 this.子リスト.Add( this.背景動画 = new 動画( StrokeStyleT.演奏スコア.背景動画ファイル名, StrokeStyleT.Config.動画デコーダのキューサイズ ) ); // 動画から BGM を作成してミキサーに追加。 - this.BGM = new FDK.メディア.サウンド.WASAPI.Sound(); + this.BGM = new FDK.メディア.サウンド.WASAPIold.Sound(); this.BGM.ファイルから作成する( StrokeStyleT.演奏スコア.背景動画ファイル名 ); StrokeStyleT.Wasapiデバイス.サウンドをミキサーに追加する( this.BGM ); // 作成に失敗した Sound を追加しても鳴らないだけなので、ノーチェックで大丈夫。 } @@ -270,7 +270,7 @@ namespace SST.ステージ.演奏 protected FDK.同期.RWLock 現在進行描画中の譜面スクロール速度の倍率 = new FDK.同期.RWLock( 0.0 ); protected double 演奏開始時刻sec = 0.0; protected bool Autoチップのドラム音を再生する = true; - protected readonly FDK.メディア.サウンド.WASAPI.SoundTimer サウンドタイマ = new FDK.メディア.サウンド.WASAPI.SoundTimer(); + protected readonly FDK.メディア.サウンド.WASAPIold.SoundTimer サウンドタイマ = new FDK.メディア.サウンド.WASAPIold.SoundTimer(); protected readonly SST.ステージ.演奏.コンボ コンボ; protected readonly SST.ステージ.演奏.レーンフレーム レーンフレーム; protected readonly SST.ステージ.演奏.スクロール譜面 スクロール譜面; @@ -285,7 +285,7 @@ namespace SST.ステージ.演奏 /// 解放は、演奏ステージクラスの非活性化後に、外部から行われる。 /// /// - protected FDK.メディア.サウンド.WASAPI.Sound BGM = null; + protected FDK.メディア.サウンド.WASAPIold.Sound BGM = null; protected FDK.カウンタ.FPS FPS = null; /// /// 動的子Activity。背景動画を再生しない場合は null のまま。 -- 2.11.0