Audio and MIDI library for .NET
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.
 
 
 
 

501 lines
17 KiB

using System;
using NAudio.Wave.Asio;
using System.Threading;
namespace NAudio.Wave
{
/// <summary>
/// ASIO Out Player. New implementation using an internal C# binding.
///
/// This implementation is only supporting Short16Bit and Float32Bit formats and is optimized
/// for 2 outputs channels .
/// SampleRate is supported only if AsioDriver is supporting it
///
/// This implementation is probably the first AsioDriver binding fully implemented in C#!
///
/// Original Contributor: Mark Heath
/// New Contributor to C# binding : Alexandre Mutel - email: alexandre_mutel at yahoo.fr
/// </summary>
public class AsioOut : IWavePlayer
{
private AsioDriverExt driver;
private IWaveProvider sourceStream;
private PlaybackState playbackState;
private int nbSamples;
private byte[] waveBuffer;
private AsioSampleConvertor.SampleConvertor convertor;
private string driverName;
private readonly SynchronizationContext syncContext;
private bool isInitialized;
/// <summary>
/// Playback Stopped
/// </summary>
public event EventHandler<StoppedEventArgs> PlaybackStopped;
/// <summary>
/// When recording, fires whenever recorded audio is available
/// </summary>
public event EventHandler<AsioAudioAvailableEventArgs> AudioAvailable;
/// <summary>
/// Occurs when the driver settings are changed by the user, e.g. in the control panel.
/// </summary>
public event EventHandler DriverResetRequest;
/// <summary>
/// Initializes a new instance of the <see cref="AsioOut"/> class with the first
/// available ASIO Driver.
/// </summary>
public AsioOut()
: this(0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AsioOut"/> class with the driver name.
/// </summary>
/// <param name="driverName">Name of the device.</param>
public AsioOut(string driverName)
{
this.syncContext = SynchronizationContext.Current;
InitFromName(driverName);
}
/// <summary>
/// Opens an ASIO output device
/// </summary>
/// <param name="driverIndex">Device number (zero based)</param>
public AsioOut(int driverIndex)
{
this.syncContext = SynchronizationContext.Current;
String[] names = GetDriverNames();
if (names.Length == 0)
{
throw new ArgumentException("There is no ASIO Driver installed on your system");
}
if (driverIndex < 0 || driverIndex > names.Length)
{
throw new ArgumentException(String.Format("Invalid device number. Must be in the range [0,{0}]", names.Length));
}
InitFromName(names[driverIndex]);
}
/// <summary>
/// Releases unmanaged resources and performs other cleanup operations before the
/// <see cref="AsioOut"/> is reclaimed by garbage collection.
/// </summary>
~AsioOut()
{
Dispose();
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
if (driver != null)
{
if (playbackState != PlaybackState.Stopped)
{
driver.Stop();
}
driver.ResetRequestCallback = null;
driver.ReleaseDriver();
driver = null;
}
}
/// <summary>
/// Gets the names of the installed ASIO Driver.
/// </summary>
/// <returns>an array of driver names</returns>
public static string[] GetDriverNames()
{
return AsioDriver.GetAsioDriverNames();
}
/// <summary>
/// Determines whether ASIO is supported.
/// </summary>
/// <returns>
/// <c>true</c> if ASIO is supported; otherwise, <c>false</c>.
/// </returns>
public static bool isSupported()
{
return GetDriverNames().Length > 0;
}
/// <summary>
/// Determines whether this driver supports the specified sample rate.
/// </summary>
/// <param name="sampleRate">The samplerate to check.</param>
/// <returns>
/// <c>true</c> if the specified sample rate is supported otherwise, <c>false</c>.
/// </returns>
public bool IsSampleRateSupported(int sampleRate)
{
return driver.IsSampleRateSupported(sampleRate);
}
/// <summary>
/// Inits the driver from the asio driver name.
/// </summary>
/// <param name="driverName">Name of the driver.</param>
private void InitFromName(string driverName)
{
this.driverName = driverName;
// Get the basic driver
AsioDriver basicDriver = AsioDriver.GetAsioDriverByName(driverName);
try
{
// Instantiate the extended driver
driver = new AsioDriverExt(basicDriver);
}
catch
{
ReleaseDriver(basicDriver);
throw;
}
driver.ResetRequestCallback = OnDriverResetRequest;
this.ChannelOffset = 0;
}
private void OnDriverResetRequest()
{
DriverResetRequest?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Release driver
/// </summary>
private void ReleaseDriver(AsioDriver driver)
{
driver.DisposeBuffers();
driver.ReleaseComAsioDriver();
}
/// <summary>
/// Shows the control panel
/// </summary>
public void ShowControlPanel()
{
driver.ShowControlPanel();
}
/// <summary>
/// Starts playback
/// </summary>
public void Play()
{
if (playbackState != PlaybackState.Playing)
{
playbackState = PlaybackState.Playing;
HasReachedEnd = false;
driver.Start();
}
}
/// <summary>
/// Stops playback
/// </summary>
public void Stop()
{
playbackState = PlaybackState.Stopped;
driver.Stop();
HasReachedEnd = false;
RaisePlaybackStopped(null);
}
/// <summary>
/// Pauses playback
/// </summary>
public void Pause()
{
playbackState = PlaybackState.Paused;
driver.Stop();
}
/// <summary>
/// Initialises to play
/// </summary>
/// <param name="waveProvider">Source wave provider</param>
public void Init(IWaveProvider waveProvider)
{
InitRecordAndPlayback(waveProvider, 0, -1);
}
/// <summary>
/// Initialises to play, with optional recording
/// </summary>
/// <param name="waveProvider">Source wave provider - set to null for record only</param>
/// <param name="recordChannels">Number of channels to record</param>
/// <param name="recordOnlySampleRate">Specify sample rate here if only recording, ignored otherwise</param>
public void InitRecordAndPlayback(IWaveProvider waveProvider, int recordChannels, int recordOnlySampleRate)
{
if (isInitialized)
{
throw new InvalidOperationException("Already initialised this instance of AsioOut - dispose and create a new one");
}
isInitialized = true;
int desiredSampleRate = waveProvider != null ? waveProvider.WaveFormat.SampleRate : recordOnlySampleRate;
if (waveProvider != null)
{
sourceStream = waveProvider;
this.NumberOfOutputChannels = waveProvider.WaveFormat.Channels;
// Select the correct sample convertor from WaveFormat -> ASIOFormat
var asioSampleType = driver.Capabilities.OutputChannelInfos[0].type;
convertor = AsioSampleConvertor.SelectSampleConvertor(waveProvider.WaveFormat, asioSampleType);
switch (asioSampleType)
{
case AsioSampleType.Float32LSB:
OutputWaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(waveProvider.WaveFormat.SampleRate, waveProvider.WaveFormat.Channels);
break;
case AsioSampleType.Int32LSB:
OutputWaveFormat = new WaveFormat(waveProvider.WaveFormat.SampleRate, 32, waveProvider.WaveFormat.Channels);
break;
case AsioSampleType.Int16LSB:
OutputWaveFormat = new WaveFormat(waveProvider.WaveFormat.SampleRate, 16, waveProvider.WaveFormat.Channels);
break;
case AsioSampleType.Int24LSB:
OutputWaveFormat = new WaveFormat(waveProvider.WaveFormat.SampleRate, 24, waveProvider.WaveFormat.Channels);
break;
default:
throw new NotSupportedException($"{asioSampleType} not currently supported");
}
}
else
{
this.NumberOfOutputChannels = 0;
}
if (!driver.IsSampleRateSupported(desiredSampleRate))
{
throw new ArgumentException("SampleRate is not supported");
}
if (driver.Capabilities.SampleRate != desiredSampleRate)
{
driver.SetSampleRate(desiredSampleRate);
}
// Plug the callback
driver.FillBufferCallback = driver_BufferUpdate;
this.NumberOfInputChannels = recordChannels;
// Used Prefered size of ASIO Buffer
nbSamples = driver.CreateBuffers(NumberOfOutputChannels, NumberOfInputChannels, false);
driver.SetChannelOffset(ChannelOffset, InputChannelOffset); // will throw an exception if channel offset is too high
if (waveProvider != null)
{
// make a buffer big enough to read enough from the sourceStream to fill the ASIO buffers
waveBuffer = new byte[nbSamples * NumberOfOutputChannels * waveProvider.WaveFormat.BitsPerSample / 8];
}
}
/// <summary>
/// driver buffer update callback to fill the wave buffer.
/// </summary>
/// <param name="inputChannels">The input channels.</param>
/// <param name="outputChannels">The output channels.</param>
void driver_BufferUpdate(IntPtr[] inputChannels, IntPtr[] outputChannels)
{
if (this.NumberOfInputChannels > 0)
{
var audioAvailable = AudioAvailable;
if (audioAvailable != null)
{
var args = new AsioAudioAvailableEventArgs(inputChannels, outputChannels, nbSamples,
driver.Capabilities.InputChannelInfos[0].type);
audioAvailable(this, args);
if (args.WrittenToOutputBuffers)
return;
}
}
if (this.NumberOfOutputChannels > 0)
{
int read = sourceStream.Read(waveBuffer, 0, waveBuffer.Length);
if (read < waveBuffer.Length)
{
// we have reached the end of the input data - clear out the end
Array.Clear(waveBuffer, read, waveBuffer.Length - read);
}
// Call the convertor
unsafe
{
// TODO : check if it's better to lock the buffer at initialization?
fixed (void* pBuffer = &waveBuffer[0])
{
convertor(new IntPtr(pBuffer), outputChannels, NumberOfOutputChannels, nbSamples);
}
}
if (read == 0)
{
if (AutoStop)
Stop(); // this can cause hanging issues
HasReachedEnd = true;
}
}
}
/// <summary>
/// Gets the latency (in samples) of the playback driver
/// </summary>
public int PlaybackLatency
{
get
{
int latency, temp;
driver.Driver.GetLatencies(out temp, out latency);
return latency;
}
}
/// <summary>
/// Automatically stop when the end of the input stream is reached
/// Disable this if auto-stop is causing hanging issues
/// </summary>
public bool AutoStop { get; set; }
/// <summary>
/// A flag to let you know that we have reached the end of the input file
/// Useful if AutoStop is set to false
/// You can monitor this yourself and call Stop when it is true
/// </summary>
public bool HasReachedEnd { get; private set; }
/// <summary>
/// Playback State
/// </summary>
public PlaybackState PlaybackState => playbackState;
/// <summary>
/// Driver Name
/// </summary>
public string DriverName => this.driverName;
/// <summary>
/// The number of output channels we are currently using for playback
/// (Must be less than or equal to DriverOutputChannelCount)
/// </summary>
public int NumberOfOutputChannels { get; private set; }
/// <summary>
/// The number of input channels we are currently recording from
/// (Must be less than or equal to DriverInputChannelCount)
/// </summary>
public int NumberOfInputChannels { get; private set; }
/// <summary>
/// The maximum number of input channels this ASIO driver supports
/// </summary>
public int DriverInputChannelCount => driver.Capabilities.NbInputChannels;
/// <summary>
/// The maximum number of output channels this ASIO driver supports
/// </summary>
public int DriverOutputChannelCount => driver.Capabilities.NbOutputChannels;
/// <summary>
/// The number of samples per channel, per buffer.
/// </summary>
public int FramesPerBuffer
{
get
{
if (!isInitialized)
throw new Exception("Not initialized yet. Call this after calling Init");
return nbSamples;
}
}
/// <summary>
/// By default the first channel on the input WaveProvider is sent to the first ASIO output.
/// This option sends it to the specified channel number.
/// Warning: make sure you don't set it higher than the number of available output channels -
/// the number of source channels.
/// n.b. Future NAudio may modify this
/// </summary>
public int ChannelOffset { get; set; }
/// <summary>
/// Input channel offset (used when recording), allowing you to choose to record from just one
/// specific input rather than them all
/// </summary>
public int InputChannelOffset { get; set; }
/// <summary>
/// Sets the volume (1.0 is unity gain)
/// Not supported for ASIO Out. Set the volume on the input stream instead
/// </summary>
[Obsolete("this function will be removed in a future NAudio as ASIO does not support setting the volume on the device")]
public float Volume
{
get
{
return 1.0f;
}
set
{
if (value != 1.0f)
{
throw new InvalidOperationException("AsioOut does not support setting the device volume");
}
}
}
/// <inheritdoc/>
public WaveFormat OutputWaveFormat { get; private set; }
private void RaisePlaybackStopped(Exception e)
{
var handler = PlaybackStopped;
if (handler != null)
{
if (syncContext == null)
{
handler(this, new StoppedEventArgs(e));
}
else
{
syncContext.Post(state => handler(this, new StoppedEventArgs(e)), null);
}
}
}
/// <summary>
/// Get the input channel name
/// </summary>
/// <param name="channel">channel index (zero based)</param>
/// <returns>channel name</returns>
public string AsioInputChannelName(int channel)
{
return channel > DriverInputChannelCount ? "" : driver.Capabilities.InputChannelInfos[channel].name;
}
/// <summary>
/// Get the output channel name
/// </summary>
/// <param name="channel">channel index (zero based)</param>
/// <returns>channel name</returns>
public string AsioOutputChannelName(int channel)
{
return channel > DriverOutputChannelCount ? "" : driver.Capabilities.OutputChannelInfos[channel].name;
}
}
}