HandleNoteOn called twice when merge "Handling MIDI events" and "Build a MIDI synthesizer" tutorials

Hi!

I’ve tried to merge the two tutorials Handling MIDI events and Build a MIDI synthesizer in order to display all midi events with their respective source names and play a synth according to all these events (on-screen keyboard and/or external MIDI).

To make it works, I used these two callbacks on the setMidiInput function:

deviceManager.addMidiInputDeviceCallback (newInput.identifier, synthAudioSource.getMidiCollector()); //from Build a MIDI synthesizer
deviceManager.addMidiInputDeviceCallback (newInput.identifier, this); // from Handling MIDI events

With theses lines, it works a little, but when a noteOn is used from external midi, it is displayed twice (External MIDI + on-screen keyboard), even with using the scopedInputFlag from the Handling MIDI events tutorial. (The function handleNoteOn is actually called three times, the flag is used to by-pass one, but it still remains two!).

I’ve try many changes on listener and callback, but I can’t find a solution. Do you have any idea to implement that properly?

Thanks by advance for any comment or track!

Here is the code, if you want to have a look. I made changes from the tutorial Build a MIDI synthesizer in order to handle midi content and source.

I only changed the MainContentComponent class.
It inherit now from MidiInputCallback and MidiKeyboardStateListener, in order to handle external midi as well as internal midi keyboard.

#pragma once

//==============================================================================
struct SineWaveSound   : public SynthesiserSound
{
SineWaveSound() {}

bool appliesToNote    (int) override        { return true; }
bool appliesToChannel (int) override        { return true; }};
         //==============================================================================
struct SineWaveVoice   : public SynthesiserVoice
{
SineWaveVoice() {}

bool canPlaySound (SynthesiserSound* sound) override
{
    return dynamic_cast<SineWaveSound*> (sound) != nullptr;
}

void startNote (int midiNoteNumber, float velocity,
                SynthesiserSound*, int /*currentPitchWheelPosition*/) override
{
    currentAngle = 0.0;
    level = velocity * 0.15;
    tailIn = tailOff;
    tailOff = 0.0;


    auto cyclesPerSecond = MidiMessage::getMidiNoteInHertz (midiNoteNumber);
    auto cyclesPerSample = cyclesPerSecond / getSampleRate();

    angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;
}

void stopNote (float /*velocity*/, bool allowTailOff) override
{
    if (allowTailOff)
    {
        //if (tailOff == 0.0) //pat
        //    tailOff = 1.0;
        tailOff = tailIn;
    }
    else
    {
        clearCurrentNote();
        angleDelta = 0.0;
    }
}

void pitchWheelMoved (int) override      {}
void controllerMoved (int, int) override {}

void renderNextBlock (AudioSampleBuffer& outputBuffer, int startSample, int numSamples) override
{
    if (angleDelta != 0.0)
    {
        if (tailOff > 0.0) // [7]
        {
            while (--numSamples >= 0)
            {
                auto currentSample = (float) (std::sin (currentAngle) * level * tailOff);

                for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
                    outputBuffer.addSample (i, startSample, currentSample);

                currentAngle += angleDelta;
                ++startSample;

                //tailOff *= 0.99; // [8]
                tailOff *= 0.9999; // [8] //pat
                //tailOff *= 0.5; // [8] //pat

                //if (tailOff <= 0.005) // pat
                if (tailOff <= 0.001)
                {
                    clearCurrentNote(); // [9]

                    angleDelta = 0.0;
                    break;
                }
            }
        }
        else
        {
            while (--numSamples >= 0) // [6]
            {


                  auto currentSample = (float) (std::sin (currentAngle) * level * tailIn); // pat

                  for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
                      outputBuffer.addSample (i, startSample, currentSample);

                  currentAngle += angleDelta;
                  ++startSample;

                if (tailIn < 0.99)
                  {
                tailIn += .0001; // pat
              }
            }
        }
    }
}

private:
double currentAngle = 0.0, angleDelta = 0.0, level = 0.0, tailOff = 0.0;
double tailIn = 0.0; //pat
};
//==========================================================================
  class SynthAudioSource   : public AudioSource
{
public:
SynthAudioSource (MidiKeyboardState& keyState)
    : keyboardState (keyState)
{
    for (auto i = 0; i < 10; ++i)                // [1] pat
        synth.addVoice (new SineWaveVoice());

    synth.addSound (new SineWaveSound());       // [2]
}

void setUsingSineWaveSound()
{
    synth.clearSounds();
}

void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override
{
    synth.setCurrentPlaybackSampleRate (sampleRate); // [3]
    midiCollector.reset (sampleRate); // [10]
}

void releaseResources() override {}

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

    MidiBuffer incomingMidi;
    midiCollector.removeNextBlockOfMessages (incomingMidi, bufferToFill.numSamples); // [11]
    keyboardState.processNextMidiBuffer (incomingMidi, bufferToFill.startSample,
                                         bufferToFill.numSamples, true);       // [4]

    synth.renderNextBlock (*bufferToFill.buffer, incomingMidi,
                           bufferToFill.startSample, bufferToFill.numSamples); // [5]
}

MidiMessageCollector* getMidiCollector()
{
return &midiCollector;
}


private:
MidiKeyboardState& keyboardState;
Synthesiser synth;
MidiMessageCollector midiCollector;
  };//==========================================================================

class MainContentComponent   : public AudioAppComponent,
                            private MidiInputCallback, // added
                            private MidiKeyboardStateListener, // added
                            private Timer
                            
                            
{
public:
MainContentComponent()
    : synthAudioSource  (keyboardState),
      keyboardComponent (keyboardState, MidiKeyboardComponent::horizontalKeyboard)
{
    addAndMakeVisible (keyboardComponent);
    setAudioChannels (0, 2);
    
    std::cout << "-- Start --" << std::endl;

    addAndMakeVisible (midiInputListLabel);
    midiInputListLabel.setText ("MIDI Input:", dontSendNotification);
    midiInputListLabel.attachToComponent (&midiInputList, true);

    addAndMakeVisible (midiInputList);
    midiInputList.setTextWhenNoChoicesAvailable ("No MIDI Inputs Enabled");
    auto midiInputs = MidiInput::getAvailableDevices();
    StringArray midiInputNames;
    for (auto input : midiInputs)
        midiInputNames.add (input.name);
    midiInputList.addItemList (midiInputNames, 1);
    midiInputList.onChange = [this] { setMidiInput (midiInputList.getSelectedItemIndex()); };

    for (auto midiInput : midiInputs)
    {
        if (deviceManager.isMidiInputDeviceEnabled (midiInput.identifier))
        {
            setMidiInput (midiInputs.indexOf (midiInput));
            break;
        }
    }
    
    if (midiInputList.getSelectedId() == 0)
        setMidiInput (0);

    keyboardState.addListener (this); // added
    
    setSize (600, 160);
    startTimer (400);
}

~MainContentComponent() override
{
    shutdownAudio();
}

void resized() override
{
    midiInputList    .setBounds (200, 10, getWidth() - 210, 20);
    keyboardComponent.setBounds (10,  40, getWidth() - 20, getHeight() - 50);
}

void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override
{
    synthAudioSource.prepareToPlay (samplesPerBlockExpected, sampleRate);
}

void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
{
    synthAudioSource.getNextAudioBlock (bufferToFill);
}

void releaseResources() override
{
    synthAudioSource.releaseResources();
}

void setMidiInput (int index)
{
    auto list = MidiInput::getAvailableDevices();

    deviceManager.removeMidiInputDeviceCallback (list[lastInputIndex].identifier, synthAudioSource.getMidiCollector()); // [13]

    auto newInput = list[index];

    if (! deviceManager.isMidiInputDeviceEnabled (newInput.identifier))
        deviceManager.setMidiInputDeviceEnabled (newInput.identifier, true);

    deviceManager.addMidiInputDeviceCallback (newInput.identifier, synthAudioSource.getMidiCollector()); // [12]
    deviceManager.addMidiInputDeviceCallback (newInput.identifier, this);
    midiInputList.setSelectedId (index + 1, dontSendNotification);

    lastInputIndex = index;
}


// These methods handle callbacks from the midi device + on-screen keyboard..
void handleIncomingMidiMessage (MidiInput* source, const MidiMessage& message) override
{
    std::cout << "handleIncomingMidiMessage: " << source -> getName() << message.getRawData () <<std::endl;
    //const ScopedValueSetter<bool> scopedInputFlag (isAddingFromMidiInput, true);
    //keyboardState.processNextMidiEvent (message);
}

void handleNoteOn (MidiKeyboardState *source, int midiChannel, int midiNoteNumber, float velocity) override
{
    //std::cout << "handleNoteOn" << std::endl;

    //if (! isAddingFromMidiInput)
    {
    std::cout << "handleNoteOn: "  <<  midiNoteNumber << std::endl;
    }
}

void handleNoteOff (MidiKeyboardState *source, int midiChannel, int midiNoteNumber, float velocity) override
{
    //std::cout << "handleNoteOff" << std::endl;
    //if (! isAddingFromMidiInput)
    {
        std::cout << "handleNoteOff: "  <<  midiNoteNumber << std::endl;

    }
}
 
 
 

private:
void timerCallback() override
{
    keyboardComponent.grabKeyboardFocus();
    stopTimer();
}

//==========================================================================
SynthAudioSource synthAudioSource;
MidiKeyboardState keyboardState;
MidiKeyboardComponent keyboardComponent;

ComboBox midiInputList;
Label midiInputListLabel;
int lastInputIndex = 0;

bool isAddingFromMidiInput = false; // added


JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

As you can see if you try, when I use an external midi source, both the handleIncomingMidiMessage and handleNoteOn/Off are called.

Could you please help me to solve this?

Thanks for your help! :rofl: :innocent:

I finally found a solution with using a timestamp and compare it in the handleIncomingMidiMessage() and handleNoteOn() function.

With this code, a function is reached (see output of cout) only once for a note on, whatever if it is external midi of internal keyboard component.

// These methods handle callbacks from the midi device + on-screen keyboard..
void handleIncomingMidiMessage (MidiInput* source, const MidiMessage& message) override
{
    std::cout << "handleIncomingMidiMessage reached: " << source -> getName() << message.getRawData () <<std::endl;
    const ScopedValueSetter<bool> scopedInputFlag (isAddingFromMidiInput, true);
    lastTimeStamp = Time::getApproximateMillisecondCounter ();
    std::cout << "lastTimeStamp: " << lastTimeStamp << std::endl;
    keyboardState.processNextMidiEvent (message);
}

void handleNoteOn (MidiKeyboardState *source, int midiChannel, int midiNoteNumber, float velocity) override
{
    
    timeStamp =  Time::getApproximateMillisecondCounter ();
    std::cout << "handleNoteOn function called" << std::endl;
    std::cout << "timeStamp: " << timeStamp << std::endl;
    std::cout << "isAddingFromMidiInput: " << isAddingFromMidiInput << std::endl;
    std::cout << "(timeStamp == lastTimeStamp): " << (timeStamp == lastTimeStamp) << std::endl;
    if ((! isAddingFromMidiInput ) && (timeStamp != lastTimeStamp))
    {
    std::cout << "handleNoteOn reached: "  <<  midiNoteNumber << std::endl;
    }
}

void handleNoteOff (MidiKeyboardState *source, int midiChannel, int midiNoteNumber, float velocity) override
{
    std::cout << "handleNoteOff function called" << std::endl;
    if (! isAddingFromMidiInput)
    {
        std::cout << "handleNoteOff reached: "  <<  midiNoteNumber << std::endl;

    }
}
 
 
 

private:
void timerCallback() override
{
    keyboardComponent.grabKeyboardFocus();
    stopTimer();
}

//==========================================================================
SynthAudioSource synthAudioSource;
MidiKeyboardState keyboardState;
MidiKeyboardComponent keyboardComponent;

ComboBox midiInputList;
Label midiInputListLabel;
int lastInputIndex = 0;

bool isAddingFromMidiInput = false; // added

float lastTimeStamp = 0;
float timeStamp = 0;


JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

By the way, this is only needed for handleNoteOn(), and not handleNoteOff(). Do you know why?

I suppose better solutions exist.
Thanks for any comment or answer!