Using setTimeStamp to arpeggiate a chord


#1

I currently have code to play a chord. I play each note separately.

I have adapted my code so that I can supply a delay (in seconds). This should let me arpeggiate a chord:

    // Major triad, arpeggiated
    playNote(0, 0.0f);
    playNote(4, 0.1f);
    playNote(7, 0.2f);

The problem is all the notes play simultaneously!

Here is my code for playing a note:

struct PiSynthAudioSource : public AudioSource 
        :
	void playNote(int n, float delay_s = 0.0f) {
		MidiMessage m = MidiMessage::noteOn(1, 60 + n, .25f);
		m.setTimeStamp(Time::getMillisecondCounterHiRes() * .001 + delay_s);
		midiCollector.addMessageToQueue(m);
	}

I have looked through the body of addMessageToQueue and it looks as though my implementation should be correct. But all notes play simultaneously.

I wonder if maybe I should be spawning a thread for each note, but that starts to look like a lot of lines of code, much better if I can do it this way.

I’m looking for some kind of executeAfterDelay(0.5, []{ myFunc(); } ) function (if I have remembered my block syntax correctly) but I don’t think the framework has such function.

Can anyone suggest something?

π

PS I found Delayed function call?

PPS Complete synthplayer code is:


#ifndef SYNTH_H_INCLUDED
#define SYNTH_H_INCLUDED

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

// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

SharedResourcePointer<AudioFormatManager> pFormatManager;

ScopedPointer<AudioSampleBuffer> UNUSED__bufferFromFile(File file)
{
	if(pFormatManager->getNumKnownFormats() < 1)
		pFormatManager->registerBasicFormats();

	ScopedPointer<AudioFormatReader> reader = pFormatManager->createReaderFor(file);
	jassert(reader);

	ScopedPointer<AudioSampleBuffer> buffer = new AudioSampleBuffer(reader->numChannels, (int)reader->lengthInSamples);
	
	reader->read(
		(int* const*)&buffer			// <-- void* no???
		, reader->numChannels
		, (int64)0						// startSampleInSource
		, (int)reader->lengthInSamples
		, false							// fillLeftoverChannelsWithCopies
		);

	return buffer;
}

// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

struct PiSynthAudioSource : public AudioSource 
{
	PiSynthAudioSource() 
	{
		// Add some voices to our synth, to play the sounds..
		for (int i = 16; --i >= 0;)	
			synth.addVoice(new SamplerVoice());

		// ..and add a sound for them to play...
		setUsingSampledSound();
	}

	void setUsingSampledSound()
	{
		WavAudioFormat wavFormat;

		synth.clearSounds();

		for (int i = 0; i < 12; i++)
		{
			// load from binary data!
			const String filename = String("st_") + String(i) + String("_wav");
			int size = 0;
			const char* data = BinaryData::getNamedResource(filename.getCharPointer(), size);
			auto memStream = new MemoryInputStream(data, size, false);
			ScopedPointer<AudioFormatReader> audioReader = wavFormat.createReaderFor(memStream, true);

			BigInteger midiNoteFlag;
			midiNoteFlag.setRange(0, 128, false);
			midiNoteFlag.setBit(60 + i); // MIDI C4 = 60

			SamplerSound* pSound = new SamplerSound(
				"Toneme_" + String(i),
				*audioReader,
				midiNoteFlag,
				60 + i,   // root midi note
				0.0,      // attack time
				0.0,      // release time
				5.0       // maximum sample length
				);

			synth.addSound(pSound);
		}
	}

	void prepareToPlay(int /*samplesPerBlockExpected*/, double sampleRate) final {
		midiCollector.reset(sampleRate);

		synth.setCurrentPlaybackSampleRate(sampleRate);
	}

	void releaseResources() override { }

	void getNextAudioBlock(const AudioSourceChannelInfo& bufferToFill) final {
		bufferToFill.clearActiveBufferRegion();

		MidiBuffer incomingMidi;
		midiCollector.removeNextBlockOfMessages(incomingMidi, bufferToFill.numSamples);
		synth.renderNextBlock(*bufferToFill.buffer, incomingMidi, 0, bufferToFill.numSamples);
	}

	// the synth itself!
	Synthesiser synth;
	MidiMessageCollector midiCollector;

	void playToneme(int n, float delay_s = 0.0f) {
		MidiMessage m = MidiMessage::noteOn(1, 60 + n, .25f);
		m.setTimeStamp(Time::getMillisecondCounterHiRes() * .001 + delay_s);
		midiCollector.addMessageToQueue(m);
	}
};

// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 

class PiSynthPlayer
{
private:
	AudioDeviceManager& deviceManager;
	PiSynthAudioSource synthAudioSource;
	AudioSourcePlayer audioSourcePlayer;

	JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PiSynthPlayer)

public:
	void playToneme(int n, float delay_s = 0.0f) {
		synthAudioSource.playToneme(n, delay_s);
	}


	PiSynthPlayer(AudioDeviceManager& outputDeviceManager)
		: deviceManager(outputDeviceManager)
		, synthAudioSource()
	{
		audioSourcePlayer.setSource(&synthAudioSource);

		deviceManager.addAudioCallback(&audioSourcePlayer);
	}

	~PiSynthPlayer()
	{
		deviceManager.removeAudioCallback(&audioSourcePlayer);
		
		audioSourcePlayer.setSource(nullptr);
	}

};

#endif // SYNTH_H_INCLUDED

Mechanism for delayed execution of a code block
#2

The MidiMessageCollector::removeNextBlockOfMessages() method will change the timestamp of the MidiMessages that you pass it to fit into the buffer size provided by the numSamples argument so you can’t use it to delay your messages by more than a buffer size. This works out to ~1ms for a buffer size of 512 samples at a sample rate of 44.1kHz which is why you are hearing your notes at the same time.

A better way to do this is to schedule your note on/offs based on sample offsets - you can work out how many samples until the next MIDI message is required and then in your AudioSource’s getNextAudioBlock() method you can subtract the buffer size in samples until the number of samples until the message should be sent is < your buffer size then add the MidiMessage.

I did something similar for a University assignment last year which might be of use to you - check out the Arpeggiator files here.

Ed