How do I make a metronome with a synchronized animation?

The SL_MetroVisualizer class mentioned above inherits from AnimatedAppComponent. If I am not mistaken, once you call setFramesPerSecond, it will continuously call update() and repaint() at the frequency you specified.

Also it’s maybe not too useful to measure the time it takes to execute the paint() method to measure graphics performance, the actual pixels are not yet rendered there, as far as I’ve understood.

Probably not, but I don’t think that the actual pixel rendering goes on anywhere in my code. I just assume that the JUCE framework is handling that part as efficiently as possible.

An audio buffer size like 5040 samples is massive and is not going to work to deliver information for fast GUI updates. Doesn’t the emulator have a way to get that down?

Maybe, although off-hand I couldn’t find it. I’ll look into it more later.

If you can get the GUI updates to happen at a decent rate but can’t get the audio buffer size smaller, it may be after all that you have to implement the animation somewhat independently from the audio processing…But you’d need to come up with some mechanism to correct the timing drifts and offsets that are inevitably going to happen.

That is the theory. However, while the AudioThread is independent of everything and runs on a high priority to ensure, there is always a buffer ready for the hardware to playback, versus the MessageManager (GUI thread), which is implemented as a message queue.
A timer schedules an event to happen in x milliseconds. But there are no guarantees, if other messages are in the queue first, they will be finished. That can be the previous paint call, some mouse events, anything.

Your idea to measure the time for the paint was good, but it probably made the problem worse, since the logger itself is quite slow.

I wonder, what musical value a metronome at 200 BPM has though, and how you want to measure the visual accuracy…

I agree with @Xenakios, it is a very long buffer. I only did a few experiments with android, but left it alone, since my phone (Galaxy Note 3 Neo) is also too slow to do something useful in audio.

Addition: IMHO metronomes would start highlighting the main beat rather than every beat from a certain bpm number…

I have heard before that Android is not a good platform for audio applications. But, this is not by any means a sophisticated audio app that I am building. I actually have worked with Pure Data on a much older version of Android before. It was able to handle this with no problem. It really baffles me because I’m pretty sure Pure Data is built with JUCE. But, based on this experience, I’m guessing they use their own GUI engine?

It appears to me, that this is not a GUI issue. The problem is to find a good source for synchronisation. To synchronise things there are different options:

Audio Clock Master: let the audio run from the driver and synchronise everything with that.
That was the solution I was voting for. However, this is a problem, if the audio driver uses buffers of this gigantic size. You can know, which buffer is playing, but there is no chance to know, where in this buffer you are playing. It could have just started, or just about to finish.
Like you said, an uncertainty of 5040 samples is not acceptable.

Timer (by MessageManager): this is running on a message queue and is sharing the resources with painting, mouse events and lots of more stuff. So this is also not a reliable way.

HighResTimer: this is a timer, which has it’s own thread to trigger the callback. So the callback will be very accurate, but the cost for that, is that you have to synchronise everything with the two other threads. I.e. trigger your click would wait, until a new audio buffer is fetched by the driver, and the visual feedback would have to wait until the paint message arrives on top of the message queue.
So unfortunately it seems there is not much won either.

You can do some experiments with these three solutions, maybe you find something satisfying.
Unfortunately I am not aware of a better solution, it all boils down to the big audio buffer size, which I don’t know, if there is a way to enforce a smaller size. Like I wrote before, with 1024 samples there should be no problem.

Good luck

Just a quick idea: What about keeping the number of samples as the main reference and updating a sample counter variable and the value of Time::getHighResolutionTicks at the start of every audio block. This way the GUI thread could calculate a sample offset to the sample counter based on the high resolution ticks difference which should be quite stable for the period of one big sample block. Maybe this needs an additional offset of one block lengt as the block that the User hears while he/she looks at the UI is the one that was actually computed in the previous processBlock call.

I haven‘t implemented anything like that myself but right now I‘m pretty sure that this should work

I have to disagree. All the evidence I have gathered points toward not just a massive buffer size (and thus a difficulty finding a suitable source for synchronization), but also the GUI updates being very slow (on average about 100 milliseconds). Also the AnimatedAppComponent::setFramesPerSecond() seems to have little to no impact on the frequency that paint() is successfully carried out. I’m basing this on the results I got from using ScopedTimeMeasurement and Time::getHighResolutionTicks(). Running three back to back Logger::writeToLog() calls, in fact, takes less than a single millisecond:

{
    double timeSec;

    {
        ScopedTimeMeasurement m (timeSec);
        Logger::writeToLog("[" + name + "] MAX time between updates: " + String(maxDelta.load()));
        Logger::writeToLog("[" + name + "] MIN time between updates: " + String(minDelta.load()));
        Logger::writeToLog("[" + name + "] AVG time between updates: " + String(avgTimeDelta.load()));
        String lastFive = "";
        for(auto i = 0; i < 5; i++){
            lastFive += String(lastFiveDeltas[i]) + ", ";
        }

        Logger::writeToLog("[" + name + "] Last 5 updates: " + lastFive);
    }

    Logger::writeToLog ("It took " + String (timeSec) + " seconds to write the log");
}

But, even if the Logger is adulterating my measurements, I believe I have effectively eliminated that argument as well. I implemented a class that can track the time between updates. It has a simple method that can be called (from paint(), for example) to update an average and a few other statistics. It also inherits from Timer, and so when it makes calls to the Logger it does so in a different thread than the GUI (I think? Or maybe Logger is asynchronous and uses up more CPU time somewhere else?), and calls the Logger infrequently (only every 8 seconds) to show an average. This is a typical output:

[VISUALIZER] MAX time between updates: 0.131872
[VISUALIZER] MIN time between updates: 0.000332
[VISUALIZER] AVG time between updates: 0.0994952
[VISUALIZER] Last 5 updates: 0.108426, 0.099429, 0.099429, 0.099429, 0.099429, 
It took 0.000282 seconds to write the log

As of now, I have still not tried this on a real device. So, the only hope I hold out is that this slowness is a problem with the emulator. Otherwise, this I’m really at a loss.

I am also thinking about purchasing the JUCE Pro so that I can get premium support. You guys have been extremely helpful, but I really want this feature for my app, and I’m thinking that maybe they could help me more. Does anyone else have experience with JUCE Pro’s premium support?

Yes, in your case this is the case. setFramesPerSeconds() just sets up a timer to call repaint continuously with that interval. If your message queue is still busy with the previous paint (which is most likely the case, as you said your painting takes ~100 ms), a higher number in setFramesPerSeconds won’t speed things up.

IMHO there is the need to optimise the paint a bit. Can you try to simplify the paint() to see, from which paint you can get a reasonable frame rate?

Getting help from the juce guys is for sure a good thing, since android is not the platform I know best. It is probably best to get in touch via email and discuss the options. Here are the details: https://juce.com/premium-support-plan (it is not included in the pro license)

Good luck

Throwing this in here, since I was just debugging in Logic and noticed that it has a dedicated MetronomeUItriggerThread. Tightly synchronizing a visual with a beat is a tricky enough task that Apple apparently gave it its own thread.

1 Like