Search code examples
c#naudiowasapi

NAudio with MultiplexingSampleProvider and WasapiOut


I've been working on a C# project with NAudio where I need to send specific Audio signals to specific speakers on a 4 channel system. Basically, I need to send 'Environmental sounds' to the back left speaker, 'Front Panel display sounds' to the Front right speaker, and 'Pre-recorded instructions' to the Front left speaker. My current design works for the 2 channel setup my desktop computer supports, but when I try to expand it to 4 channels on my test hardware I keep getting the following error: 'Value does not fall within the expected range.' when I call WasapiOut.init(). I think that the issue is caused in some manner by the MultiplexingSampleProvider, but I'm not able to figure out why/how to fix the problem. I verify that the AudioDevice that I am using supports enough channels before I try to load it, so it is not a lack of channels that causes the issue.

Here is the code that causes the issue:

// Create mixer provider for each channel.
for( int Count = 0; Count < Channels; Count++ )
{
    _Mixers.Add( new MixingSampleProvider( _Format.AsStandardWaveFormat() ) );
    _Mixers[Count].MixerInputEnded += SoundEndedEvent;
}

// Create and configure multiplexer provider.
_Multi = new MultiplexingSampleProvider( _Mixers, Channels );
for( int count = 0; count < Channels; count++ )
{
    _Multi.ConnectInputToOutput( count, 0 );
}

// Add master volume control provider.
_Volume = new VolumeSampleProvider( _Multi );
_Volume.Volume = 1.0f;

// Initialize output device.
_OutputDev.Init( p_Volume );

_Format is a WaveFormatExtensible class with the settings 44.1Khz, 32bits, 1 channel.

Everything works until I call _OutputDev.Init( p_Volume ), which is where I get the 'Value does not fall within the expected range.' exception.

Any ideas why I am getting this exception, and how I can fix the issue?

Thank you.

Edit

This is the stack trace I'm getting.

System.ArgumentException: Value does not fall within the expected range. at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo) at NAudio.Wave.WasapiOut.Init(IWaveProvider waveProvider) at NAudio.Wave.WaveExtensionMethods.Init(IWavePlayer wavePlayer, ISampleProvider sampleProvider, Boolean convertTo16Bit) at GSound.Audio.Player..ctor(Int32 Channels, Int32 Device) in E:\Dev\GSound_Wasapi\Projects\GSound\Audio\Player.cs:line 220 at GSound.Audio.Handler..ctor(Config TheConfig) in E:\Dev\GSound_Wasapi\Projects\GSound\Audio\Handler.cs:line 78 at GSound.UIData..ctor() in E:\Dev\GSound_Wasapi\Projects\GSound\UIData.cs:line 63

I think the issue is occurring at the AudioClient.Initialize call, which is generating an E_INVALIDARG error. Which would indicate either an issue with the WasapiOut generated format value, or an issue with the client properties. I'll be examining both issues, but any pointers would be helpful.

Thank you again.


Solution

  • I've gotten 4 channel audio to work on one system (Still need to test on another system, but don't see why it won't.)

    The following adjustments were made:

    I removed the MultiplexingSampleProvider and changed my custom SampleProvider to handle multiple channel output by only copying signal data to the desired channel, and 0 data to the others. One of the problems is that the MultiplexingSampleProvider uses the input sample providers waveformat for its output, including said input sample providers number of channels. Since the original custom sample provider was mono channel, the Multiplexer would error when I tried to use it with 4 channels. I'm wondering how hard it would be to rewrite the Multiplexer to use its own waveformat instead, which would be the same as the input format, but with the channel number set via constructor.

    Another issue with the MultiplexingSampleProvider as well as the WasapiOut class, is that they don't handle the WaveFormatExtensible class, I ended up modifying MixingSampleProvider and SampleToWaveProvider classes to allow for WaveFormat.Encoding to be of type Extensible (subFormat MEDIASUBTYPE_IEEE_FLOAT) as well as IeeeFloat.

    /// <summary>
    /// Creates a new MixingSampleProvider, with no inputs, but a specified WaveFormat
    /// </summary>
    /// <param name="waveFormat">The WaveFormat of this mixer. All inputs must be in this format</param>
    public MixingSampleProvider( WaveFormat waveFormat )
    {
        if( waveFormat.Encoding == WaveFormatEncoding.Extensible )
        {
            if( ( ( WaveFormatExtensible )waveFormat ).SubFormat != NAudio.Dmo.AudioMediaSubtypes.MEDIASUBTYPE_IEEE_FLOAT )
            {
                throw new ArgumentException( "Must be already floating point" );
            }
        }
        else if( waveFormat.Encoding != WaveFormatEncoding.IeeeFloat )
        {
            throw new ArgumentException( "Mixer wave format must be IEEE float" );
        }
        sources = new List<ISampleProvider>( );
        WaveFormat = waveFormat;
    }
    

    The final thing I did was add a property to WaveFormatExtensible to allow me to set the dwChannelMask setting. This setting indicates to the system which speakers to actually use for output, which in my case is 0x33 (FL, FR, RL, RR speakers). The normal WaveFormatExtensible constructor assumes that dwChannelMask would be set to 0x7, (FL, FR, FC, LowFreq speakers) since that is the first 4 speaker values. By setting dwChannelMask I can indicate the speakers to use.

    /// <summary>Gets or sets the channel mask.</summary>
    /// <value>The channel mask.</value>
    public int ChannelMask
    {
        get
        {
            return dwChannelMask;
        }
        set
        {
            dwChannelMask = value;
        }
    }
    

    I will say that this has been a very interesting lesson in dealing with audio and audio codecs.

    Hope this helps others.

    Thank you again.