What's best practice for GUI change notification?

Hi Vinnie,
I just studied your vf_concurrent classes and the SimpleDJ example. This is really nice work!

I have one question:
In the Param.cpp class of your SimpleDJ you stated that the doGetValue() should only be accessed from the owning thread.
Is there also a clean way to access the value from the GUI/MessageLoop thread?
I mean is it possible to access the values of a state that is owned by the audio thread from the GUI thread (independently from the Listener callback)?
Thanks.

Jan

[quote=“friscokid”]Hi Vinnie,In the Param.cpp class of your SimpleDJ you stated that the doGetValue() should only be accessed from the owning thread.
Is there also a clean way to access the value from the GUI/MessageLoop thread?
I mean is it possible to access the values of a state that is owned by the audio thread from the GUI thread (independently from the Listener callback)?[/quote]

No, you have to use the Listener mechanism. It is the only way to be notified of parameter changes safely.

Damn.
Thanks anyway for the quick response.

You could always save the value on the listener notification and make it available through an accessor function that is for the gui only.

Hi Vinnie,
I’ve implemented your concurrent system in a simple test plugin and although the idea behind the system is great I’m facing a few problems that make me doubt that it’s suitable for audio plugins.
Maybe you could give me some hints to get back on the right track.

Let’s assume we have a standard audio plugin. So we have to deal with at least three threads:

  1. Mainthread:
    • This thread calls the AudioProcessor constructor, prepareToPlay() and some initial getParameter() / setParameter()

  2. AudioIO Callback:
    • This thread calls the processBlock() and is the time critical one
  3. Messageloop /GUI:
    • This one obviously calls everything related to the GUI / user interaction.

Let’s say further we have a ParameterManager object that holds a bunch of parameters which are implementing your concurrent state / listener system.
The first time we have valid information about the thread that should be the owner is when we call synchronize() within the processBlock. But before the processBlock is entered for the first time, there are a lot of operations like creating and initializing the AudioProcessor and the Editor GUI that need information about the current parameter state of the plugin.
It even gets worse when we have to deal with a host that doesn’t call the processBlock when no audio is streaming. In this case there will be no synchronization untill the synchronize() call in the processBlock.
But what if a user adjusts the GUI controls or is loading a new preset or changes automation data while the playback is stopped? There will be no proper synchronization because the thread (Audio IO Callback) that is responsible for synchronization and the update of the correct parameter state simply doesn’t do anything.

Am I doing things the wrong way or does the whole system rely on the assumption that the AudioIO thread (which is the owner of the parameter state) is continuously running and that no synchronization takes place before it is called for the first time?

Any help to build a clean system to handle parameters greatly appreciated.

Typically you would use a vf::ConcurrentState object to synchronize within your addListener function, to give a listener the initial state. Here’s an example of production code:

struct Param
{
  struct State
  {
    State (const double defaultNativeValue) : nativeValue (defaultNativeValue)
    {
    }

    double nativeValue;
  };

  typedef vf::ConcurrentState <State> StateType;

  StateType m_state;

  void addListener (Listener* listener, vf::CallQueue& thread)
  {
    StateType::ReadAccess state (m_state);

    m_listeners.add (listener, thread);

    // give initial value
    m_listeners.queue1 (listener, &Listener::onParamChanged, this, state->nativeValue);
  }

  void setValue (double nativeValue)
  {
    m_thread->call (&Param::doSetValue, this, nativeValue);
  }

private:
  void doSetValue (double nativeValue)
  {
    StateType::UnlockedAccess state (m_state);

    if (state->nativeValue != nativeValue)
    {
      StateType::WriteAccess state (m_state);

      state->nativeValue = nativeValue;

      m_listeners.update (&Listener::onParamChanged, this, nativeValue);
    }
  }

  //...
};

Param::setValue is called from any thread. doSetValue would be called only from the audio i/o thread.

In this example the ConcurrentState isn’t really needed since nativeValue could be updated atomically (I think). But if it was a more complex object like a multi-member structure or array you would need the lock. Note that the lock used by ConcurrentState is extremely efficient, it only costs one interlocked increment / decrement when there’s no contention. And if you study this example you see that the contention can only happen during a call to addListener, which is something that should only be happening at program or plugin startup.

Actually
SimpleDJ has a full working example of a Param object that works this way:

You are right that this model requires that some “owner” thread is continuously running. The way I solve that in my own application is to create a “null audio device” that stands in place when a real audio device is not available for some reason. Here’s the code:

/**
  An AudioDeviceManager that also provides the NullAudioIODevice

  To allow the Mixer to synchronize after failing to open an actual audio
  device, the null device is substituted. Otherwise, user actions such as loading
  a track or manipulating parameters would fail to produce visible results.
*/
class NullAudioDeviceManager : public AudioDeviceManager
{
public:
  static String getNullDeviceName ();

  NullAudioDeviceManager ();
  ~NullAudioDeviceManager ();
  void createAudioDeviceTypes (OwnedArray <AudioIODeviceType>& list);
};

//==============================================================================
/**
  A Thread for the NullAudioIODevice.

  This mimics the behavior of a real time device audio thread.
*/
class NullAudioThread : protected Thread
{
private:
  AudioIODeviceCallback* m_callback;
  AudioIODevice* m_device;

private:
  class TimerWithHistory
  {
  private:
    enum
    {
      maxHist = 20
    };

    int64 m_ticksPerSecond;
    double m_ticksPerCall;
    int64 m_curTime;
    int64 m_startTime;
    int64 m_hist[maxHist];
    int m_index; // of oldest

  private:
    void record (int64 when)
    {
      m_hist [m_index] = when;
      m_index = (m_index + 1) % maxHist;
    }

  public:
    TimerWithHistory (double milliSecondsPerCall)
      : m_index (0)
    {
      m_ticksPerSecond = Time::getHighResolutionTicksPerSecond ();
      m_ticksPerCall = m_ticksPerSecond * milliSecondsPerCall / 1000;
      m_curTime = Time::getHighResolutionTicks();
      // stuff history with perfect timings
      for (int i = maxHist; i > 0; --i)
        record (static_cast <int64> (m_curTime - i * m_ticksPerCall));
    }

    void call ()
    {
      m_startTime = m_curTime;

      record (m_startTime);
    }

    void wait (Thread* thread)
    {
      m_curTime = Time::getHighResolutionTicks();

      int64 const nextTime = static_cast <int64> (
        m_hist [m_index] + maxHist * m_ticksPerCall + 0.5);
     
      if (nextTime > m_curTime)
      {
        int const milliSeconds = static_cast <int> (
          (1000 * (nextTime - m_curTime) + (m_ticksPerSecond/2)) / m_ticksPerSecond);
        
        thread->wait (milliSeconds);
      }
      else
      {
        // call took too long
      }
    }
  };

public:
  NullAudioThread () : Thread ("NullAudioThread")
  {
  }

  ~NullAudioThread ()
  {
    stop ();
  }

  void start (AudioIODeviceCallback* callback,
              AudioIODevice* device)
  {
    m_callback = callback;
    m_device = device;

    startThread ();
    setPriority (10);
  }

  void stop ()
  {
    stopThread (-1);
  }

  void run ()
  {
    m_callback->audioDeviceAboutToStart (m_device);

    const int bufferSamples = m_device->getCurrentBufferSizeSamples();
    const double sampleRate = m_device->getCurrentSampleRate();

    const int inputCount = m_device->getActiveInputChannels().countNumberOfSetBits();
    const int outputCount = m_device->getActiveOutputChannels().countNumberOfSetBits();
    
    AudioSampleBuffer buffer (inputCount + outputCount, bufferSamples);

    TimerWithHistory timer (1000 * bufferSamples / sampleRate);

    while (!threadShouldExit())
    {
      timer.call ();

      m_callback->audioDeviceIOCallback (const_cast<const float**>
                                          (buffer.getArrayOfChannels() + outputCount),
                                          inputCount,
                                          buffer.getArrayOfChannels(),
                                          outputCount,
                                          bufferSamples);

      timer.wait (this);
    }

    m_callback->audioDeviceStopped ();
  }
};

//==============================================================================
/**
  AudioIODevice with thread that sends output to null.
*/
class NullAudioIODevice : public AudioIODevice
{
private:
  bool m_isOpen;
  AudioIODeviceCallback* m_callback;
  int m_bufferSizeSamples;
  double m_sampleRate;
  NullAudioThread m_thread;
  BigInteger m_activeInputs;
  BigInteger m_activeOutputs;

public:
  NullAudioIODevice ()
    : AudioIODevice ("None", "None")
    , m_isOpen (false)
    , m_callback (0)
  {
    m_activeInputs.clear();
    m_activeOutputs.clear();
  }

  StringArray getOutputChannelNames ()
  {
    StringArray names;
    names.add ("1");
    names.add ("2");
    names.add ("3");
    names.add ("4");
    names.add ("5");
    names.add ("6");
    return names;
  }

  StringArray getInputChannelNames ()
  {
    StringArray names;
    names.add ("1");
    return names;
  }

  int getNumSampleRates()
  {
    return 5;
  }

  /* We need to support as many sample rates as possible so that we
     can encompass all possible real settings if a device fails to open.
  */
  double getSampleRate (int index)
  {
    double rate=0;

    switch (index)
    {
    default:
    case 0: rate = 44100; break;
    case 1: rate = 48000; break;
    case 2: rate = 96000; break;
    case 3: rate = 176400; break;
    case 4: rate = 192000; break;
    };

    return rate;
  }

  int getNumBufferSizesAvailable ()
  {
    return 8;
  }

  int getBufferSizeSamples (int index)
  {
    int bufferSize;

    switch (index)
    {
    default:
    case 0: bufferSize = 48; break;
    case 1: bufferSize = 64; break;
    case 2: bufferSize = 128; break;
    case 3: bufferSize = 256; break;
    case 4: bufferSize = 512; break;
    case 5: bufferSize = 1024; break;
    case 6: bufferSize = 2048; break;
    case 7: bufferSize = 2560; break;
    };
    
    return bufferSize;
  }

  int getDefaultBufferSize()
  {
    return 64;
  }

  String open (BigInteger const& inputChannels,
               BigInteger const& outputChannels,
               double sampleRate,
               int bufferSizeSamples)
  {
    if (m_isOpen)
      close ();

    m_isOpen = true;
    m_sampleRate = sampleRate;
    m_bufferSizeSamples = bufferSizeSamples;
    m_activeInputs = inputChannels;
    m_activeOutputs = outputChannels;

    return String::empty;
  }

  void close()
  {
    if (m_isOpen)
      m_isOpen = false;
  }

  bool isOpen()
  {
    return m_isOpen;
  }

  void start (AudioIODeviceCallback* callback)
  {
    m_callback = callback;
    m_thread.start (callback, this);
  }

  void stop()
  {
    m_thread.stop ();
  }

  bool isPlaying ()
  {
    return m_callback != 0;
  }

  String getLastError ()
  {
    return String::empty;
  }

  int getCurrentBufferSizeSamples ()
  {
    return m_bufferSizeSamples;
  }

  double getCurrentSampleRate ()
  {
    return m_sampleRate;
  }

  int getCurrentBitDepth ()
  {
    return 16;
  }

  BigInteger getActiveOutputChannels () const
  {
    return m_activeOutputs;
  }

  BigInteger getActiveInputChannels () const
  {
    return m_activeInputs;
  }

  int getOutputLatencyInSamples ()
  {
    return 0;
  }

  int getInputLatencyInSamples ()
  {
    return 0;
  }
};

//==============================================================================
/**
  AudioIODeviceType that provides the NullAudioIODevice.
*/
class NullAudioIODeviceType : public AudioIODeviceType
{
public:
  explicit NullAudioIODeviceType (String deviceName)
    : AudioIODeviceType (deviceName)
  {
  }

  void scanForDevices ()
  {
  }

  StringArray getDeviceNames (bool wantInputNames) const
  {
    StringArray names;
    names.add (NullAudioDeviceManager::getNullDeviceName ());
    return names;
  }

  int getDefaultDeviceIndex (bool forInput) const
  {
    return 0;
  }

  int getIndexOfDevice (AudioIODevice* device, bool asInput) const
  {
    return 0;
  }

  bool hasSeparateInputsAndOutputs () const
  {
    return false;
  }

  AudioIODevice* createDevice (
    const String& outputDeviceName,
    const String& inputDeviceName)
  {
    return new NullAudioIODevice;
  }
};

//==============================================================================

NullAudioDeviceManager::NullAudioDeviceManager ()
{
}

NullAudioDeviceManager::~NullAudioDeviceManager()
{
}

String NullAudioDeviceManager::getNullDeviceName ()
{
  return "None";
}

void NullAudioDeviceManager::createAudioDeviceTypes (OwnedArray <AudioIODeviceType>& list)
{
  AudioDeviceManager::createAudioDeviceTypes (list);

  list.add (new NullAudioIODeviceType (getNullDeviceName ()));
}

You might have to do something similar in your plugin. To be honest, I am unsure - at this point I think you’re a pioneer!

Thanks a lot for the detailed response.

Yes, I’ve implemented the parameter using a concurrent state as you showed in the first one of your answers (I’ve been studying the SimpleDJ example).

That’s exactly my problem. If I load the plugin in an host that doesn’t call the audio IO untill the playback is started, I have no proper owner thread and the calls that depend on a thread id of the m_thread variable can’t work correctly.

That’s not an easy one. Maybe I have to handle calls to the parameters’ setValue differently if the owner thread isn’t initialized yet and then call some function to init the thread from the processBlock or something like that.
However when the playpack stops again this thread will be “sleeping” and the synchronization won’t work.
Hmmm


Okay, then you have solved the “initial value” problem. One down!

Well the CallQueue for your audio thread is going to be a ManualCallQueue with the synchronize being the first line of your audio i/o call back (processBlock I guess). Since its a manual call queue, you should be able to synchronize it from your own utility thread until the audio i/o call back is fired up.

I don’t think that could be done in a clean fashion. Instead, you would want something like this:

  • Have a utility thread running all the time
  • The utility thread starts out “owning” the ManualCallQueue
  • Utility thread will call synchronize every so often as long as it owns the call queue
  • When the “prepareToPlay” equivalent is called in your plugin, you transfer ownership of the queue from the utility thread to the audio i/o callback
  • When “releaseResources” equivalent is called in your plugin, ownership of the queue is transferred back to the utility thread

The trick is implementing ownership transfer correctly using synchronization primitives.

With this implementation, the code for your “Param” object doesn’t care about who is processing the queue and you wont have to have nasty special cases and state variables.

[quote]- When the “prepareToPlay” equivalent is called in your plugin, you transfer ownership of the queue from the utility thread to the audio i/o callback

  • When “releaseResources” equivalent is called in your plugin, ownership of the queue is transferred back to the utility thread[/quote]
    That won’t work because “prepareToPlay” and “releaseResources” are called on the main thread and not the audio IO thread.

Is there a way to force a call to synchronize() on a specific thread?
For example I’m on the main thread but I’d like to initiate a call to synchronize() on an instance of the messageLoop.
So is it possible to force another thread than the currently calling one to perform a synchronize() call?

A little off topic but related:
If a parameter structure contains a value of type juce::String and that value changes, wouldn’t that lead to memory allocation, too?

[quote=“friscokid”][quote]- When the “prepareToPlay” equivalent is called in your plugin, you transfer ownership of the queue from the utility thread to the audio i/o callback

  • When “releaseResources” equivalent is called in your plugin, ownership of the queue is transferred back to the utility thread[/quote]
    
“prepareToPlay” and “releaseResources” are called on the main thread and not the audio IO thread.
    [/quote]

Right. The “utility thread” is a simple thread whose only purpose is to synchronize the ManualCallQueue when the audio device I/O callback is not running.

In prepareToPlay you tell the utility thread to stop calling synchronize on the manual call queue. In releaseResources you tell the utility thread to start calling synchronize on the manual call queue.

[quote]Is there a way to force a call to synchronize() on a specific thread?
For example I’m on the main thread but I’d like to initiate a call to synchronize() on an instance of the messageLoop.
So is it possible to force another thread than the currently calling one to perform a synchronize() call?[/quote]

I’m not really sure I understand. synchronize is a public function that you call yourself. If you want it to be called from a specific thread, then just call the function.

You can call synchronize on the ManualCallQueue from the message thread if you want, to process messages when the audio device I/O callback isn’t running. This is not much different than using a separate “utility thread”, except that if you use the message thread you might get hung up by long GUI operations.

What I meant is:
Thread A is active and enters let’s sy “setValue”. But instead of calling synchronize() from the currently active thread A, is there a way to force Thread B to call synchronize().
Ok, probably there is no way. The idea behind this is to not let thread B continuously call synchronize() but to force a call to synchronize() on a specific event that could be initiated from any other thread.
I’d like to make thread B listen to an event that could be send from any other thread and if it receives that event a call to synchronize() is performed from thread B instead of performing synchronize() from the thread that is the sender of the event.

In this way I could avoid the need to continuously call synchronize() but just do it if a certain event occours.

[quote]A little off topic but related:
If a parameter structure contains a value of type juce::String and that value changes, wouldn’t that lead to memory allocation, too?[/quote]
Am I right that the concurrent state object should only contain primitives? If a juce::String object is a member of the state object it would lead to memory allocations on the audio IO thread, right?

[quote=“friscokid”]What I meant is:
Thread A is active and enters let’s sy “setValue”. But instead of calling synchronize() from the currently active thread A, is there a way to force Thread B to call synchronize()
The idea behind this is to not let thread B continuously call synchronize() but to force a call to synchronize() on a specific event that could be initiated from any other thread.[/quote]

CallQueue::call calls CallQueue::signal which is a pure virtual function. The derived class does whatever is necessary. The ManualCallQueue does nothing, it relies on synchronize being called periodically. But ThreadWithCallQueue signals a waitable event object which will eventually lead to a call to synchronize. You can see this loop in ThreadWithCallQueue::threadRun (vf_ThreadWithCallQueue.cpp).

Notice that the implementation of Param::setValue (in SimpleDJ) is perfectly compatible with this behavior. This line:

m_thread.call (&Param::doSetValue, this, value);

would work even if m_thread was a ThreadWithCallQueue, in which case it would signal the waitable event object and wake up the thread.

If thread B is a ThreadWithCallQueue, this behavior is automatic. Any thread can use call on the ThreadWithCallQueue and the thread will be woken up and have the functor processed.

You’re still going to have to address the case where the audio device I/O callback is not called because the plugin is not processing yet. For that, the scheme I described earlier should work.

It doesn’t have to contain primitives but all the rules about memory allocations from the audio I/O thread are applicable.

Great answer, thanks.
I’ll see what I can get out of this.

[quote=“friscokid”]Great answer, thanks.
I’ll see what I can get out of this.[/quote]

I think you’re asking all the right questions. Keep in mind that this is complicated stuff. A good approach would be to try to fully understand each of the classes involved. I will answer any questions you have.

Finally I managed to get a working parameter handling with the following benefits:

  • doesn’t allocate memory
  • is lock free in the critical audio IO
  • doesn’t need an extra thread
  • is thread safe
  • doesn’t use timers
  • enables communication between audio IO thread and GUI
  • works even when there are no calls to the audio IO

The drawbacks:

  • little overhead because every parameter has two state structures - one for the critical audio IO and one that is accessed by all other threads. These two states automatically synchronize each other when needed.
    However, this doesn’t seem to be a problem because I couldn’t recognize any differences in performance or lags when the user or the host interacts with the parameters.

One problem left:
As already mentioned in the first post of this topic, there are some leak detections:

  • 1 of class Thread
  • 3 of class WaitableEvent
    In the first post it was mentioned that these leak detections are false positives. Is this for sure?
    Do you have an idea how to cleanly avoid these leak detections?

Thanks a lot for your help so far.

This is to be expected. The call queue model leads to having a separate copy of the state for each thread. So it sounds like you’re doing the right thing.

[quote]it was mentioned that these leak detections are false positives. Is this for sure?
Do you have an idea how to cleanly avoid these leak detections?[/quote]

The issue is that the order of destruction of objects with static storage duration is not guaranteed. This can be fixed but it would require changes in JUCE.

You can try making a special .cpp file that includes both juce_basics.cpp and vf_core.cpp and see if that fixes it. If not, then reverse the order: include vf_core.cpp first and then juce_basics.cpp.

How did you address that finally ? do you call synchronise from the message thread ?
How do you know when the audio device I/O callback is not running anymore?
If the user bypass the plugin from the host, releaseResources may not get called, and processBlockBypassed neither


I just stumbled upon another problem.

In your vf_Bind.h there is the following code:

#if JUCE_MSVC // Visual Studio has these in std. using std::ref; using std::bind; using std::function; using std::placeholders::_1; using std::placeholders::_2;
But the stdint.h is not included in VS before VS2010. In VS2008 this doesn’t exists.
I’m using VS2012, however to get working RTAS debug and release versions on Windows they have to be compiled using the VS2008 Platformtoolset.

Any idea how to workarround the missing functionality in VS2008?
I guess the vf_Bind.h is required for the vf_concurrent module to work properly?