Skip to content

[CLAP] Sidechain input is declared but not forwarded to the DSP #156

Description

@spkfb

[CLAP] Sidechain input is declared but not forwarded to the DSP

Summary

The current CLAP wrapper exposes secondary audio input ports correctly, but sidechain audio is not forwarded to the YUP processor.

A host can provide a secondary CLAP audio input, but the wrapper only processes input buses that have a matching output bus. In a standard sidechain layout with two inputs and one output, the sidechain bus is ignored.

The current AudioProcessContext also exposes only one in-place audio buffer, so processors have no API for accessing auxiliary input buses separately.

Severity

P0 / Blocking

This prevents the implementation of functional sidechain-based processors, including:

  • compressors;
  • gates;
  • dynamic equalizers;
  • envelope followers;
  • external-key detectors.

Typical affected layout

Input 0  Main stereo input
Input 1  Stereo sidechain input
Output 0 Main stereo output

Current behavior

Port declaration is mostly correct

The CLAP wrapper marks only the first input port as the main port:

uint32_t flags = (index == 0)
    ? CLAP_AUDIO_PORT_IS_MAIN
    : 0;

CLAP does not define a dedicated sidechain flag. A secondary input port without CLAP_AUDIO_PORT_IS_MAIN is therefore a valid auxiliary input that may be used as a sidechain.

However, this role is inferred from the port index rather than represented explicitly in YUP's bus model.

The sidechain bus is ignored during processing

The wrapper processes only input buses with a corresponding output bus:

for (uint32_t busIdx = 0;
     busIdx < std::min (
         process->audio_inputs_count,
         process->audio_outputs_count);
     ++busIdx)
{
    const auto& inBus = process->audio_inputs[busIdx];
    const auto& outBus = process->audio_outputs[busIdx];

    // Copy input to matching output.
}

For the standard sidechain layout:

audio_inputs_count  = 2
audio_outputs_count = 1

the loop executes only once.

audio_inputs[0] is processed, while audio_inputs[1], containing the sidechain signal, is never read.

This affects both the 32-bit and 64-bit processing paths.

The processor receives only output channel pointers

The processing buffer is built exclusively from output buses:

wrapper->outputChannelsFloat.clear();

for (uint32_t busIdx = 0;
     busIdx < process->audio_outputs_count;
     ++busIdx)
{
    for (uint32_t channel = 0;
         channel < process->audio_outputs[busIdx].channel_count;
         ++channel)
    {
        wrapper->outputChannelsFloat.push_back (
            process->audio_outputs[busIdx].data32[channel]);
    }
}

AudioSampleBuffer audioBuffer (
    wrapper->outputChannelsFloat.data(),
    static_cast<int> (wrapper->outputChannelsFloat.size()),
    0,
    static_cast<int> (process->frames_count));

Secondary input buffers are therefore not included in the context passed to the DSP.

AudioProcessContext is not bus-aware

The current processing API exposes a single audio buffer:

template <typename FloatType>
struct AudioProcessContext
{
    AudioBuffer<FloatType>& audio;
    MidiBuffer& midi;
    ParameterChangeBuffer& params;
    AudioPlayHead* playHead = nullptr;
};

There is no API for accessing:

  • the main input bus;
  • auxiliary input buses;
  • the sidechain input;
  • separate input and output buses;
  • per-bus channel layouts.

Even if the CLAP wrapper preserved the sidechain samples, a processor could not access them independently.

Expected behavior

The wrapper should support a layout such as:

Input 0  Main Input   CLAP_AUDIO_PORT_IS_MAIN
Input 1  Sidechain    No main flag
Output 0 Main Output  CLAP_AUDIO_PORT_IS_MAIN

During processing, the DSP should receive separate views for:

  • the main input;
  • the sidechain input;
  • the main output.

The sidechain input must remain available even though there is no corresponding output bus.

Proposed solution

1. Add an explicit bus role

Extend AudioBus with a role:

class AudioBus
{
public:
    enum class Role
    {
        Main,
        Auxiliary
    };

    AudioBus (
        Type type,
        Direction direction,
        String name,
        int numChannels,
        Role role = Role::Main);

    Role getRole() const noexcept;
};

The CLAP wrapper can then map the role explicitly:

info->flags =
    audioBus->getRole() == AudioBus::Role::Main
        ? CLAP_AUDIO_PORT_IS_MAIN
        : 0;

This avoids relying on the convention that the first port is main and every following port is auxiliary.

2. Make AudioProcessContext bus-aware

Replace the single-buffer model with separate input and output bus views:

template <typename SampleType>
struct AudioProcessContext
{
    Span<AudioBusBufferView<const SampleType>> inputs;
    Span<AudioBusBufferView<SampleType>> outputs;

    MidiBuffer& midi;
    ParameterChangeBuffer& params;
    AudioPlayHead* playHead = nullptr;

    auto getMainInput() const;
    auto getMainOutput() const;
    auto getAuxiliaryInput (int index) const;
};

A processor could then access the sidechain explicitly:

const auto mainInput = context.getMainInput();
const auto sidechain = context.getAuxiliaryInput (0);
const auto mainOutput = context.getMainOutput();

compressor.process (
    mainInput,
    sidechain,
    mainOutput);

3. Preserve all CLAP input ports

The wrapper should build input views directly from:

process->audio_inputs
process->audio_inputs_count

and output views directly from:

process->audio_outputs
process->audio_outputs_count

Input and output buses must not be paired globally by matching indices.

Only the main input should be copied to the main output when required for non-in-place processing.

4. Handle missing and inactive sidechain buffers safely

The implementation must support:

  • missing channel pointers;
  • inactive auxiliary ports;
  • mono and stereo sidechains;
  • 32-bit processing;
  • 64-bit processing;
  • in-place main processing;
  • separate main input and output buffers;
  • silent sidechain inputs.

An unavailable sidechain should be exposed as silence and must not cause a crash.

Steps to reproduce

  1. Create a YUP processor with:
Main input:      Stereo
Sidechain input: Stereo
Main output:     Stereo
  1. Load the CLAP plugin in a compatible host.

  2. Route a second track to the plugin's secondary input.

  3. Read the auxiliary input from the processor.

Actual result

  • The secondary input may be exposed by the host.
  • The host can provide sidechain samples through audio_inputs[1].
  • The wrapper does not process or forward audio_inputs[1].
  • The processor receives only a buffer based on the output channels.
  • The sidechain signal is unavailable to the DSP.

Expected result

  • The host exposes the secondary input as an auxiliary audio port.
  • audio_inputs[1] is preserved independently from the main input.
  • The DSP can read the sidechain through the processing context.
  • Sidechain-driven gain reduction works correctly.

Acceptance criteria

  • Audio buses can be explicitly marked as Main or Auxiliary.
  • Only the main CLAP port uses CLAP_AUDIO_PORT_IS_MAIN.
  • All CLAP input ports are forwarded to the DSP.
  • Input ports without matching output ports are not discarded.
  • Main and auxiliary inputs remain separate.
  • AudioProcessContext exposes individual input and output buses.
  • The processor can access the first auxiliary input as a sidechain.
  • Mono sidechain input is supported.
  • Stereo sidechain input is supported.
  • 32-bit processing is covered by tests.
  • 64-bit processing is covered by tests.
  • Missing or inactive sidechain buffers produce silence without crashing.
  • In-place and non-in-place main processing are supported.
  • The resulting plugin passes clap-validator.
  • Sidechain routing is manually validated in at least two CLAP hosts.

Regression tests

Add an automated sidechain processing test with:

Main input: constant signal
Sidechain:  silence, followed by an impulse or sine wave
Expected:   gain reduction occurs only while the sidechain is active

The test should verify:

  1. the main input reaches the processor;
  2. the auxiliary input reaches the processor independently;
  3. changing the sidechain signal changes the DSP result;
  4. disabling the sidechain restores unkeyed behavior;
  5. the main output does not accidentally contain copied sidechain samples.

Additional note

The CLAP port declaration alone is not the primary issue.

A secondary port without CLAP_AUDIO_PORT_IS_MAIN is a valid auxiliary input. The blocking issue is that the wrapper discards auxiliary input data before invoking the processor.

The complete fix therefore requires changes to both:

  • the CLAP audio-buffer transport;
  • YUP's internal multibus processing API.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions