Square wave for automation confusion

The issue I am running into is that, in Waveform, when I have 2 automation nodes that occur at the same time (square wave), both nodes seem to be read from.

The example I ran into this with is with a plugin I made that is essentially a killswitch but for MIDI data (probably a dumb plugin, but it exemplifies this problem well). Just a boolean value to decide whether or not to let midi notes through. If I set the automation such that the switch turns on the same moment that a new “note on” message occurs, the new note passes through unaffected.

If I shift the “killswitch off”-node to the left in the automation, the problem disappears. This is why I think both automation nodes are being read.

  1. Am I correct that this is what is occurring?

  2. If so, is there a good reason to do it this way?

  3. Clearly, the code has some way of understanding which node is “first” even though they occur at the same time since the lines on the automation curve draw correctly. Could this be extended to when the information is being read into a plugin?

EDIT: I have confirmed that if the automation starts with the killswitch activated, it behaves as expected, so I am reasonably confident the issue is with both automation nodes being read.

EDIT 2: I think I have found a simple solution. Would it be possible/advisable to change AutomationCurve::nextIndexAfter from this:

int AutomationCurve::nextIndexAfter (double t) const
{
    auto num = getNumPoints();

    for (int i = 0; i < num; ++i)
        if (getPointTime(i) >= t)
            return i;

    return num;
}

to this?:

int AutomationCurve::nextIndexAfter (double t) const
{
    auto num = getNumPoints();
    for (int i = 0; i < num; ++i)
    {
        t_i = getPointTime(i);
        if (t_i >= t)
        {
            if (i + 1 < num)
            {
                if (getPointTime(i + 1) > t_i)
                    return i;
            }
            else
                return i;
        }
    }
    return num;
}

That way, it will skip over automation indexes that have the exact same time until it gets to the final index having the desired time.

Alternatively, this logic could be put into AutomationCurve::getValueAt when choosing the index in case changing AutomationCurve::nextIndexAfter would break something.

It’s a bit more complicated than that as the automation being played back is an interpolated version of the full bezier curves for speed.

But I’m not quite sure I understand the problem. Aren’t both value changes sent to your plugin? The 0 state and then immediately the 1 state? If so, can’t you just update to the most recent value?

That’s very fair. The problem is that both get sent through (e.g. if it’s operating on midi and you’re using a reverb-y instrument, the note that shouldn’t have played rings out)

But it is not a huge problem and there is a trivial workaround (shift the 0 node left ever so slightly). I just assumed both nodes being read was an undesirable effect.

Is there a reason why you would want both to be read? Or is it just a consequence of the implementation?

Well I guess it’s more a case of why wouldn’t you send them both?
I think plugins really need to be able to deal with any automation as depending on the host, they could send anything.

I can imagine a situation where having both actually makes a difference to the plugin…


But I still can’t see the problem? If a setParameter call with a value of 0.0 and then 1.0 happens, then the process block, isn’t the “current” value just 1.0?

How is that different to just getting a setParameter (1.0), then processBlock?

I wouldn’t expect the user has to move automation points in the host to get this effect. But I might well be missing something obvious here…

It’s very possible my plugin code doesn’t do what I think it does. I am very new to C++ and JUCE.

But in my processBlock, if the parameter is 1.0, no “note on” messages are allowed through, and every currently playing note receives a “note off”. If it’s 0.0, they play as normal.

When i put a square wave automation to activate the killswitch on the same instant that a new “note on” is sent, the message passes through, which is not the desired outcome.

The next frame behaves as expected.

If my code did what I thought it did, would I expect the note to pass through or not?

P.S. If you’re curious here is the source code, though I do not expect you to debug this for me.

Edit: So are you saying that it should only hit the processBlock after reading both nodes? Because my understanding was that it did a full cycle with both values.

That’s a bit too much code for me to read and fully understand.
But there may be another misconception you have which is that automation is sample accurate…
It’s not.

If a block straddles a two nodes composing a vertical segment, you’ll get the value to the left of the segment in one block, then the value to right in the second block.

I don’t think you’ll get both values unless a block just happens to start on the same time as the two nodes.

(This may become out of date in the future if we add sample-accurate automation but currently we don’t have this feature).

I think I mostly understand what you just said, but could you please define “block”?

Edit: oh is it some number of samples grouped together to be processed? Like the smallest resolution of code?

Edit: Okay yeah, I get what you’re saying. That is definitely the core misconception I had. Thank you tremendously for your time

And to reiterate what Dave said, the host will control what data blocks are sent to you and when. You have no control over this! So, the host might send a data block of 256 samples (or Midi notes), or it might send a data block of 1 sample. The block can be any size. And it sent on the hosts schedule, not yours. You have to design in a way that can accomodate this reallity. Often this can involve a FIFO of some description.

And all of this can vary from one host to another! Do not expect consistent behavior across hosts!

So, as it appears you have realized, data comes in to the processBlock as a group of data. You process it accordingly and pass it along.

Such is the world of DAWs and plugin processing.

2 Likes

Yeah, I get what you’re saying. I’m sure a lot of frustration has happened over the lack of standardization in this

And just to be clear, there is no way to query whether a change happened at some point within the block, right?

Yes, if you follow this forum, you will hear the frustration of developers who had everything working on most DAWs only to find that it did not work on the next DAW they tried! And standarization is not likely to happen since the inner workings of DAWs is proprietary.

Actually, Tracktion/Waveform is something of an exception here, since it is a DAW based on JUCE and the newly updated Tracktion Engine, thus allowing you to see how many things work by examining the code. Of course, this will not give you all the implementation details of Tracktion/Waveform itsellf, but it is far more than can be known with the vast majority of DAWs.

As far as querying the processBlock, you can place some flags in there, assuming you are the developer. But, if you are needing to know what is happening in some third party plugin or DAW, then no.

In Waveform/tracktion_engine at least, changes don’t happen within the processBlock, they precede it.

DAWs may chunk up your process block in to smaller chunks and hence make more fine-grained changes to parameters. This you will just need to deal with. I.e. variable block sizes.

VST3 has a different notion of parameter changes but JUCE doesn’t implement that so it’s not worth discussing here.

I’m not sure if any hosts actually call setParameter and processBlock concurrently but it’s a good idea to code defensively against this anyway. I have some tests in pluginval that test this.