using NAudio.CoreAudioApi.Interfaces; using NAudio.Wasapi.CoreAudioApi; using NAudio.Wave; using System; using System.Runtime.InteropServices; using System.Threading.Tasks; namespace NAudio.CoreAudioApi { /// /// Windows CoreAudio AudioClient /// public class AudioClient : IDisposable { private IAudioClient audioClientInterface; private WaveFormat mixFormat; private AudioRenderClient audioRenderClient; private AudioCaptureClient audioCaptureClient; private AudioClockClient audioClockClient; private AudioStreamVolume audioStreamVolume; private AudioClientShareMode shareMode; /// /// Activate Async /// public static async Task ActivateAsync(string deviceInterfacePath, AudioClientProperties? audioClientProperties) { var icbh = new ActivateAudioInterfaceCompletionHandler( ac2 => { if (audioClientProperties != null) { IntPtr p = Marshal.AllocHGlobal(Marshal.SizeOf(audioClientProperties.Value)); try { // TODO: consider whether we can marshal this without the need for AllocHGlobal Marshal.StructureToPtr(audioClientProperties.Value, p, false); ac2.SetClientProperties(p); } finally { Marshal.FreeHGlobal(p); } } /*var wfx = new WaveFormat(44100, 16, 2); int hr = ac2.Initialize(AudioClientShareMode.Shared, AudioClientStreamFlags.EventCallback | AudioClientStreamFlags.NoPersist, 10000000, 0, wfx, IntPtr.Zero);*/ }); var IID_IAudioClient2 = new Guid("726778CD-F60A-4eda-82DE-E47610CD78AA"); NativeMethods.ActivateAudioInterfaceAsync(deviceInterfacePath, IID_IAudioClient2, IntPtr.Zero, icbh, out var activationOperation); var audioClient2 = await icbh; return new AudioClient((IAudioClient)audioClient2); } public AudioClient(IAudioClient audioClientInterface) { this.audioClientInterface = audioClientInterface; } /// /// Retrieves the stream format that the audio engine uses for its internal processing of shared-mode streams. /// Can be called before initialize /// public WaveFormat MixFormat { get { if (mixFormat == null) { Marshal.ThrowExceptionForHR(audioClientInterface.GetMixFormat(out var waveFormatPointer)); var waveFormat = WaveFormat.MarshalFromPtr(waveFormatPointer); Marshal.FreeCoTaskMem(waveFormatPointer); mixFormat = waveFormat; } return mixFormat; } } /// /// Initializes the Audio Client /// /// Share Mode /// Stream Flags /// Buffer Duration /// Periodicity /// Wave Format /// Audio Session GUID (can be null) public void Initialize(AudioClientShareMode shareMode, AudioClientStreamFlags streamFlags, long bufferDuration, long periodicity, WaveFormat waveFormat, Guid audioSessionGuid) { this.shareMode = shareMode; int hresult = audioClientInterface.Initialize(shareMode, streamFlags, bufferDuration, periodicity, waveFormat, ref audioSessionGuid); Marshal.ThrowExceptionForHR(hresult); // may have changed the mix format so reset it mixFormat = null; } /// /// Retrieves the size (maximum capacity) of the audio buffer associated with the endpoint. (must initialize first) /// public int BufferSize { get { Marshal.ThrowExceptionForHR(audioClientInterface.GetBufferSize(out uint bufferSize)); return (int) bufferSize; } } /// /// Retrieves the maximum latency for the current stream and can be called any time after the stream has been initialized. /// public long StreamLatency => audioClientInterface.GetStreamLatency(); /// /// Retrieves the number of frames of padding in the endpoint buffer (must initialize first) /// public int CurrentPadding { get { Marshal.ThrowExceptionForHR(audioClientInterface.GetCurrentPadding(out var currentPadding)); return currentPadding; } } /// /// Retrieves the length of the periodic interval separating successive processing passes by the audio engine on the data in the endpoint buffer. /// (can be called before initialize) /// public long DefaultDevicePeriod { get { Marshal.ThrowExceptionForHR(audioClientInterface.GetDevicePeriod(out var defaultDevicePeriod, out _)); return defaultDevicePeriod; } } /// /// Gets the minimum device period /// (can be called before initialize) /// public long MinimumDevicePeriod { get { Marshal.ThrowExceptionForHR(audioClientInterface.GetDevicePeriod(out _, out var minimumDevicePeriod)); return minimumDevicePeriod; } } // TODO: GetService: // IID_IAudioSessionControl // IID_IChannelAudioVolume // IID_ISimpleAudioVolume /// /// Returns the AudioStreamVolume service for this AudioClient. /// /// /// This returns the AudioStreamVolume object ONLY for shared audio streams. /// /// /// This is thrown when an exclusive audio stream is being used. /// public AudioStreamVolume AudioStreamVolume { get { if (shareMode == AudioClientShareMode.Exclusive) { throw new InvalidOperationException("AudioStreamVolume is ONLY supported for shared audio streams."); } if (audioStreamVolume == null) { var audioStreamVolumeGuid = new Guid("93014887-242D-4068-8A15-CF5E93B90FE3"); Marshal.ThrowExceptionForHR(audioClientInterface.GetService(audioStreamVolumeGuid, out var audioStreamVolumeInterface)); audioStreamVolume = new AudioStreamVolume((IAudioStreamVolume)audioStreamVolumeInterface); } return audioStreamVolume; } } /// /// Gets the AudioClockClient service /// public AudioClockClient AudioClockClient { get { if (audioClockClient == null) { var audioClockClientGuid = new Guid("CD63314F-3FBA-4a1b-812C-EF96358728E7"); Marshal.ThrowExceptionForHR(audioClientInterface.GetService(audioClockClientGuid, out var audioClockClientInterface)); audioClockClient = new AudioClockClient((IAudioClock)audioClockClientInterface); } return audioClockClient; } } /// /// Gets the AudioRenderClient service /// public AudioRenderClient AudioRenderClient { get { if (audioRenderClient == null) { var audioRenderClientGuid = new Guid("F294ACFC-3146-4483-A7BF-ADDCA7C260E2"); Marshal.ThrowExceptionForHR(audioClientInterface.GetService(audioRenderClientGuid, out var audioRenderClientInterface)); audioRenderClient = new AudioRenderClient((IAudioRenderClient)audioRenderClientInterface); } return audioRenderClient; } } /// /// Gets the AudioCaptureClient service /// public AudioCaptureClient AudioCaptureClient { get { if (audioCaptureClient == null) { var audioCaptureClientGuid = new Guid("c8adbd64-e71e-48a0-a4de-185c395cd317"); Marshal.ThrowExceptionForHR(audioClientInterface.GetService(audioCaptureClientGuid, out var audioCaptureClientInterface)); audioCaptureClient = new AudioCaptureClient((IAudioCaptureClient)audioCaptureClientInterface); } return audioCaptureClient; } } /// /// Determines whether if the specified output format is supported /// /// The share mode. /// The desired format. /// True if the format is supported public bool IsFormatSupported(AudioClientShareMode shareMode, WaveFormat desiredFormat) { return IsFormatSupported(shareMode, desiredFormat, out _); } private IntPtr GetPointerToPointer() { return Marshal.AllocHGlobal(Marshal.SizeOf()); } /// /// Determines if the specified output format is supported in shared mode /// /// Share Mode /// Desired Format /// Output The closest match format. /// True if the format is supported public bool IsFormatSupported(AudioClientShareMode shareMode, WaveFormat desiredFormat, out WaveFormatExtensible closestMatchFormat) { IntPtr pointerToPtr = GetPointerToPointer(); // IntPtr.Zero; // Marshal.AllocHGlobal(Marshal.SizeOf()); closestMatchFormat = null; int hresult = audioClientInterface.IsFormatSupported(shareMode, desiredFormat, pointerToPtr); var closestMatchPtr = Marshal.PtrToStructure(pointerToPtr); if (closestMatchPtr != IntPtr.Zero) { closestMatchFormat = Marshal.PtrToStructure(closestMatchPtr); Marshal.FreeCoTaskMem(closestMatchPtr); } Marshal.FreeHGlobal(pointerToPtr); // S_OK is 0, S_FALSE = 1 if (hresult == 0) { // directly supported return true; } if (hresult == 1) { return false; } if (hresult == AudioClientErrorCode.UnsupportedFormat) { // documentation is confusing as to what this flag means // https://docs.microsoft.com/en-us/windows/desktop/api/audioclient/nf-audioclient-iaudioclient-isformatsupported // "Succeeded but the specified format is not supported in exclusive mode." return false; // shareMode != AudioClientShareMode.Exclusive; } Marshal.ThrowExceptionForHR(hresult); // shouldn't get here throw new NotSupportedException("Unknown hresult " + hresult); } /// /// Starts the audio stream /// public void Start() { audioClientInterface.Start(); } /// /// Stops the audio stream. /// public void Stop() { audioClientInterface.Stop(); } /// /// Set the Event Handle for buffer synchro. /// /// The Wait Handle to setup public void SetEventHandle(IntPtr eventWaitHandle) { audioClientInterface.SetEventHandle(eventWaitHandle); } /// /// Resets the audio stream /// Reset is a control method that the client calls to reset a stopped audio stream. /// Resetting the stream flushes all pending data and resets the audio clock stream /// position to 0. This method fails if it is called on a stream that is not stopped /// public void Reset() { audioClientInterface.Reset(); } #region IDisposable Members /// /// Dispose /// public void Dispose() { if (audioClientInterface != null) { if (audioClockClient != null) { audioClockClient.Dispose(); audioClockClient = null; } if (audioRenderClient != null) { audioRenderClient.Dispose(); audioRenderClient = null; } if (audioCaptureClient != null) { audioCaptureClient.Dispose(); audioCaptureClient = null; } if (audioStreamVolume != null) { audioStreamVolume.Dispose(); audioStreamVolume = null; } Marshal.ReleaseComObject(audioClientInterface); audioClientInterface = null; GC.SuppressFinalize(this); } } #endregion } }