Using MIDI Functions


When the Juggler plays a MIDI score, it automatically calls MIDI playback functions through the MIDI object's interpreter procedure. Those MIDI playback functions are also available directly to a task, which allows a task to feed MIDI messages to the functions using its own techniques. It also allows a task to perform MIDI actions such as starting and stopping a note, all without directly using MIDI messages. And because these MIDI functions use the MIDI environment's voice allocation mechanism, their effects are fully integrated with a score that might be playing in the background or other audio activities taking place in the MIDI environment.

This section explains how the MIDI functions work so that a task can use them directly. This is useful if you wish to take advantage of the score player's voice management for something other than playing back a MIDI score (e.g. a sound effects system , see tsc_soundfx.c for an example of how to do this). You should note that the functions require a full MIDI environment to be set up, a score context with all of its attendant data structures.

Interpreting and Executing a MIDI Message

To execute any of the MIDI messages recognized by the Music library, use this call:

int32 InterpretMIDIMessage( ScoreContext *ScoreCon, char *MIDIMsg, int32 IfMute )
This is the call made by InterpretMIDIEvent(), which is used as an interpreter procedure for MIDI objects. InterpretMIDIMessage() accepts three arguments: *ScoreCon, a pointer to the score context used to play a MIDI score; *MIDIMsg, a pointer to a character string containing the MIDI message to be executed; and IfMute, a boolean flag that turns muting on and off.

When InterpretMIDIMessage() executes, it reads the specified MIDI message and if it recognizes the message, it passes the message and its accompanying data on to the appropriate MIDI function, which executes the message. If the IfMute flag is set to TRUE, InterpretMIDIMessage() does not recognize any Note On messages with velocity values greater than 0. In other words, it does not start any new notes, although it turns notes off and executes other MIDI messages such as program change messages and control change messages.

If successful, InterpretMIDIMessage() returns 0. If unsuccessful, it returns a negative value (an error code).

MIDI messages are defined by the MIDI standard as being a series of bytes. The first byte is the message, subsequent bytes (if necessary) are data pertaining to the message. The string that InterpretMIDIMessage() accepts is a string of MIDI message bytes, with the MIDI message in the first byte and data bytes following it.

Starting and Releasing a Note

When InterpretMIDIMessage() receives a Note On message, it makes this call:

int32 StartScoreNote( ScoreContext *scon, int32 Channel, int32 Note, int32 Velocity )
The call accepts four arguments: *scon, which is a pointer to the score context keeping track of note allocation; Channel, which is the channel number (1 to 16) of the note being started; Note, which is a MIDI pitch value from 0 to 127; and Velocity, which is a MIDI velocity value from 0 to 127.

When it executes, StartScoreNote() uses the voice allocation algorithm to find an appropriate instrument and then start a note on that instrument using the specified pitch and velocity. The channel number specified determines which instrument template should be used for the instrument. If successful, it returns 0. If unsuccessful, it returns a negative value (an error code).

When InterpretMIDIMessage() receives a Note Off message, it makes this call:

int32 ReleaseScoreNote( ScoreContext *scon, int32 Channel, int32 Note, int32 Velocity )
The call accepts four arguments: *scon, which is a pointer to the score context keeping track of note allocation; Channel, which is the channel number (1 to 16) of the note being released; Note, which is a MIDI pitch value from 0 to 127; and Velocity, which is a MIDI velocity value from 0 to 127.

When it executes, ReleaseScoreNote() finds the note of the specified pitch using the specified channel, and releases the note using the specified velocity. A released note does not necessarily stop playing immediately, so the instrument used for the note may continue to play for a while longer until the note stops and the instrument is abandoned. If successful, the call returns 0. If unsuccessful, it returns a negative value (an error code).

Directly Starting and Releasing an Instrument

ReleaseScoreNote() and StartScoreNote() use voice allocation as they start and release notes. Once a voice is allocated, they call lower level functions to start or release an instrument for the voice.

This call starts a specified instrument without using the MIDI environment's voice allocation mechanism:

int32 NoteOnIns( Item Instrument, int32 Note, int32 Velocity )
The call accepts three arguments: Instrument, which is the item number of the instrument to start; Note, which is a MIDI pitch value from 0 to 127; and Velocity, which is a MIDI velocity value from 0 to 127.

When it executes, NoteOnIns() uses the Audio folio to start the specified instrument using the pitch specified by Note and the amplitude specified by Velocity. If successful, the call returns 0. If unsuccessful, it returns a negative value (an error code).

This call releases a note on a specified instrument without using the MIDI environment's voice allocation mechanism:

int32 NoteOffIns( Item Instrument, int32 Note, int32 Velocity )
The call accepts three arguments: Instrument, which is the item number of the instrument to release; Note, which is a MIDI pitch value from 0 to 127; and Velocity, which is a MIDI velocity value from 0 to 127.

When executed, NoteOnIns() uses the Audio folio to release the specified instrument using the release value specified by Velocity. If successful, the call returns 0. If unsuccessful, it returns a negative value (an error code).

Changing a Program

When InterpretMIDIMessage() receives a Program Change message, it makes this call:

int32 ChangeScoreProgram( ScoreContext *ScoreCon, int32 Channel, int32 ProgramNum )
The call accepts three arguments: *ScoreCon, which is a pointer to the score context controlling the MIDI environment; Channel, which is the number of the channel (0 to 15) for which the program should be changed; and ProgramNum, a number from 1 to 128 that specifies the new program to be used for the specified channel.

When it executes, ChangeScoreProgram() changes the program number associated with a given channel in the MIDI environment. Any future notes played in that channel use the instrument template specified in the PIMap for the new program. If successful, the call returns 0. If unsuccessful, it returns a negative value (an error code).

Setting Channel Panning and Volume

MIDI Control messages set panning and volume levels for each channel in the MIDI environment. When InterpretMIDIMessage() receives a Control message, it makes this call:

int32 ChangeScoreControl( ScoreContext *scon, int32 Channel, int32 Index, int32 Value )
The call accepts four arguments: *scon, which is a pointer to the score context controlling the MIDI environment; Channel, which is the number of the channel (0 to 15) for which a controller should be set; Index, which is the number of the controller to be set; and Value, which contains the value of the new controller setting.

ChangeScoreControl() accepts only two different Index values: 7, which specifies a change in volume; and 10, which specifies a change in panning. When ChangeScoreControl() executes, it checks the Index value. If it is 7, it looks for a value from 0 to 127 in Value, which it uses to set the overall volume for the channel. 0 is no volume, 127 is maximum volume.

If the Index value is 10, the function looks for a value from 0 to 127 in Value, which it uses to set panning for the channel. Panning is the position of the channel's notes in the stereo output signal, which the function sets in the MIDI mixer. 0 is all the way to the left of the output, 63 is the middle of the output, and 127 is all the way to the right of the output.

Bending Pitch

Most dedicated synthesizers have a pitch bend wheel next to the keyboard. When you rotate the pitch bend wheel up, it bends up the pitch of all voices in the current channel. When you rotate the wheel down, it bends down the pitch of all the voices. The amount of pitch bend depends on the pitch bend range set for the synthesizer. The range is often one semitone (a half step), which means that the wheel can bend pitch up by a maximum of one semitone or down by a maximum of one semitone. If the range is set higher, say to an octave, then the pitch wheel can bend pitch much further, an octave in each direction.

When a pitch wheel is turned, it sends a stream of Pitch Bend Change messages with accompanying data values that range from 0x0 to 0x3FFF. These data values describe the positions through which the wheel moves. Value 0x0 means that the wheel is turned all the way down, signifying maximum pitch bend down. Value 0x3FFF means that the wheel is turned all the way up, signifying maximum pitch bend up. And value 0x2000 means that the wheel is set to its center position, so it does not bend pitch at all. When the wheel has stopped moving, it stops sending Pitch Bend Change messages, and pitch bend freezes at the wheel's last reported position.

The actual amount of pitch bend applied to a channel's voices is the result of the MIDI pitch bend value (the wheel's position) applied to the pitch bend range. For example, if the pitch bend value is 0x1000 (half way down to the lowest possible pitch bend value of 0x0) and the pitch bend range is a whole step, then the final pitch bend applied to the channel's voices is down by one half step, one half of the whole step range.

Setting a Pitch Bend Range Value

In the Music library's internal MIDI environment, the current pitch bend range value, which applies to all channels within a score, is stored in the ScoreContext data structure. The pitch bend range value is used together with a MIDI pitch bend value to determine how far the pitch of the channel's voices should be bent.

To set a score's pitch bend range value (the maximum distance from normal pitch that any score voices can be bent), use this call:

Err SetScoreBendRange( ScoreContext *scon, int32 BendRange )
The call accepts two arguments: *scon, a pointer to the ScoreContext data structure in which to set the pitch bend range; and BendRange, a pitch bend range value from 1 to 12 that specifies the number of semitones (half steps) in the pitch bend range. A minimum BendRange value of 1 means that all voices in a score can be bent up or down a maximum of one half step. And a maximum BendRange value of 12 means that all voices in a score can be bent up or down a maximum of one octave (12 half steps).

When it executes, SetScoreBendRange() writes the BendRange value into the appropriate element of the specified score context. It returns 0 if successful; it returns a negative value (an error code) if unsuccessful.

To see what a score context's current pitch bend range value is, use this call:

int32 GetScoreBendRange( ScoreContext *scon )
It accepts one argument: *scon, a pointer to the ScoreContext data structure you want to check. When it executes, it returns the current pitch bend range value of the score context if successful. If unsuccessful, it returns a negative value (an error code).

Acting on Pitch Bend Change Messages

Dynamic pitch bending occurs whenever a MIDI score object containing a stream of Pitch Bend Change messages is played back by the Juggler. Those messages are passed on to InterpretMIDIMessage(), which makes this call once for each Pitch Bend Change message:

Err ChangeScorePitchBend( ScoreContext *scon, int32 Channel, int32 Bend )
The call accepts three arguments: *scon, a pointer to the ScoreContext data structure controlling playback; Channel, the number of the channel (0 to 15) specified for the pitch bending; and Bend, a MIDI pitch bend value from 0x0 to 0x7FFF, which specifies the amount of pitch bend.

ChangeScorePitchBend(), when it executes, reads the score's current pitch bend range value from the specified score context. It then calls ConvertPitchBend() and passes it the pitch bend range value and the MIDI pitch bend value (Bend). ConvertPitchBend() returns an internal pitch bend value, which ChangeScorePitchBend() writes to the ScoreChannel data structure as the channel's current pitch bend value. ChangeScorePitchBend() also calls the audio function BendInstrumentPitch() to bend each of the instruments used for the specified channel's voices. Any new instruments created to support the channel's voices are set to bend pitch by the amount specified in the ScoreChannel data structure.

If the preceding description seems complicated, consider the final results of ChangeScorePitchBend()'s execution: all channel voices are bent to the amount specified in the MIDI message, and the current bend setting is saved until a new bend setting is specified.

If successful, ChangeScorePitchBend() returns 0. If unsuccessful, it returns a negative value (an error code).

Note that although ChangeScorePitchBend() is usually called by InterpretMIDIMessage(), a task can call it directly if it wants to reset a channel's pitch bend.

Creating an Internal Pitch Bend Value

A task can call ConvertPitchBend() directly if it wants to translate a MIDI pitch bend value (contained as data in a Pitch Bend Change message) into an internal pitch bend value that can be used by the Audio folio:

Err ConvertPitchBend( int32 Bend, int32 SemitoneRange, frac16 *BendFractionPtr )
The call accepts three arguments: Bend, which is a MIDI pitch bend value from 0x0 to 0x3FFF; SemitoneRange, which is the pitch bend range value measured in semitones (half steps); and *BendFractionPtr, which is a pointer to a frac16 variable in which to store the internal pitch bend value calculated by the function.

When it executes, ConvertPitchBend() applies the MIDI pitch bend value to the pitch bend range to see just how far pitch must be bent. It calculates an internal pitch bend value appropriate for that much pitch bend, and writes it into the frac16 variable. If successful, it returns 0; if unsuccessful, it returns a negative value (an error code). The BendFraction can then be used with BendInstrumentPitch().