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.
[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.
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:
Mainthread:
This thread calls the AudioProcessor constructor, prepareToPlay() and some initial getParameter() / setParameter()âŠ
AudioIO Callback:
This thread calls the processBlock() and is the time critical one
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:
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.
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!
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.
[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?
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âŠ
#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?