[linux-audio-dev] XAP: Control events

David Olofson david at olofson.net
Tue Dec 17 14:13:01 UTC 2002


On Tuesday 17 December 2002 16.11, Sami P Perttu wrote:
[...]
> Thanks for the explanations... there is still one thing I don't
> understand. Do you intend to have both frequency (Hz) and pitch
> (logarithmic Hz) controls? It seems redundant.

There will not be Hz at all, if I can help it; just linear PITCH, and 
hopefully "linear" pitch hinted as "I really mean *linear*" - ie 
NOTEPITCH. :-)


[...]
> 1) A control is a signal whose value against time is determined by
> control events.

Yes.


> 2) The control events divide the signal into sections. The signal
> is identically zero before the first control event arrives.

You should probably not assume that. (0 might be an illegal value.) 
The metadata will contain default values for all controls, it seems 
to make sense that the host should load the defaults as part of the 
instantiation of a plugin. That would make it a matter of "do it once 
and for all in the host SDK."


> 3) Each section is a third-order polynomial. This gives continuity
> of first derivatives where it is needed and is a compromise between
> efficiency and flexibility. Note that some or all of the
> coefficients may be zero.

Yes, this looks nice.


> 4) What happens between sample values? This is up to each plugin,
> mainly because the continuous signal is, in general, not
> bandlimited. However, to most practical purposes, it can be made
> "smooth enough" . Most plugins that oversample will use the macros
> below to get an interpolated version. Some might do additional
> low-pass filtering before applying the control signal internally.

Yes. The best we can do is polynomial splines - but that doesn't 
automatically eliminate the problem. It's just a bit better than 
without ramps, or with just linear, but it's not perfect. Some 
plugins might still need to do some conditioning.


> 5) The event type SET_CONTROL (name?) begins a new section of a

I call it XAP_A_SET, IIRC. (XAP "Action" SET.)


> control signal. It has these attributes:
>
>    float start_value; /* value now */
>    float start_slope; /* slope now */
>    float duration;    /* in samples */
>    float end_value;   /* value at now + duration */
>    float end_slope;   /* slope at now + duration */

Well, we'd have to switch to 64 byte events for that. However, 
there's no good reason to double the start/end data, for reasons 
explained below.


> All attributes must be real-valued.

Yes, probably. Not sure about the duration, though, as you'll need it 
in both integer and float if you must implement both ramping and 
"ramp stops here" handling efficiently.

With the "ramp stops when I tell you" design (target + duration or 
just slope), this is not an issue, since the control data generator 
will tell you what to do in time.


> Duration must be greater than
> zero.

Not sure. Short ramps easilly round to 0 length. Where you you want 
the special case; in senders or receivers? Well, probably in senders. 
(See below.)


> Slopes are given as "(instantaneous) change in value per
> sample".

Yes.


> As stated above, the attributes should be taken as constraints
> specifying a third-order polynomial. Denoting the polynomial as
> f(x), the constraints are:
>
> f(now)             = start_value
> f'(now)            = start_slope
> f(now + duration)  = end_value
> f'(now + duration) = end_slope
>
> Note: the section is specified completely in each event to make the
> handling of controls as stateless as possible.

That's basically a good idea, but in my experience, it's pointless in 
real life applications. These events are not asynchronous messages, 
but rather a continous stream of structured data. If you can't keep 
track of what you're telling other plugins, you can't generate useful 
control data anyway.

Besides, all you need to do to have a complete section is send 
XAP_A_SET followed by XAP_A_RAMP, with the same timestamp value. 
(Assuming that XAP_A_SET comes with value + slope, and XAP_A_RAMP 
comes with target value + target slope. A normal SET would always 
have 0 for slope.)


> The section lasts until the next SET_CONTROL event.

How? Either it ends when the ramp is finished, or it doesn't.

BTW, Audiality's internal voice control events don't even keep track 
of that, so you *explicitly* have to tell a voice when to stop 
ramping a control, by sending a "SET" event with the exact target 
value. (*) This isn't nearly as hard to manage as it may sound, since 
you *have* to keep track of what you're doing anyway, as a sender.

That said, giving ramp events a specific duration, after which the 
ramping should stop *might* be nice. I'm still suspecting that this 
is an illusion that won't help real code at all, though. (It didn't 
in Audiality, at least, and I can't think of a real situation where 
it would be useful.)

Want to have safe disconnections? Just do what you should do anyway: 
Load the default value with a slope of 0. That kills any ramping in 
progress, and reverts to the default value.

Click free? Just have the host take over the port for a while, before 
really disconnecting it. (The plugin that owns the input will never 
notice this.) Send a ramp event of suitable duration, that takes the 
control back to the default value. Then disconnect.

Obiously, this *requires* the chained ramp approach I'm using in 
Audiality, since you can't read back the value of a XAP control 
input. You have to eavesdrop to do that, if you really want to. 
(Quite possible to do, of course.)


(*) This is to avoid ramping rounding errors affecting DC levels
    between ramps. I'm basically just doing it to avoid a special
    "STOP_RAMP" event. It's not required to avoid drift, since
    ramping is always chasing a target value, rather than building
    on the current state. You can actually ramp to any value
    without even knowing the current value of the control!


> The timing of
> the next event need not agree with the duration given in the
> current event - the next event may arrive at any time.

What's the point? As a control data generator, you can't just jump in 
and out as you like. Either you're connected, or you're not. When 
you're connected, you do your job, and deliver a stream of structured 
data that represents a continous signal.


[...]
> Note: it is assumed that plugins choose their approximation
> irregardless of the actual polynomial, which can be constant or
> linear. I think this is a good assumption. Exact linearity cannot
> be detected anyway due to the inaccuracy of floating-point
> arithmetic.

Yes, that's an important point. Plugins should be concistent rather 
than "smart" in this case. It would be rather annoying if some 
plugins toggled "randomly" between two different sets of artifacts... 
*heh* (That's a great way to make it glaringly obvious that there 
*are* artifactsl even if they're very subtle.)


> 7) The following struct and macros provide a "control iterator".
> They are an efficient way of implementing the full third-order
> form.
>
> /* TODO: should some/all of these be floats? */

Well, given that control ramping is not reliable enough to be used 
for long sections and big changes, I don't think there's much point 
in using doubles at all. Control ramping is just an *approximation*, 
and since you can't know how well the receiver actually implements 
it, you can't rely too much on the accuracy anyway.

In short, if we can deal with ramps over a few hundred samples at 
most, that should be just fine. If you do longer ramps than that, 
you're in for unpleasant surprises with many of the plugins that 
interpolate internal coefficients. Besides, you can't really make a 
longer ramp than one block anyway, without breaking this "future" 
rule. Process one block at a time...

As to "hitting the target", well, I think we must analyze this more 
carefully, to see how big a difference float vs double makes here, 
and when. How far off can you land?


> struct control_variable {
>    double value;
>    double dvalue;
>    double ddvalue;
>    double dddvalue;
> };
>
> /* Initializes a control variable. */
> #define INITIALIZE_CONTROL(var) \
>    (var).value = (var).dvalue = (var).ddvalue = (var).dddvalue = 0;

Or just use XAP_A_SET, and wait for the host to give you your default 
values that way. I think that's the best way, actually, since it 
eliminates lots of boring and potentially incorrect initialization 
code in plugins.


> /* Initializes a control variable from a SET_CONTROL event. */
[...]
> /* Initializes an oversampled control variable from a SET_CONTROL
> event. */
[...]

Yes. Thanks for doing the maths! :-)


> /* Increments a control variable. Should be called after each
> sample is processed. */
> #define INCREMENT_CONTROL(var) \
>    (var).value += (var).dvalue; \
>    (var).dvalue += (var).ddvalue; \
>    (var).ddvalue += (var).dddvalue;
>
> I have checked the above for correctness.

Great. :-)


> 8) The following macros generate control events.
>
> /* TODO: set event type, too? */

Well, in Audiality, I just have inline functions that allocate, fill 
in and send events of the common kinds. That's the cleanest way by 
far, from the plugin code POV.

As to inline funcs; how likely is it that we'll find a compiler these 
days that won't do it, or will generaty "slow" code?

(Not that I care all that much, really. gcc seems to do well enough, 
and that's what most real OSes come with anyway. ;-)


[...fill-in-event macros...]

> Comments?

Nice work!

We'll have to settle on the conventions for control generators, 
though. IMNSHO, *they* should be the "smart side" of the connection, 
while the receiver just does what it's told. I'm basically trying to 
achieve this:

	1. Plugins that only receive control data become very
	   simple and easy to get right.

	2. Control generators contain all logic, and as a result,
	   have more alternatives when it comes to optimizations.

	3. Keeping the "active" logic in one place will most
	   probably make debugging easier, since you can
	   basically run the output into a "control tester"
	   plugin to find out if the output data is correct.
	   If you have a problem that's caused by *two* plugins
	   being broken, you're in for some great fun...

	4. Keeping the most complex part right behind a standard
	   output interface is less likely to result in
	   weirdness that shows up in the middle of your plugin,
	   where you can't easily view the data in useful way.

	5. Optimized control ramp implementations will probably
	   be hairy enough without conditionals and stuff
	   required by the API.


//David Olofson - Programmer, Composer, Open Source Advocate

.- The Return of Audiality! --------------------------------.
| Free/Open Source Audio Engine for use in Games or Studio. |
| RT and off-line synth. Scripting. Sample accurate timing. |
`---------------------------> http://olofson.net/audiality -'
   --- http://olofson.net --- http://www.reologica.se ---



More information about the Linux-audio-dev mailing list