Most effectiv way to paint waveforms


#1

Hey all,
i want to paint 2 waveforms from my Audiosignal in the Plugin. First the original, second the edited.
So actual my way is this:
Waveforms

void WaveMeter::paint(Graphics &g) {
	const MessageManagerLock mmLock;
	int balkenWidth = 1;
	int x = myWidth; // myWidth == Component width
	while(inWrite == 1) // This checks a flag that actual no other thread is writing in to the Ringbuffer
		SLEEP(1);
	inRead = 1; // Set flag that the other thread can't write while i'm reading
        Colour fillColour = Colour(0xff7BD1B8); // Set colour for waveform 1
	g.setOpacity(1);
	g.setColour(fillColour);
	for (int i = 0; i < myWidth *size; i+= size) {
		float height = sampleBuffer->ReadOneSample(i, 1); // Read a sample
		if (height < 0)
			height = -height;
		height = (height * myHeight); // height == max 1 so -> scaling
		int y = halfHeight - (height);    // getting y position
		g.fillRect(x, y, balkenWidth, (int)height); // paint
		x -= 1; // go one step back for the next sample
	}
        // Waveform 2
	x = myWidth;
        Colour fillColour = Colour(0xffFC752D); // Set colour for waveform 2
	g.setOpacity(1);
	g.setColour(fillColour);
	for (int i = 0; i < myWidth *size; i += size) {
		float height = sampleBuffer->ReadOneSample(i, 0);
		if (height < 0)
			height = -height;
		height = (height * myHeight);
		int y = halfHeight;
		g.fillRect(x, y, balkenWidth, (int)height);
		x -= 1;
	}
	inRead = 0;
}

Everything is working fine but it’s very CPU heavy.
Is there any way to do this more efficent? Maybe with OpenGL ? Never worked before with it and it will need time to get in… Is that more efficent?

Any tipps & idea’s are welcome thank you :slight_smile:


#2

Ouch! So much badness in one line of code! If that’s how you’re approaching multithreading then you should profile your app before assuming that it’s the graphics that are the slow bit…

Also have a look at how the juce audio thumbnail works - it’s much faster to build a RectangleList and fill it rather than making thousands of separate calls to fillRect


#3

thanks and no the inWrite flag is not in use, was testing some stuff there


#4

Oh is that what you are doing in there? I’ve just been calling paintVerticalLine for my zoomed out views…


#5

It doesn’t look right to me … what’s size though?


#6

Roughly the strategy I’ve been using is, in pseudo code.

for (int x = 0; x < width; ++x)
{
   auto range = getMinMaxValuesForRange(xToSamplePosition(x), xToSamplePosition(x + 1)
   paintVerticalLine(x, ampToY(range.min), ampToY(range.mix));
}

Where you optimise the hell out of getMinMaxValuesForRange by using some appropriate caching for the zoom level.

If you’re scrolling a waveform in real time you can optimise further by saving an Image and moving it around and then only redrawing any new bits.


#7

Size is for smoothing the waveform, sorry i copied a little bit wrong
code looks like this with size:

int size = 16;
for (int i = 0; i < myWidth *size; i+= size) {
	float height = 0;
	for(int s = 0; s < size; s++) {
		height += sampleBuffer->ReadOneSample(i+s, 1);
	}
	height /= size;

So it’s painting the average from 16 Sample’s and yes i know that it’s not realy effectiv to read 16x one sample. I have some other Functions to call directly 16 or more samples. I was just testing which “smoothing” looks good.

I’m pretty sure that my “painting” rect function is the problem because if i use the plugin with a small window size the Plugin didn’t need much cpu. If i scaling the plugin bigger the cpu usage increase.

The idea with the image sounds great.
I will check this and the Rectangle List and give you a feedback :slight_smile:

Thanks for your help.


#8

Oh right, so size is like ‘samples per pixel’?

Well so rectangles will be your problem with only 16 samples per pixel for sure.

If you want to zoom out so you have 1000 samples per pixel or so you’ll find that reading the sample becomes a bottle neck too…

Try Jules’ suggestions -and have a peek into the AudioThumbnail classes…


#9

First a side note:

average is probably the worst choice, as it is more or less meaningless for audio samples. It becomes obvious if you remember, that the average in the end will always be 0, unless you have a DC offset.

The measures you want to look at are: max = peak values or RMS = root mean square.
And because they are important and used all over the place, they exist in optimised versions, to be found e.g. float FloatVectorOperations::findMaximum (const float *src, int numValues).
Each time you type a for loop, try to replace that with one of these.

The RMS can be obtained via Type AudioBuffer< Type >::getRMSLevel (int channel, int startSample, int numSamples) const. Currently this is not a SIMD operation, but might be in the future, so it is always worth it not to roll your own.

And some code inspiration, although it is a slightly different thing: I wrote this code for a circular buffer, displaying the buffers rolling in (currently mono), maybe it gives you some inspiration:

// members:
std::vector<float>           minBuffer;
std::vector<float>           maxBuffer;
std::atomic<int>             writePointer;
int                          fraction     = 0;

// in preparToPlay
minBuffer.resize (1024, 0.0f);
maxBuffer.resize (1024, 0.0f);
writePointer = writePointer % 1024;

// create peak values in processBlock
{
    int blockSize = 128;
    int samples = 0;
    Range<float> minMax;
    while (samples < numSamples) {
        int leftover = numSamples - samples;
        if (fraction > 0) {
            minMax = FloatVectorOperations::findMinAndMax (buffer.getReadPointer (0), blockSize - fraction);
            maxBuffer [writePointer] = std::max (maxBuffer [writePointer], minMax.getEnd());
            minBuffer [writePointer] = std::min (minBuffer [writePointer], minMax.getStart());
            samples += blockSize - fraction;
            fraction = 0;
            writePointer = (writePointer + 1) % maxBuffer.size();
        }
        else if (leftover > blockSize) {
            minMax = FloatVectorOperations::findMinAndMax (buffer.getReadPointer (0, samples), blockSize);
            maxBuffer [writePointer] = minMax.getEnd();
            minBuffer [writePointer] = minMax.getStart();
            samples += blockSize;
            writePointer = (writePointer + 1) % maxBuffer.size();
        }
        else {
            minMax = FloatVectorOperations::findMinAndMax (buffer.getReadPointer (0, samples), leftover);
            maxBuffer [writePointer] = minMax.getEnd();
            minBuffer [writePointer] = minMax.getStart();
            samples += blockSize - fraction;
            fraction = leftover;
        }
    }
} 

and create a path to be painted like this:

Path MyAudioProcessor::getChannelOutline (const Rectangle<float> bounds, const int numSamples) const
{
    const int bufferSize = static_cast<int> (maxBuffer.size());
    Path outline;
    int latest = writePointer > 0 ? writePointer - 1 : bufferSize - 1;
    int oldest = (latest >= numSamples) ? latest - numSamples : latest + bufferSize - numSamples;
    
    const float dx = bounds.getWidth() / numSamples;
    const float dy = bounds.getHeight() * 0.35f;
    const float my = bounds.getCentreY();
    float x  = bounds.getX();
    int   s  = oldest;
    
    outline.startNewSubPath (x, my);
    for (int i=0; i<numSamples; ++i) {
        outline.lineTo (x, my + minBuffer [s] * dy);
        x += dx;
        if (s < minBuffer.size() - 1)
            s += 1;
        else
            s = 0;
    }
    for (int i=0; i<numSamples; ++i) {
        outline.lineTo (x, my + maxBuffer [s] * dy);
        x -= dx;
        if (s > 1)
            s -= 1;
        else
            s = bufferSize - 1;
    }
    
    
    return outline;
}

HTH

EDIT: just realised, that writePointer needs to be atomic, as it is written in processBlock but also read from paint()

EDIT 2: FYI: I added this now to my publicly available meters module: github:ff_meters


#10

small tip when using the rectangleList, use addWithoutMerging otherwise it will be CPU heavy