High Resolution Timer

Hi all.

I’m trying to accomplish something that I imagine is pretty simple, but I’m still lacking in some of the fundamental concepts, and am struggling as a result.

Im building a piano app which uses a high resolution timer to trigger some randomised piano melodies in key. Ive accomplished this and it works as I want it to. I have some settings to change number of bars, ‘beats’ or ‘divisions’ per bar and BPM, which I use to calculate the timer intervals.

What I’m now trying to do is create a second lot of controls to be able to generate parts for the bass register that the left hand would play. To do this I need a separate high resolution timer, given the individual timer intervals necessary.

Is there any way to have 2 high resolution timers running concurrently the way you can with the standard ones?

I don’t think there is and so assume the solution would be to have a separate class inherit from the high resolution timer, and trigger the other notes from there. The problem I see with this approach is that I only want one one MidiKeyboardComponent, and therefore one MidiKeyboardState and SynthAudioSource, given that the keyboard component will display the notes as they’re played (from both timers).

How can I call note on function on a single object from 2 separate classes? I’m wondering if a static function might help.

Thanks for any tips.

To keep things in sync, you might want to use the audio callback as the source of time. Ie. You know the passage of time by how many samples come through the callback at a given sample rate.

But, this does not answer your question. :nerd_face:

cpr is right. you should actually rewrite what the first timer was doing to just be stuff on the audio callback (processBlock). for example you could have a member variable called “index” and increment it by 1 for each sample like this:

int idx = 0;

// in processBlock
for(auto s = 0; s < numSamples; ++s)
    ++idx;

now idx gets increased each block. what does it mean when idx reaches sampleRate? it means 1sec has passed. let’s say that your event occurs every 2 secounds. that would look like this:

// in prepareToPlay
length = int(sampleRate * 2.);

// in processBlock
for(auto s = 0; s < numSamples; ++s)
{
    ++idx;
    if(idx >= length)
    {
        // code that triggers the event.
        idx = 0;
    }
}

that is how you work with time in the audio callback. now you can just let as many index values run in parallel as you want, with whatever lengths you want. it’s more cpu efficient than high resolution timer and even more precise. there is literally no downside to this

1 Like

I wouldn’t use a loop to count each sample. That’s extremely inefficient. You know how many samples there are (numSamples). Simply do

idx += numSamples;
if ( idx >= length )
{
    idx %= length;
    // code that triggers the event
}
2 Likes

yeah, or splitting the block into steps of 32 or 64 samples to get a good compromise between being accurate and being efficient. making it directly depend on numSamples can make inconsistent behaviour on different blocksizes

To process something with a different block size (which is related to running a timer using the sample count) I normally check how many samples are needed up to the next trigger (which would be a full block or the need for a timer callback). This could be something like this:

class TriggerEveryNSamples
{
public:
    
    double counter = 0;
    double length = timeInSec * sampleRate;    
    std::function<void()> triggerFunction = nullptr;
    
    void process(int numSamples)
    {
        while (numSamples > 0)
        {
            auto nextNumSamples = std::min(numSamples, static_cast<int> (length - counter));
            counter += nextNumSamples;
            if (counter >= length)
            {                
                counter -= length;
                triggerFunction();
            }
            numSamples -= nextNumSamples;
        }    
    }
};

Be aware that this isn’t very pretty code which shouldn’t be used as is but I hope it clarifies the approach. It allows to work as block-wise as possible without missing any triggers if the period is shorter than numSamples. One useful addition left out for simplicity would be to also pass the sample offset in triggerCallback.

Unfortunately that only works if you only have one event in each block. The larger the block the higher the chances are it fails.

Here’s a timer and timer queue classes that can be used for sample accurate timers:

#include "../JuceLibraryCode/JuceHeader.h"

//! Timer queue class, Timer instances are not owned by the queue, single threaded
class TimerQueue
{
public:
    using Tick = int64_t;
    
    //! Timer class
    class Timer
    {
    public:
        using Function = std::function<void()>;
        
        Timer(Function f = nullptr) : callback(f) { }
        Timer(Tick t, Function f) : tick(t), callback(f) { }
        ~Timer() { cancel(); }
        
        void cancel();
        
        void setCallback(Function f) { callback = f; }
        
        friend class TimerQueue;
        
    private:
        Tick        tick { 0 };
        Function    callback;
        TimerQueue* queue { nullptr };
    };
    
    // schedule using an internal absolute time
    void schedule(Timer* timer);
    
    // schedule using a new absolute time
    void schedule(Timer* timer, int64_t newTick);
    
    // schedule using a relative time
    void scheduleRelative(Timer* timer, int64_t samples);
    
    // process timer event if the tick matches and return the number of samples until the next timer event in the queue
    uint32_t process(uint32_t samples);
    
    //! Get current tick
    int64_t getTick() const { return currentTick; }
    
private:
    friend class Timer;
    
    //! Cancel timer
    bool cancel(Timer* timer);
    
    std::vector<Timer*> timers;
    int64_t             currentTick { 0 };
};

inline void TimerQueue::Timer::cancel()
{
    if (queue)
    {
        queue->cancel(this);
        assert(!queue);
    }
}

inline void TimerQueue::schedule(Timer* timer)
{
    if (timer)
    {
        timer->cancel();
        
        for (auto it = timers.begin(); it != timers.end(); ++it)
        {
            if ((*it)->tick > timer->tick)
            {
                timers.insert(it, timer);
                return;
            }
        }
        
        timers.push_back(timer);
    }
}

inline void TimerQueue::schedule(Timer* timer, int64_t newTick)
{
    if (timer)
    {
        timer->tick = newTick;
        schedule(timer);
    }
}

inline void TimerQueue::scheduleRelative(Timer* timer, int64_t samples)
{
    schedule(timer, currentTick + std::max(int64_t(0), samples));
}

inline uint32_t TimerQueue::process(uint32_t samples)
{
    while (!timers.empty())
    {
        auto timer = timers[0];
        
        if (timer->tick > currentTick)
            break;
        
        timers.erase(timers.begin());
        timer->queue = nullptr;
        timer->callback();
    }
    
    if (timers.empty())
    {
        currentTick += samples;
        return samples;
    }

    auto untilNext = timers[0]->tick - currentTick;
    currentTick += untilNext;
    return std::min(samples, static_cast<uint32_t>(untilNext)); // fixed
}

inline bool TimerQueue::cancel(Timer* timer)
{
    if (!timer || !timer->queue)
        return false;
    
    if (timer->queue != this) // not owned
        return false;
    
    auto it = std::find(timers.begin(), timers.end(), timer);
    
    if (it != timers.end())
    {
        timers.erase(it);
        timer->queue = nullptr;
        return true;
    }
    
    return false;
}

int main (int argc, char* argv[])
{
    TimerQueue timerQueue;
    
    TimerQueue::Timer timer1([&]()
    {
        DBG("timer1: REPEATIVE: " << (int)timerQueue.getTick());
        
        timerQueue.scheduleRelative(&timer1, 10); // repeative timer
    });
    
    TimerQueue::Timer timer2([&]()
    {
        DBG("timer2: ONESHOT: " << (int)timerQueue.getTick());
    });
    
    timerQueue.scheduleRelative(&timer1, 0);
    timerQueue.scheduleRelative(&timer2, 50);
    
    int offset = 0;
    int samples = 100;
    
    while (offset < samples)
    {
        auto count = timerQueue.process(samples - offset);
        
        // process count samples here!
        
        offset += count;
    }
    
    return 0;
}

JUCE v6.0.7
timer1: REPEATIVE: 0
timer1: REPEATIVE: 10
timer1: REPEATIVE: 20
timer1: REPEATIVE: 30
timer1: REPEATIVE: 40
timer2: ONESHOT: 50
timer1: REPEATIVE: 50
timer1: REPEATIVE: 60
timer1: REPEATIVE: 70
timer1: REPEATIVE: 80
timer1: REPEATIVE: 90
Program ended with exit code: 0