mirror of https://github.com/naudio/NAudio.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
393 lines
15 KiB
393 lines
15 KiB
using System;
|
|
|
|
namespace NAudio.Wave.Asio
|
|
{
|
|
/// <summary>
|
|
/// Callback used by the AsioDriverExt to get wave data
|
|
/// </summary>
|
|
public delegate void AsioFillBufferCallback(IntPtr[] inputChannels, IntPtr[] outputChannels);
|
|
|
|
/// <summary>
|
|
/// AsioDriverExt is a simplified version of the AsioDriver. It provides an easier
|
|
/// way to access the capabilities of the Driver and implement the callbacks necessary
|
|
/// for feeding the driver.
|
|
/// Implementation inspired from Rob Philpot's with a managed C++ ASIO wrapper BlueWave.Interop.Asio
|
|
/// http://www.codeproject.com/KB/mcpp/Asio.Net.aspx
|
|
///
|
|
/// Contributor: Alexandre Mutel - email: alexandre_mutel at yahoo.fr
|
|
/// </summary>
|
|
public class AsioDriverExt
|
|
{
|
|
private readonly AsioDriver driver;
|
|
private AsioCallbacks callbacks;
|
|
private AsioDriverCapability capability;
|
|
private AsioBufferInfo[] bufferInfos;
|
|
private bool isOutputReadySupported;
|
|
private IntPtr[] currentOutputBuffers;
|
|
private IntPtr[] currentInputBuffers;
|
|
private int numberOfOutputChannels;
|
|
private int numberOfInputChannels;
|
|
private AsioFillBufferCallback fillBufferCallback;
|
|
private int bufferSize;
|
|
private int outputChannelOffset;
|
|
private int inputChannelOffset;
|
|
/// <summary>
|
|
/// Reset Request Callback
|
|
/// </summary>
|
|
public Action ResetRequestCallback;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AsioDriverExt"/> class based on an already
|
|
/// instantiated AsioDriver instance.
|
|
/// </summary>
|
|
/// <param name="driver">A AsioDriver already instantiated.</param>
|
|
public AsioDriverExt(AsioDriver driver)
|
|
{
|
|
this.driver = driver;
|
|
|
|
if (!driver.Init(IntPtr.Zero))
|
|
{
|
|
throw new InvalidOperationException(driver.GetErrorMessage());
|
|
}
|
|
|
|
callbacks = new AsioCallbacks();
|
|
callbacks.pasioMessage = AsioMessageCallBack;
|
|
callbacks.pbufferSwitch = BufferSwitchCallBack;
|
|
callbacks.pbufferSwitchTimeInfo = BufferSwitchTimeInfoCallBack;
|
|
callbacks.psampleRateDidChange = SampleRateDidChangeCallBack;
|
|
|
|
BuildCapabilities();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows adjustment of which is the first output channel we write to
|
|
/// </summary>
|
|
/// <param name="outputChannelOffset">Output Channel offset</param>
|
|
/// <param name="inputChannelOffset">Input Channel offset</param>
|
|
public void SetChannelOffset(int outputChannelOffset, int inputChannelOffset)
|
|
{
|
|
if (outputChannelOffset + numberOfOutputChannels <= Capabilities.NbOutputChannels)
|
|
{
|
|
this.outputChannelOffset = outputChannelOffset;
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException("Invalid channel offset");
|
|
}
|
|
if (inputChannelOffset + numberOfInputChannels <= Capabilities.NbInputChannels)
|
|
{
|
|
this.inputChannelOffset = inputChannelOffset;
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException("Invalid channel offset");
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the driver used.
|
|
/// </summary>
|
|
/// <value>The ASIOdriver.</value>
|
|
public AsioDriver Driver => driver;
|
|
|
|
/// <summary>
|
|
/// Starts playing the buffers.
|
|
/// </summary>
|
|
public void Start()
|
|
{
|
|
driver.Start();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops playing the buffers.
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
driver.Stop();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the control panel.
|
|
/// </summary>
|
|
public void ShowControlPanel()
|
|
{
|
|
driver.ControlPanel();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases this instance.
|
|
/// </summary>
|
|
public void ReleaseDriver()
|
|
{
|
|
try
|
|
{
|
|
driver.DisposeBuffers();
|
|
} catch (Exception ex)
|
|
{
|
|
Console.Out.WriteLine(ex.ToString());
|
|
}
|
|
driver.ReleaseComAsioDriver();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the specified sample rate is supported.
|
|
/// </summary>
|
|
/// <param name="sampleRate">The sample rate.</param>
|
|
/// <returns>
|
|
/// <c>true</c> if [is sample rate supported]; otherwise, <c>false</c>.
|
|
/// </returns>
|
|
public bool IsSampleRateSupported(double sampleRate)
|
|
{
|
|
return driver.CanSampleRate(sampleRate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the sample rate.
|
|
/// </summary>
|
|
/// <param name="sampleRate">The sample rate.</param>
|
|
public void SetSampleRate(double sampleRate)
|
|
{
|
|
driver.SetSampleRate(sampleRate);
|
|
// Update Capabilities
|
|
BuildCapabilities();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the fill buffer callback.
|
|
/// </summary>
|
|
/// <value>The fill buffer callback.</value>
|
|
public AsioFillBufferCallback FillBufferCallback
|
|
{
|
|
get { return fillBufferCallback; }
|
|
set { fillBufferCallback = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the capabilities of the AsioDriver.
|
|
/// </summary>
|
|
/// <value>The capabilities.</value>
|
|
public AsioDriverCapability Capabilities => capability;
|
|
|
|
/// <summary>
|
|
/// Creates the buffers for playing.
|
|
/// </summary>
|
|
/// <param name="numberOfOutputChannels">The number of outputs channels.</param>
|
|
/// <param name="numberOfInputChannels">The number of input channel.</param>
|
|
/// <param name="useMaxBufferSize">if set to <c>true</c> [use max buffer size] else use Prefered size</param>
|
|
public int CreateBuffers(int numberOfOutputChannels, int numberOfInputChannels, bool useMaxBufferSize)
|
|
{
|
|
if (numberOfOutputChannels < 0 || numberOfOutputChannels > capability.NbOutputChannels)
|
|
{
|
|
throw new ArgumentException(
|
|
$"Invalid number of channels {numberOfOutputChannels}, must be in the range [0,{capability.NbOutputChannels}]");
|
|
}
|
|
if (numberOfInputChannels < 0 || numberOfInputChannels > capability.NbInputChannels)
|
|
{
|
|
throw new ArgumentException("numberOfInputChannels",
|
|
$"Invalid number of input channels {numberOfInputChannels}, must be in the range [0,{capability.NbInputChannels}]");
|
|
}
|
|
|
|
// each channel needs a buffer info
|
|
this.numberOfOutputChannels = numberOfOutputChannels;
|
|
this.numberOfInputChannels = numberOfInputChannels;
|
|
// Ask for maximum of output channels even if we use only the nbOutputChannelsArg
|
|
int nbTotalChannels = capability.NbInputChannels + capability.NbOutputChannels;
|
|
bufferInfos = new AsioBufferInfo[nbTotalChannels];
|
|
currentOutputBuffers = new IntPtr[numberOfOutputChannels];
|
|
currentInputBuffers = new IntPtr[numberOfInputChannels];
|
|
|
|
// and do the same for output channels
|
|
// ONLY work on output channels (just put isInput = true for InputChannel)
|
|
int totalIndex = 0;
|
|
for (int index = 0; index < capability.NbInputChannels; index++, totalIndex++)
|
|
{
|
|
bufferInfos[totalIndex].isInput = true;
|
|
bufferInfos[totalIndex].channelNum = index;
|
|
bufferInfos[totalIndex].pBuffer0 = IntPtr.Zero;
|
|
bufferInfos[totalIndex].pBuffer1 = IntPtr.Zero;
|
|
}
|
|
|
|
for (int index = 0; index < capability.NbOutputChannels; index++, totalIndex++)
|
|
{
|
|
bufferInfos[totalIndex].isInput = false;
|
|
bufferInfos[totalIndex].channelNum = index;
|
|
bufferInfos[totalIndex].pBuffer0 = IntPtr.Zero;
|
|
bufferInfos[totalIndex].pBuffer1 = IntPtr.Zero;
|
|
}
|
|
|
|
if (useMaxBufferSize)
|
|
{
|
|
// use the drivers maximum buffer size
|
|
bufferSize = capability.BufferMaxSize;
|
|
}
|
|
else
|
|
{
|
|
// use the drivers preferred buffer size
|
|
bufferSize = capability.BufferPreferredSize;
|
|
}
|
|
|
|
unsafe
|
|
{
|
|
fixed (AsioBufferInfo* infos = &bufferInfos[0])
|
|
{
|
|
IntPtr pOutputBufferInfos = new IntPtr(infos);
|
|
|
|
// Create the ASIO Buffers with the callbacks
|
|
driver.CreateBuffers(pOutputBufferInfos, nbTotalChannels, bufferSize, ref callbacks);
|
|
}
|
|
}
|
|
|
|
// Check if outputReady is supported
|
|
isOutputReadySupported = (driver.OutputReady() == AsioError.ASE_OK);
|
|
return bufferSize;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the capabilities internally.
|
|
/// </summary>
|
|
private void BuildCapabilities()
|
|
{
|
|
capability = new AsioDriverCapability();
|
|
|
|
capability.DriverName = driver.GetDriverName();
|
|
|
|
// Get nb Input/Output channels
|
|
driver.GetChannels(out capability.NbInputChannels, out capability.NbOutputChannels);
|
|
|
|
capability.InputChannelInfos = new AsioChannelInfo[capability.NbInputChannels];
|
|
capability.OutputChannelInfos = new AsioChannelInfo[capability.NbOutputChannels];
|
|
|
|
// Get ChannelInfo for Inputs
|
|
for (int i = 0; i < capability.NbInputChannels; i++)
|
|
{
|
|
capability.InputChannelInfos[i] = driver.GetChannelInfo(i, true);
|
|
}
|
|
|
|
// Get ChannelInfo for Output
|
|
for (int i = 0; i < capability.NbOutputChannels; i++)
|
|
{
|
|
capability.OutputChannelInfos[i] = driver.GetChannelInfo(i, false);
|
|
}
|
|
|
|
// Get the current SampleRate
|
|
capability.SampleRate = driver.GetSampleRate();
|
|
|
|
var error = driver.GetLatencies(out capability.InputLatency, out capability.OutputLatency);
|
|
// focusrite scarlett 2i4 returns ASE_NotPresent here
|
|
|
|
if (error != AsioError.ASE_OK && error != AsioError.ASE_NotPresent)
|
|
{
|
|
var ex = new AsioException("ASIOgetLatencies");
|
|
ex.Error = error;
|
|
throw ex;
|
|
}
|
|
|
|
// Get BufferSize
|
|
driver.GetBufferSize(out capability.BufferMinSize, out capability.BufferMaxSize, out capability.BufferPreferredSize, out capability.BufferGranularity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback called by the AsioDriver on fill buffer demand. Redirect call to external callback.
|
|
/// </summary>
|
|
/// <param name="doubleBufferIndex">Index of the double buffer.</param>
|
|
/// <param name="directProcess">if set to <c>true</c> [direct process].</param>
|
|
private void BufferSwitchCallBack(int doubleBufferIndex, bool directProcess)
|
|
{
|
|
for (int i = 0; i < numberOfInputChannels; i++)
|
|
{
|
|
currentInputBuffers[i] = bufferInfos[i + inputChannelOffset].Buffer(doubleBufferIndex);
|
|
}
|
|
|
|
for (int i = 0; i < numberOfOutputChannels; i++)
|
|
{
|
|
currentOutputBuffers[i] = bufferInfos[i + outputChannelOffset + capability.NbInputChannels].Buffer(doubleBufferIndex);
|
|
}
|
|
|
|
fillBufferCallback?.Invoke(currentInputBuffers, currentOutputBuffers);
|
|
|
|
if (isOutputReadySupported)
|
|
{
|
|
driver.OutputReady();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback called by the AsioDriver on event "Samples rate changed".
|
|
/// </summary>
|
|
/// <param name="sRate">The sample rate.</param>
|
|
private void SampleRateDidChangeCallBack(double sRate)
|
|
{
|
|
// Check when this is called?
|
|
capability.SampleRate = sRate;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asio message call back.
|
|
/// </summary>
|
|
/// <param name="selector">The selector.</param>
|
|
/// <param name="value">The value.</param>
|
|
/// <param name="message">The message.</param>
|
|
/// <param name="opt">The opt.</param>
|
|
/// <returns></returns>
|
|
private int AsioMessageCallBack(AsioMessageSelector selector, int value, IntPtr message, IntPtr opt)
|
|
{
|
|
// Check when this is called?
|
|
switch (selector)
|
|
{
|
|
case AsioMessageSelector.kAsioSelectorSupported:
|
|
AsioMessageSelector subValue = (AsioMessageSelector)Enum.ToObject(typeof(AsioMessageSelector), value);
|
|
switch (subValue)
|
|
{
|
|
case AsioMessageSelector.kAsioEngineVersion:
|
|
return 1;
|
|
case AsioMessageSelector.kAsioResetRequest:
|
|
ResetRequestCallback?.Invoke();
|
|
return 0;
|
|
case AsioMessageSelector.kAsioBufferSizeChange:
|
|
return 0;
|
|
case AsioMessageSelector.kAsioResyncRequest:
|
|
return 0;
|
|
case AsioMessageSelector.kAsioLatenciesChanged:
|
|
return 0;
|
|
case AsioMessageSelector.kAsioSupportsTimeInfo:
|
|
// return 1; DON'T SUPPORT FOR NOW. NEED MORE TESTING.
|
|
return 0;
|
|
case AsioMessageSelector.kAsioSupportsTimeCode:
|
|
// return 1; DON'T SUPPORT FOR NOW. NEED MORE TESTING.
|
|
return 0;
|
|
}
|
|
break;
|
|
case AsioMessageSelector.kAsioEngineVersion:
|
|
return 2;
|
|
case AsioMessageSelector.kAsioResetRequest:
|
|
ResetRequestCallback?.Invoke();
|
|
return 1;
|
|
case AsioMessageSelector.kAsioBufferSizeChange:
|
|
return 0;
|
|
case AsioMessageSelector.kAsioResyncRequest:
|
|
return 0;
|
|
case AsioMessageSelector.kAsioLatenciesChanged:
|
|
return 0;
|
|
case AsioMessageSelector.kAsioSupportsTimeInfo:
|
|
return 0;
|
|
case AsioMessageSelector.kAsioSupportsTimeCode:
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Buffers switch time info call back.
|
|
/// </summary>
|
|
/// <param name="asioTimeParam">The asio time param.</param>
|
|
/// <param name="doubleBufferIndex">Index of the double buffer.</param>
|
|
/// <param name="directProcess">if set to <c>true</c> [direct process].</param>
|
|
/// <returns></returns>
|
|
private IntPtr BufferSwitchTimeInfoCallBack(IntPtr asioTimeParam, int doubleBufferIndex, bool directProcess)
|
|
{
|
|
// Check when this is called?
|
|
return IntPtr.Zero;
|
|
}
|
|
}
|
|
}
|