Inject notes to a synthesiser

I would like to make a synthesiser play some notes in an “artificial” way.

Let’s say I have the following list of values: juce::Array<int> notesToPlay = (46, 98, 55, 21, 120, 46, 34, 23, 65). I would like to pass these values to a synthesiser in order to play them.

I’m modifying the code available in this tutorial: https://docs.juce.com/master/tutorial_synth_using_midi_input.html.

This is my progresses so far:

    void playANote()
    {
        juce::Array<int> notesToPlay;
        notesToPlay.add(46, 98, 55, 21, 120, 46, 34, 23, 65);
        auto numNotes = notesToPlay.size();

        juce::MidiBuffer buff;

        double theTime = (double)0;
        for (int i = 0; i < notesToPlay.size(); i++)
        {
            juce::MidiMessage aMessage;
            aMessage.noteOn(10, notesToPlay[i], (float)1);
            auto timestamp = theTime;
            buff.addEvent(aMessage, timestamp);
            theTime += 100;
        }
        
        keyboardState.processNextMidiBuffer(buff, buff.getFirstEventTime(), buff.getNumEvents(), false);
    
        
    }

But when I launch the program, nothing happens. Any suggestions? Thank you in advance

Ok, I tried the code posted here (since the topic is very similar to my task): https://forum.juce.com/t/midi-generator-creation/12912/15, but the insertion of code inside the processBlock does not produce any sound.

This is the code of my synth’s processBlock:

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

        int numSamples = buffer.getNumSamples();
        double numSecondsInThisBlock = numSamples / sampleFrequence;
        double endTime = playTime + numSecondsInThisBlock;
        static int nextEventID;
        const juce::MidiMessageSequence::MidiEventHolder* m;
        for (; m = msgSeq.getEventPointer(nextEventID); nextEventID++)
        {
            double timeStamp = m->message.getTimeStamp();
            if (timeStamp >= endTime)
                break;
            if (timeStamp >= playTime)
                keyStatePtr->processNextMidiEvent(m->message);
                
        }
        playTime += numSecondsInThisBlock;
        
        keyStatePtr->processNextMidiBuffer(midiBuffer, bufferToFill.startSample,
            bufferToFill.numSamples, true);
        
        synth.renderNextBlock(*bufferToFill.buffer, midiBuffer,
            bufferToFill.startSample, bufferToFill.numSamples;


    }

Because MidiMessage::noteOn is a static function you need to do

auto midiMessage = MidiMessage::noteOn(10, notesToPlay[i], 1.0f);

And furthermore, while you’re using midi channel 10, which is designated to drums/rhytm instruments, there’s an even bigger chance you’ll hear something if you limit the values in notesToPlay to between 35 and 81. MidiMessage::getRhythmInstrumentName will give you a clue.

1 Like

Hi oxxyyd,
in the meantime, I moved on on with my development and I wrote another version of the code.

Taking inspiration from an older topic of this forum (i.e.: https://forum.juce.com/t/midi-generator-creation/12912/15)

class MidiSource : public juce::AudioProcessor
{
public:
    MidiSource()
    {
        keyStatePtr.reset(new juce::MidiKeyboardState());
        for (auto i = 0; i < 5; i++)
            synth.addVoice(new SineWaveVoice());

        synth.addSound(new SineWaveSound());

        notesToPlay.add(56, 76, 65, 77, 81, 45);
        playNotes();

    }
/===========================================================/

    juce::Synthesiser synth;
    std::unique_ptr<juce::MidiKeyboardState> keyStatePtr;
}

Then, I call the function playNotes() which is in charge of populating a MidiBuffer with the notes to be played:

    void playNote()
    {
        juce::MidiBuffer myBuff;
        int time;
        
        for (auto note : notesToPlay)
        {
            juce::MidiMessage myMessage;
            myMessage = juce::MidiMessage::noteOn(1, note, (std::uint8_t)1.0);
            myMessage.setTimeStamp(time);
            myBuff.addEvent(myMessage, time);

            juce::MidiMessage mySecondMessage;
            mySecondMessage = juce::MidiMessage::noteOff(1, note, (std::uint8_t)1.0);
            mySecondMessage.setTimeStamp(time + 1000);
            time = time + 100;
            myBuff.addEvent(mySecondMessage, time);

            time = time + 100;
        }

Finally, I would like to reproduce this buffer by my processBlock. In the previous post, I tried to use a MidiMessageSequence, (i.e.: juce::MidiMessageSequence msgSeq in the previous messages), but with no results

Yeah, I noticed, and, as a matter of fact, I corrected my code :slightly_smiling_face:

I’m now using 1 as a channel. I can hear a very low note, but then the sequence is not played in its entirety.

I’d suggest you start with just one noteOn message and follow it’s way in the processBlock in the debugger. When you see that’s working, add a noteOn, and so on…

By the way, there’s two versions of MidiMessage::noteOn, noteOn(uint8 velocity) and noteOn(float velocity). They take 127 resp 1.0f to render at full velocity. Your code produces a note of the absolute minimum velocity/volume possible, 1 /127, which can be a reason you don’t hear much…

And try a midi note higher than 1, e.g. 60. 1 is so low it’s barely reproducible in ordinary audio equipment.

noteOn(60, 127) is a good start.

1 Like

I checked my code and actually I’m using this value for velocity: (std::uint8_t)1.0, both for noteOn and noteOff methods. As a matter of fact, if I want to reproduce a single note, I can hear the note really loud.

The problem is that: I’m not able to reproduce the entire sequence of MIDI notes declared inside the buffer juce::MidiBuffer myBuff :confounded:

Yeap, it’s also depending on if your synth is velocity sensitive or not. If it’s not, it doesn’t matter much if you feed it with 127 or 1, as long as it’s not 0

I confirm you that my synth is sensitive to velocity :slightly_smiling_face:

Ok, this is another interesting topic which seems very similar to my issue: https://forum.juce.com/t/create-chords-from-one-midi-note/31132

I can’t follow his logic on that, but that could be a shortcoming of mine.

One common mistake people make when experimenting with midi is to shove a ton of messages up a single processBlock.
They’re lucky if a single midi message survives.

A processBlock is typically called every 10:th millisecond.
If you add a noteOn in a processBlock call you have to wait another 100 processBlock calls before
it’s time to add the corresponding notOff if you want the tone to last a second. Typically.

It won’t work to inject a noteOn and its noteOff in the same processBlcok call.

This means you have to implement a process call counter of some kind (or a timer, but that’s never gonna be precise enough for pratical use) to keep track of the time so you can inject the midi events at proper time inteval.

1 Like

Hi @balthus89, I had to do something similar recently for testing some synths. Here’s my processBlock, the first parameter is a slider that enables playback if its value is 1.

void MIDIGenerator::processBlock (juce::AudioSampleBuffer& buffer, juce::MidiBuffer& midiBuffer)
{    	
	int numSamples = buffer.getNumSamples();
	
	if(getParameters()[0]->getValue() == 1)
	{
		for (int i = 0; i < numSamples; i++, sampleIndex++)
		{
			int note = Random::getSystemRandom().nextInt(Range<int>(60, 72));
			if (sampleIndex == 1)
			{
				midiBuffer.addEvent(MidiMessage::noteOff(1, lastMidiNote, 0.f), 0); //this event is within our time slot. 
				midiBuffer.addEvent(MidiMessage::noteOn(1, note, .7f), 0); //this event is within our time slot. 
				lastMidiNote = note;
			}
			sampleIndex = sampleIndex > 44100 ? 0 : sampleIndex + 1;
		}
	}
}
1 Like

Hi rory,
thank you so much for your reply.

I tried your code, but nothing happens :frowning_face:

I suspect that the problem is how to inject these notes in the buffers. In my processBlock, I got something like:

        std::unique_ptr<juce::MidiKeyboardState> keyStatePtr;
        juce::Synthesiser synth;

        keyStatePtr->processNextMidiBuffer(midiBuffer, bufferToFill.startSample,
            bufferToFill.numSamples, true);
        
        synth.renderNextBlock(*bufferToFill.buffer, midiBuffer,
            bufferToFill.startSample, bufferToFill.numSamples;

Where do I have to place these instructions inside the code that you have suggested?

Ok, the probelm is that I can’t understand how you manage sampleIndex: how do I have to declare it?

I performed some debug print and I noticed that the processBlock never enters the if(sampleIndex == 1) statement

Ok, this is the version of my code which now does something: I can hear a sound similar to rain drops :grin:

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

        int numSamples = buffer.getNumSamples();

        for (int i = 0; i < numSamples; i++, sampleIndex++)
        {
            int note = juce::Random::getSystemRandom().nextInt(juce::Range<int>(60, 72));
            if (sampleIndex == 1)
            {
                midiBuffer.addEvent(juce::MidiMessage::noteOff(1, lastMidiNote, 0.f), 10); //this event is within our time slot. 
                midiBuffer.addEvent(juce::MidiMessage::noteOn(1, note, .7f), 20); //this event is within our time slot. 
                lastMidiNote = note;
            }
            sampleIndex = sampleIndex > buffer.getNumSamples() ? 0 : sampleIndex + 1;

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

            synth.renderNextBlock(*bufferToFill.buffer, midiBuffer,
                bufferToFill.startSample, bufferToFill.numSamples);
        }
    }
/=====================================================================/
int sampleIndex= 0;

Still stuck here. Any help? :slightly_smiling_face:

Here’s the last version of my code. Now the program emits one single note eternally…

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

        for (int i = 0; i < noOfNotes; i++)
        {
            auto note = juce::Random::getSystemRandom().nextInt(juce::Range<int>(60, 72));
            auto message = juce::MidiMessage::noteOn(1, note, (juce::uint8)100);
            message.setTimeStamp(theTime);
            theTime += 100;
            messageSequence.addEvent(message);

            notes.add(note);

        }

    }

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

        auto numSamples = buffer.getNumSamples();
        midiBuffer.clear();
        juce::MidiBuffer myBuff;

        for (auto i = 0; i < messageSequence.getNumEvents(); i++)
        {
            juce::MidiMessageSequence::MidiEventHolder* eventHolder = messageSequence.getEventPointer(i);
            auto samplePosition = juce::roundToInt((eventHolder->message.getTimeStamp()) * getSampleRate());

            myBuff.addEvent(eventHolder->message, samplePosition);
            
        }

        midiBuffer.swapWith(myBuff);
 

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

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

    }

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

private:
    std::unique_ptr<juce::MidiKeyboardState> keyState;
    juce::Synthesiser synth;

    juce::Array<int> notes;
    int noOfNotes = 6;
    int theTime;
    juce::MidiMessageSequence messageSequence;

My example was just a quick hack for a MIDI output plugin. It wasn’t really meant to be wrapped up as a synth voice. I’m sorry, but I don’t have to time to delve further into this.

1 Like

It’s ok, rory. However, thank you for your answer :wink:

1 Like