--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace FDK
+{
+ public static class Extensions
+ {
+ /// <summary>
+ /// COM オブジェクトの参照カウントを取得して返す。
+ /// </summary>
+ /// <param name="unknownObject">COMオブジェクト。</param>
+ /// <returns>現在の参照カウントの値。</returns>
+ public static int GetRefferenceCount( this SharpDX.IUnknown unknownObject )
+ {
+ try
+ {
+ unknownObject.AddReference();
+ }
+ catch( InvalidOperationException )
+ {
+ // すでに Dispose されている。
+ return 0;
+ }
+
+ return unknownObject.Release();
+ }
+
+ /// <summary>
+ /// 文字列が Null でも空でもないなら true を返す。
+ /// </summary>
+ public static bool Nullでも空でもない( this string 検査対象 )
+ {
+ return !string.IsNullOrEmpty( 検査対象 );
+ }
+
+ /// <summary>
+ /// 文字列が Null または空なら true を返す。
+ /// </summary>
+ public static bool Nullまたは空である( this string 検査対象 )
+ {
+ return string.IsNullOrEmpty( 検査対象 );
+ }
+ }
+}
<Compile Include="カウンタ\単純増加後反復カウンタ.cs" />
<Compile Include="カウンタ\定間隔進行.cs" />
<Compile Include="フォルダ.cs" />
+ <Compile Include="メディア\サウンド\WASAPI\Decoder.cs" />
+ <Compile Include="メディア\サウンド\WASAPI\Device.cs" />
+ <Compile Include="メディア\サウンド\WASAPI\Mixer.cs" />
+ <Compile Include="メディア\サウンド\WASAPI\Sound.cs" />
+ <Compile Include="メディア\サウンド\WASAPI\SoundTimer.cs" />
<Compile Include="メディア\動画.cs" />
<Compile Include="メディア\XML.cs" />
<Compile Include="メディア\テクスチャフォント.cs" />
<Compile Include="メディア\画像.cs" />
<Compile Include="メディア\画像フォント.cs" />
<Compile Include="メディア\矩形リスト.cs" />
- <Compile Include="メディア\サウンド\WASAPI\Device.cs" />
- <Compile Include="メディア\サウンド\WASAPI\MFAsyncCallback.cs" />
- <Compile Include="メディア\サウンド\WASAPI\Mixer.cs" />
- <Compile Include="メディア\サウンド\WASAPI\Sound.cs" />
- <Compile Include="メディア\サウンド\WASAPI\SoundTimer.cs" />
+ <Compile Include="メディア\サウンド\WASAPIold\Device.cs" />
+ <Compile Include="メディア\サウンド\WASAPIold\MFAsyncCallback.cs" />
+ <Compile Include="メディア\サウンド\WASAPIold\Mixer.cs" />
+ <Compile Include="メディア\サウンド\WASAPIold\Sound.cs" />
+ <Compile Include="メディア\サウンド\WASAPIold\SoundTimer.cs" />
<Compile Include="メディア\テクスチャ.cs" />
<Compile Include="メディア\デバイスリソース.cs" />
<Compile Include="入力\IInputDevice.cs" />
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using CSCore;
+
+namespace FDK.メディア.サウンド.WASAPI
+{
+ /// <summary>
+ /// 指定されたメディアファイル(動画, 音楽)をデコードして、CSCore.IWaveStream オブジェクトを生成する。
+ /// </summary>
+ internal class Decoder : CSCore.IWaveSource
+ {
+ /// <summary>
+ /// シークは常にサポートする。
+ /// </summary>
+ public bool CanSeek => ( true );
+
+ /// <summary>
+ /// デコード後のオーディオデータの長さ[byte]。
+ /// </summary>
+ public long Length
+ {
+ get { return this._EncodedWaveData.Length; }
+ }
+
+ /// <summary>
+ /// 現在の位置。
+ /// 先頭からのオフセット[byte]で表す。
+ /// </summary>
+ public long Position
+ {
+ get { return this._Position; }
+ set
+ {
+ if( ( 0 > value ) || ( this.Length <= value ) )
+ throw new ArgumentOutOfRangeException();
+
+ this._Position = value;
+ }
+ }
+
+ /// <summary>
+ /// デコード後のオーディオデータのフォーマット。
+ /// </summary>
+ public CSCore.WaveFormat WaveFormat
+ {
+ get;
+ protected set;
+ }
+
+ /// <summary>
+ /// メディアファイル(動画、音楽)をデコードする。
+ /// </summary>
+ /// <param name="path">メディアファイル(MediaFoundation でデコードできるもの)</param>
+ /// <param name="waveFormat">デコード先のフォーマット。</param>
+ 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._解放する();
+ }
+
+ /// <summary>
+ /// 連続した要素を読み込み、this.Position を読み込んだ要素の数だけ進める。
+ /// </summary>
+ /// <param name="buffer">
+ /// 読み込んだ要素を格納するための配列。
+ /// このメソッドから戻ると、buffer には offset ~ (offset + count - 1) の数の要素が格納されている。
+ /// </param>
+ /// <param name="offset">
+ /// buffer に格納を始める位置。
+ /// </param>
+ /// <param name="count">
+ /// 読み込む最大の要素数。
+ /// </param>
+ /// <returns>
+ /// buffer に読み込んだ要素の総数。
+ /// </returns>
+ 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<Guid>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.MajorType, SharpDX.MediaFoundation.MediaTypeGuids.Audio );
+ partialMediaType.Set<Guid>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.Subtype, SharpDX.MediaFoundation.AudioFormatGuids.Float );
+ partialMediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioNumChannels, this.WaveFormat.Channels );
+ partialMediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioSamplesPerSecond, this.WaveFormat.SampleRate );
+ partialMediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBlockAlignment, this.WaveFormat.BlockAlign );
+ partialMediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioAvgBytesPerSecond, this.WaveFormat.BytesPerSecond );
+ partialMediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBitsPerSample, this.WaveFormat.BitsPerSample );
+ partialMediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AllSamplesIndependent, 1 ); // TRUE
+
+ if( this.WaveFormat.WaveFormatTag == AudioEncoding.Extensible )
+ {
+ var wfmEx = this.WaveFormat as CSCore.WaveFormatExtensible;
+ partialMediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioChannelMask, (int) wfmEx.ChannelMask );
+ partialMediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioSamplesPerBlock, wfmEx.SamplesPerBlock );
+ partialMediaType.Set<int>( 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
+ }
+}
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 )
+ /// <summary>
+ /// レンダリングボリューム。
+ /// 0.0 (0%) ~ 1.0 (100%) 。
+ /// </summary>
+ 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._初期化する( 希望フォーマット );
+ }
+
+ /// <summary>
+ /// メディアファイル(動画、音声)からサウンドインスタンスを生成して返す。
+ /// </summary>
+ 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 ) )
+ /// <summary>
+ /// ミキサーの出力を開始する。
+ /// 以降、ミキサーに Sound を追加すれば、自動的に再生される。
+ /// </summary>
+ 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();
+ }
+ }
+ }
+ }
+
+ /// <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
{
- this.更新間隔100ns = Math.Max( FDK.Utilities.変換_sec単位から100ns単位へ( 希望更新間隔sec ), 排他モードでの最小間隔100ns );
+ Debug.WriteLine( "WASAPIのレンダリングを停止しようとしましたが、すでに停止しています。" );
}
- //-----------------
- #endregion
- #region " デバイスフォーマットを決定する。"
- //----------------
- if( this.共有モード == CSCore.CoreAudioAPI.AudioClientShareMode.Shared )
+ }
+ }
+
+ /// <summary>
+ /// ミキサーの出力を一時停止する。
+ /// ミキサーに登録されているすべての Sound の再生が一時停止する。
+ /// ResumeRendering()で出力を再開できる。
+ /// </summary>
+ 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
+ }
+ }
+
+ /// <summary>
+ /// ミキサーの出力を再開する。
+ /// PauseRendering() で一時停止状態にあるときのみ有効。
+ /// </summary>
+ 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 作業項目をキューに格納する()
+ /// <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 );
+ // 以下、メインループ。
+
+ 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
}
}
using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using CSCore;
namespace FDK.メディア.サウンド.WASAPI
{
/// <summary>
- /// Sound のリストを持ち、そのサウンドデータを合成してWASAPIデバイスへ出力するミキサー。
+ /// オーディオミキサー。
+ /// 自身が ISampleSource であり、そのまま AudioClient のレンダリングターゲットに指定する。
/// </summary>
- public unsafe class Mixer : IDisposable
+ internal class Mixer : CSCore.ISampleSource
{
- public Mixer()
+ /// <summary>
+ /// true なら、サンプルの値を、合成したソースの数で割る。
+ /// (例えば、ソースを3つ合成した場合、合成した結果のサンプル値を3で割る。各サウンドはそれぞれ小さく聞こえる。)
+ /// </summary>
+ public bool DivideResult
{
+ get;
+ set;
+ } = false;
+
+ /// <summary>
+ /// 音量。0.0(無音)~1.0(原音)。
+ /// </summary>
+ 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()
+
+ /// <summary>
+ /// ミキサーのフォーマット。
+ /// </summary>
+ public CSCore.WaveFormat WaveFormat
{
- this.初期化する( エンドポイントバッファサイズsample );
+ get { return _WaveFormat; }
}
- public void 初期化する( int エンドポイントバッファサイズsample )
+
+ /// <summary>
+ /// ミキサーはループするので、Position には 非対応。
+ /// </summary>
+ public long Position
{
- lock( this.スレッド間同期 )
- {
- if( エンドポイントバッファサイズsample == this.エンドポイントバッファサイズsample )
- return; // サイズに変更があったときのみ初期化する。
+ get { return 0; }
+ set { throw new NotSupportedException(); }
+ }
- this.エンドポイントバッファサイズsample = エンドポイントバッファサイズsample;
- this.サウンドリスト.Clear();
+ /// <summary>
+ /// ミキサーはシークできない。
+ /// </summary>
+ public bool CanSeek => ( false );
- if( null != this.合成用バッファ )
- FDK.Memory.Free( this.合成用バッファ );
- this.合成用バッファ = FDK.Memory.Alloc( this.エンドポイントバッファサイズsample * ( 4 * 2 ) ); // 1sample = 32bit×2ch
- }
+ /// <summary>
+ /// ミキサーはループするので、長さの概念はない。
+ /// </summary>
+ public long Length => ( 0 );
+
+ /// <summary>
+ /// 指定したフォーマットを持つミキサーを生成する。
+ /// </summary>
+ 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 サウンドリストをクリアする()
+
+ /// <summary>
+ /// Sound をミキサーに追加する。
+ /// 追加されると同時に、Sound の再生が開始される。
+ /// </summary>
+ 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 )
+
+ /// <summary>
+ /// Sound をミキサーから除外する。
+ /// 除外されると同時に、Sound の再生は終了する。
+ /// </summary>
+ 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 )
+
+ /// <summary>
+ /// Sound がミキサーに登録されているかを調べる。
+ /// </summary>
+ /// <returns>Sound がミキサーに追加済みなら true 。</returns>
+ 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.サウンドリスト )
+ /// <summary>
+ /// バッファにサウンドデータを出力する。
+ /// </summary>
+ /// <returns>出力したサンプル数。</returns>
+ 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<int>( 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<Sound> サウンドリスト = new List<Sound>();
- private void* 合成用バッファ = null;
- private readonly object スレッド間同期 = new object();
+ private float _Volume = 1.0f;
+ private CSCore.WaveFormat _WaveFormat = null;
+ private readonly List<Sound> _Sounds = new List<Sound>();
+ private float[] _中間バッファ = null;
+ private readonly object _スレッド間同期 = new object();
}
}
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再生状態.停止中;
-
- /// <summary>
- /// 0.0(最小)~1.0(原音) の範囲で指定する。再生中でも反映される。
- /// </summary>
- 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<Guid>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.MajorType, SharpDX.MediaFoundation.MediaTypeGuids.Audio );
- mediaType.Set<Guid>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.Subtype, SharpDX.MediaFoundation.AudioFormatGuids.Pcm );
- mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioNumChannels, this.WAVEフォーマット.Channels );
- mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioSamplesPerSecond, this.WAVEフォーマット.SampleRate );
- mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBlockAlignment, this.WAVEフォーマット.BlockAlign );
- mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioAvgBytesPerSecond, this.WAVEフォーマット.AverageBytesPerSecond );
- mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBitsPerSample, this.WAVEフォーマット.BitsPerSample );
- mediaType.Set<int>( 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 最初の出力である )
+ /// <summary>
+ /// 音量。0.0(無音)~1.0(原音)。
+ /// </summary>
+ 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()
+ /// <summary>
+ /// Sound の生成は、コンストラクタではなく Device.CreateSound() で行うこと。
+ /// (Device 内部で持っている Mixer への参照が必要なため。)
+ /// </summary>
+ /// <param name="path">サウンドファイルパス</param>
+ /// <param name="mixer">使用する Mixer。</param>
+ internal Sound( string path, Mixer mixer )
{
- this.Dispose( false );
+ this._MixerRef = new WeakReference<Mixer>( 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<Mixer> _MixerRef = null;
+ private float _Volume = 1.0f;
}
}
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using CSCore;
namespace FDK.メディア.サウンド.WASAPI
{
/// <summary>
- /// WASAPIデバイス(のIAudioClock)をソースとするタイマー。
+ /// WASAPIデバイス(のIAudioClock)をソースとするタイマー。
/// </summary>
/// <remarks>
- /// WASAPIデバイスと厳密に同期をとる場合には、QPCTimer ではなく、このクラスを使用する。
+ /// WASAPIデバイスと厳密に同期をとる場合には、QPCTimer ではなく、このクラスを使用する。
/// </remarks>
public class SoundTimer
{
- /// <summary>
- /// エラー時は double.NaN を返す。
- /// </summary>
+ /// <returns>エラー時は double.NaN を返す。</returns>
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;
// そこで、この時点で最新のパフォーマンスカウンタを取得し、時間の増加分をデバイス位置に加えて精度を上げる。(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();
}
}
--- /dev/null
+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<SharpDX.MediaFoundation.IAsyncCallback>( 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();
+ }
+ }
+
+ /// <summary>
+ /// このメソッドは、WASAPIイベント発生時にワークキューに投入され作業項目から呼び出される。
+ /// </summary>
+ 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
+ }
+}
using System;
-namespace FDK.メディア.サウンド.WASAPI
+namespace FDK.メディア.サウンド.WASAPIold
{
/// <summary>
/// IMFAsyncCallback の汎用的な実装。
--- /dev/null
+using System;
+using System.Collections.Generic;
+
+namespace FDK.メディア.サウンド.WASAPIold
+{
+ /// <summary>
+ /// Sound のリストを持ち、そのサウンドデータを合成してWASAPIデバイスへ出力するミキサー。
+ /// </summary>
+ 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<Sound> サウンドリスト = new List<Sound>();
+ private void* 合成用バッファ = null;
+ private readonly object スレッド間同期 = new object();
+ }
+}
--- /dev/null
+using System;
+
+namespace FDK.メディア.サウンド.WASAPIold
+{
+ public unsafe class Sound : IDisposable
+ {
+ public enum E再生状態
+ {
+ 停止中, // 初期状態
+ 再生中,
+ 一時停止中,
+ 再生終了,
+ };
+ public E再生状態 再生状態 = E再生状態.停止中;
+
+ /// <summary>
+ /// 0.0(最小)~1.0(原音) の範囲で指定する。再生中でも反映される。
+ /// </summary>
+ 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<Guid>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.MajorType, SharpDX.MediaFoundation.MediaTypeGuids.Audio );
+ mediaType.Set<Guid>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.Subtype, SharpDX.MediaFoundation.AudioFormatGuids.Pcm );
+ mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioNumChannels, this.WAVEフォーマット.Channels );
+ mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioSamplesPerSecond, this.WAVEフォーマット.SampleRate );
+ mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBlockAlignment, this.WAVEフォーマット.BlockAlign );
+ mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioAvgBytesPerSecond, this.WAVEフォーマット.AverageBytesPerSecond );
+ mediaType.Set<int>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.AudioBitsPerSample, this.WAVEフォーマット.BitsPerSample );
+ mediaType.Set<int>( 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
+ }
+}
--- /dev/null
+using System;
+
+namespace FDK.メディア.サウンド.WASAPIold
+{
+ /// <summary>
+ /// WASAPIデバイス(のIAudioClock)をソースとするタイマー。
+ /// </summary>
+ /// <remarks>
+ /// WASAPIデバイスと厳密に同期をとる場合には、QPCTimer ではなく、このクラスを使用する。
+ /// </remarks>
+ public class SoundTimer
+ {
+ /// <summary>
+ /// エラー時は double.NaN を返す。
+ /// </summary>
+ 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();
+ }
+}
{
get { return StrokeStyleT.bs_フォルダ; }
}
- public static FDK.メディア.サウンド.WASAPI.Device Wasapiデバイス
+ public static FDK.メディア.サウンド.WASAPIold.Device Wasapiデバイス
{
get { return StrokeStyleT.bs_Wasapiデバイス; }
}
#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
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;
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()
// コンテキストを作成する。
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 ] );
}
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 を追加しても鳴らないだけなので、ノーチェックで大丈夫。
}
protected FDK.同期.RWLock<double> 現在進行描画中の譜面スクロール速度の倍率 = new FDK.同期.RWLock<double>( 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.ステージ.演奏.スクロール譜面 スクロール譜面;
/// 解放は、演奏ステージクラスの非活性化後に、外部から行われる。
/// <see cref="SST.ステージ.演奏.演奏ステージ.BGMを解放する"/>
/// </remarks>
- protected FDK.メディア.サウンド.WASAPI.Sound BGM = null;
+ protected FDK.メディア.サウンド.WASAPIold.Sound BGM = null;
protected FDK.カウンタ.FPS FPS = null;
/// <summary>
/// 動的子Activity。背景動画を再生しない場合は null のまま。