Inject notes to a synthesiser

I tried to implement a policy of injection by taking into consideration your suggestion

    void processBlock(juce::AudioSampleBuffer& buffer, juce::MidiBuffer& midiBuffer) override
    {
        
        juce::AudioSourceChannelInfo bufferToFill(buffer);

        bufferToFill.clearActiveBufferRegion();
        juce::MidiBuffer myBuff;
        myBuff.clear();


        for (int i = 0; i < customBuffer.getNumEvents(); i++)
        {
            juce::MidiMessageSequence::MidiEventHolder* eventHolder = messageSequence.getEventPointer(i);

            if (processBlockTime == 0)
            {
                myBuff.addEvent(eventHolder->message, 0);
                processBlockTime++;
                lastIndex = i;
                midiBuffer.swapWith(myBuff);


            }
            else if (processBlockTime > 0 && processBlockTime < bufferToFill.numSamples)
            {
                i = lastIndex;
                processBlockTime++;

            }
            else if (processBlockTime == bufferToFill.numSamples)
            {
                processBlockTime = 0;

                
            }

            keyState->processNextMidiBuffer(midiBuffer, bufferToFill.startSample,
                bufferToFill.numSamples, true);


            synth.renderNextBlock(*bufferToFill.buffer, midiBuffer,
                bufferToFill.startSample, bufferToFill.numSamples);
        }
    }

Unfortunately with this code, no sound is emitted

Here’s a short description of what I mean. I’s not a complete working example, but hopefully it will clarify somewhat how to insert midi events on a time line.

class MidiTest : public AudioProcessor
{
public:
	MidiTest()
	{
		std::vector<MidiMessage> notesToPlay;

		//stuff some midi notes to play in an array
		notesToPlay.push_back(MidiMessage::noteOn(1, 60, 1.0f));	//c
		notesToPlay.push_back(MidiMessage::noteOn(1, 64, 1.0f));	//e
		notesToPlay.push_back(MidiMessage::noteOn(1, 67, 1.0f));	//g

		notesToPlay.push_back(MidiMessage::noteOff(1, 60));
		notesToPlay.push_back(MidiMessage::noteOff(1, 64));
		notesToPlay.push_back(MidiMessage::noteOff(1, 67));

		//now create the array containing the sample positions the midi events above should play at
		//you could of course put them as time stamps in the midi events themselves,
		//but this time we'll keep them apart...
		
		//play with 1s interval, starting at 2 s (if samplerate 44100)
		
		std::vector<int64>timeToPlay;
		for (int i = (int)notesToPlay.size(); --i >= 0;)
			timeToPlay.push_back((i + 2) * 44100);	//should of course call getSampleRate() or applicable in "real" code

		jassert(timeToPlay.size() == notesToPlay.size());	//o/w you'll have to modify the counters in processblock

		nextPlayTime = timeToPlay[0];
	}

	void processBlock(AudioSampleBuffer& buffer, MidiBuffer& midiMessages) override
	{
		int numSamples = buffer.getNumSamples();
		auto endOfThisBuffer = sampleCounter + numSamples;

		/*
		* we check for every processBlock if we have any notes to play in this time spot [sampleCounter .. sampleCounter + numSamples]
		* N.B we us while() here instead of if() while there could (most probably) be more than one midi event at the same position
		* (you could check this logic by modifying timeToPlay and set the notes to start at the same time)
		* */
		while (nextPlayTime < endOfThisBuffer)
		{
			/*this term is really important. It controls where in time the midi event will be placed
			* in the time duration of the processblock, 0 means at the start, numSamples -1 at the very end
			* if ouside this interval it will most probably be disregarded and never hear of 
			* */
			auto positionInBuffer = int(nextPlayTime - sampleCounter);

			midiMessages.addEvent(notesToPlay[currentNote], positionInBuffer);

			//get the next playTime
			if (++currentNote < timeToPlay.size())
				nextPlayTime = timeToPlay[currentNote];
			else
				nextPlayTime = std::numeric_limits<int64>::max();	//run out of playTimes, set nextPlayTime to eternity...
		}
		
		/*this is the key to be able to play a sample anywhere on the timeline i.e at any time after start
		* we add the played number of samples of every processBlock being played
		* after some amount of time (processblock calls) we reach the first playPos (nextPlayTime above)
		* */
		sampleCounter += numSamples;
	}

private:
	int64 sampleCounter = 0;
	int64 nextPlayTime;
	int currentNote = 0;

	std::vector<MidiMessage> notesToPlay;
	std::vector<int64>timeToPlay;
};
2 Likes

Thank you oxxyyd! I will try to implement this code ASAP :heart_eyes:

oxxyyd, you’re my hero!

This is the final code. I post this, just in case someone will have the same necessity as me.

The code permits to emit the notes C, E and G and then release them. The duration of the emission/release is 2 seconds, and among the notes the interval corresponds to 1 second.

#pragma once
#include <JuceHeader.h>
#include "ProcessorBase.h"
#include "BetaVoice.h"

class AnotherMidiSource : public ProcessorBase
{
public:
    AnotherMidiSource()
    {
        for (auto i = 0; i < 5; i++)
            synth.addVoice(new SineWaveVoice());

        synth.addSound(new SineWaveSound());

        //stuff some midi notes to play in an array
        notesToPlay.push_back(juce::MidiMessage::noteOn(1, 60, 1.0f));	//c
        notesToPlay.push_back(juce::MidiMessage::noteOn(1, 64, 1.0f));	//e
        notesToPlay.push_back(juce::MidiMessage::noteOn(1, 67, 1.0f));	//g

        notesToPlay.push_back(juce::MidiMessage::noteOff(1, 60));
        notesToPlay.push_back(juce::MidiMessage::noteOff(1, 64));
        notesToPlay.push_back(juce::MidiMessage::noteOff(1, 67));

        for (int i = 0; i < notesToPlay.size(); i++)
        {
            DBG(notesToPlay[i].getDescription());
        }
    }

    void prepareToPlay(double sampleRate, int samplesPerBlockExpected) override
    {
        synth.setCurrentPlaybackSampleRate(sampleRate);
        //play with 1s interval, starting at 2 s (if samplerate 44100)

        for (int i = 0; i < notesToPlay.size(); i++)
        {
            timeToPlay.push_back((i + 2) * getSampleRate());
        }

        for (int j = 0; j < timeToPlay.size(); j++)
        {
            DBG("Time to play: " << timeToPlay[j]);
        }

        jassert(timeToPlay.size() == notesToPlay.size());

        nextPlayTime = timeToPlay[0];
    }

    void processBlock(juce::AudioSampleBuffer& buffer, juce::MidiBuffer& midiBuffer) override
    {
        juce::AudioSourceChannelInfo bufferToFill(buffer);
        midiBuffer.clear();
        bufferToFill.clearActiveBufferRegion();

        int numSamples = buffer.getNumSamples();
        auto endOfThisBuffer = sampleCounter + numSamples;
        DBG("End of this buffer: " << endOfThisBuffer);

        /*
        * we check for every processBlock if we have any notes to play in this time spot 
        * [sampleCounter .. sampleCounter + numSamples]
        * N.B we us while() here instead of if() while there could (most probably) be more 
        * than one midi event at the same position
        * (you could check this logic by modifying timeToPlay and set the notes to start at the same time)
        * */
        while (nextPlayTime <= endOfThisBuffer)
        {
            /*this term is really important. It controls where in time the midi event will be placed
            * in the time duration of the processblock, 0 means at the start, numSamples -1 at the very end
            * if ouside this interval it will most probably be disregarded and never hear of
            * */
            
            auto positionInBuffer = int(nextPlayTime - sampleCounter);

            midiBuffer.addEvent(notesToPlay[currentNote], positionInBuffer);

            //midiBuffer.swapWith(processedBuffer);



            //get the next playTime
            if (++currentNote < timeToPlay.size())
                nextPlayTime = timeToPlay[currentNote];
            else
                nextPlayTime = std::numeric_limits<juce::int64>::max();
            //run out of playTimes, set nextPlayTime to eternity...
        }
        /*this is the key to be able to play a sample anywhere on the timeline i.e at any time after start
        * we add the played number of samples of every processBlock being played
        * after some amount of time (processblock calls) we reach the first playPos (nextPlayTime above)
        * */
        sampleCounter += numSamples;
        synth.renderNextBlock(*bufferToFill.buffer, midiBuffer,
            bufferToFill.startSample, bufferToFill.numSamples);
    }

    void releaseResources() override
    {

    }

    ~AnotherMidiSource() override
    {

    }
private:
    std::vector<juce::MidiMessage> notesToPlay;
    std::vector<juce::int64>timeToPlay;
    juce::int64 nextPlayTime;
    juce::int64 sampleCounter = 0;
    int currentNote = 0;

    juce::Synthesiser synth;
    juce::MidiBuffer processedBuffer;

};

It is importanto to remark that: you have to thank oxxyyd for his amazing contribution!

1 Like

Maybe could be of interest for someone: by modifying the processBlock, there’s the chance to perform a loop of the notes to be played. Here’s the code updated with this feature:

#pragma once
#include <JuceHeader.h>
#include "ProcessorBase.h"
#include "BetaVoice.h"

class AnotherMidiSource : public ProcessorBase
{
public:
    AnotherMidiSource()
    {
        for (auto i = 0; i < 5; i++)
            synth.addVoice(new SineWaveVoice());

        synth.addSound(new SineWaveSound());

        //stuff some midi notes to play in an array
        notesToPlay.push_back(juce::MidiMessage::noteOn(1, 60, 1.0f));	//c
        notesToPlay.push_back(juce::MidiMessage::noteOn(1, 64, 1.0f));	//e
        notesToPlay.push_back(juce::MidiMessage::noteOn(1, 67, 1.0f));	//g

        notesToPlay.push_back(juce::MidiMessage::noteOff(1, 60));
        notesToPlay.push_back(juce::MidiMessage::noteOff(1, 64));
        notesToPlay.push_back(juce::MidiMessage::noteOff(1, 67));

    }

    void prepareToPlay(double sampleRate, int samplesPerBlockExpected) override
    {
        synth.setCurrentPlaybackSampleRate(sampleRate);

        for (int i = 0; i < notesToPlay.size(); i++)
        {
            timeToPlay.push_back((i + 2) * getSampleRate());
        }

        jassert(timeToPlay.size() == notesToPlay.size());

        nextPlayTime = timeToPlay[0];
    }

    void processBlock(juce::AudioSampleBuffer& buffer, juce::MidiBuffer& midiBuffer) override
    {
        juce::AudioSourceChannelInfo bufferToFill(buffer);
        midiBuffer.clear();
        bufferToFill.clearActiveBufferRegion();

        int numSamples = buffer.getNumSamples();
        auto endOfThisBuffer = sampleCounter + numSamples;

        while (nextPlayTime <= endOfThisBuffer)
        {
         
            auto positionInBuffer = int(nextPlayTime - sampleCounter);
            midiBuffer.addEvent(notesToPlay[currentNote], positionInBuffer);


            if (++currentNote < timeToPlay.size())
                nextPlayTime = timeToPlay[currentNote];
            else
            {
                for (int i = 0; i < notesToPlay.size(); i++)
                {
                    timeToPlay[i] = timeToPlay[i] + (int)sampleCounter/divider;
                }

                nextPlayTime = timeToPlay[0];
                currentNote = 0;
                divider++;

            }

        }

        sampleCounter += numSamples;

        synth.renderNextBlock(*bufferToFill.buffer, midiBuffer,
            bufferToFill.startSample, bufferToFill.numSamples);
    }

    void releaseResources() override
    {

    }

    ~AnotherMidiSource() override
    {

    }
private:
    std::vector<juce::MidiMessage> notesToPlay;
    std::vector<juce::int64>timeToPlay;
    juce::int64 nextPlayTime;
    juce::int64 sampleCounter = 0;
    int currentNote = 0;
    int divider = 1;

    juce::Synthesiser synth;
    juce::MidiBuffer processedBuffer;

};
1 Like

I’m glad to hear you found my code helpful. :slight_smile:

Corrected a small…bug. This line should read as below and nothing else.

while (nextPlayTime < endOfThisBuffer)
1 Like

You made my day, sir! Thank you, from the bottom of my heart. Moreover, your code teached me many things on how to work with the processBlock, in terms of buffer calculation :slightly_smiling_face:

1 Like