Table of Contents
Platforms: Windows, Mac OS X, Linux, iOS, Android
- This tutorial assumes that you are familiar with MIDI in general. You should also be familiar with using JUCE buttons and sliders (see Tutorial: Slider values and Tutorial: Listeners and broadcasters).
Download the demo project for this tutorial here: tutorial_midi_message.zip. Unzip the project and open it in your IDE.
If you need help with this step, see Tutorial: Getting started with the Projucer.
The demo project presents four buttons to create MIDI messages on MIDI channel 10. These buttons create note-on messages for four of the standard (General MIDI) drum sounds: bass drum, snare drum, closed hi-hat, and open hi-hat. There is also a slider that creates a volume controller message (continuous controller 7). The interface is shown in the following screenshot.
The panel on the right-hand side displays the list of MIDI messages than have been generated, along with a timestamp (relative to the time that the application was launched).
- The application doesn't send any MIDI data, or make any sound, it only displays the MIDI data.
This tutorial illustrates the code required to create some MIDI message types. It also includes some code to parse most MIDI message types. In general, the MidiMessage class contains a range of
static member functions for creating MidiMessage objects (for example the MidiMessage::noteOn() function for creating note-on messages). There are also a range of member functions for querying and accessing MidiMessage objects (for example, the MidiMessage::isNoteOn() and MidiMessage::getNoteNumber() functions).
Have a look at the public static member functions for the MidiMessage class. This lists all of the functions for creating different types of MIDI message. You can also create MidiMessage objects from the individual bytes or raw data but these must be valid MIDI messages according to the MIDI specification. (An assertion will be generated in a debug build if you create an invalid MidiMessage object.)
- MidiMessage objects should normally be stored as local or member variables and passed by value.
To create a note-on message use the MidiMessage::noteOn() function. This needs the MIDI channel (numbered 1 .. 16), the note number (0 .. 127), and the velocity (as a
uint8 value 0 .. 127). Alternatively, the velocity can be expressed as a
float value which will be converted to 0 .. 127 internally (rounded to the nearest integer).
- A note-on with zero velocity is actually a note-off message so note-on velocities are in the range 1 .. 127 (which makes the minimum floating-point velocity for a note-on around
0.004f). There is also the MidiMessage::noteOff() function for specifically creating note-off messages that also allows you to specify a note-off velocity (which is recognised by some synthesisers).
In our demo project we create a note-on message with velocity of 100 and a different note number depending on which button was clicked:
Notice the value of
-1 that is used to check that at least one of our buttons set the
noteNumber value. We also need to cast the value
100 to the
uint8 type. If we don't do this then there is a compiler ambiguity regarding which of the versions of the MidiMessage::noteOn() function should be called.
Setting the timestamp of a MidiMessage is optional but it's very useful for keeping track of the time that events were generated or received. The default timestamp is zero and the time units of the timestamp are not defined. In general, it is up to the application to decide what time units to use. In this simple case we are using seconds as the units by obtaining the current time using the Time::getMillisecondCounterHiRes() function and multiplying by 0.001 (and subtracting the time that the application started so that this is relative to that point in time).
The volume slider is used to create a continuous controller (CC) message. CC7 is the the volume control change message:
addMessageToList() function parses the timestamp and the MIDI message so that it can be displayed in the list of messages in our interface:
getMidiMessageDescription() function actually parses the MIDI data to get a human-readable description of the message.
- The same functionality is already available through the member function MidiMessage::getDescription(). We do not use the ready-made implementation here but implement it ourselves to illustrate how to work with MIDI messages of different types.
This function attempts to parse all types of MIDI message (even though we have only looked at creating note-on and controller messages so far). Here you can see the recommended method of accessing the data in a MidiMessage object:
- determine the type of MIDI message (using one of the functions that start with "is"); then
- use appropriate functions for accessing that type of MIDI message.
We would only reach the final line of this function if the message was a system message (system exclusive, for example). You can access the raw data of any message using MidiMessage::getRawData() but generally it is easier (and more readable) to use the range of built-in functions for most purposes.
- Using functions to access data in a MidiMessage for messages of the wrong type will lead to errors. For example, the MidiMessage::getNoteNumber() function will return a value from any MidiMessage object but this doesn't confirm that the message is either a note-on or note-off message. You must check first with one of the functions MidiMessage::isNoteOn(), MidiMessage::isNoteOff(), or MidiMessage::isNoteOnOrOff().
- Modify the
getMidiMessageDescription()function so that it lists the velocity of note-on messages. Check the API reference to find out which function you should use.
One problem with our demo application is that it doesn't create note-off messages. We are just creating MIDI messages intended for percussion sounds, so this doesn't seem like a big problem. But, it's bad practice not to create note-off messages for corresponding note-on messages (with sustaining sounds it will lead to stuck notes).
We could just add a note-off immediately following the note-on in the
We could even change the timestamp of the note-off message (for example 0.1s after the note-on message) but this won't change when the messages are posted to the list:
The MidiBuffer class provides functions for iterating over buffers of MIDI messages based on their timestamps. To illustrate this we will set up a simple scheduling system where we add MidiMessage objects with specific timestamps to a MidiBuffer object. Then we use a Timer object that checks regularly whether any MIDI messages are due to be delivered.
- The Timer class is not suitable for high-precision timing. This is used to keep the example simple by keeping all function calls on the message thread. For more robust timing you should use another thread (in most cases the audio thread is appropriate for rendering MidiBuffer objects in to audio).
Add some members to our
- : The MidiBuffer object itself.
- : The MidiBuffer class uses samples as the units for the timestamps of MIDI messages. Although we are not generating audio we need to choose something to use as the sample rate. We use this member to store the sample rate. (We use 44,100 since this is a common value.)
- : We need to keep track of which timestamp we have already reached within the MidiBuffer. We use this member to store the this timestamp in samples.
We need to initialise the
previousSampleNumber members in our
Now modify the
sliderValueChanged() functions to make use of this function. This allows us to schedule MIDI message events into the future:
To read the messages from the buffer we need to implement our timer. Add the Timer class as a base class:
And implement the Timer::timerCallback() function:
- : Calculate the current time in samples.
- : Use a MidiBuffer::Iterator object to iterate over the messages in the buffer. The MidiBuffer::Iterator::getNextEvent() function updates the
sampleNumber(timestamp) variables with the values for the next MIDI message in the buffer.
- : If the timestamp for the MIDI message most recently retrieved from the MidiBuffer object is in the future, then we have finished processing and we exit the
- : The timestamps of the MidiMessage objects obtained will have the timestamps based on sample numbers. Let's reset this to our seconds-based timestamp system so that it works with our
addMessageToList()function without having to modify it.
- : The MidiBuffer::clear() function clears MIDI messages from the buffer that have timestamps within a certain range. We use this to remove messages that we have just processed.
- : Keep track of the time that this function executed for use the next time that the
timerCallback()function is called.
Finally, we need to start the timer in our
- The code for these modifications can be found in the
MainComponent_02.hfile within the
Sourcedirectory of the demo project.
- Add buttons for the crash cymbal (note number 49) and ride cymbal (note number 51). Add a slider for panning control (CC10). Space has been left for you to add these three components in the
- Create MidiMessage objects of specific types — note-on, note-off, continuous controller (control change), and so on.
- Parse a MidiMessage object to discover its type and obtain useful data from it.
- Store MIDI messages in a MidiBuffer object.
- Iterate over MIDI messages in a MidiBuffer object based on their timestamps.
Generated on Fri Jan 12 2018 09:51:15 for JUCE by 1.8.13