Tutorial: Handling MIDI events - updating Pitchwheel

Hi

Ive added a pitchwheel slider to the example in the “Tutorial:Handling MIDI events”. I have incoming midi and i added a slider that gets updated with the incoming pitchwheel changes via the timerCallback() method:

void timerCallback() override
	
	if (midiMessage.isPitchWheel())
		{
			pitchWheel.setValue(midiMessage.getPitchWheelValue(),dontSendNotification);
		}

i’ve set the Timer::startTimer(20); … i can move the hardware pitchwheel for a while and it works fine but after a while it gets stuck and slow. I have tried with lower startTimer values and it’s the same.

what are my unexperienced eyes, missing ?

im guessing, it is a miss aligned timing of the midi message callback and the timercallback … ? any hint is appreciated !!!

@jules or @timur is it possible to update the zip file with the last exercise “add modwheel and pitchwheel” as a _2 or _3 source code … this would be very educational !

1 Like

I think it may be @martinrobinson-2 who wrote the tutorial.

Yes I wrote that one. I’ll have a think, but for that exercise I was probably expecting you to use a CallbackMessage to get stuff back onto the message thread (as that technique was used in the IncomingMessageCallback class within the tutorial).

Looking at your code excerpt above using the Timer: if you’re storing the MIDI message in midiMessge, which looks like it’s a member variable (?), then if other MIDI is arriving within the 20ms (or even 1ms if you set the minimum Timer interval), then the value is likely to get overwritten before you get into the timer callback. Using the CallbackMessage will mean all the messages are queued.

The longer answer is that in a real audio application (where you are using an audio callback and generating or processing audio) then you probably wouldn’t do any of this as you’d be using the audio callback to deal with the MIDI too. Then you’d need something else to send the MIDI to the message thread (as the CallbackMessage technique can’t be used as it allocates memory).

2 Likes

thanks a lot @martinrobinson-2 for your explanation … i will try it with both ( message thread & audio callback ) so i can learn both concepts.

Hello!

I also write a synthesizer based on the tutorial Handling Midi Events (sorry @martinrobinson-2 :grin: ). I realized that the pitchWheelMoved function of my synth is fired only once after a first note-on event have been send to this synth.

The pitch wheel is well detected by the functions of the tutorial, however it is not send to my synth before the first note-on event.

Is there a rational for this behavior? How can I fix that issue? Thanks a lot!

I think we’d need to see some code on what you have tried so far.

The Build a MIDI synthesiser tutorial is probably a better starting point for a synth.

https://docs.juce.com/master/tutorial_synth_using_midi_input.html

Thanks. Actually I’ve tried to merge the both tutorials (build a midi synth & handling midi events).
Here is some code. The part that handle midi is made on a component, in a file called MidiInputComponent.h :

class MidiInputComponent  : public Component,
                          private MidiInputCallback,
                          private MidiKeyboardStateListener
{
public:
    MidiInputComponent(SynthComponent * synthCompo):synthAudioSource(keyboardState, synthCompo), keyboardComponent (keyboardState, MidiKeyboardComponent::horizontalKeyboard),
    startTime (Time::getMillisecondCounterHiRes() * 0.001)
{
    
    setOpaque (true);
    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()); };

    // find the first enabled device and use that by default
    for (auto input : midiInputs)
    {
        if (deviceManager.isMidiInputDeviceEnabled (input.identifier))
        {
            setMidiInput (midiInputs.indexOf (input));
            break;
        }
    }

    // if no enabled devices were found just use the first one in the list
    if (midiInputList.getSelectedId() == 0)
        setMidiInput (0);
    addAndMakeVisible (keyboardComponent);
    keyboardState.addListener (this);

    addAndMakeVisible (midiMessagesBox);
    midiMessagesBox.setMultiLine (true);
    midiMessagesBox.setReturnKeyStartsNewLine (true);
    midiMessagesBox.setReadOnly (true);
    midiMessagesBox.setScrollbarsShown (true);
    midiMessagesBox.setCaretVisible (false);
    midiMessagesBox.setPopupMenuEnabled (true);
    midiMessagesBox.setColour (TextEditor::backgroundColourId, Colour (0x32ffffff));
    midiMessagesBox.setColour (TextEditor::outlineColourId, Colour (0x1c000000));
    midiMessagesBox.setColour (TextEditor::shadowColourId, Colour (0x16000000));
}

~MidiInputComponent() override
{
    keyboardState.removeListener (this);
    deviceManager.removeMidiInputDeviceCallback (MidiInput::getAvailableDevices()[midiInputList.getSelectedItemIndex()].identifier, this);
}

void paint (Graphics& g) override
{
    g.fillAll (Colours::black);
}

void resized() override
{
    auto area = getLocalBounds();

    midiInputList    .setBounds (area.removeFromTop (36).removeFromRight (getWidth() - 150).reduced (8));
    keyboardComponent.setBounds (area.removeFromTop (80).reduced(8));
    midiMessagesBox  .setBounds (area.reduced (8));
}

void timerCallback()
{
    keyboardComponent.grabKeyboardFocus();
    
}



private:
    static String getMidiMessageDescription (const MidiMessage& m)
{
    if (m.isNoteOn())           return "Note on "          + MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3);
    if (m.isNoteOff())          return "Note off "         + MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3);
    if (m.isProgramChange())    return "Program change "   + String (m.getProgramChangeNumber());
    if (m.isPitchWheel())       return "Pitch wheel "      + String (m.getPitchWheelValue());
    if (m.isAftertouch())       return "After touch "      + MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) +  ": " + String (m.getAfterTouchValue());
    if (m.isChannelPressure())  return "Channel pressure " + String (m.getChannelPressureValue());
    if (m.isAllNotesOff())      return "All notes off";
    if (m.isAllSoundOff())      return "All sound off";
    if (m.isMetaEvent())        return "Meta event";

    if (m.isController())
    {
        String name (MidiMessage::getControllerName (m.getControllerNumber()));

        if (name.isEmpty())
            name = "[" + String (m.getControllerNumber()) + "]";

        return "Controller " + name + ": " + String (m.getControllerValue());
    }

    return String::toHexString (m.getRawData(), m.getRawDataSize());
}

void logMessage (const String& m)
{
    midiMessagesBox.moveCaretToEnd();
    midiMessagesBox.insertTextAtCaret (m + newLine);
}

/** Starts listening to a MIDI input device, enabling it if necessary. */
void setMidiInput (int index)
{
    auto list = MidiInput::getAvailableDevices();

    deviceManager.removeMidiInputDeviceCallback(list[lastInputIndex].identifier, this);

    auto newInput = list[index];

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

    deviceManager.addMidiInputDeviceCallback (newInput.identifier, synthAudioSource.getMidiCollector()); // needed to process the midi messages and play audio
    deviceManager.addMidiInputDeviceCallback (newInput.identifier, this); // needed to display messages
    midiInputList.setSelectedId (index + 1, dontSendNotification);
    lastInputIndex = index;    
}

void handleIncomingMidiMessage (MidiInput* source, const MidiMessage& message) override
// Handling all external midi inputs
{
    const ScopedValueSetter<bool> scopedInputFlag (isAddingFromMidiInput, true);
    keyboardState.processNextMidiEvent (message);
    postMessageToList (message, source->getName());
    std::cout << getMidiMessageDescription(message) << newLine;
}

void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override
// Handling midi inputs "NoteOn" from on-screen keyboard
{
    
    if (! isAddingFromMidiInput)
    {
        auto m = MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity);
        m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001);
        postMessageToList (m, "On-Screen Keyboard");
        std::cout << getMidiMessageDescription(m) << newLine;
    }
    
    
}

void handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float /*velocity*/) override
// Handling midi inputs "NoteOff" from on-screen keyboard
{
    if (! isAddingFromMidiInput)
    {
        auto m = MidiMessage::noteOff (midiChannel, midiNoteNumber);
        m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001);
        postMessageToList (m, "On-Screen Keyboard");
        
        std::cout << getMidiMessageDescription(m) << newLine;
    }
   
}



void postMessageToList (const MidiMessage& message, const String& source)
{
    (new IncomingMessageCallback (this, message, source))->post();
}

void addMessageToList (const MidiMessage& message, const String& source)
{
    auto time = message.getTimeStamp() - startTime;

    auto hours   = ((int) (time / 3600.0)) % 24;
    auto minutes = ((int) (time / 60.0)) % 60;
    auto seconds = ((int) time) % 60;
    auto millis  = ((int) (time * 1000.0)) % 1000;

    auto timecode = String::formatted ("%02d:%02d:%02d.%03d",
                                       hours,
                                       minutes,
                                       seconds,
                                       millis);

    auto description = getMidiMessageDescription (message);

    String midiMessageString (timecode + "  -  " + description + " (" + source + ")"); // [7]
    logMessage (midiMessageString);
}



// This is used to dispach an incoming message to the message thread
class IncomingMessageCallback   : public CallbackMessage
{
public:
    IncomingMessageCallback (MidiInputComponent* o, const MidiMessage& m, const String& s)
       : owner (o), message (m), source (s)
    {}

    void messageCallback() override
    {
        if (owner != nullptr)
            owner->addMessageToList (message, source);
    }

    Component::SafePointer<MidiInputComponent> owner;
    MidiMessage message;
    String source;
};


AudioDeviceManager deviceManager;
ComboBox midiInputList;
Label midiInputListLabel;
int lastInputIndex = 0;
bool isAddingFromMidiInput = false;

SynthAudioSource synthAudioSource;

MidiKeyboardState  keyboardState;
MidiKeyboardComponent keyboardComponent;

TextEditor midiMessagesBox;
double startTime;

And then I’ve got a class SynthAudioSource in a file wisely called SynthAudioSource.h:

 class SynthAudioSource   : public juce::AudioAppComponent
{
public:
SynthAudioSource (MidiKeyboardState& keyState, SynthComponent * synthCompo) : keyboardState (keyState)
{
    setAudioChannels (0, 2);   
    synth.addVoice (new MySynth(std::string("Synth name"), std::string("Synth type"), synthCompo));
    synth.addSound (new MySynthSound());       
}

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

void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override
{
    std::cout << "Preparing to play audio ------------------------------------------------- " << newLine;
    std::cout << " samplesPerBlockExpected = " << samplesPerBlockExpected << newLine;
    std::cout << " sampleRate = " << sampleRate << newLine;
    
    synth.setCurrentPlaybackSampleRate (sampleRate);
    midiCollector.reset (sampleRate);
}

void releaseResources() override {}

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

    juce::MidiBuffer incomingMidi;
    
    midiCollector.removeNextBlockOfMessages (incomingMidi, bufferToFill.numSamples);
    keyboardState.processNextMidiBuffer (incomingMidi, bufferToFill.startSample,
                                         bufferToFill.numSamples, true);
   
    synth.renderNextBlock (*bufferToFill.buffer, incomingMidi,
                           bufferToFill.startSample, bufferToFill.numSamples);
}

MidiMessageCollector* getMidiCollector()
{
    return &midiCollector;
}

private:
    juce::Synthesiser synth;
    juce::MidiMessageCollector midiCollector;
    MidiKeyboardState& keyboardState;

};

And off course I’ve got the class MySynth that is a SynthesiserVoice and implements the current functions (startNote(…), pitchWheelMoved(…), …).

As I said previsoulsy, the strange thing is that the pitchWheelMoved() in that class is called only after a first startNote() occurs. Thanks a lot for any feedback ! :wink:

Any feedback on this code will be appreciate.
By the way, I’ve got the same problem with the Control Change. The function controllerMoved(…) of my SynthesiserVoice is called only after the sending of a first noteOn event (and then it works fine). Any idea on how to solve this issue? thanks!