Writing a plugin which takes audio and outputs control rate

Hi,

I’m writing my first SC plugin and I’m having some trouble. It’s using a new pitch detector which takes audio samples, pre-processes them (lpf, hpf and compression) and then passes these to the pitch tracker.

I wrote a version which used audio rate (BitstreamAutocorrelation.ar) and it didn’t seem to work unless I called .poll on the output. I have no idea why this should be:

# didn't work - crunchy output
SinOsc.ar(A2K.kr(BitstreamAutocorrelation.ar(PlayBuf.ar(1, b, rate, loop:1))));
# did work
SinOsc.ar(A2K.kr(BitstreamAutocorrelation.ar(PlayBuf.ar(1, b, rate, loop:1)).poll));

I’ve been trying to convert it to use control rate instead, but maintaining a full resolution audio input. I used the code from the Amplitude.kr UGen as a starting point but nothing seems to work. The full code is as follows:

BitstreamAutocorrelation::BitstreamAutocorrelation() :
            m_pd(std::make_shared<cycfi::q::pitch_detector>(60_Hz, 600_Hz, sampleRate(), -45_dB)),
            m_pp(std::make_shared<cycfi::q::pd_preprocessor>(cfg, 60_Hz, 600_Hz, sampleRate()))
    {
        m_pp = std::make_shared<cycfi::q::pd_preprocessor>(cfg, 60_Hz, 600_Hz, sampleRate());
        m_pd = std::make_shared<cycfi::q::pitch_detector>(60_Hz, 600_Hz, sampleRate(), -45_dB);

        mCalcFunc = make_calc_function<BitstreamAutocorrelation, &BitstreamAutocorrelation::next>();

        m_frequency = 0.0f;
        next(1);
    }

void BitstreamAutocorrelation::next(int nSamples) {
    auto unit = this;
    const float* input = in(0);

    for(int i = 0; i < FULLBUFLENGTH; i++) {
        float s = m_pp->operator()(input[i]);
        bool is_ready = m_pd->operator()(s);

        if (is_ready) {
            m_frequency = m_pd->get_frequency();
        }
    }

    ZOUT0(0) = std::abs(m_frequency);
}

I’m expecting here that all the audio samples from the input will get processed from each block, and then that the control rate output should be “last frequency wins” which gets output once for each control block. Something seems to be different about the iteration when changing the UGen definition to 'control'.

Apologies for the long post - any help would be gratefully appreciated!

The value of sampleRate() is different for control rate and audio rate UGens. For audio rate UGens it’s the actual audio sample rate, but for control rate UGens it’s FULLRATE / FULLBUFLENGTH.

In your case, you have to use FULLRATE instead, just like you use FULLBUFLENGTH in the next function.


Some unrelated comments:

float s = m_pp->operator()(input[i]);

can be written as:

float s = (*m_pp)(input[i]);


m_pd(std::make_shared<cycfi::q::pitch_detector>(60_Hz, 600_Hz, sampleRate(), -45_dB)), m_pp(std::make_shared<cycfi::q::pd_preprocessor>(cfg, 60_Hz, 600_Hz, sampleRate()))

don’t use plain std::make_shared because it allocates memory on the heap - which is forbidden on the audio thread!

You always have to use RTAlloc and RTFree. You could write your own allocator class which uses these functions under the hood, but it would probably be more work than managing memory manually. After all, you only have to allocate it once in the constructor and free it in the destructor. There’s not much that can go wrong.

1 Like

Thank you! These are all useful points

One more query if I may - I’ve written an audio rate version and I can see from Print statements that the pitch tracker is working as expected (ie outputting 440 with a SinOsc.ar(440) input) but when polling the output of the UGen I’m seeing values in the -1 to +1 range.

I realise that outputting values (as opposed to audio samples) is unusual behaviour for an audio rate UGen. Is there something special I need to do with regards to anti-aliasing etc. to get it to pass the values directly? I’m also wondering

Hopefully that makes sense. Please let me know if I need to clarify anything.

I would need to see the full C++ code and a sclang example.

Thanks - as is usually the way, in forming a question to ask you I’ve realised what was wrong. I think I’ve figured it out for now:

PluginLoad(BitstreamAutocorrelationUGens) {
    // Plugin magic
    ft = inTable;
    registerUnit<QlibUGens::BitstreamAutocorrelation>(ft, "BitstreamAutocorrelation", true);
}

The last argument to registerUnit (disableBufferAliasing) needs to be true in my case. It maps to DefineDtorCantAliasUnit in the older style plugin API and it seems to allow values from the buffer to be passed straight out to other Ugens.

I’ll be open sourcing the plugin when its working so I’ll link it back here in case its useful to others in future. Thanks again for the tips.

Super excited that you’re porting bitstream autocorrelation to SC, I’ve been meaning to do this for ages!

1 Like

I’ve got a first pass at this uploaded here https://github.com/xavriley/qlibugens/blob/main/plugins/BitstreamAutocorrelation/BitstreamAutocorrelation.cpp It works at tracking audio and outputs at audio rate, but it still makes use of std::make_shared.

I’m just getting started with C++ really so I’m still confused around how to convert my 2 uses of std::make_shared into something using RtAlloc. For example:

m_pp = std::make_shared<cycfi::q::pd_preprocessor>(cfg, 60_Hz, 600_Hz, sampleRate());

I’m basically guessing at this point, but I’m imagining something like:

m_pp = (cycfi::q::pd_preprocessor*)RTAlloc(mWorld, sizeof(cycfi::q::pd_preprocessor));

I’m not sure on the next step for how to initialize it properly though. Apologies if this is a really obvious question - I have tried to read through other plugins but I’m drawing blanks so far.

I’m not sure on the next step for how to initialize it properly though

You would need to use “placement new” (new expression - cppreference.com):

// allocate raw block of memory
void *mem = RTAlloc(mWorld, sizeof(cycfi::q::pd_preprocessor));
// don't forget to check for OOM!
if (mem) {
   // construct the object in place with placement new
   m_pp = new (mem) cycfi::q::pd_preprocessor(cfg, 60_Hz, 600_Hz, sampleRate());
} else {
    // handle OOM
    m_pp = nullptr;
}

On the other hand, maybe it’s not really necessary to allocate the cycfi::q::pd_preprocessor on the heap at all. You could make it a class member and directly initialize it in the member initializer list:

class BitstreamAutocorrelation: public Unit {
public:
    BitstreamAutocorrelation();
private:
    cycfi::q::pd_preprocessor::config m_cfg;
    cycfi::q::pitch_detector m_pd;
    cycfi::q::pd_preprocessor m_pp;
    float m_frequency = 0.0f;
};

// Directly initialize the members in the constructor member initializer list.
// If the classes have a default constructor and support copy assignment, you could also do it in the constructor body.
// NOTE: `m_cfg` will be default constructed first (because it is declared first), so it can be safely used to construct `m_pp`.
BitstreamAutocorrelation::BitstreamAutocorrelation() :
    m_pd(60_Hz, 600_Hz, sampleRate(), -45_dB),
    m_pp(m_cfg, 60_Hz, 600_Hz, sampleRate()),
    m_frequency(0.0)
{
    next(1);
}

BTW, is it even necessary to have cycfi::q::pd_preprocessor::config as a member variable? Does m_pp hold on to the object? I haven’t looked at the code but judging from the name I would assume the object is only needed in the constructor. In that case, you could simply create a temporary object:

m_pp(cycfi::q::pd_preprocessor::config{}, 60_Hz, 600_Hz, sampleRate())

As a side note: In https://github.com/xavriley/qlibugens/blob/main/plugins/BitstreamAutocorrelation/BitstreamAutocorrelation.cpp#L17 you’re effectively creating m_pp and m_pd twice - once in the initializer list and once in the constructor body. I guess that was just an oversight.

Generally, always prefer std::unique_ptr over std::shared_ptr whenever possible. The latter is only necessary if you have unclear ownership. And finally think about whether you really need to allocate on the heap in the first place :slight_smile:

Thanks again for the help. I’m learning C++ as I go but this is really helping to make things more clear for me.

To finish this up, I got this working with placement new like you described. The binaries for OSX and Ubuntu are up here if anyone is interested https://github.com/xavriley/qlibugens/releases I’d be grateful for any bug reports etc.

Cool!

Just one more thing:

You have to do the nullptr check before calling placement new because passing a nullptr to placement new is undefined behavior! Do it like I’ve already showed you:

// allocate raw block of memory
void *mem = RTAlloc(mWorld, sizeof(cycfi::q::pd_preprocessor));
// *first* check for OOM!
if (mem) {
   // now construct the object in place with placement new
   m_pp = new (mem) cycfi::q::pd_preprocessor(cycfi::q::pd_preprocessor::config{}, 60_Hz, 600_Hz, sampleRate());
} else {
    // handle OOM, e.g. print error message
    m_pp = nullptr;
}
1 Like