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 ! 