Custom panned effects still bleeds on panning

Hi… while I will study some DSP programming, I’m still using CCRMA Synthesis Toolkit effects. I have that custom object:

    class PannedPitch {

public:

PannedPitch()
{
    pitchL = stk::PitShift();
    pitchR = stk::PitShift();
}

void clear()
{
    pitchL.clear();
    pitchR.clear();
}

void setVal (float value)
{
    val = value;
}

void setPan (float pan)
{
    if (pan < 0.0)
    {
        lMix = 0.5 + fabsf(pan / 2);
        rMix = 0.5 - fabsf(pan / 2);
    }
    
    if (pan == 0.0)
    {
        lMix = 0.5;
        rMix = 0.5;
    }
    
    if (pan > 0.0)
    {
        lMix = 0.5 - fabsf(pan / 2);
        rMix = 0.5 + fabsf(pan / 2);
    }
}

void setGain (float value)
{
    gain = value;
}

void process (AudioSampleBuffer& buffer)
{
    buffer.copyFrom(1, 0, buffer, 0, 0, buffer.getNumSamples());
    
    pitchL.setShift(val);
    pitchL.setEffectMix(lMix * gain);
    
    pitchR.setShift(val);
    pitchR.setEffectMix(rMix * gain);
    
    for (int i = 0; i < buffer.getNumSamples(); ++i)
    {
        float* lBuf = buffer.getWritePointer(0);
        lBuf[i] = pitchL.tick(lBuf[i]);
        
        float* rBuf = buffer.getWritePointer(1);
        rBuf[i] = pitchR.tick(rBuf[i]);
    }
}

private:

stk::PitShift pitchL, pitchR;

float val, pan, gain;
float lMix, rMix;

};

And this code in MainComponent.cpp to implement the effect (PannedPitch object) in prepare to play:

    pitchA = PannedPitch();
pitchA.setVal(params[1] * 0.1667);
pitchA.setPan(params[2] / 50.0);
pitchA.setGain(params[5] / 100.0);
pitchA.clear();

pitchB = PannedPitch();
pitchB.setVal(params[3] * 0.1667);
pitchB.setPan(params[4] / 50.0);
pitchB.setGain(params[5] / 100.0);
pitchB.clear();

and in getNextAudioBlock (…) that code:

    pitchA.setVal(params[1] * 0.1667);
pitchA.setPan(params[2] / 50.0);
pitchA.setGain(params[5] / 100.0);

pitchB.setVal(params[3] * 0.1667);
pitchB.setPan(params[4] / 50.0);
pitchB.setGain(params[5] / 100.0);

if (pitchOn)
{
    pitchA.process(*bufferToFill.buffer);
    pitchB.process(*bufferToFill.buffer);
}

But in this way only the pitchB instance works with panning (perfectly).
The pitchA instance only sounds in total left panning.But if I comment one of them, the effects is perfect…
Any suggestion/ idea?

I’ve tried to pass to the process in getNextAudioBlock() the current inputChannel and the outputChannels but it’s the same.
If i change the input channel to 1 instead 0, the panning swaps (only right channel sounds).

Really need help plz!

The problem is, that your PannedPitch::process() each one already change the input of the second one to be processed. I think you have to reconsider that setup.

Also another problem: to pan a stereo to stereo signal, there are different approaches:
you can either simply keep left to left and right to right, and then apply a gain to either one, depending of the pan setting (center -> both gains 1.0, left -> multiplying the right with the cos (angle) and vice versa).
A nicer approach is to have two pans, so the user can actually decide the width of the outgoing signal, e.g. mixing both to both to get a mono signal, or even reverse left to right. You find that often in DAWs.

HTH

I don’t understand what is wrong in PannedPitch::process(), can you explain me better?
Or give me a solution?

void process (AudioSampleBuffer& buffer)
{
    buffer.copyFrom(1, 0, buffer, 0, 0, buffer.getNumSamples());

this first line already copies the left buffer onto the right one, so the right input is gone forever.
The second problem is, that each instance process both channels.

I would suggest, to have one instance per input channel to apply the PitShift effect and have the panning afterwards by mixing the channels after the pitchOn block.

HTH

BTW: your setPan function is identical to:

void setPan (float pan)
{
    lMix = 0.5 + (pan / 2.0);
    rMix = 0.5 - (pan / 2.0);
}

Ok now I have to go to bed… tomorrow I’ll try your solution, thanks!

I was thinking… how can I mix the channels after the pitchOn block?
And how can I have a single instance per input channels?

PS I have always a mono in and need a stereo out.

Ok, so if there is only a mono signal, the lines:

        float* rBuf = buffer.getWritePointer(1);
        rBuf[i] = pitchR.tick(rBuf[i]);

will always process silence.

I realise I am assuming a lot, so can you please write down, what the signal flow is, that you want to achieve? (that is good to do as a first step anyway, helps one self to get clarity)
I was assuming, a signal coming in, shall be pitch shifted and then panned to a stereo output?

In this case you don’t even need two instances:

// processing:
    auto* ptr = buffer.getWritePointer(0);
    for (int i = 0; i < buffer.getNumSamples(); ++i, ++ptr)
        *ptr = pitch.tick (*ptr);

// equal gain panning while copying from left to right channel
    auto angle = pan * float_Pi * 0.5f;  // the cos and sin from 0-90 degrees is perfect for panning
    buffer.copyFrom (1, 0, buffer, 0, 0, buffer.getNumSamples(), std::sin (angle));
    buffer.applyGain (0, 0, buffer.getNumSamples(), std::cos (angle));

HTH

1 Like

So I have to delete form PannedPItch.h the second pitch and leave one single pitch that I process in the way you wrote me? Or I put the code directly in MainComponent.cpp after pitchOn? (I need two pitch shifts in the MainComponent the works in parallel)


in this way I get an error, but I can’t understand why… in the other process I did’n have it…

nevermind… it was a wrong parameter in buffer.copyFrom
it should be buffer.getReadPointer(0), BUT it doe’s not produces any sound…

Ah ok, have a look at the overloads of copyFrom(), you need one with a gain parameter, like you figured out.
It should read:

// equal gain panning while copying from left to right channel
    auto angle = pan * float_Pi * 0.5f;  // the cos and sin from 0-90 degrees is perfect for panning
    buffer.copyFrom (1, 0, buffer.getReadPointer (0), buffer.getNumSamples(), std::sin (angle));
    buffer.applyGain (0, 0, buffer.getNumSamples(), std::cos (angle));

Is that better?

no… I ear no effect on dx channel for the first pitch, and for the second no sound in extreme pan (-1, 1).
Also if I set the setEffectMix of the stk::PitShift() to 1.0, that produces no sound

I think that your function for the pan is from 0 to 1, but I need from -1 to 1.
Is it possible?

Yes, I assumed pan to be 0…1, so you have to normalise that from your value range to 0… float_Pi * 0.5.
There is a convenience method called jmap:

auto angle = jmap (pan, -1.0f, 1.0f, 0.0f, float_Pi * 0.5f);

With the stk call, maybe I was wrong here, I don’t normally use stk. Maybe somebody who uses stk can chime in, or go back to your initial version. My only point was that you need only to process the buffer, where there is an actual signal.

Good luck!

ok, it is a mess…
now I ear only one channel and the pitchA affects the pithB

Sorry, if that wasn’t helpful… I have to leave over the weekend, maybe somebody else can help you further…
Good luck!

ok, thank you @daniel.

I found the solution!!
I use a copy of the main buffer, i process it and then I add the copy to the main buffer meanwhile I use the gain option of copyBuffer() to add the pan effect.
Here is the code:

//Declare the copy buffer

AudioSamepleBuffer pitchABuffer, pitchBBuffer;

// Set size of the copy buffer in prepareToPlay()

pitchABuffer.setSize(2, samplesPerBlockExpected);
pitchBBuffer.setSize(2, samplesPerBlockExpected);

// The process in getNextAudioBlock():

const auto numSamples = bufferToFill.buffer->getNumSamples();

if (pitchOn)
{
    // Pitch A
    pitchA.setShift(params[1] * 0.1667);
    pitchA.setMix(params[3] / 100.0);
    
    pitchABuffer.copyFrom(0, 0, *bufferToFill.buffer, selectedInputChannel, 0, numSamples);
    
    pitchA.process(pitchABuffer);

    auto angleA =  jmap(params[2] / 50.0, -1.0, 1.0, 0.0, float_Pi * 0.5);
    
    bufferToFill.buffer->copyFrom(selectedOutputChannels[0], 0, pitchABuffer.getReadPointer(0), numSamples, std::cos(angleA));
    bufferToFill.buffer->copyFrom(selectedOutputChannels[1], 0, pitchABuffer.getReadPointer(0), numSamples, std::sin(angleA));

    // Pitch B
    pitchB.setShift(params[4] * 0.1667);
    pitchB.setMix(params[6] / 100.0);
    
    pitchBBuffer.copyFrom(0, 0, *bufferToFill.buffer, selectedInputChannel, 0, numSamples);
    
    pitchB.process(pitchBBuffer);

    auto angleB =  jmap(params[5] / 50.0, -1.0, 1.0, 0.0, float_Pi * 0.5);

    if (params[3] > 0)
    {
        bufferToFill.buffer->addFrom(selectedOutputChannels[0], 0, pitchBBuffer.getReadPointer(0), numSamples, std::cos(angleB));
        bufferToFill.buffer->addFrom(selectedOutputChannels[1], 0, pitchBBuffer.getReadPointer(0), numSamples, std::sin(angleB));
    }
    else
    {
        bufferToFill.buffer->copyFrom(selectedOutputChannels[0], 0, pitchBBuffer.getReadPointer(0), numSamples, std::cos(angleB));
        bufferToFill.buffer->copyFrom(selectedOutputChannels[1], 0, pitchBBuffer.getReadPointer(0), numSamples, std::sin(angleB));
    }
}

And finally the PannedPitch class:

class PannedPitch {

public:

PannedPitch()
{
    pitch = stk::PitShift();
}

void clear()
{
    pitch.clear();
}

void setShift (float value)
{
    shift = value;
}

void setPan (float value)
{
    pan = value;
}

void setMix (float value)
{
    mix = value;
}

void process (AudioSampleBuffer& buffer)
{
    pitch.setShift(shift);
    pitch.setEffectMix(mix);

    for (int i = 0; i < buffer.getNumSamples(); ++i)
    {
        float* lbuf = buffer.getWritePointer(0);
        lbuf[i] = pitch.tick (lbuf[i]);
    }
}

private:

stk::PitShift pitch;

float shift, pan, mix;

};

Thank you guys!