How to apply effects to just one effect in a chain of effects?

I have 2 samplers like this on my audio processor:

void WiringProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiBuffer) {
        sampler1->processBlock(buffer, midiBuffer);
        sampler2->processBlock(buffer, midiBuffer);
}

but I cannot apply separate gains to each one. If I apply a gain to sampler2 like this:

void HelloSampler::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiBuffer) {
       buffer.applyGain(myGain);
       //..
      sampler.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());

}

where juce::Synthesiser sampler;

then I don’t hear sampler1, because buffer on sampler2 had the buffer from sampler1.

How can I apply things to one sampler only?

Hi,

The problem is that your HelloSampler should not apply directly the gain to the the audio buffer that is input to it. You can for instance :

  • 1/ Render your sampler into a temporary buffer : sampler.renderNextBlock(tempBuffer, midiMessages, 0, buffer.getNumSamples());
  • 2/ Then add the temporary buffer with gain to the buffer that is passed to your processBlock, buffer.addFrom (..., tempBuffer, ...) see this method : JUCE: AudioBuffer< Type > Class Template Reference

Hope this helps

void HelloSamplerAudioProcessor::processBlock (juce::AudioBuffer<float>& bufferOriginal, juce::MidiBuffer& midiMessages)
{

    juce::AudioBuffer<float>& buffer(bufferOriginal);
    sampler.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());

    for (int i=0; i<buffer.getNumChannels(); i++)
        bufferOriginal.addFrom(i, 0, buffer, i, 0, bufferOriginal.getNumSamples());
}

but I get

I/JUCE: JUCE Assertion failure in juce_AudioSampleBuffer.h:754

at

void addFrom (int destChannel,
              int destStartSample,
              const AudioBuffer& source,
              int sourceChannel,
              int sourceStartSample,
              int numSamples,
              Type gainToApplyToSource = Type (1)) noexcept
{
    jassert (&source != this || sourceChannel != destChannel);//line 754

I think I don’t get buffer channels. Shouldn’t I connect the same channels of each buffer? And shouldn’t I start at sample 0 on both?

I don’t know why but I had to do like this:

void HelloSamplerAudioProcessor::processBlock (juce::AudioBuffer<float>& bufferOriginal, juce::MidiBuffer& midiMessages)
{
    juce::AudioBuffer<float>& buffer(bufferOriginal);
    sampler.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
    buffer.applyGain(gainValue.load());
    bufferOriginal.addFrom(0, 0, buffer, 1, 0, bufferOriginal.getNumSamples());
    bufferOriginal.addFrom(1, 0, buffer, 0, 0, bufferOriginal.getNumSamples());
}

somehow I had to wire the buffer's channel 1 to originalBuffer's channel 0 and vice versa. Puttin the same channels gave me an error on

    jassert (&source != this || sourceChannel != destChannel); //line 754 of juce_AudioSampleBuffer.h

But the same problem persists. Applying gain to one sampler will make the other that comes before experience the gain too.

Doesn’t this line just make buffer refer to the exact same data as bufferOriginal? I would think you’d want to remove the & from that, to declare a copy of bufferOriginal, right?

juce::AudioBuffer<float> buffer(bufferOriginal);

still gives the same problem

Shouldn’t it use the copy constructor?

AudioBuffer (const AudioBuffer &other)

The description:

Copies another buffer.

This buffer will make its own copy of the other’s data, unless the buffer was created using an external data buffer, in which case both buffers will just point to the same shared block of data.

How do I know if it’s copying or referencing?

Without that & symbol, it does create a copy using the copy constructor. If you’ve still got other problems, then that’s separate from the issue of using a reference to bufferOriginal. But using that reference was definitely a problem, even if fixing that does not fix your actual problem.

indeed. But I’ve been trying without success. Do you know why I can’t connect channel 0 to channel 0?

And I suspect juce::AudioBuffer<float> buffer(bufferOriginal); isn’t copying the buffer, otherwise the gain wouldn’t be applied to buffer as it’s happening.

Just as a reminder, here it is:

void HelloSamplerAudioProcessor::processBlock (juce::AudioBuffer<float>& bufferOriginal, juce::MidiBuffer& midiMessages)
{
    juce::AudioBuffer<float> buffer(bufferOriginal);
    sampler.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
    buffer.applyGain(gainValue.load());
    bufferOriginal.addFrom(0, 0, buffer, 1, 0, bufferOriginal.getNumSamples());
    bufferOriginal.addFrom(1, 0, buffer, 0, 0, bufferOriginal.getNumSamples());
}

You need to change back to calling addFrom specifying from channel 0 to channel 0 and from channel 1 to channel 1. The assertion you got earlier was because you were using a reference instead of the copy constructor, causing buffer and bufferOriginal to be the very same buffer, not because the channels were a problem.

indeed this works. (changing the channels back)

Look, as an experiment, I did:

void HelloSamplerAudioProcessor::processBlock (juce::AudioBuffer<float>& bufferOriginal, juce::MidiBuffer& midiMessages)
{
    juce::AudioBuffer<float> buffer(bufferOriginal);
    buffer.clear();
    sampler.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
}

remember that there are 2 samplers wired like this:

void HelloSamplerAudioProcessor::processBlock (juce::AudioBuffer<float>& bufferOriginal, juce::MidiBuffer& midiMessages)
{
   sampler1->processBlock(buffer, midiMessages);
   sampler2->processBlock(buffer, midiMessages);
}

what do I get? Only the output from sampler2, which means sampler2 sucessfully clears bufferOriginal, which contains the sound from sampler1. The fact that I still hear sampler2 even if I didn’t add buffer back to bufferOriginal means that buffer is not a copy of bufferOriginal but internally references it

This is my latest try:

void HelloSamplerAudioProcessor::processBlock (juce::AudioBuffer<float>& bufferOriginal, juce::MidiBuffer& midiMessages)
{

    float* const* data = const_cast<float *const *>(bufferOriginal.getArrayOfReadPointers());
    juce::AudioBuffer<float> buffer(data, bufferOriginal.getNumChannels(), bufferOriginal.getNumSamples());

    sampler.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());

    buffer.applyGain(gainValue.load());

    bufferOriginal.addFrom(0, 0, buffer, 0, 0, bufferOriginal.getNumSamples());
    bufferOriginal.addFrom(1, 0, buffer, 1, 0, bufferOriginal.getNumSamples());
}

as you see I’m now passing the original buffer as a pointer to the new buffer. Even with this, silencing sampler2 makes sampler1 silent :jack_o_lantern:

If you’re trying to make the gain specific to the sampler, then why are you applying the gain outside of sampler.renderNextBlock? I think this will be easiest to figure out if you encapsulate as much as possible inside the actual sampler object.

As others have mentioned, if you’re trying to process two separate samplers in parallel, and not serially, then each sampler object needs to not alter the actual input audio, but should store its rendered audio in its own internal buffer that can be retrieved later. Perhaps your sampler object could have an API something like this:

class Sampler
{
public:
    using AudioBuffer = juce::AudioBuffer<float>;

    /* note that the input audio buffer is const here. Internally, this function should write the rendered audio into the `storage` member buffer. */
    void renderNextBlock (const AudioBuffer& audio, MidiBuffer midi);

    /* use this to later access the stored output */
    const AudioBuffer& getStorage() const { return storage; };

private:
    AudioBuffer storage;
};

All you need now is a little helper function to copy all the channels of a buffer:

void copyBuffer (const AudioBuffer& source, AudioBuffer& dest)
{
    const auto numSamples = source.getNumSamples();
    const auto numChannels = std::min (source.getNumChannels(), dest.getNumChannels());

    for (auto c = 0; c < numChannels; ++c)
        dest.addFrom (c, 0, source, c, 0, numSamples);
}

And now your top level processBlock can look something like this:

void processBlock (juce::AudioBuffer<float>& audio, juce::MidiBuffer& midi)
{
    sampler1.renderNextBlock (audio, midi);
    sampler2.renderNextBlock (audio, midi);

    audio.clear();
    copyBuffer (sampler1.getStorage(), audio);
    copyBuffer (sampler2.getStorage(), audio);
};

If you’re trying to make the gain specific to the sampler, then why are you applying the gain outside of sampler.renderNextBlock ? I think this will be easiest to figure out if you encapsulate as much as possible inside the actual sampler object.

I couldn’t find a way to apply the gain to the sampler object, but even if I did, it’s still better to do the processing in a separate buffer as you mentioned.

I understood your API idea, however, there’s still one problem: I cannot create a copy buffer from AudioBuffer& audio as you suggests.

I think you meant this for my sampler processor:

    void renderNextBlock (const AudioBuffer& audio, MidiBuffer midi) {
        //copy contents of `audio` to `storage` here, which is the thing I cannot do
        sampler.renderNextBlock(storage, midiMessages, 0, audio.getNumSamples())
    }

For example, to create a new buffer from AudioBuffer& audio, I tried

juce::AudioBuffer<float> storage(audio)

but this does not copy, it internally references audio, so when I render to storage I end up rendering to audio.

By the way, this is something very basic, isn’t there something in juce that renders for me in a separate buffer already?

Is this Sampler object a class you’ve written yourself, or a juce class? Because even if you’re using a juce class, I would recommend writing your own wrapper class around it, like so:

struct MySampler
{
public:
    void renderAudio (const AudioBuffer& audio, MidiBuffer& midi)
    {
        // use that helper function I posted above -- "audio" is still const, we are just copying all the samples from there into the "storage" buffer
        copyBuffer (audio, storage); 

        // now do processing on "storage"
        sampler.process (storage, midi);

        // and now your gain can be encapsulated in here -- so you can make 2 of these sampler objects and each one can have a gain that doesn't affect the other one!
        storage.applyGain (0, storage.getNumSamples(), gain.load());
    }

    void setGain (float newGain) { gain.store (newGain); }

    const AudioBuffer& getOutput() const { return storage; }

private:
    AudioBuffer storage;
    juce::Sampler sampler; // or whatever

    std::atomic<float> gain;
};

This is actually not my suggestion. There is a difference between creating a new AudioBuffer object every callback from the one sent into processBlock, vs having a pre-allocated AudioBuffer object live as a member of your class and simply copying the samples from the buffer sent to processBlock each time. Hopefully the code I just wrote above makes this a bit clearer.

The way to do this is to make another buffer that is a member variable. Either a buffer is its own standalone object with its own memory, or it’s a reference to memory also being used by another buffer. You can’t have it both ways. The first option is what you want for this scenario.

void someFunction (AudioBuffer buffer)
{
    // "alias" is just a local reference to the same memory as "buffer". You might as well just type "buffer", there's no difference 
    AudioBuffer alias (buffer);
}

struct SomeStruct
{
    // unless you do something special to force this to refer to another buffer's memory, this object now has its own distinct memory to use, because it's declared as a member variable in a class, not just locally in a function
    AudioBuffer buffer;

    void doSomething (AudioBuffer& other)
    {
        // we can copy the samples from "other" into "buffer" -- "buffer" is preallocated memory that is separate from "other" and waiting to be used
    }
};

Hmmmm now I see. Yes, even creating a new buffer on every processBlock call wouldn’t be a good idea.

I created a juce::AudioBuffer<float> storage like you suggested, as a member variable. However I was getting sigfault. So I thought maybe the buffer was empty, so I setted its size in prepareToPlay like this:

void HelloSamplerAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    storage.setSize(2, samplesPerBlock);
}

it solves the problem of the gain on one sampler affecting the other, however the sound is coming out kinda wrong.

This is what I’m doing now for each sampler:


void HelloSamplerAudioProcessor::processBlock (juce::AudioBuffer<float>& bufferOriginal, juce::MidiBuffer& midiMessages)
{
    sampler.renderNextBlock(storage, midiMessages, 0, bufferOriginal.getNumSamples());

    storage.applyGain(gainValue.load());
    juce::dsp::AudioBlock<float> block(storage);

    bufferOriginal.addFrom(0, 0, storage, 0, 0, bufferOriginal.getNumSamples());
    bufferOriginal.addFrom(1, 0, storage, 1, 0, bufferOriginal.getNumSamples());
}

for higher gains the sound is kinda “bouncy”, but for 50% of gain it looks kinda ok. Perhaps it has something to do with the size of storage?

I tried changing bufferOriginal.getNumSamples() with storage.getNumSamples() but then I get crashes

by the way, why you do audio.clear() here?