Windows Phone 8 and XAudio2

Jun 17, 2013 at 8:20 AM
Hi,

I'm writing a game for WP8 using MonoGame/SharpDX. The reason being that Microsoft no longer supports XNA. I wanted to use your library to play mod-files but MonoGame doesn't support DynamicSoundEffect on WP8 yet.

But using SharpDX it's possible to use XAudio so I wrote a SharpMik-driver for XAudio2.

Since I don't want any references to XNA in the library (it kinda messes up MonoGame) I used the WP7XNA-project, cut lose the XNA-parts and instead referenced SharpDX.

Is this something you want for the general codebase? I'd be happy to contribute.
Coordinator
Jun 17, 2013 at 8:52 PM
I'd be interested to see how you did it, as I too made a driver using xaudio2, but due to bugs in it I had to create a thread to not starve the audio system.
    public class XAudio2Driver : VirtualSoftwareDriver
    {
        uint BUFFERSIZE = 2048;
        sbyte[][] m_buffer;
        

        XAudio2 xaudio2;
        MasteringVoice masteringVoice;

        WaveFormat waveFormat;
        SourceVoice sourceVoice;

        DataStream[] dataStream;
        AudioBuffer[] audioBuffer;
        bool m_Running = false;

        public XAudio2Driver()
        {
            m_Next = null;
            m_Name = "XAudio2 Driver";
            m_Version = "XAudio2 v1.0";
            m_HardVoiceLimit = 0;
            m_SoftVoiceLimit = 255;
            m_AutoUpdating = false;
            m_buffer = new sbyte[3][];
            dataStream = new DataStream[3];
            audioBuffer = new AudioBuffer[3];


            for (int i = 0; i < 3; i++)
            {
                m_buffer[i] = new sbyte[BUFFERSIZE];
                dataStream[i] = DataStream.Create(m_buffer[i], true, true);
                audioBuffer[i] = new AudioBuffer
                {
                    Stream = dataStream[i],
                    AudioBytes = (int)dataStream[i].Length,
                    Flags = BufferFlags.EndOfStream
                };

            }
        }

        public override void CommandLine(string command)
        {
        
        }

        public override bool IsPresent()
        {
            return true;
        }

        public override bool Init()
        {
            
            xaudio2 = new XAudio2();
            xaudio2.StartEngine();

            // Let windows autodetect number of channels and sample rate.
            masteringVoice = new MasteringVoice(xaudio2);
            
            //masteringVoice.SetVolume(1.0f, 0);

            waveFormat = new WaveFormat(ModDriver.MixFreq, (ModDriver.Mode & SharpMikCommon.DMODE_STEREO) == SharpMikCommon.DMODE_STEREO ? 2 : 1);
            sourceVoice = new SourceVoice(xaudio2, waveFormat);

            return base.Init();
        }

        public override bool PlayStart()
        {
            bool result = base.PlayStart();

            sourceVoice_BufferEnd();
            //next();

            sourceVoice.Start();
            m_Running = true;

            
            Task.Run(async () =>
            {
                int lastCount = sourceVoice.State.BuffersQueued;

                while (m_Running)
                {
                    if (sourceVoice.State.BuffersQueued == 1)
                    {
                        next();
                    }
                    System.Diagnostics.Debug.WriteLine(sourceVoice.State.BuffersQueued + " " + sourceVoice.State.SamplesPlayed+ "  ");
                    //await Task.Delay(10);
                }
            });

            return result;
        }

        public override void PlayStop()
        {
            sourceVoice.Stop();
            m_Running = false;
            base.PlayStop();
        }


        int m_Last = 0;

        void next()
        {
            uint done = WriteBytes(m_buffer[m_Last], BUFFERSIZE);

            /*
            // Create a DataStream with pinned managed buffer
            var dataStream = DataStream.Create(m_buffer[m_Last], true, true);

            var buffer = new AudioBuffer
            {
                Stream = dataStream,
                AudioBytes = (int)dataStream.Length,
                Flags = BufferFlags.EndOfStream
            };
            */
            sourceVoice.SubmitSourceBuffer(audioBuffer[m_Last], null);
            m_Last++;

            if (m_Last >= 3)
            {
                m_Last = 0;
            }
        }


        void sourceVoice_BufferEnd()
        {
            for (int i = 0; i < 3; i++)
            {               
                uint done = WriteBytes(m_buffer[i], BUFFERSIZE);

                /*
                // Initialization phase, keep this buffer during the life of your application
                // Allocate 10s at 44.1Khz of stereo 16bit signals
                //var myBufferOfSamples = new short[44100 * 10 * 2];

                // Create a DataStream with pinned managed buffer
                var dataStream = DataStream.Create(m_buffer[i], true, true);

                var buffer = new AudioBuffer
                {
                    Stream = dataStream,
                    AudioBytes = (int)dataStream.Length,
                    Flags = BufferFlags.EndOfStream
                };

                //Array.Copy(m_Audiobuffer, myBufferOfSamples, (int)done);


                // PCM 44.1Khz stereo 16 bit format
                //var waveFormat = new WaveFormat();

                //XAudio2 xaudio = new XAudio2();
                //MasteringVoice masteringVoice = new MasteringVoice(xaudio);
                //var sourceVoice = new SourceVoice(xaudio, waveFormat, true);
                */
                // Submit the buffer
                sourceVoice.SubmitSourceBuffer(audioBuffer[i], null);
            }
            //sourceVoice.Start();
        }

        public override void Exit()
        {
            base.Exit();
        }

        public override void Update()
        {
            
        }

    }
as the events for the buffer played never seemed to be called.
Jun 17, 2013 at 9:40 PM
I used the "BufferEnd" event on my SourceVoice object to know when it had reached the end of the stream and that seemed to work alright. To avoid "glitching" I ended up using double buffers so that when SourceVoice reaches the end of #1 it continues to play #2 and at the same time signals BufferEnd. At that point I use a thread to put new data into #1 and re-submits it. The process is repeated when SourceVoice reaches the end of #2, and so on.

I used buffers large enough to cover 1 second of 44100 stereo data. The reason being that I used Task.Factory.StartNew(..) in the BufferEnd event listener to fill the next buffer. I could have used smaller buffers had I set up a "real" worker thread and instead used an AutoResetEvent to notify the thread from the BufferEnd-call.

Anyhow.. The current implementation works fine when I run it on my Lumia 920 so I'm gonna stick with it until something crashes or breaks.
internal class XAudio2Driver : VirtualDriver1
    {
        private int _sharpMikBufferSize;
        private int _xAudioBufferSize;

        // SharpMik buffer
        private sbyte[] _vcBuffer;

        // XAudio2 buffer 1
        private short[] _signedBuffer;
        private DataStream _audioStream;
        private AudioBuffer _audioBuffer;
        
        // XAudio2 buffer 2
        private short[] _signedBuffer2;
        private DataStream _audioStream2;
        private AudioBuffer _audioBuffer2;

        // currently selected buffer
        private int _currentBuffer = 0;
        
        // XAudio2 stuff
        private XAudio2 _xaudio2;
        private WaveFormat _waveFormat;
        private MasteringVoice _masteringVoice;
        private SourceVoice _sourceVoice;
        
        // current state
        private bool _isPlaying;

        public XAudio2Driver()
        {
            m_Name = "XAudio2 Driver";
            m_Version = "XAudio2 Driver v1.0";
            m_HardVoiceLimit = 0;
            m_SoftVoiceLimit = 255;
            m_AutoUpdating = true;
        }

        public override bool Init()
        {
            // 1 second of sound data
            _sharpMikBufferSize = ModDriver.MixFreq * 1 * ((ModDriver.Mode & SharpMikCommon.DMODE_STEREO) == SharpMikCommon.DMODE_STEREO ? 2 : 1) * 2;
            _xAudioBufferSize = _sharpMikBufferSize/2;
            _vcBuffer = new sbyte[_sharpMikBufferSize];
            _signedBuffer = new short[_xAudioBufferSize];
            _signedBuffer2 = new short[_xAudioBufferSize];

            _audioStream = DataStream.Create(_signedBuffer, true, true);
            _audioStream2 = DataStream.Create(_signedBuffer2, true, true);
            _audioBuffer = new AudioBuffer
            {
                Stream = _audioStream,
                AudioBytes = (int)_audioStream.Length,
                Flags = BufferFlags.None
            };
            _audioBuffer2 = new AudioBuffer
            {
                Stream = _audioStream2,
                AudioBytes = (int)_audioStream2.Length,
                Flags = BufferFlags.None
            };

            _waveFormat = new WaveFormat(ModDriver.MixFreq, (ModDriver.Mode & SharpMikCommon.DMODE_STEREO) == SharpMikCommon.DMODE_STEREO ? 2 : 1);
            _xaudio2 = new XAudio2();
            _masteringVoice = new MasteringVoice(_xaudio2);
            _sourceVoice = new SourceVoice(_xaudio2, _waveFormat, true);
            _sourceVoice.BufferEnd += SourceVoiceOnBufferEnd;
            _sourceVoice.StreamEnd += SourceVoiceOnStreamEnd;
            return base.Init();
        }



        public override void CommandLine(string command){}

        public override bool IsPresent()
        {
            return true;
        }

        public override bool PlayStart()
        {
            _isPlaying = true;
            base.PlayStart();
            // que both samples so that one is playing while the other is re-buffered when OnBufferEnd is hit.
            _sourceVoice.Start(0);
            SubmitNextSample();
            SubmitNextSample();
            return true;
        }

        public override void PlayStop()
        {
            _isPlaying = false;
            _sourceVoice.Stop();
            base.PlayStop();
        }

        private void SourceVoiceOnStreamEnd() { }

        private void SourceVoiceOnBufferEnd(IntPtr intPtr)
        {
            Task.Factory.StartNew(SubmitNextSample);
        }

        private void SubmitNextSample()
        {
            if (!_isPlaying) return;
            VC_WriteBytes(_vcBuffer, (uint)_sharpMikBufferSize);
            if (_currentBuffer == 0)
            {
                Buffer.BlockCopy(_vcBuffer, 0, _signedBuffer, 0, _sharpMikBufferSize);
                _sourceVoice.SubmitSourceBuffer(_audioBuffer, null);
                _currentBuffer = 1;
            }
            else
            {
                Buffer.BlockCopy(_vcBuffer, 0, _signedBuffer2, 0, _sharpMikBufferSize);
                _sourceVoice.SubmitSourceBuffer(_audioBuffer2, null);
                _currentBuffer = 0;
            }
        }
    }
Jun 17, 2013 at 9:42 PM
This is, btw, in no way optimized and there are parts of the code that are pretty ugly. I would definitely clean it up some before commiting it to any project. The code currently resides in my "dev test bed".. :)
Coordinator
Jun 18, 2013 at 11:28 AM
I tried to use the BufferEnd event, but it must have been broken in the version of SharpDX I was using for Windows8, I'd like to get an update out with a Win8/WP8 driver included which might end up being a mix of the 2 drivers.

In the XNA driver I also used a number of buffers to make sure there was always data for the audio engine to play, as without it you get the glitching and popping you noticed.

I should also try and spend some time trying to work out a decent way of having both signed byte and unsigned byte data being processed to get rid of the need for coping buffers around after.

Thanks for using the library, its great to hear people are wanting it
Jun 18, 2013 at 12:52 PM
One way of avoiding the extra copy would be to create new AudioBuffer instances at each call to SubmitNextSample. This would work since the DataStream structure can take a sbyte[] structure as data input.

But you would need to keep track of all the DataStreams being created or you would got a memory leak. In my DynamicSoundEffectInstance implementation in MonoGame I use a Queue-object. Every created DataStream is pushed to the queue and the oldest DataStream is poped from the queue and Disposed when the BufferEnd event is raised. It would make for a very lightweight implementation of the driver. :)

Have you looked into adding the library to MonoGame thus letting it be the de-facto choice when wanting to use Modules for game music?
Jun 19, 2013 at 8:21 PM
Edited Jun 19, 2013 at 8:23 PM
In lack for better things to do tonight I cleaned the code a bit, added smarter buffer management and removed the unnecessary buffer copy at SubmitNextSample. I still use TaskFactory as it works fine on the hardware I have here using buffers that holds 1 sec of audio data. I guess this could be tweaked to use a combination of a real thread and a AutoResetEvent instead, but I much prefer to use .Net's own threadpool.. :) Anyway.. Here's the code. Hope you find it useful..
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SharpDX;
using SharpDX.Multimedia;
using SharpDX.XAudio2;
using SharpMik;
using SharpMik.Drivers;
using SharpMik.Player;

    public class XAudio2Driver : VirtualDriver1
    {
        // buffer management
        private int _vcBufferSize;
        private sbyte[] _vcBuffer;
        private int _bufferCount = 0;
        private const int MAX_BUFFER_COUNT = 2;
        private Queue<DataStream> _dataStreams;

        // XAudio2 stuff
        private XAudio2 _xaudio2;
        private WaveFormat _waveFormat;
        private MasteringVoice _masteringVoice;
        private SourceVoice _sourceVoice;

        // current state
        private bool _isPlaying;

        public XAudio2Driver()
        {
            m_Name = "XAudio2 Driver";
            m_Version = "XAudio2 Driver v1.0";
            m_HardVoiceLimit = 0;
            m_SoftVoiceLimit = 255;
            m_AutoUpdating = true;
        }

        public override bool Init()
        {
            // 1 second of sound data
            _vcBufferSize = ModDriver.MixFreq * 1 * ((ModDriver.Mode & SharpMikCommon.DMODE_STEREO) == SharpMikCommon.DMODE_STEREO ? 2 : 1) * 2;
            _vcBuffer = new sbyte[_vcBufferSize];
            _dataStreams = new Queue<DataStream>();

            _waveFormat = new WaveFormat(ModDriver.MixFreq, (ModDriver.Mode & SharpMikCommon.DMODE_STEREO) == SharpMikCommon.DMODE_STEREO ? 2 : 1);
            _xaudio2 = new XAudio2();
            _masteringVoice = new MasteringVoice(_xaudio2);
            _sourceVoice = new SourceVoice(_xaudio2, _waveFormat, true);
            _sourceVoice.BufferEnd += SourceVoiceOnBufferEnd;
            _sourceVoice.StreamEnd += SourceVoiceOnStreamEnd;
            return base.Init();
        }

        public override void CommandLine(string command){}

        public override bool IsPresent()
        {
            return true;
        }

        public override bool PlayStart()
        {
            _isPlaying = true;
            base.PlayStart();
            _sourceVoice.Start(0);
            SubmitNextSample();
            return true;
        }

        public override void PlayStop()
        {
            _isPlaying = false;
            _sourceVoice.Stop();
            base.PlayStop();
        }

        private void SourceVoiceOnStreamEnd() { }

        private void SourceVoiceOnBufferEnd(IntPtr intPtr)
        {
            _bufferCount--;
            Task.Factory.StartNew(SubmitNextSample);
        }

        private void SubmitNextSample()
        {
            if (_dataStreams.Count > 0)
            {
                var dataStream = _dataStreams.Dequeue();
                if (dataStream != null) dataStream.Dispose();
            }
            if (!_isPlaying) return;

            while (_bufferCount < MAX_BUFFER_COUNT)
            {

                VC_WriteBytes(_vcBuffer, (uint)_vcBufferSize);
                var dataStream = DataStream.Create(_vcBuffer, true, true);
                var audioBuffer = new AudioBuffer
                                      {
                                          Stream = dataStream,
                                          AudioBytes = _vcBuffer.Length,
                                          Flags = BufferFlags.None
                                      };
                _dataStreams.Enqueue(dataStream);
                _sourceVoice.SubmitSourceBuffer(audioBuffer, null);
                _bufferCount++;
            }
        }
    }