ClipEffects: Build render Nodes at render time so plugins get the real sample rate - #432
Merged
drowaudio merged 2 commits intoSep 21, 2026
Merged
Conversation
…l sample rate (fixes #417) AudioNodeRenderJob took a fully built Node at createRenderJob time, but a PluginNode initialises its plugin in its constructor, so the plugin was prepared before the render sample rate was known. PluginEffect and PitchShiftEffect passed ProcessState's placeholder 44100.0, and VolumeEffect passed the source file's rate - which is 0 when the source is an earlier effect stage that hasn't rendered yet. Plugin::baseClassInitialise only re-runs initialise() when the rate or block size changes, so the wrong rate could not be corrected later; PluginNode::prepareToPlay just asserted the mismatch and carried on, leaving anything rate-dependent in the plugin (delay lines, filters, oversampling) silently wrong in the render. AudioNodeRenderJob::initialise now takes a builder function which is invoked from createAndPrepareRenderContext, where the source has been rendered by any preceding jobs, and is passed the sample rate the render will actually use. All five effects that used the job build their Nodes from there, which also fixes the WaveNode channel count being read from a not-yet-rendered source. Added a jassert in PluginNode::initialisePlugin so an invalid rate or block size is caught at the point the contract is broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #432 +/- ##
===========================================
+ Coverage 59.81% 60.42% +0.61%
===========================================
Files 568 568
Lines 79964 80082 +118
Branches 12381 12381
===========================================
+ Hits 47828 48390 +562
+ Misses 32136 31692 -444 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The previous commit moved Node building for VolumeEffect, FadeInOutEffect, StepVolumeEffect, PitchShiftEffect and PluginEffect into a builder that only runs when the render runs, and only the PluginEffect path had a test. Adds a test case that renders each of the other types through ClipEffects and checks the proxy keeps the source's sample rate and channel count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
drowaudio
deleted the
bugfix/issue_417_clip_effect_plugin_sample_rate
branch
September 21, 2026 16:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
PluginEffectclip effect prepared its hosted plugin at the wrong sample rate, so anything rate-dependent inside the plugin (delay lines, filters, oversamplers) was silently wrong in the rendered file.AudioNodeRenderJob::initialisetook an already-builttracktion::graph::Node, so every effect built its graph insidecreateRenderJob- before the render sample rate was known. Node building is now deferred tocreateAndPrepareRenderContext, which knows the rate.Root cause
PluginNode's constructor callsinitialisePlugin (sampleRateToUse, ...)→plugin->baseClassInitialise (...), so the plugin is initialised at construction time. The rates the clip effects were passing in were all wrong:PluginEffectandPitchShiftEffectpassedjob->processState.sampleRate, which isProcessState's placeholder default of44100.0- the render's real rate is only set later, from the writer.VolumeEffectpassedsourceFile.getInfo().sampleRate, which is0when the source is an earlier effect stage's output file that hasn't been rendered yet.Plugin::baseClassInitialiseonly re-runsinitialise()when the rate or block size changed, so the constructor's wrong rate could not be corrected by preparing the plugin again later.PluginNode::prepareToPlaythen hitsjassert (sampleRate == info.sampleRate)and carries on regardless.The same "read the source file too early" problem affected the channel count:
createWaveNodeForFilereadsfile.getInfo().numChannels, which is0(clamped to mono) for a not-yet-rendered chained source.The change
AudioNodeRenderJob::initialisenow takes aNodeBuilder-std::function<std::unique_ptr<Node> (AudioNodeRenderJob&, double sampleRate)>- instead of a Node. The old Node-taking overload is gone, so the mistake can't recur.createAndPrepareRenderContextreadssource.getInfo().sampleRate(valid by then - preceding jobs in the chain have run) and passes it to the builder.VolumeEffect,FadeInOutEffect,StepVolumeEffect,PitchShiftEffectandPluginEffectall build their Nodes from the builder.FadeInOutEffectandStepVolumeEffectcapture theirCachedValueproperties by value up-front rather than capturingthis.PluginNode::initialisePlugingainedjassert (sampleRateToUse > 0.0)/jassert (blockSizeToUse > 0)so a bad rate is caught where the contract is broken, not three layers down.Regression test
ClipEffects: PluginEffect is prepared at the render sample rateintracktion_ClipEffects.test.cpp. It registers a test-onlySampleRateProbePluginthat records thePluginInitialisationInfoit was initialised with, renders a clip effect chain over a 96 kHz source (deliberately not 44100, so theProcessStatedefault can't accidentally pass), and checks:Two subcases: the plugin effect on its own, and chained after a
VolumeEffect(the case where the source file doesn't exist yet when the jobs are created).Before the fix both subcases fail with
44100 == Approx( 96000 ); after, they pass.Testing
TestRunner --no-juce-tests: 370/370 test cases, 21280 assertions, all passing in Release. ClipEffects, Plugins, ExternalPlugin, RackNode, RackInstance and EditNodeBuilder suites also run clean in Debug, so the new assertions don't fire anywhere else.Fixes #417
🤖 Generated with Claude Code