Supersonic C# Wrapper (DSP callback problem)

Hi, i’m using an FMOD C# wrapper that you can found here https://github.com/PizzaKun/SupersonicSound

All works correctly except the DSP callback on OSX using FMOD 1.07.0, the problem is that when i call DSP.getUserData(ref IntPtr Data) mono throw a StackOverflowException; the same code works perfectly on windows, the library version is the same.

this is an example of my code:

Initialize DSP:

FMOD.DSP_READCALLBACK Callback = new FMOD.DSP_READCALLBACK (CallbackGlitcher);

char [] Name = new char[32];
"Test".ToCharArray().CopyTo(Name, 0);
Desc.name = Name;
Desc.version = 0x00010000;
Desc.numinputbuffers = 1;
Desc.numoutputbuffers = 1;
Desc.read = Callback;
Desc.userdata = (IntPtr)GCHandle.Alloc (this);

Res = system._system.createDSP(ref Desc, out handler);

DSP callback:

static FMOD.RESULT CallbackGlitcher(ref FMOD.DSP_STATE S, IntPtr In, IntPtr Out, uint Len, int ChInCount, ref int ChOutCount)
{
	// Get current class pointer
	FMOD.DSP Handler;
	Handle = new FMOD.DSP(S.instance);

	IntPtr Data;
	Handler.getUserData (out Data); // <- Stack overflow

	return FMOD.RESULT.OK;
}

If I call getUserData out of the callback it works.

Thanks.

(ps. The exception is thrown by FMOD_DSP_GetUserData on fmod.cs)

I would check all the usual issues with managed code. Make sure all the data being passed to native code is being preserved (i.e. not being garbage collected). This includes your delegate objects.

As you can imagine getUserData is a simple function. It’s unlikely to be overflowing the stack. It’s more likely a corruption of stack variables because of some other reason.

Hi Nicholas Wilcox, sorry for the late reply and thank you for answering; as you requested I’ll give you ll the code related to the DSPs and will link the repository where you can find the classes and the audo system I’m implementing; you’ll find a C# solution in the repository which contains 5 projects. You can find all the code referring to FMOD in the pEngine/Audio project; the TestShared/TestScene.cs file contains the part I’m using to test the DSPs.

Repository: https://github.com/PizzaKun/pEngine
Audio folder: https://github.com/PizzaKun/pEngine/tree/master/pEngine/Audio

This is the (generic) DSP class

using System;
using System.Collections.Generic;

using System.Runtime.InteropServices;

using SupersonicSound.LowLevel;

namespace pEngine.Audio
{
	public class DSP : IAudioDSP, IDisposable
    {
        public interface ISharedData
        {
			
        }

        public DSP(AudioManager System, string Name)
        {
            system = System;
            name = Name;
        }

        protected void Initialize(FMOD.DSP_READCALLBACK Callback, object Instance)
        {
            FMOD.RESULT Res;

			///////////////////////////////////////
			/// Make sure that is all allocated ///
			///////////////////////////////////////

			descriptor = new FMOD.DSP_DESCRIPTION();

			objectReference = (IntPtr)GCHandle.Alloc (Instance);
			callbackReference = Callback;

            char [] N = new char[32];
            Name.ToCharArray().CopyTo(N, 0);

			descriptor.name = N;
			descriptor.version = 0x00010000;
			descriptor.numinputbuffers = 1;
			descriptor.numoutputbuffers = 1;
			descriptor.read = callbackReference;
			descriptor.userdata = objectReference;

            Res = system.Handle.createDSP(ref descriptor, out handler);
            if (Res != FMOD.RESULT.OK)
                throw new InvalidDSPException();

			// Prevent DSP handler be collected
			DSPHandle = (IntPtr)GCHandle.Alloc(handler);
        }

		public void Dispose()
		{
			handler.release ();
		}

        /// <summary>
        /// Bypass this DSP.
        /// </summary>
        public bool Bypass
        {
            get
            {
                bool bypass;
                handler.getBypass(out bypass);
                return bypass;
            }
            set
            {
                handler.setBypass(value);
            }
        }

        #region Meta

        string name;

        /// <summary>
        /// Name of the current DSP
        /// </summary>
        public string Name { get { return name; } }

        #endregion

        #region Handlers

		FMOD.DSP_READCALLBACK callbackReference;
		FMOD.DSP_DESCRIPTION descriptor;
		FMOD.DSP handler;

		IntPtr objectReference;
		IntPtr DSPHandle;

        protected AudioManager system;

        /// <summary>
        /// Hanler to this DSP.
        /// </summary>
        public FMOD.DSP Handle { get { return handler; } }

        #endregion
    }

    #region Exceptions

    public class InvalidDSPException : Exception
    {

    }

    #endregion
}

This is a specific DSP

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

using SupersonicSound.LowLevel;

namespace pEngine.Audio
{
	public class BarGateDSP : DSP
    {
        static FMOD.RESULT CallbackBarGate(ref FMOD.DSP_STATE S, IntPtr In, IntPtr Out, uint Len, int ChInCount, ref int ChOutCount)
        {
            float Volume = 0;

            // Get current class pointer
			FMOD.DSP FmodDSP = new FMOD.DSP(S.instance);

			IntPtr EngineDSP;

			//TODO: on OSX stack overflow.
			FmodDSP.getUserData (out EngineDSP);

			BarGateDSP H = (((GCHandle)EngineDSP).Target) as BarGateDSP;

            for (uint Sample = 0; Sample < Len; Sample++)
            {

                // Current time count
                H.Time += (float)(1.0 / (H.Frequency / 1000.0));

                // Bar calculation
                if (((int)(((H.Time / 1000.0) / 60.0) * H.BPM * H.Divisor) % H.Bars) == 0 && H.Low)
                {
                    H.Low = false;
                    H.StartEasing = H.Time;
                }
                if (((int)(((H.Time / 1000.0) / 60.0) * H.BPM * H.Divisor) % H.Bars) != 0 && !H.Low)
                {
                    H.Low = true;
                    H.StartEasing = H.Time;
                }

                // Easing
                if (!H.Low)
                    Volume = (H.MaxVol / 100) - (H.EasingTime == 0 ? 0 : ((H.Time - H.StartEasing) / (float)H.EasingTime));
                else if (H.Low)
                    Volume = (H.EasingTime == 0 ? 0 : ((H.Time - H.StartEasing) / (float)H.EasingTime));

                Volume = Math.Min(Volume, 1.0F);
                Volume = Math.Max(Volume, 0.0F);

                // Output
                for (int CH = 0; CH < ChOutCount; CH++)
                {
                    unsafe
                    {
                        float* BOut = (float*)Out;
                        float* BIn = (float*)In;
                        BOut[(Sample * ChOutCount) + CH] = BIn[(Sample * ChInCount) + CH] * Volume;
                    }
                }
            }

			// Main DSP code...
			return FMOD.RESULT.OK;
        }

        /// <summary>
        /// Make a new instance of <see cref="BarGateDSP"/>.
        /// </summary>
        /// <param name="System">Audio system reference.</param>
        public BarGateDSP(AudioManager System) : base(System, "BarGate")
        {

        }

        /// <summary>
        /// Initialize this DSP.
        /// </summary>
        public void Initialize()
        {
            Callback = new FMOD.DSP_READCALLBACK(CallbackBarGate);
            base.Initialize(Callback, this);

			Bypass = true;

			IntPtr Test;

			// If i call getUserData at this point, stack overflow exception will
			// not triggered, but an FMOD thread will not close when i try to close the
			// application
			// Handle.getUserData(out Test);
			// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        }

        /// <summary>
        /// Reset the DSP internal state.
        /// </summary>
        public void Reset()
        {
            Time = 0;
            StartEasing = 0;
            Low = false;
        }

        /// <summary>
        /// Enable this DSP.
        /// </summary>
        public void Enable()
        {
            Bypass = false;
            Reset();
        }

        /// <summary>
        /// Disable this DSP.
        /// </summary>
        public void Disable()
        {
            Bypass = true;
            Reset();
        }

        #region Properties

        uint freq = 0;

        /// <summary>
        /// Beat divisor.
        /// </summary>
        public uint Divisor { get; set; } = 4;

        /// <summary>
        /// Number of bars between each state change.
        /// </summary>
        public uint Bars { get; set; } = 2;

        /// <summary>
        /// Song beats per minute.
        /// </summary>
        public uint BPM { get; set; } = 120;

        /// <summary>
        /// Song reproduction frequency;
        /// Default value: 0
        /// 0 means the DSP autodetect the frequency.
        /// </summary>
        public uint Frequency
        {
            get
            {
                if (freq == 0)
                    return (uint)system.Frequency;
                else return freq;
            }
            set
            {
                freq = value;
            }
        }

        /// <summary>
        /// Volume on gate open.
        /// </summary>
        public uint MaxVol { get; set; } = 100;

        /// <summary>
        /// Volume on gate close.
        /// </summary>
        public uint MinVol { get; set; } = 0;

        /// <summary>
        /// Milliseconds of the high - low voume transition
        /// (Default 2 for smooth volume change)
        /// </summary>
        public double EasingTime { get; set; } = 2;

        #endregion

        #region State

        static FMOD.DSP_READCALLBACK Callback;

        float Time = 0;

        float StartEasing = 0;

        bool Low = false;


        #endregion
    }
}

The code looks OK as first pass. Things you can try: create all your GCHandles with the pinned type, skip creating a FMOD.DSP object in your callback and directly call DSP.FMOD_DSP_GetUserData with S.instance (you’ll have to hack the source to make it public), wrap the body of all callbacks with try/catch to make sure no exceptions are propagated back to native code which cannot handle them properly.