No, that’s not at all what I meant.
Currently, you get one value per buffer, that’s the parameters value at sample 0 in this buffer. It’s just a snapshot, and you don’t know anything else.
The automation curve in the DAW could be a continuous ramp, or it could be an instantaneous change to a new value. With sample accurate automation, you can tell these two scenarios apart. This is a major benefit we’ll get with support for sample accurate automation.
@eyalamir Yes, the current way things work is the fallback, and it needs to stay in place for all the cases where the final automation curve is not known (ie UI interactions or realtime control of the parameter in the DAW, you can’t have control points in the future then).
Smoothing-Sidenote: You can dynamically adapt your smoothing coefficient. Automation gives you one value per buffer, and you know the buffer size. UI interaction will give you something that may or may not depend on framerate, mouse update rate, and so on - it’s probably a good idea to smooth this much more. With sample accurate automation, we gain a third case that can be handled more cleverly
Sure, but at the start of the block no matter what happens you will need to have that value set, no matter if a smooth curve is about to come in or not. So at sample pos #0 it really doesn’t matter if you call param.load() or eventList.interpolateCurveAtPos(0).
Since at the start of the block you still have no information on what happened between the previous call and current in terms of the UI, you will have to do an initial read for all your parameters right there and then, because any of them may have changed either by means known to the host, or means known to you.
You can, but this would be a very very difficult thing to manage.
First, If you’ve ever done midi learn on your parameters, you know that even though the MIDI itself is sample accurate, the way different controllers send you those values is anything but smooth.
Even in sample accurate automation, your users don’t always know about the smooth curves that would make your DSP sound right, and they’re more than happy to write abrupt movements, especially when they’re used to all other internal and external plugins in the world smoothing things for them.
You could expose this UI that tells the user “If you know what you’re doing, click here”, and then adjust your internal state with a way to store the user’s selection, and pass that into all your smoothers so they don’t smooth or smooth differently.
I just suspect this would be a major development overhead (UI, state, DSP) with very little value to the user (most of the time their song will sound worse with no smoothing), unless your plugin is geared toward modular synthesizers and places where users expect this kind of granular control.
Hey,
I’ve seen (and handled) all sorts of scenarios in various kinds plugins. The most complex of them is (no surprise) a modular system that processes modulation at audio rate, supports feedback loops, and can process hundreds of ops per sample in realtime. It behaves deterministically, declicks properly, performs latency compensation on the graph (also on modulation, MIDI, and transport data), and has correct reset behavior. For this reset behavior and some other cases, I’m using dynamic smoothing, because a reset is one of the situations in which smoothing has to be temporarily disabled for instant takeover of an initial value. I’m also using different smoothing coefficients depending on the type of smoothed parameter and desired “sound”. It definitely took me more than one day to get this right and be fast to calculate It’s a difficult thing to integrate, but it’s not impossible.
Since from my experience, users just expect stuff to just magically work, I tend to hide all these mechanics, as you mentioned.
Anyways, back on topic: I disagree that there is a necessity to always fetch the current parameter value at sample 0. The VST3 documentation explicitly mentions that automation can be stateful, only changes need to get sent. Read the section on “Implicit points” in the docs about IParamValueQueue (posted before). This means, it’s left to the host to provide the necessary data. As long as your plugin handles all changes correctly (including reset), there are performance gains when not always loading all parameter values. The devil is in the details.
that’s what i meant yesterday, too. i mean why are we even asking for sample accurate parameters? not just to have a new little marketing gimmick, but for real applications. and i wouldn’t say the user lowkey touching their master keyboard knob to record a rough automation via midi learn or by dragging a knob on the interface is something where sample accuracy matters. you barely hear the difference when the modulation is already rather natural. it only makes a difference when you make something really snappy, like ramping up and then drop it down hard. with sample accurate parameters this will sound consistent for different block sizes, like eyal already said. but my additional point is that we also get away with shorter parameter smoothing due to more control over the parameter updates. cause idk if you noticed, but when you use really short parameter smoothing on a large buffer then that can sound pretty steppy
Exactly that. I’ve worked on one project once (a generator style plugin) that had a button to trigger a sound, and it was controlled by automation. Bad luck when both rise and fall of the parameter happen within a single block, which is more likely on large buffer sizes: The plugin simply doesn’t see it and nothing happens.
Two other often seen use cases: “Snappy trance gates” and “Custom ducking curves”, when created as automation curves. It’s terribly annoying that these kinds of precision-demanding curves are quantised to the buffer size. Explaining this to users, who always expect things to magically work as expected, is no fun for the support team Doing this with MIDI is not a real alternative (tons of other shortcomings).
Sure. But anyway if you already have such scheme in place it’s probably something you can implement even today (for example, implement a different smoothing with MIDI, which won’t change with automation being sample accurate) and then just write an additional layer that calls the same function in the appropriate automation situations, if the host/JUCE will provide you that information.
If you know a parameter curve started 10 blocks ago, and is still going up, you can definitely keep processing correctly without getting another point from the host. If you’ve ever implemented an ADSR, for example, you’ve experienced that.
However, in a current JUCE plugin that has more mechanisms in place, a change could come in from places that aren’t the host and have nothing to do with VST3, like MIDI, presets, GUI, OSC.
So you will have to read whatever is in param.getValue(), or alternatively JUCE will have to add an extra point under the hood at sample #0 to represent that change in param.getValue() between two blocks, otherwise your block will ignore those messages and process incorrect parameter values.
We want sample accurate information because the host already provides them and they’re important to process the control values the user is sending in the correct time inside of the block.
Things like creating a plugin that can both smooth and not smooth are interesting from a product design standpoint. You can also try those today with existing sample accurate mechanisms like MIDI and Side Chain and see if you think that those are beneficial to your product and users.
My point is: smoothing is something you do in your plugin internally. What we need from JUCE is access to the full information from the host to decide whatever we want to do with it.
Thought I’d chip in. Conceptually, automation is an automatic way of automating parameter modification that we’d normally do by hand. When automating by hand you’d normally have smoothing for certain parameters to avoid zipper artefacts and so on. Considering that automation could be any modulation (square steps, or even noise) you’ll still want to smooth internally.
As for sample accurate parameters it’s worth remembering that the parameter sample value is your target for smoothing. In a pathological case when there’s lot’s of distinct samples you may never actually hit a target but just reach an equilibrium between them (unlikely in realiistic usage, but still).
Finally, how to handle sample accurate data in your DSP loops. One way is to have a buffer allocated for each parameter that’s the size of your audio block. Then, at the start of processBlock you fill out these buffers with smoothed values. Inside DSP loops you just read out values from these buffers. It’s trivial to organise to only do this for changing values. The benefit of doing this is that you don’t have to care about smoothing inside DSP code and the same technique applies well to mod matrixes.
I’ve been using sample-accurate automation for a few years now.
In general it’s up to the DAW to specify up front all the changes to a parameter that happens during a process call. And on the subsequent process call it is implicit that each parameter remains at the the final value that it ended up at the end of the previous process call.
What I’m saying is that with sample-accurate parameters you can forget about ‘polling’ the value of a parameter, and in the case where the plugin receives no events you don’t need to check for updates ‘just in case’ because parameters simlpy don’t change without an event.
Right, so what I was suggesting is that a message from the GUI, etc, could show up at sample pos #0 to avoid such polling.
So, your UI calls param->setValueNotifyingHost(), the host knows about it, and gives you an event back at sample #0.
However, some parameters in a JUCE plugin aren’t known to the host, like internal parameters. So for those, you would either have to read them at sample pos #0 by yourself, or somehow connect them to the internal JUCE system so JUCE will do the polling for you and report that message at sample pos #0.
Even with parameters known to the host, are also some issues to do with timing here.
Currently if you call setValueNotifyingHost from the UI.
That message is on the message thread, and it’s possible that by the time the host receives it the next block will pass until the host can prepare a timestamped event for your plugin.
In the JUCE system, the parameter is atomic and is changed instantly, so I suspect using the current system (atomics) for UI messages would still prove to be more responsive, and will eliminate bugs related to internal parameters.
In many of our plugins there are actually many ‘sub plugins’ grouped together to create one product. So it’s very common that a processor doesn’t even know if its parameter is reported to the VST3 host or to some internal host that never gets synced with time stamped events the way Cubase might be.
I think that having two different systems for setting parameters in use at the same time is likely to lead to race conditions and extra complexity.
So regarding internal ‘private’ parameters, it might be easiest to use an event-driven system also.
i.e. both DAW-provided, and internally-provided parameter changes should behave as much as possible in the same manner.
in my VST3 plugins, I have a stream of events coming from the DAW (‘public’ parameters), and also a stream of parameter-change events coming from the GUI (‘private’ parameters only). And they get merged into a single interleaved list.
This seems simpler to me.
I agree on that one, but the JUCE current parameter system (single shared atomic) isn’t going away without rewriting JUCE, so we’re ‘stuck’ with that one being the main driven behind most JUCE systems.
So I’m a practical person. Rewriting JUCE from scratch won’t happen. Adding an array of parameter changes with sample positions is additive and practical.
Yes, that’s pretty much what I’m suggesting. There needs to be away to hook up singular values from he ‘old’ system/UI values into this system, and to make it work those values should also be ‘user’ values that may not be known to DAW.
You could have something like registerCustomParam(param) for your special parameters at the constructor and then just read the queue.
Being able to read all your parameters in two ways (when you care about sample accuracy and when you don’t care) where each is a ‘view’ over the same data sounds like a good idea to me.
I agree, the current system is needed for legacy support, in parallel to the new system.
I just want to be able to choose one or the other, not an unholy mixture of the two.
Right!
Since JUCE is used for hosting and not just for plugins, to implement this properly, JUCE would need to add a mechanism that works on the host side as well.
I assume that kind of mechanism would allow to write the sample accurate values so that the sample accurate system would know about them.
But it would also allow a plugin to write their own values in case it keeps a ‘secret’ list of parameters that it wants to have read inside of the sample accurate queue.
How are VST3 non-JUCE plugins handling private parameters/parameters that are GUI-only, etc?
For GUI-only items, we didn’t use parameters at all. (Well, we did for a number of old ones, for backwards compatibility with really old versions where everything was a parameter!) We used VSTGUI, and just responded to button clicks, etc., in our own code and set values in our editor (or in the processor class, in some cases). I still prefer that method in general for things the host doesn’t need to know about, but using the APVTS is just more convenient in some cases because of the parameter attachments we can use. But what I don’t like is that some hosts (Studio One using AU, for example) show even the NON-automated parameters in their automation lanes. For those, we give the parameters names like “n/a”.
Right. My solution for that is to add those parameters to a ‘dummy’ processor the host knows nothing about. That gives me a way to still safely pass messages from GUI to the processor, or from one processor to its sub processors, but the host is never involved in this.
Yeah, Like Eyal, I find that my code is simpler and more consistent when “everything is a parameter”.
In my case, with ‘private parameters’ the GUI passes a message to the Processor that contains:
The parameter ID
The new parameter value.
and the processor creates a sample-accurate event for that update (at block position 0), and merges it with the updates from the DAW at the start of the next process() call.
I like that the Processor doesn’t need special-case code for handling two types of updates (there is only one code path to unit-test), and that one has the flexibility to switch a parameter from ‘public’ (exposed to the DAW) to ‘private’ without much work.