google / google/eclipsa-audio-plugin
Renderer leaves output channels above its output bus count unwritten, passing raw substream audio through
- Dominant language
- C++
- Stars
- 90
- Forks
- 16
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 1
Description
## Summary
`RendererProcessor::processBlock()` writes only the first `totalNumOutputChannels` channels of the buffer JUCE hands it and never clears the remainder. When the output bus is narrower than the input bus, which is the normal case for this plugin, the channels above the output count retain whatever the host placed there. On a wide track this means the raw pre-render substream audio remains visible downstream instead of being silenced.
Other binaural renderers I have used (MPEG-H reference renderer, Fiedler Audio Dolby Atmos Composer, and various ambisonic binauralizers) explicitly zero the channels they do not write, so a 16 channel track running a binauralizer meters on two channels only. Eclipsa currently does not, so the track continues to meter on all 16.
## The code
```cpp
// rendererplugin/src/RendererProcessor.cpp, processBlock() (lines 216-265, condensed)
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
// In case we have more outputs than inputs, this code clears any output
// channels that didn't contain input data ...
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear(i, 0, buffer.getNumSamples());
// ... processing into processingBuffer_ ...
int channelsToOutput =
juce::jmin(totalNumOutputChannels, buffer.getNumChannels(),
processingBuffer_.getNumChannels());
for (int ch = 0; ch < channelsToOutput; ++ch) {
buffer.copyFrom(ch, 0, processingBuffer_, ch, 0, buffer.getNumSamples());
}
```
That first loop is the stock JUCE Projucer boilerplate, which is written for the common case of an output bus wider than the input bus (mono in, stereo out). It clears the range `[totalNumInputChannels, totalNumOutputChannels)`.
This plugin's normal configuration is the opposite. `ProcessorBase::getHostWideLayout()` (`ProcessorBase.h:76`) fixes the input bus at ambisonic order 5 (36 channels) in a standard build, or order 3 (16 channels) under Premiere. (A separate `ECLIPSA_LOGIC_PRO_BUILD` returns 7.1.4 instead and uses the same layout for its output bus, so it is not affected.) The output bus meanwhile is declared stereo in the `RendererProcessor` constructor (`RendererProcessor.cpp:32-36`) and, on REAPER, re-negotiated by `configureOutputBus()` (`RendererProcessor.cpp:521`, which returns early on every other host) to match the selected speaker layout - 2 channels for Binaural. With input 36 and output 2, the clear loop's range is empty and never executes, and the copy back writes channels 0 and 1 only. Channels 2 through 35 are never touched anywhere in the function.
## Suggested fix
Clear the tail after the copy back:
```cpp
for (int ch = channelsToOutput; ch < buffer.getNumChannels(); ++ch)
buffer.clear(ch, 0, buffer.getNumSamples());
```
One caveat worth raising, since it concerns the same function: the comment above the copy-in loop notes that "ProTools makes channels beyond the playback layout channel read-only in the buffer", and that is exactly the range this tail clear writes to. On AAX it may need to be gated on the host, or handled wherever the plugin is actually permitted to write. The leak itself was observed on the VST3 build in REAPER.
Equivalently, an unconditional `buffer.clear()` works if it is placed after the input has been copied into `processingBuffer_` and before the copy back, so that the copy back repopulates the channels that carry signal. That is the pattern already used one layer down in `RenderProcessor::processBlock()`, which reads its input at `:221` and only clears at `:259`, immediately before writing its output:
```cpp
// common/processors/render/RenderProcessor.cpp:259
buffer.clear();
// then selectively copy only the channels that carry real signal
```
Note the placement matters: an unconditional `buffer.clear()` at the *top* of `RendererProcessor::processBlock()`, where the boilerplate partial clear currently sits, would zero the input before `processingBuffer_.copyFrom(ch, 0, buffer, ...)` reads it, and the plugin would output silence.
`RenderProcessor`, the chain stage that does the actual rendering, already clears everything it does not write. Only `RendererProcessor::processBlock()` - the plugin's own top-level `processBlock`, the one JUCE hands the host buffer to - is missing it.
## Why the leftover channels are not harmless scratch
It would be reasonable to assume the host simply ignores channels above the declared output bus count. That is not what happens in REAPER, and it is worth being explicit about since it is the condition under which this becomes visible.
REAPER leaves channels above a plugin's output count **passing through unchanged** rather than zeroing them. Clearing what you do not write is the plugin's responsibility. JUCE's own boilerplate comment says as much, that unwritten channels "aren't guaranteed to be empty". So the consequences go past metering:
- The meters do not reflect what the plugin is actually producing, which makes the plugin's real output impossible to read at a glance.
- Anything fed from that track, whether another plugin, a hardware output or a bus, receives raw un-rendered ambisonics on the remaining channels alongside the binaural pair.
- A render or export from that track carries the leaked channels too, producing a file whose first two channels are binaural and whose remainder is untouched source material, with nothing to indicate it.
## Reproduction
1. 16 channel track carrying a 3OA Audio Element, Renderer instance on it.
2. Set Speaker Setup to Binaural, and the element's Headphone Spatialization to Binaural (3D Spatial).
3. Play, and watch the track meters.
Expected: two channels metering.
Actual: all 16 meter, with channels 3 to 16 carrying the raw pre-render ambisonic signal.
This has been reproduced independently by a second user on different hardware and a different session, so it is not specific to one routing setup. Both of us also confirmed the contrast with other binaural renderers: MPEG-H's reference renderer and Fiedler Audio's Dolby Atmos Composer both meter on two channels only in the same situation, regardless of track width.
The only current workaround is forcing the track or master to 2 channels, which defeats the point of authoring on a wide bus.
Related report: #126 (binaural output silently digital black below 32 sample buffers) - found in the same investigation and setup. That one sits in `RenderProcessor`/`BinauralRdr`, this one in the outer `RendererProcessor::processBlock()`, so a pass over the render path could address both.
Contributor guide
Research direction
Start in rendererplugin/src/RendererProcessor.cpp at RendererProcessor::processBlock(), then compare its buffer handling with common/processors/render/RenderProcessor.cpp:259. Verify the VST3 behavior in REAPER with a wide input bus and binaural output, while checking the ProTools read-only-channel caveat. Done means only the declared rendered output channels carry signal and the remaining channels are silent without clearing input data too early.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- audio-video-rtc
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100