Working with AudioBuffer to do Pitch Detection on Low Frequencies

I am working on a AudioPlugin that does pitch detection as well as play style recognition on an electric bass. I found this thread on pitch tracking in JUCE but haven’t had much success with adamski’s module tracking low frequencies so I am looking at implementing my own or modifying what he has done, but I am not entirely clear on how JUCE AudioBuffer works in tandem with other audio programs running.
For example:
If I need an audio buffer or 4096 samples or possibly higher for detection of some low frequencies do I need to set the AudioBuffer size of my JUCE plug-in to 4096 as well as the buffer size in the pitch tracking module? In the pitch_detection module there doesn’t seem to be any code to get more samples if say my AudioBuffer in my plug in is set to 1024. I can’t imagine it just magically grabs the upcoming required samples? How does this work?

Please let me know if my question is not clear/ if anyone has any in sight into this.

1 Like

You can’t set the host to provide you a particular number of samples from a plugin, you have to work with what you are given. If a library you are using requires a particular number of input samples, you need to do some additional buffering of your own in the plugin.

It’s common for audio algorithms to require ‘large’ sample sizes to work correctly. You need to do some manual buffering, this will obviously cause latency. Although it sounds like in your case that doesn’t matter.

Here’s an example of buffering with a FIFO

// Look up juce::AbstractFifo for how to implement this
HeapFIFO<float> sampleBuffer{ 1024 * 2 };

void process(const AudioSampleBuffer& inputBuffer, AudioSampleBuffer& outputBuffer, int numSamples)
{
	const int audioEngineBlockSize = numSamples; // 512
	const int fftAlgoBlockSize = 1024;

	// fill the sampling buffer from 'live' input data
	sampleBuffer.write(inputBuffer.getReadPointer(0), audioEngineBlockSize);

	// check if we have enough data to fire the FFT
	if (sampleBuffer.getNumElementsWaiting() >= fftAlgoBlockSize)
	{
		float tempBuffer[fftAlgoBlockSize]{};

		sampleBuffer.read(tempBuffer, fftAlgoBlockSize);

		doFFT(tempBuffer, fftAlgoBlockSize);
	}
}
1 Like

Hey,
Thank you for this! This opens up a whole other stream of questions makes me feel like I am doing everything entirely wrong but I’ll have to figure that out… some clarifying questions.

So I would have something like what you posted in my processBlock of the plugin? or would I put that elsewhere.

What I did which seems like it should work (at least logically) to me (however I don’t think it is working) is I put an if statement in my process block that looks something like this

if(fillPos < (pitchTrackingBuffer.getNumSamples() / buffer.getNumSamples() )) {
    pitchTrackingBuffer.addFrom(0, fillPos * buffer.getNumSamples(), buffer, 0, 0, buffer.getNumSamples());
    fillPos++;
}else{
    fillPos  = 0;

    rms = pitchTrackingBuffer.getRMSLevel(0, 0, pitchTrackingBuffer.getNumSamples());
    
    if (rms > 0.35 &&  lastPitchPlayed == 0) {
        pitchEstimate = pitchMpm.getPitch(pitchTrackingBuffer.getReadPointer(0));
        midiVel = ((midiRangeMax - midiRangeMin) / (1.0f - 0.0f)) * rms;
        midiNote = int((log(pitchEstimate / 440.0) / log(2)) * 12) + 69;
        lastPitchPlayed = pitchEstimate;
        midiOutProcess.createNoteOn(midiMessages, midiNote, midiVel);

    }else if (rms  < 0.2 && lastPitchPlayed != 0) {
        midiNote = int((log(lastPitchPlayed / 440.0) / log(2)) * 12) + 69;
        midiOutProcess.createNoteOff(midiMessages, midiNote);
        lastPitchPlayed = 0;
    }

Essentially every time the processBlock run it fills up the first “chunk” of the buffer for pitch tracking once the buffer fills it run the pitch tracking code and this process repeats.

You don’t want to be using pitchTrackingBuffer.addFrom because that mixes in the audio with the previous audio! (Use copyFrom instead.) Also your other buffer position logic and calculations don’t seem really right, but it’s hard to tell since I don’t know about that particular library, how it should be used. Doesn’t the documentation tell how it should be used?

1 Like

Hey Thank you!
I don’t fully understand what you mean in regards to addFrom vs copyFrom but will use copyFrom and look to the documentation to get a better grasp on what you are saying.

the pitch detection module isn’t particularly well documented/ there’s not much documentation at all just one example. Which is part of why I’m thinking of just trying to implement one myself (however I am also in a bit of a time crunch and just keep running into obstacles I can’t find the answer to). I think the module is something that user adamski started to suit his needs but didn’t get finished as certain option aren’t implemented at all. It’s probably also out of date w.r.t current JUCE framework

but essentially what the module documentation says is you set the sample rate and buffer size of PitchMPM object using the respective functions and then call getPitch passing it your audioBuffer. I set the buffer and sample rate in the prepareToPlay function.

does that make sense?

I feel like I am very likely doing things wrong because it’s not working as expected. I am also pretty new to JUCE framework and very out of touch with my C++ skills so I’ve been really struggling through this project for the last few months. If you have any resources much appreciated I (I am familiar with TheAudioProgrammer and on the discord chat)

AudioBuffer::addFrom mixes the audio you are adding to the audio that was already in the buffer, so that’s definitely not going to work, the buffer will just fill up with junk audio that the pitch detector won’t be able to make any sense out of. copyFrom replaces the buffer contents with the new content, so that’s a step in the right direction to get it working. And as a final clarification : addFrom has nothing to do with appending to the end of the buffer, you need to keep track of the appropriate write index position yourself.

I briefly looked at the pitch detector documentation. It does indeed require the audio to be fed in as constant sized buffers, that also need to be of power-of-2 lengths. So it’s not very nice to work with it in plugin projects. You can’t expect the buffer sizes from the host are going to be constant and power-of-2 long. But it is possible to work around that by doing your own buffering.

If you’re going to use an AudioSampleBuffer to hold your accumulated data, you’re going to want to use ‘fillPos’ with finer granularity.

if (fillPos < pitchTrackingBuffer.getNumSamples())
{
    // we might not be able to copy a full block of audio into the tracking buffer
    // find out how much space we can safely copy over
    const auto writeSize = jmin(blockSize, pitchTrackingBuffer.getNumSamples() - buffer.getNumSamples());

    // CopyFrom will copy and overwrite the samples instead of mixing them with the old data
    pitchTrackingBuffer.copyFrom(
	0, fillPos, buffer,
	0, startSample, writeSize
    );

    fillPos += writeSize;
}

const auto pitchTrackingSamplesRequired = 1024;
if (fillPos >= pitchTrackingSamplesRequired)
{
	pitchMpm.getPitch(pitchTrackingBuffer.getReadPointer(0));
	fillPos = 0;
}

Although I do recommend a FIFO like data structure to do this kind of thing.

Ok that makes sense. Thank you!

I’m surprised that no-one mentioned the FFT example which requires a fixed buffer size before it can do its analysis. You should be able to use the same concepts there to create the buffer of the size you need for your pitch detection algorithm to work.

1 Like

I see, thanks I will try this approach or something like it. I guess I just assumed (foolishly) that it would be able to copy each chunk un-interrupted :confused: . I am familiar with FIFO data structures so I might give that a try. Thanks for all the replies and resources everyone!

Excuse my late response to this thread!

I solved the issue of low frequencies by increasing the buffer size, and allowing the user to select from a few “frequency sensitivity” settings with buffer sizes from 1024 up to 32768. Of course as you increase buffer size latency increases. As dictated by the FFT I am using the buffer size always has to be a power of 2. I am using a “Ring Buffer” which is a kind of FIFO. As new values arrive, old ones are pushed out. The ring buffer’s size is the same as the processing buffer. The ring buffer is filled from the audio thread. I have a “worker thread” which does the processing (in this case pitch detection), which copies the current data in the ring buffer, applies the processing, and then posts via an atomic for the UI thread to read. The worker thread is then put to sleep, and woken up again when the UI thread requests a new value. In this way, depending on the buffer size provided by the OS, the processing buffer size, and the UI frame rate, it may be that blocks or partial blocks of audio are missed, or that the sampled blocks overlap each other. In the case of pitch detection it does not matter - what matters is that the user gets an accurate as possible idea of the fundamental frequency of the current input audio.

I would also like to try out “probabilistic” algorithms, which try different pitch threshold values, and may improve detection of lower frequencies. The “McLeod Pitch Method” which I am using is apparently known for having difficulty with lower frequencies. See https://github.com/sevagh/pitch-detection/tree/master/misc/probabilistic-mcleod

1 Like