Multiple Audio Sources

Jul 23, 2014 at 12:14 AM
I am trying to write a project in c# Visual Studio 2013 that uses CScore to play 3 audio sources at the same time. 1 mp3 or wav file, and 2 sinewave generated files that can be between 20hz and 20khz .
I am looking for code for a mixer to mix these 3 sources if necessary, and also how to give each source its own volume control and EQ control. Will CScore do that and where can I get the info on the source code samples I need to get my project going? Thanks
Coordinator
Jul 23, 2014 at 8:56 AM
Of course CSCore can do that. There is currently no implementation of an mixer because finding a good algorithm is pretty hard. Since you only have about 3 input signals a very, very, very simple implementation of an mixer should do its job. For example something like this:
using System;
using System.Collections.Generic;

namespace CSCore.Streams
{
    public class SimpleMixer : ISampleSource
    {
        private readonly WaveFormat _waveFormat;
        private readonly List<ISampleSource> _sampleSources = new List<ISampleSource>();
        private readonly object _lockObj = new object();
        private float[] _mixerBuffer;

        public bool FillWithZeros { get; set; }

        public bool DivideResult { get; set; }

        public SimpleMixer(int channelCount, int sampleRate)
        {
            if(channelCount < 1)
                throw new ArgumentOutOfRangeException("channelCount");
            if(sampleRate < 1)
                throw new ArgumentOutOfRangeException("sampleRate");

            _waveFormat = new WaveFormat(sampleRate, 32, channelCount, AudioEncoding.IeeeFloat);
        }

        public void AddSource(ISampleSource source)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            if(source.WaveFormat.Channels != WaveFormat.Channels ||
               source.WaveFormat.SampleRate != WaveFormat.SampleRate)
                throw new ArgumentException("Invalid format.", "source");

            lock (_lockObj)
            {
                if (!Contains(source))
                    _sampleSources.Add(source);
            }
        }

        public void RemoveSource(ISampleSource source)
        {
            //don't throw null ex here
            lock (_lockObj)
            {
                if (Contains(source))
                    _sampleSources.Remove(source);
            }
        }

        public bool Contains(ISampleSource source)
        {
            if (source == null)
                return false;
            return _sampleSources.Contains(source);
        }

        public int Read(float[] buffer, int offset, int count)
        {
            int numberOfStoredSamples = 0;

            if (count > 0 && _sampleSources.Count > 0)
            {
                lock (_lockObj)
                {
                    _mixerBuffer = _mixerBuffer.CheckBuffer(count);
                    List<int> numberOfReadSamples = new List<int>();
                    foreach (var sampleSource in _sampleSources)
                    {
                        int read = sampleSource.Read(_mixerBuffer, 0, count);
                        for (int i = offset, n = 0; n < read; i++, n++)
                        {
                            if (numberOfStoredSamples < i)
                            {
                                buffer[i] = _mixerBuffer[n];
                            }
                            else
                            {
                                buffer[i] += _mixerBuffer[n];
                            }
                        }
                        if (read > numberOfStoredSamples)
                            numberOfStoredSamples = read;

                        if(read > 0)
                            numberOfReadSamples.Add(read);
                    }

                    if (DivideResult)
                    {
                        numberOfReadSamples.Sort();
                        int currentOffset = offset;
                        int remainingSources = numberOfReadSamples.Count;

                        foreach (var readSamples in numberOfReadSamples)
                        {
                            if (remainingSources == 0)
                                break;

                            while (currentOffset < offset + readSamples)
                            {
                                buffer[currentOffset] /= remainingSources;
                                buffer[currentOffset] = Math.Max(-1, Math.Min(1, buffer[currentOffset]));
                                currentOffset++;
                            }
                            remainingSources--;
                        }
                    }
                }
            }

            if (FillWithZeros && numberOfStoredSamples != count)
            {
                Array.Clear(
                    buffer,
                    Math.Max(offset + numberOfStoredSamples - 1, 0),
                    count - numberOfStoredSamples);

                return count;
            }

            return numberOfStoredSamples;
        }

        public WaveFormat WaveFormat
        {
            get { return _waveFormat; }
        }

        public long Position
        {
            get { return 0; }
            set
            {
                throw new NotSupportedException();
            }
        }

        public long Length
        {
            get { return 0; }
        }

        public void Dispose()
        {
            lock (_lockObj)
            {
                foreach (var sampleSource in _sampleSources)
                {
                    sampleSource.Dispose();
                    _sampleSources.Remove(sampleSource);
                }
            }
        }
    }
}
You just can add some input signals and play it:
    private static void Main(string[] args)
    {
        const string testfile0 = @"D:\Temp\test.mp3";
        const string testfile1 = @"D:\Temp\test1.mp3";

        var soundSource = new SimpleMixer(2, 44100)
        {
            FillWithZeros = true,
            DivideResult = true //you may play around with this
        };
        soundSource.AddSource(
            CodecFactory.Instance.GetCodec(testfile0).ChangeSampleRate(44100).ToStereo().ToSampleSource());
        soundSource.AddSource(
            CodecFactory.Instance.GetCodec(testfile1).ChangeSampleRate(44100).ToStereo().ToSampleSource());
        soundSource.AddSource(
            new SineGenerator(500, 0.5, 0).ChangeSampleRate(44100).ToStereo().ToSampleSource());

        var soundOut = new WasapiOut() {Latency = 200}; //better use a quite high latency
        soundOut.Initialize(soundSource.ToWaveSource());
        soundOut.Play();

        Console.ReadKey();

        //... don't forget to dispose the soundOut and the soundSource variables
    }
Note: While testing this, I found a little bug inside of the ChangeSampleRate method. Please download the latest commit of cscore and built it yourself to make it working.
Jul 23, 2014 at 12:55 PM
Thank you! This is just what I needed I am going over this code and will implement it! Is there a way to have 2 sineGenerators, and make one the left channel and the other the right channel of a stereo output? I see where you can take mono and make stereo. using .ToStereo(); So I am hopeing they can be split into left and right channels.
Coordinator
Jul 24, 2014 at 8:47 AM
Sorry for the late reply. Of course CSCore can split into left and right channels. But the SimpleMixer class, I've posed above, needs to have the same number of channels for each input. So the only way to achieve that, would be to use stereo inputs but to mute simply one channel. There are multiple possibilities to do that:
  1. Simply use any audio editor to create a stereo mp3 file which uses only one channel.
  2. Implementing a custom class which mutes one channel.
  3. The usage of the DmoChannelResampler class. I've just updated that class which allows you to specify a channel conversion matrix. This would be probably the best way to go. I've added a few comments to the new sample code. It is may a bit complicated when you see it for the first time. If there are any questions just feel free to ask me:
private static void Main(string[] args)
{
    const string testfile1 = @"any sound file...";

    const int mixerSampleRate = 44100; //44.1kHz

    var mixer = new SimpleMixer(2, mixerSampleRate)
    {
        FillWithZeros = true,
        DivideResult = true //you may play around with this
    };

    var monoToLeftOnlyChannelMatrix = new ChannelMatrix(ChannelMask.SpeakerFrontCenter,
        ChannelMask.SpeakerFrontLeft | ChannelMask.SpeakerFrontRight);
    var monoToRightOnlyChannelMatrix = new ChannelMatrix(ChannelMask.SpeakerFrontCenter,
        ChannelMask.SpeakerFrontLeft | ChannelMask.SpeakerFrontRight);

    /*
        * Set the channel conversion matrix. 
        * The y-axis specifies the input. This in only one channel since the SineGenerator only uses one channel. 
        * The x-axis specifies the output. There we have to use two channels since we want stereo output. 
        * The first value on the x-axis specifies the volume of the left channel, the second value 
        * on the x-axis specifies the volume of the right channel.
        * 
        * If we take look at the left only channel conversion matrix, we can see that we are mapping one channel (y-axis)
        * to two channels (x-axis). The left channel receives a volume of 1.0 (which means 100%) and the right channel 
        * receives a volume of 0.0 (which means 0.0% -> muted). 
        */
    monoToLeftOnlyChannelMatrix.SetMatrix(
        new[,]
        {
            {1.0f, 0.0f}
        });

    monoToRightOnlyChannelMatrix.SetMatrix(
        new[,]
        {
            {0.0f, 1.0f}
        });

    //Add any sound track.
    mixer.AddSource(
        CodecFactory.Instance.GetCodec(testfile1).ChangeSampleRate(mixerSampleRate).ToStereo().ToSampleSource());

    //Add a 700Hz sine with a amplitude of 0.5 which plays only on the left channel.
    mixer.AddSource(
        new SineGenerator(700, 0.5, 0).ToWaveSource().AppendSource(x => new DmoChannelResampler(x, monoToLeftOnlyChannelMatrix, mixerSampleRate)).ToSampleSource());

    //Add a 300Hz sine with a amplitude of 0.5 which plays only on the right channel.
    mixer.AddSource(
        new SineGenerator(300, 0.5, 0).ToWaveSource().AppendSource(x => new DmoChannelResampler(x, monoToRightOnlyChannelMatrix, mixerSampleRate)).ToSampleSource());

    //Initialize the soundout with the mixer.
    var soundOut = new WasapiOut() {Latency = 200}; //better use a quite high latency
    soundOut.Initialize(mixer.ToWaveSource());
    soundOut.Play();

    Console.ReadKey();

    //...
}
Jul 24, 2014 at 6:20 PM
Thank you for the time you are putting into this.
The first code works great.
but the second code you posted gives this error

An unhandled exception of type 'System.ArgumentException' occurred in CSCore.dll

Additional information: Channels has to equal the set bits in the channelmask.

Debug stops here and highlights

//Add a 700Hz sine with a amplitude of 0.5 which plays only on the left channel.
        mixer.AddSource(new SineGenerator(700, 0.5, 0).ToWaveSource().AppendSource(x => new DmoChannelResampler(x, monoToLeftOnlyChannelMatrix, mixerSampleRate)).ToSampleSource());
I am trying to figure it out, but no luck so far.
Coordinator
Jul 24, 2014 at 7:52 PM
Are you using the latest commit (commit with id 72b53a5aecf3)? The source I've posted above works perfectly on my system.
Jul 25, 2014 at 3:56 PM
Edited Jul 25, 2014 at 6:26 PM
Thank you for all the code and the trouble you went to, as to generating it for me.
The issue was an error on my part and it works extremely well now.
I was even able to incorporate an EQ into the code and that works well.
Everything is as I need it, except there is one common volume control for all three mixer inputs.
Is there a way to change the volume of each mixer input while each is playing, instead
of setting each one at the time it is added to the mixer?
and secondly, is there a trigger event for when one of the mp3 inputs stop playing like I put below,
or do I need to just keep checking the soundOut.PlaybackState status?

soundOut.PlaybackStopped += audio_stopped;

private void audio_stopped()
{
// put in code to find next mp3 in playlist and play
}
Thank you!
Coordinator
Jul 26, 2014 at 9:11 AM
You can use the VolumeSource class for adjusting the volume of each input:
VolumeSource volumeSource0, volumeSource1, volumeSource2;

            //Add any sound track.
            mixer.AddSource(
                CodecFactory.Instance.GetCodec(testfile1)
                .ChangeSampleRate(mixerSampleRate)
                .ToStereo()
                .AppendSource(x => new VolumeSource(x), out volumeSource0));

            //Add a 700Hz sine with a amplitude of 0.5 which plays only on the left channel.
            mixer.AddSource(
                new SineGenerator(700, 0.5, 0).ToWaveSource()
                .AppendSource(x => new DmoChannelResampler(x, monoToLeftOnlyChannelMatrix, mixerSampleRate))
                .AppendSource(x => new VolumeSource(x), out volumeSource1));

            //Add a 300Hz sine with a amplitude of 0.5 which plays only on the right channel.
            mixer.AddSource(
                new SineGenerator(300, 0.5, 0).ToWaveSource()
                .AppendSource(x => new DmoChannelResampler(x, monoToRightOnlyChannelMatrix, mixerSampleRate))
                .AppendSource(x => new VolumeSource(x), out volumeSource2));

            //Initialize the soundout with the mixer.
            var soundOut = new WasapiOut() {Latency = 200}; //better use a quite high latency
            soundOut.Initialize(mixer.ToWaveSource());
            soundOut.Play();

            //adjust the volume of the input signals (default value is 100%):
            volumeSource0.Volume = 0.5f; //set the volume of the testfile input signal to 50%
            volumeSource1.Volume = 0.7f; //set the volume of the 700Hz sine to 70%

            Console.ReadKey();

            //...
Should be quite easy to understand.

To get a notification whenever an input signal finishes, you would have to modify the SimpleMixer class.
For example you could replace the foreach loop in the Read method with something like this:
                    for (int m = _sampleSources.Count -1; m >= 0; m--)
                    {
                        var sampleSource = _sampleSources[m];
                        int read = sampleSource.Read(_mixerBuffer, 0, count);
                        for (int i = offset, n = 0; n < read; i++, n++)
                        {
                            if (numberOfStoredSamples < i)
                                buffer[i] = _mixerBuffer[n];
                            else
                                buffer[i] += _mixerBuffer[n];
                        }
                        if (read > numberOfStoredSamples)
                            numberOfStoredSamples = read;

                        if (read > 0)
                            numberOfReadSamples.Add(read);
                        else
                        {
                            //raise event here
                            RemoveSource(sampleSource); //remove the input to make sure that the event gets only raised once.
                        }
                    }