Jonatan Liljedahl (Kymatica AB)
This article will give you some useful hints regarding AUParameter implementation. It’s mainly written for plugin developers.
My host app AUM only lists the parameters that are marked as writable.
So make sure to add kAudioUnitParameterFlag_IsWritable
to your param.flags
for all parameters that you want to expose for external control.
Another flag worth mentioning is kAudioUnitParameterFlag_DisplayLogarithmic
, which is suitable for frequency, ADSR time values, etc.
AUM checks this flag and if set it maps the linear input value to the param like this:
paramValue = pow(maxValue/minValue, inValue) * minValue
Note that a logarithmic parameter can never have values <= 0!
If the flag is not set, it treats the parameter as linear:
paramValue = inValue * (maxValue-minValue) + minValue
What many plugins do is to only expose generic linear parameters with ranges like 0.0 to 1.0 or 0 to 100%, and then do any exp/log mapping internally.
There are two ways for a host to change a parameter of a plugin.
One is the simple setValue:
family of methods suitable for UI control. Setting a parameter this way will call
the plugins implementorValueObserver
block.
The other is by realtime parameter events, which the host schedules using the audioUnit.scheduleParameterBlock
.
These events does not call the implementorValueObserver
block, instead they must be handled in the plugins internalRenderBlock
.
Realtime events allows to use timestamps for precise and jitter-free value changes.
First we make a common realtime-safe function to update values in the DSP state. Make sure you don’t use any Swift code, ARC, Obj-C calls, memory allocation, file I/O, or other unsafe code here, since this will be called from the audio thread!
static void MyPluginSetParameter(__unsafe_unretained MyPluginAudioUnit *THIS, AUParameterAddress adr, AUValue val) {
if(adr < _MyPluginParameterCount)
DSPValueSet(&THIS->_dspValues[adr], val); // Replace this with whatever code you need to update your DSP state
}
After creating your AUParameterTree
, implement your implementorValueObserver
and implementorValueProvider
blocks.
The implementorValueObserver
block is used to update your DSP state when the host changed a parameter via setValue:
.
It’s not called when we receive parameter events in the render block, unlike tokenByAddingParameterObserver:
.
The block is called on the main thread (on “AUParameterTree.valueAccessQueue”).
tree.implementorValueObserver = ^(AUParameter *param, AUValue value) {
MyPluginSetParameter(THIS, param.address, value);
};
The implementorValueProvider
block is called when the AUParameter value needs to be refreshed from your DSP state.
tree.implementorValueProvider = ^(AUParameter *param) {
AUParameterAddress adr = param.address;
return adr < _MyPluginParameterCount ?
(AUValue) DSPValueGet(&THIS->_dspValues[adr]) // Replace this with whatever code you need to read your DSP state
: (AUValue) 0.0;
};
In your plugins internalRenderBlock
, we need to iterate through the realtime events and look for parameter events.
We’ll get events here when the host schedules parameter events via the audioUnit.scheduleParameterBlock
,
but not when the host calls param setValue:
(in which case implementorValueObserver
is called instead).
Again, make sure you don’t use any Swift code, ARC, Obj-C calls, memory allocation, file I/O, or other unsafe code in your render block!
- (AUInternalRenderBlock)internalRenderBlock {
__unsafe_unretained MyPluginAudioUnit *THIS = self;
return ^AUAudioUnitStatus(AudioUnitRenderActionFlags *actionFlags, const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, NSInteger outputBusNumber, AudioBufferList *outputData, const AURenderEvent *realtimeEventListHead, AURenderPullInputBlock pullInputBlock) {
const AURenderEvent *ev = realtimeEventListHead;
while (ev) {
if(ev->head.eventType == AURenderEventParameter
|| ev->head.eventType == AURenderEventParameterRamp) {
MyPluginSetParameter(THIS, ev->parameter.parameterAddress, ev->parameter.value);
}
// NOTE: this is also where you'd handle MIDI events coming from the host
ev = ev->head.next;
}
// Do any signal processing and MIDI output here
return noErr;
};
}
For jitter-free changes of parameter values, make use of the ev->head.eventSampleTime
to see where in the current buffer the change should take place.
Timestamping follows the same logic as for MIDI, see iOS MIDI timestamps.