Mechanism for delayed execution of a code block


#1

I’m trying to build a function that lets me do e.g.

delayedExecute( 0.5, []{ playNote(50); } );
delayedExecute( 0.3, []{ playNote(66); playNote(63); } );

etc.

Something like this:

void func() { 
    std::cout << "foo" << std::endl; 
} 

class Delay;
std::vector<Delay> delayQueue;

class Delay : Timer {
public:
    Delay(double delay_ms, std::function<void()> _f) : f(_f) {
        startTimer(delay_ms);
    }
private:
    std::function<void()> f;
    
    void timerCallback() final {
        f();
        delayQueue.remove(this);
        delayQueue.erase(
            std::remove(delayQueue.begin(), delayQueue.end(), this), 
            delayQueue.end()
        );
        delete this;
    }
};

void executeAfterDelay(double delay_ms, std::function<void()> codeBlock) {
    auto obj = new Delay( delay_ms, codeBlock ); // <-- how to persist?
    
    delayQueue.push_back( obj );
}

:
executeAfterDelay(0.5, []{ func(); }); 
:

… but I wonder if this is a sensible way to go about doing it. I’m creating a bunch of objects each of which has its own Timer, I wonder if I should be using different threads?

Would anyone care to comment?


#2

If you’re playing notes, don’t you want to do it sample accurately? Timers can be way off.


#3

If you just want to play MIDI notes, I don’t suppose you should be doing anything like that. Why not just count audio samples as the audio plays and add/play your notes based on that?


#4

Actually it’s a musical training game I’m (re)writing. So I don’t need any kind of high precision accuracy, but I do want to do something like play each note in a triad with some slight random temporal deviation (~.1s), and then play the whole thing a couple of seconds later but arpeggiated this time.

So whereas most people here will require the notes of a chord to sound simultaneously, my requirement is actually the opposite, I want to introduce slight delays so that the brain has a better chance of picking out the constituent notes.

I will also be updating visuals as each note plays. etc.

It’s not the end of the world if a particular timer gets delayed.

I was trying to do this using setTimeStamp Using setTimeStamp to arpeggiate a chord but I wasn’t able to get it to work.


#5

You can try it out, but I think the timer will have a noticable pattern in it’s deviation… Particularly, the resolution is very low and depends on how strained the computer is. So closely related notes will probably end up being fired at the same time anyway, for better or worse. You also have to deal with main thread -> audio thread interference, which probably means you need a lock somewhere or some overkill lock free fifo structure.

Since you’re worried about performance, why not just fire the notes with some random() deviation?


#6

Hi, just interested to read what’s being said here - are these concerns valid about hi res timers too which automatically run in their own thread, or just the standard timers?


#7

Yes, HighResolutionTimer should be pretty accurate.


#8

Thx. I use them for midi timing and haven’t seen any issues so far, so just confirming


#9

This thread has gotten derailed.

The original idea was to be able to execute a block after some delay.

How about this:

class DelayedCall : Timer {
public:
    DelayedCall(double delay_ms, std::function<void()> _f) : f(_f) {
        startTimer(delay_ms);
    }
private:
    std::function<void()> f;
    
    void timerCallback() final {
        stopTimer();
        f();
        delete(this);
    }
};

void executeAfterDelay(double delay_ms, std::function<void()> codeBlock) {
    new DelayedCall(delay_ms, codeBlock);
}

// usage:
executeAfterDelay(5000, [] { DBG("foo"); });

This works! However I am warned that delete this creates potential for undefined behaviour, as the class inherits from Timer whose destructor makes use of the object pointer:


->

So my question here is: is this really a problem? While I cannot see clearly that it is safe, I also cannot see clearly that it is unsafe. Can anyone?

π

PS Pretty sure this is safe!

PPS https://forum.juce.com/t/delayed-function-call


#10

In quest for ´perfect´ MIDI timing, I came with a solution which works for me. I’ll let you know and criticize it, since I’m still struggling with the subtleties of both C++ and JUCE. I wrote and tested a few months ago and did not review later.

I created a MIDI_Interface class, initially to emulate Kontakt’s callback way of handling MIDI and ended up with a scheduler which is sample accurate.

´MIDI_Interface.h´

[CODE] #pragma once

class MIDI_Interface
{
public:
	MIDI_Interface();
	virtual ~MIDI_Interface();

	/// Multimap of MidiMessages to be scheduled
	typedef multimap<uint64 , MidiMessage> schMapType;
	schMapType schMidiMsg;

	/// Indicates the type of Message: 0 - input (DAWI_INPUT) event, 1 - output (PLAY_MIDI_OUTPUT) event
	enum eMESSAGE_TYPE
	{ DAW_INPUT , PLAY_NOTE_OUTPUT , SAMPLE_LOOP };

	/// Number of samples played since playback start (prepareToPlay() )
	/// no need to reset later, 8 bytes unsigned long long reaches 18,446,744,073,709,551,615 (enough play time!)
	uint64 sampleCounter;
	/// Reset in prepareToPlay, incs every new processblock()
	uint64 bufferCounter;
	/// Used to count MidiMessags, context dependent
	int midiMsgCounter;

	/// Passed from processorBlock()
	AudioPlayHead::CurrentPositionInfo PB_CurrentPosInfo;

	/// This is the MidiBuffer for manipulation. Every edition is made within this block,
	/// then exchanged with original ´midiMessages´ before sending the buffer to audio device (end of processorBlock() method )
	MidiBuffer MI_MidiBuffer;

	/// Time stamp for MidiMessages described in MI_MidiMessage (in samples within processorBlock)
	int MI_BufferPos; // MidiMessage timestamp is ´int´

	/// MidiMessages handled from MI_MidiBuffer. Not necessarily modified. Optional: contextual use.
	MidiMessage MI_MidiMessage;

	/// This is the integrator of the AudioProcessor processorBlock() to our MIDI_Interface
	void MI_ProcessBlock(const MidiBuffer& midiMessages, const AudioPlayHead::CurrentPositionInfo& CurrentPosInto, const int& blockSize);

	/// This will play the note (offset in samples, have to build the offset handler)
	/// offset defaults to 0, msgType defaults to DAW_INPUT
	/// For the sake of simplicity, it'll fill schMidiMsg (since it's checked for every sample)
	void playMIDI(const MidiMessage&, const int& timestamp, const int& offset = 0, const int& msgType = DAW_INPUT);

	/// Callback emulation. When a MidiMessage is iterated in the original MidiBuffer, it is sent to this
	void ON_MIDI(MidiMessage&, const int& timestamp, const int& msgType);

private:

	JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR( MIDI_Interface )
};

[/CODE]

´MIDI_interface.cpp´

[CODE] /*
Every MIDI Mesasge will be sent to the scheduler (schMidiMsg).
Notes passing through (unprocessed) will be scheduled with 0 offset
The notes are gonna be added to Midi Buffer in the ´Sample Loop´
*/

#include "MIDI_Interface.h"
MIDI_Interface::MIDI_Interface() {}

MIDI_Interface::~MIDI_Interface() {}

void MIDI_Interface::MI_ProcessBlock(const MidiBuffer& PB_MidiBuffer, const AudioPlayHead::CurrentPositionInfo& MI_CurrentPosInto, const int& blockSize)
{
	MI_MidiBuffer.clear(); // creates an empty MidiBuffer that will be filled by

	if (PB_MidiBuffer.isEmpty() == false) // Any midi messages for this block?
	{
		/* --- MIDI BUFFER LOOP --- */
		for (MidiBuffer::Iterator mbIterator(PB_MidiBuffer); mbIterator.getNextEvent(MI_MidiMessage, MI_BufferPos);)
		{
			// Callback emulation, every MIDI message will be sent to ON_MIDI (callback emulation)
			ON_MIDI(MI_MidiMessage, MI_BufferPos, DAW_INPUT);
		}
		/* --- End of MIDI BUFFER LOOP --- */
	}

	/* --- SAMPLE LOOP --- */
	for (int bufferPos = 0; bufferPos < blockSize; ++bufferPos)
	{
		if (schMidiMsg.find(sampleCounter) != schMidiMsg.end()) // If there's some MidiMessage scheduled to actual sampleCounter
		{
			schMapType::const_iterator itLower = schMidiMsg.lower_bound(sampleCounter);
			schMapType::const_iterator itUpper = schMidiMsg.upper_bound(sampleCounter);
			for (auto& it = itLower; it != itUpper; ++it)
			{
				MI_MidiBuffer.addEvent(it->second, bufferPos);
			}
			schMidiMsg.erase(sampleCounter); // all events for this sample are removed
		}

		++sampleCounter; // now increment Samples Counter
	};
	/* --- End of SAMPLE ACCURATE LOOP --- */
}

void MIDI_Interface::ON_MIDI( MidiMessage& midiMessage , const int& timestamp , const int& msgType )
{
	playMIDI( midiMessage , timestamp , 0 );
}

void MIDI_Interface::playMIDI(const MidiMessage& midiMessage, const int& timeStamp, const int& offset, const int& msgType)
{
	//  ´offset´ default = 0. If offset < 0 (illegal), it's ignored
	if (offset >= 0)
	{
		schMidiMsg.emplace(sampleCounter + offset, midiMessage);
	}
}

[/CODE]

Then I inherit my AudioProcessor from MIDI_Interface:

and add this code:

    void MIDI_Int_AudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
    {
    	// Initializes counters and schMidiMsg (MidiMessage Scheduler Multimap)
    	bufferCounter = 0;
    	sampleCounter = 0;
    	midiMsgCounter = 0;
    	schMidiMsg.clear();
    }

    void MIDI_Int_AudioProcessor::releaseResources()
    {
    	midiMsgCounter = 0;
    	schMidiMsg.clear(); // clear all scheduled events
    }

    void MIDI_Int_AudioProcessor::processBlock(AudioSampleBuffer& PB_AudioBuffer, MidiBuffer& PB_MidiBuffer)
    {
    	PB_AudioBuffer.clear();

    	++bufferCounter;

    	getPlayHead()->getCurrentPosition(PB_CurrentPosInfo);

    	MI_ProcessBlock(PB_MidiBuffer, PB_CurrentPosInfo, PB_AudioBuffer.getNumSamples());

    	// Replaces received MidiBuffer with edited MI_MidiBuffer
    	PB_MidiBuffer.swapWith(MI_MidiBuffer);
}

Since I don’t process audio at all, this method works with zero overhead for me and you can use playMIDI with any uint64 offset, it’ll be sample accurate, no threads and timers involved for maximum accuracy. I had to keep my eyes a few months away from programming, I don’t know I would do it like this right now, but since it worked well for my initial tests, I’m restarting from here. This is probably not the most elegant solution, and I can’t predict how it would work combined with audio processing.


#11

Re: using delete this in Timer::timerCallback(), it feels dodgy but is actually safe to do, and I’ve used the same trick in various places. Obviously you need to be super-careful not to accidentally use the timer object after deleting it.

(We must add a lambda based function for a one-shot timer… I might do that now…)


#12

Do say if you put something in. That would give this thread a nice closure.


#13

OK, have added Timer::callAfterDelay(). Should be a handy method!


#14

What’s the reasoning behind making a local copy of the std::function, deleting the object first then calling the copy?


#15

Not a big deal, it just felt better to get the Timer object deleted and removed from the list before running some unknown user code. E.g. the user function may quit the app or invoke a modal loop or something, so the less that happens afterwards, the better.


#16

Yeah, that’s what I assumed.

It’s a bit of a micro optimisation but wouldn’t it make sense to move the std::function member in to the local to avoid copying any state potentially contained in the std::function?
E.g. std::function<void()> f (std::move (function));


#17

That would certainly be a micro-optimisation!


#18

Well, it depends on what state the std::function is holding. Sure, if it’s a simple function pointer then the’d be no gain but it could be a lambda that holds a whole document or similar…


#19

Hmm, that’s actually a good point!


#20

In my own implementation I was doing stopTimer() first, then calling the function, then deleting the object.

This was in case the function uses a significant time slice in which case it’s possible the timer might fire a second time.

I think it’s equivalent.

PS For future reference, Julies’ commit being discussed is here