Vibe Coding Log - Sharing Journey - Refining Audio Routings - Explicit Buses, etc

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

Touch The Universe wrote: Tue Jun 09, 2026 7:59 am What could be causing this weird release behavior? Sounds like gating?
Not having seen that video, not knowing what the code looks like, I'm confident that automated tests could have caught that. Then you can fix problems as they surface, and be assured the whole big bowl of mud still behaves as expected with every small change.
Automated testing is a good industry-standard software engineering practice. It takes an investment to implement, an investment that will pay itself back. Just saying...
We are the KVR collective. Resistance is futile. You will be assimilated. Image
My MusicCalc is served over https!!

Post

BertKoor wrote: Tue Jun 09, 2026 11:06 am
Touch The Universe wrote: Tue Jun 09, 2026 7:59 am What could be causing this weird release behavior? Sounds like gating?
Not having seen that video, not knowing what the code looks like, I'm confident that automated tests could have caught that. Then you can fix problems as they surface, and be assured the whole big bowl of mud still behaves as expected with every small change.
Automated testing is a good industry-standard software engineering practice. It takes an investment to implement, an investment that will pay itself back. Just saying...
Not even sure what you are refering to. Im still on this little bug after more passes then I care to share. I'm here atm. I'll look up what you are refering to.




Ran 27 commands
Now I have the complete picture. Let me synthesize everything I found:


Ran 4 commands
Ran 4 commands
Now I have everything needed. Here is the complete analysis:

Root cause found
The choke/gate is caused by three independent problems stacking in the shared Filter 1 path. None of them exists in Vital's self-contained per-voice SVF path.

Problem 1 — The audio entering Filter 1 is already amplitude-gated by selectedAmpGain
Signal chain with Shared mode + Filter 1:

vitalL (raw oscillator)
→ voiceL += vitalL [no amp yet — line 15003]
→ voiceL *= finalVoiceAmpGain [= selectedAmpGain = Amp1/4 envelope × velocity]
→ mixedL
→ left = mixedL * masterGain
→ filter rawL = left [Filter 1 reads this]
→ f1L = processFilter(rawL)
→ finalL = f1L * outputGain
The amplitude envelope selectedAmpGain is multiplied in before Filter 1 processes the audio. When the amp envelope releases (decays to zero), the signal feeding Filter 1 goes to zero — even if the filter cutoff is still open. This is amplitude gating masquerading as a filter gate.

With Self Amp + Filter1 mode:

vitalL
→ voiceL += vitalL * vitalSelfAmpGain [VA envelope gates here instead]
→ voiceL *= 1.0f [finalVoiceAmpGain bypassed — line 16785]
→ passes through Filter 1
finalVoiceAmpGain is forced to 1.0f because vitalSelfAmpOwnsTail = true. So the VA envelope gates the signal cleanly per-voice, and Amp 1/4 can't choke it after the fact.

Problem 2 — sharedFilterEnvBlock uses jmax accumulation, making it track the "loudest" voice's envelope
cpp
sharedFilterEnvBlock = juce::jmax(sharedFilterEnvBlock, filterEnvForSharedBridge);
This is a shared single-voice accumulator for a polyphonic instrument. When you play two notes and release one, the surviving note's filter envelope keeps Filter 1 open. When the last voice releases, the envelope falls from some non-neutral level toward 0, passing through applyFilterEnvRange which interprets envLevel=0.0 as maximum negative mod relative to the 0.5 center.

In applyFilterEnvRange:

cpp
const float bipolarEnv = (envLevel - 0.5f) * 2.0f; // 0.0 → bipolarEnv = -1.0
So sharedFilterEnv = 0.0 → bipolarEnv = -1.0 → cutoff modulation is maximum negative — the filter closes as hard as the env amount and range allow. With high env amount and positive polarity, a decaying env approaching 0 pulls the cutoff sharply downward during release, acting like an amplitude gate.

In the Vital per-voice SVF path, the envelope is unipolar [0..1] applied multiplicatively to octave offset. envLevel = 0.0 simply means 0 octaves of modulation — it returns to baseCutoff. The filter cannot be closed below base cutoff by a decaying envelope. There is no such symmetry break.

Problem 3 — No per-sample smoothing on Filter 1 cutoff at the shared filter section level
The Vital SVF path has an explicit declick smoother on the envelope modulation signal:

cpp
const float smoothMs = 0.20f + declick * declick * 64.0f;
const float coeff = std::exp(-1.0f / (sampleRateHz * smoothMs * 0.001f));
voice.oscVitalFilterEnvSmoothed += (envShaped - voice.oscVitalFilterEnvSmoothed) * (1.0f - coeff);
The shared Filter 1 section receives sharedFilterEnvBlock and immediately passes it to applyFilterEnvRange with no smoothing. Large envelope drops (end of release) produce instant per-sample cutoff jumps across many octaves. At IIR filter cutoffs that jump discontinuously, this causes output transients that sound like a hard gate click.

Answering your specific questions
1. Where exactly does Filter 1 differ from Vital's self-contained VCF/SVF path?

Vital SVF Shared Filter 1
Amp gate before DSP vitalSelfAmpGain (VA env), OR nothing if Self Amp + Filter1 routes selectedAmpGain (Amp1/4 env) gates audio before filter reads it
Envelope modulation center Unipolar [0..1], env=0 = base cutoff, cannot go below base Bipolar [0.5=center], env=0 = max negative mod, can close far below base
Envelope accumulation Per-voice, voice-owned ADSR instance jmax across all voices into one shared block
Cutoff smoothing Optional declick one-pole per voice None at the shared filter section
Voice lifetime Vital local ADSR owns tail releaseCleanupEnv can kill voice before filter tail finishes
2. Is Filter 1 envelope modulating output gain, not just cutoff?

Yes, indirectly. selectedAmpGain multiplies the audio before Filter 1 receives it (line ~16787–16789 → voiceL *= finalVoiceAmpGain → left → rawL). So the amp envelope is effectively a pre-filter gain gate. When selectedAmpGain reaches zero, Filter 1 receives silence regardless of cutoff.

3. Is Filter 1 output multiplied by Amp 1 / selectedAmpGain after the local Vital amp?

Not directly post-filter — but the pre-filter multiplication achieves the same effect. outputGain at line 17862 (finalL = outL * outputGain) is the FILTER OUTPUT knob, not an ADSR, so it doesn't choke.

4. Is there code that clears, zeros, or kills filter output during release?

Yes — two places:

voiceL *= finalVoiceAmpGain (Amp 1/4 going to zero) starves Filter 1 of input
The releaseCleanup check at line 8909 can kill the voice (voice.active = false) while the filter's internal delay-line state still has energy. The filter state is not cleared, so on the next note the old tail may bleed through briefly
5. Is the Filter 1 envelope bipolar/centered in a way that closes the filter below base cutoff?

Yes. This is the central bug. applyFilterEnvRange converts envLevel to bipolar around 0.5. An envelope decaying toward 0 produces bipolarEnv = -1.0, which with positive amt still results in sign = -1, closing the filter. The Vital SVF env is unipolar and cannot produce this behavior.

6. Does the filter path have smoothing comparable to Vital VCF?

No. Vital SVF has per-voice declick smoothing on the envelope modulation. The shared Filter 1 section applies sharedFilterEnvBlock directly each sample without smoothing.

7. Minimal patch to make Filter 1 behave cleanly without full refactor?

Three targeted changes:

cpp
// PATCH A — In applyFilterEnvRange: treat env=0 as neutral (don't close below base).
// Change the bipolar center so a decaying envelope closes toward base, not below.
// Replace:
const float bipolarEnv = juce::jlimit(-1.0f, 1.0f, (envLevel - 0.5f) * 2.0f);
// With (unipolar: 0=base, 1=max open/close):
const float bipolarEnv = juce::jlimit(-1.0f, 1.0f, (envLevel * 2.0f - 1.0f) * amt >= 0.0f ? envLevel : -(1.0f - envLevel));
// Or more simply: use envLevel directly as a unipolar [0..1] depth, same as Vital SVF:
const float depth = std::abs(envLevel) * std::abs(amt);
const float sign = amt >= 0.0f ? 1.0f : -1.0f;
// (drop the bipolar conversion entirely for the shared bridge)
cpp
// PATCH B — Add a one-pole smoother on sharedFilterEnv before applyFilterEnvRange.
// In the filter section loop, add a per-filterState smoothed value:
const float smoothCoeff = std::exp(-1.0f / (sampleRateHz * 0.005f)); // ~5ms
st.sharedFilterEnvSmoothed = st.sharedFilterEnvSmoothed * smoothCoeff
+ sharedFilterEnv * (1.0f - smoothCoeff);
const float smoothedEnvForFilter = st.sharedFilterEnvSmoothed;
// Use smoothedEnvForFilter in place of sharedFilterEnv in applyFilterEnvRange calls.
cpp
// PATCH C — In Shared mode with oscVitalRouteFilter1, apply finalVoiceAmpGain
// AFTER the filter section rather than before, so the amp envelope cannot
// pre-starve Filter 1. This is effectively what Self Amp + Filter1 already does,
// just with selectedAmpGain instead of vitalSelfAmpGain.
// The cleanest version: when Vital is in Shared mode routed through Filter 1,
// treat it the same as Self Amp + Filter1 — move the amp gate post-filter.
Patch A removes the bipolar behavior that closes the filter past base during release. Patch B removes the cutoff jump clicks. Patch C removes the pre-filter amplitude starvation. Together they make the shared Filter 1 path behave like the Vital per-voice SVF path without touching the polyphony architecture.





EDIT - THIS WAS NOT THE SOLUTION

TRASNFERING THE FILTER 1 MENU INTO A PARELLE FILTER/ENV SYSTEM THAT IS CLEAN
100 High Quality Soundsets: Omnisphere 2, Dune 3, Tone 2 Synths, Pigments, Uhe Synths, Halion, Spire, and others.

TTU Youtube

Post

Touch The Universe wrote: Tue Jun 09, 2026 7:59 am Hoping to get some help. What could be causing this weird release behavior? Sounds like gating? Then I switch to an osc that has its own ADSR and its the normal release behavior.
Asking Urs to fix the monstrosity is not vibe coding :D

Post

at first it's weird or even awkward, then it gets better, then "OMG I LOVE THIS, I'M THE KING OF THE WORLD"
and then things go downward

where else have i seen this sort of pattern

but more seriously
before i could write C++, i found a modular "visual-programming" environment that can make synthesizers and effects called SynthEdit (who doesn't know it)
after i figured out what's going on i began making a "huge MONSTER monosynth" (literally i called it "monster", it didn't have a name chosen yet but this hinted at the goal, it had to have EVERYTHING you might want in a monosynth)

i was similarly euphoric and happy as what you can see in the early pages here
then progress kinda collided with a wall
some things were too complex to be done with normal modules (too much overhead piles up or something) and were much better to be done in a custom module...
other things turned into just dumb ideas that only sounded good as an idea but evil ugly problems come out when you get to the actual details (which you don't do when you're just dreaming and everything looks wonderful, and the path from A to B is a straight line)

i went to the forums and mailing lists where the module developers were "hiding" (actually they were discussing developement).
so i go there with my much-more-broken-than-now engrish and ask for a custom module that does this and this and that but in a certain way (i don't know how but you'll figure it out)... <crickets.wav>
reality struck when i was just told "look dude, no one in here has the free time to make a custom module for you, so download the SDK and write one yourself, like we all do here", and when i told them "but i don't know C++" - "then learn it"

anyway, some of the important lessons i've learned the hard way is...
if something is very big (maybe even if it isn't), you should make a plan, this can even be just a plain-text file
write everything then look for potential problems in the plan (put on your pessimistic critique hat and try to find areas where this optimistic dude will fail)
in this process the plan will probably morph, and if you're lucky you might end up with a very detailed and maybe realistic plan which can actually be used to develope/build something!

and a tip - don't try to put everything in a synth, or at least not in your very very first synth
instead, make something that might have a more limited usage, but it does what it does well
a smaller synth will have less places for bugs to hide, and will be easier to test and debug
It doesn't matter how it sounds..
..as long as it has BASS and it's LOUD!

irc.libera.chat >>> #kvr

Post Reply

Return to “DSP and Plugin Development”