Is Path slow? What is the best tool for spectrum analyser graph?

Hello,
I’ve just made a spectrum analyser. And to draw the graph I used Juce Paint class, and Path.lineTo function. But for bigger resolution (more path points) it starts to be very slow and lag frienfly. Bigger resolution I mean something about for example 10000 points path.
Of course I also suppose maybe my code isn’t optimal. But I think it’s the Path and Paint class. Why I think that? I am not sure but for simply sine wave it works great even for 20000 points of path. But for more complex signal, like saw wave - which is still not very complex of course - but my analyser starts lagging. Why?

1 Like

As juce uses a scanline renderer, rendering paths that intersects a lot with horizontal lines is slow(ish). Unfortunately a spectrum usually does that a lot while a sine wave doesn’t.

The usual thing to do with analyzers is to avoid creating a path with a lot more resolution than the screen. That means in the spectra case for high frequencies an average value of multiple fft bins is used and a path with maybe ~1000 points is used for rendering.

If you still want to render 20000 points you could try rendering into an image that is ninty degrees rotated and then rotate the image back. I never tried this (but maybe I should), but it should be a LOT faster for a spectrum which has precisely one y value per x value.

Personally I find it a bit annoying that juce uses horizontal scanline rendering as it’s mainly used for audio things and most audio graphs have a lot more horizontal line intersections than vertical ones. (spectrum/waveform/envelopes).

2 Likes

I use OpenGL for any substantial real-time visual feedback.
It’s not for the faint of heart, but the method I use is:

  1. Pass points locations to GPU
  2. Use geometry shader to generate lines with joints
  3. Use the fragment shader to anti-alias those lines base on line thickness and distance from edge

I have dozens of lines taking up a lot of space and are drawing at 60 fps. The CPU usage remains pretty low. That said I’d rather not know how much time development time I’ve spent just on rendering lines :sweat_smile:

9 Likes

Some SR products written directly with OpenGL. When it works, it’s much smoother.
However, in terms of compatability OpenGL especially with bad drivers under Windows could generate crashes the moment context being created.

1 Like

Can you share some code? I’m facing a similar problem - juce::Path’s performance simply doesn’t cut it for me.
While I sorta know my way around OpenGL and shaders, I’m not really looking forward to implementing it all myself - any head-start would be appreciated :slight_smile:

There’s quite a lot. There are also changes you need to make to your overall graphics project for it to work (e.g. you can’t layer an OpenGL component on top of a component rendered with paint())

I’d take a look at how nanovg works as a starting point. I think some people on the forum here use it for line rendering: https://github.com/memononen/nanovg

3 Likes

Have you seen this? https://github.com/JanosGit/OpenGLRealtimeVisualization4JUCE

7 Likes

That looks promising, given I can get it to work on my Macbook (I saw your issue on Github).
Perhaps I’ll create a simple, stripped-down open source library for OpenGL paths based on this!

2 Likes

Imho OpenGL is the way to go. I’d recommend using as low a feature set of OpenGL as possible, I’m usually going for 2.0 (even though that can be a pain, the really nice shader features arrived later). If you then test on AMD, nVidia and Intel, and it turns out to work fine on HD3000, you’re most likely safe. The only issue I had so far was with a customer who somehow managed to not install gpu drivers at all on his windows machine - and he didn’t know until our support figured it out :wink:

1 Like

What platforms does nanoVG support? Will it also work on mobile/openGL ES? I guess Mac, Windows, Linux are no problem?

(e.g. you can’t layer an OpenGL component on top of a component rendered with paint())

How do you mean this? If I create a juce::Component, implement juce::OpenGLRenderer and attach it to an OpenGLContext and just render it, I seem to get some pretty good performance…

However, of course, my OpenGL component’s paint function is empty, which is fine, I can render everything with OpenGL.
Is there a different way I should go about it?

1 Like

Following up on this, I actually went ahead and created a small, header-only library to render thick 2D paths using OpenGL!

Here’s my forum thread: Drawing paths using OpenGL: Polyline2D

Here’s what it looks like:

Github: Polyline2D
Example JUCE App: Polyline2DExample

7 Likes

I’m running a spectrum analyser using Path.lineTo without any issues here. I use it for development/debugging purposes so it’s not about performance. However it runs smoothly and I’m using FFTSizes up to 8192. I think there is something else taking cpu - did you run some profiling to check for the bottleneck?

i’m running into a similar problem to the author. How did you manage to have 8192 and running smoothly?

I’m running a simplest plugin with the below inside Paint method and my CPU spikes up 70-80%.

path.clear();
 path.preallocateSpace(2048*3);
 path.startNewSubPath ( 0, 0);
 for (auto i = 0; i < 2048; ++i)
 {
     auto randY = random.nextInt (juce::Range<int> (0, getLocalBounds().getHeight()));
     path.lineTo ( i, randY);
 }
 g.strokePath(path, juce::PathStrokeType (.1));

What is your trick to make this run smoothly?

It’s just not gonna happen using juces paint method. You need to run your component as an OpenGLRenderer, then in the render() callback, do the whole vertex buffer thing to draw in opengl code. For a line you’d use draw line strip.

This has been a hot topic for a while now, I wonder if the JUCE team is any closer to making the default paint routines any more suitable for dynamic components?

OpenGL is massiely overkill for this IMO.

@MarcelK, do you really need 2048 points? Even if your window was 2048px wide that’d be 1 point per pixel which really isn’t necessary unless you need that level of precision. I usually plot a point around every 5px for a spectrum analyser so for a window size of say 500 px that’s only 100 points which JUCE’s software renderer can handle easily.

1 Like

I thought that this would be the case. But placing:

openGLContext.attachTo (*getTopLevelComponent());

is not making any difference.
Is somewhere out there any code example to illustrate how to use render() callback?

Yes, I kind of need even 8192. I’m building a multichannel frequency spectrum plugin and so that users can choose from different sizes of FFT.

You’re going to have multiple points per pixel though, that’s quite literally impossible to see… You should just find the min and max points within a given range and draw a line for each x-position which will bring your points count down a lot.

Right, so the idea is to still be able to choose whatever FFT size and then calculate the min and max?
What’s the unspoken rule to calculate (smooth) the frequencies…is it from 2kHz upwards?

But, wouldn’t then let’s say 2048 and 4096 look the same after smoothing?