MIDI Programing with XOJO Stanley Humphries, Ph.D. Copyright 2014
KBD-Infinity PO Box 13595, Albuquerque, NM 87192 U.S.A. Telephone: +1-505-220-3975 Fax: +1-617-752-9077 E mail:
[email protected] Internet: http://www.kbd-infinity.com
1
Contents 1 Introduction
3
2 MIDI basics 2.1 Serial port properties . . . 2.2 MIDI messages . . . . . . 2.3 Status byte types . . . . . 2.4 System common messages 2.5 MIDI channels . . . . . .
5 5 5 6 7 9
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
3 MIDI files
10
4 Opening a MIDI port
14
5 Receiving MIDI data
18
6 Sending real-time MIDI data
20
7 Playing MIDI files
23
8 MIDI programing – challenges and resources
28
9 Appendix 9.1 Variable length quantities . . . . . . . . 9.2 Receiving System Exclusive data . . . . . 9.3 General MIDI voices (program numbers) 9.4 Standard drum set . . . . . . . . . . . . 9.5 Reading a MIDI file . . . . . . . . . . .
31 31 32 34 35 36
2
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
Figure 1: Augmenting digital keyboard functions with Xojo programs.
1
Introduction
Xojo1 is a coding environment for creating compiled, cross-platform programs. This report describes how to create music software with Xojo using the MIDI (Musical Instrument Digital Interface) convention. Xojo programs can address a wide range of applications: • Recording and playing performances • Arranging and mixing tracks • Home or professional studio control • Composing and arranging songs • Creating and playing accompaniments • Computer-generated music • MIDI file processing The common feature of the applications is interactivity. They involve complex connections between the performer/composer, the computer and one or more output devices. For this reason, the event-driven Xojo environment is an ideal programing platform. 1
Information at http://www.xojo.com
3
The content of this report divides into two areas: • How MIDI works. • How to use Xojo to control MIDI events. Note that the Xojo distribution does not support MIDI communication. The applications I will discuss require the Audio plugin from Monkeybread Software2 . The plugin handles the full range of music interfacing, both MIDI and audio. This is a good point to emphasize the difference between the two. A MIDI message from a computer to an output device (like a synthesizer) is typically a set of three 8-bit numbers telling the operation (e.g., NoteOn), the note value (e.g. F5 ) and the volume. It is up to the output device to create a complex waveform to represent an instrument playing the note and then to send it to an amplifier/speaker. For audio output, the computer creates the waveform and then sends it directly to the amplifier/speaker. Clearly, MIDI involves much less work for the computer than audio and considerably less data transfer. Therefore, MIDI applications are a good match to Xojo, which has extensive functionality but rather slow speed. The following section covers MIDI basics including the types of MIDI messages. Section 3 reviews the format of MIDI binary files. Section 4 shows how to open a line of communication with connected MIDI devices using the Monkeybread routines. Section 5 illustrates how Xojo programs can read data from devices like digital keyboards, while Sect. 6 shows how programs can control output devices. Section 7 covers the advanced topic of reading and playing MIDI files. Finally, Sect. 8 reviews some problems of MIDI programing and resources that I have made available. The Appendix (Sect. 9) contains extended code examples and MIDI reference material. I assume that the reader is familiar with Xojo and with binary and hexadecimal numbers. The code excerpts in the report illustrate how a FORTRAN programer does Xojo. To avoid offending anyone, I did not use goto statements. As with most code examples, there is latitude to make them more sophisticated. The code extracts have been tested on Yamaha keyboards.
2
Information at http://www.monkeybreadsoftware.de
4
2
MIDI basics
2.1
Serial port properties
The acronym MIDI stands for Musical Instrument Digital Interface – a standard for sharing musical information3 between digital instruments via a serial interface. It was developed in the 1980s by a consortium of manufacturers and has remained largely unchanged since the days of the Commodore 64. The inviolability of MIDI is both a blessing and curse – Section 8 covers some challenges for creating MIDI programs. A MIDI serial port operates at a baud rate of 31,250 bits/second, roughly 15,000 times slower than a USB 2.0 port. Information is sent as a series of bytes (8 bit) with start and stop bits, so that the byte rate is 3,125 bytes/s. The glacial standard has remained intact largely because digital music is a relatively undemanding application. A performance of Flight of the Bumblebee typically involves less than 20 note-change messages per second. The one advantage of the low speed is that there are no problems with long cable runs. In principle, it would be possible to wire a stage several kilometers wide.
2.2
MIDI messages
Most MIDI messages consist of three bytes: Status, Data1, Data2. The standard specifies that status bytes have bit 8 set, so they can represent numbers in the range &h80-&hFF. Bit 8 is unset for the data bytes, so their values are limited to the range &h00-&h7F. As shown in Fig. 2, the top four bits of the status byte represent the message type, and the lower four bits represent the MIDI channel. Therefore, there are a maximum of 8 MIDI message types and 16 MIDI channels. As an example, the MIDI message &h90+&h0A
&h64
&h68
has the following meaning. The &h90 part of the status byte designates a NoteOn message and the &h0A part means that the operation should be applied to MIDI channel 10. The values of Data1 and Data2 imply that note 100 should be turned on with a volume of 104. The maximum number of notes (127) is not too restrictive when you consider that a high-range instrument like a piano can generate only 88 notes. Although three-byte messages constitute the bulk of MIDI communications, keep in mind that there are longer types like tempo changes and system exclusive messages.
3
To be more specific, MIDI is limited to information based on the western twelve-tone musical scale.
5
Figure 2: Byte usage in MIDI. Table 1: Types of MIDI operations StatusByte &h80 &h90 &hA0 &hB0 &hC0 &hD0 &hE0 &hF0
2.3
Function Note off Note on Polyphonic aftertouch Control change Program change Channel aftertouch Pitch wheel System common
Status byte types
The status byte determines the operation type and the channel to which it applies. This expression extracts the operation part: StatusBase = Status and &hF0
The expression ChanNo = Status and &h0F
gives the channel number. Table 1 shows the eight possible operations. A review of the functions demonstrates the ad hoc nature of MIDI, a challenge to the programer. You might assume that if only eight numbers were available for all present and future operations, they would be parceled out carefully. Instead, the distribution of commands has grown haphazardly from the needs of manufacturers. To begin, assigning individual types to NoteOff and NoteOn seems sensible, except for the fact that NoteOff is redundant. The same result can be achieved by sending a NoteOn message with a volume of zero, usually the case in practice (Sect.7). In a program, you must always test for both possibilities. Two of the operation bytes (&hA0 and &hD0) are devoted 6
to an effect called aftertouch. The idea is that after pressing a key, you can press it harder to indicate that you want something else to happen. What something else is depends on the specific hardware. It is hard to imagine that aftertouch support was added in response to the demands of performers – the only analogy on a real keyboard instrument is the technique of bebung on a clavichord. A cynic might posit that the primary purpose of aftertouch is to add more transducers to increase the value of high-end keyboards. In any case, aftertouch functions could have been merged into a single operation type. Control change messages (&hB0) generally define voice parameters for the synthesizers. The Data1 value identifies the parameter and Data2 gives the value. There are 128 possible parameter types, many of them unused. Some of the parameters are well standardized, such as setting the reverberation or pan level of a channel voice. Others are more esoteric, and many apply only to specific devices. A complete review is beyond the scope of this report – for a detailed description, see the references listed in Sect. 8. There are two control commands that are particularly useful for programers: • LocalOn/LocalOff. In the LocalOn state, a digital keyboard sends messages generated by key presses directly to its output synthesizer. Turn off this option when your program is echoing the device (Sect. 6). [&hB0+ChanNo &h7A &h7F/&h00] • All notes off. This is a useful safety command to extinguish hanging notes (Sect. 8). [&hB0+ChanNo &h7B &h00] The program change type (&hC0) has the sole function of specifying the GM (general MIDI) program number, which gives the musical instrument represented by a synthesizer or virtual instrument. Section 9.3 lists the options. In contrast to other MIDI messages, a program change includes only two bytes: [&hC0+ChanNo GMNumber]. The irregular number of bytes requires some vigilance interpreting MIDI files. Devoting an operation type to this command was an unfortunate choice – the function could have been accomplished with a standard three-byte control command. The operation type &hE0 represents pitch wheel control, a time-dependent shift of note pitch. Pitch bend is used to enhance the character of music with twangy or bluesy notes. Commands have the form [&hE0+ChanNo LSB MSB]. The pitch shift P b is determined from the 7-bit data values as Pb = (&h80)(MSB) + LSB - 8192.
The range is therefore -8192 ≤ Pb ≤ 8191. Unfortunately, there is no rigid rule that relates the number Pb to the actual pitch shift. The MIDI standard specifies ±2 half steps, but in almost all devices the range is ±1 octave (24 semitones). Pitch control is far more data intensive that simple NoteOn/NoteOff messages, so it is well-deserving of its own operation type.
2.4
System common messages
Messages with status byte type &hF0 are called system common messages and differ from the previous types in two aspects: 1) they do not apply to a specific MIDI channel and 2) their lengths generally exceed three bytes. Because the channel bits of the status byte are not used, they may be used to define up to 16 subtypes of system common messages. Table 2 lists the options. A complete description of all the options is beyond the scope of this report. Again, 7
Table 2: Subtypes of system common StatusByte &hF0 &hF1 &hF2 &hF3 &hF6 &hF7 &hF8 &hFA &hFB &hFC &hFE &hFF
Function System exclusive MIDI time code quarter frame Song position pointer Song select Tune request End of system exclusive Timing clock Start Continue Stop Active sensing Reset
the references of Sect. 8 give detailed information. Briefly, types &hF1, &hF6, &hF8 and &hFE would be useful if your program were intended to synchronize an array of output devices for a performance or add a soundtrack to a video. The types &hF2, &hF3, &hFA, &hFB and &hFC are aimed at the control of MIDI sequencers, software programs or hardware devices to organize multiple-song programs. The bytes &hF0 and &hF7 mark the beginning and end of a system exclusive message. These messages have two distinguishing characteristics: 1) they may have any length and 2) they are usually intended only for products of a specific manufacturer. The system exclusive message allows manufacturers to set up internal communications between the components of a device using the MIDI convention or to create MIDI files that function fully only on their products. In a standard system exclusive message, a one- or three-byte manufacturer code follows the status byte (e.g., Yamaha is &h43). Generally, you would not use system exclusive messages in programs unless you were working for manufacturers and had access to their proprietary data. The exception is a universal system exclusive message, a data sequence that follows a standard format. A useful example is the master volume control which can be used to implement smooth fades for all channels by lowering the volume of output devices: &hF0 &h7F &h7F &h04 &h01 &h00 VLevel &hF7
The second byte (&h7F) indicates that the message is of type real-time and should be applied immediately, the third byte (&h7F) that the operation should be applied to all channels, the bytes &h04 and &h01 specify that the message is a master volume control, and the bytes &h00 and VLevel gives the LSB and MSB of the volume (most devices ignore the LSB value). This is a complicated way to set the volume, but it works. Finally, a message with status byte &hFF resets the output device. Note that this byte code has an entirely different function in a MIDI file (Sect. 3).
8
2.5
MIDI channels
The sixteen available MIDI channels are typically used to carry information for different musical instruments. The information may be directed to multiple hardware synthesizers, or to a single polyphonic synthesizer. Modern synthesizers (even on low-end keyboards) are able to create several simultaneous instrument voices. The information could also be used inside the computer for virtual instruments (software programs or VST plugins). Again, some virtual instruments are polyphonic and others are intended to create audio only for a single channel. MIDI commands may be divided into two functional classes: setup and real-time. NoteOff (&h80), NoteOn (&h90), aftertouch (&hA0 and &hD0) and pitch bend (&hE0) messages are clearly in the real-time group. Control change (&hB0), program change (&hC0) and system exclusive messages are usually sent before the start of a song. Their primary function is to set the voices of channel synthesizers. For example, after sending the command &hCA &h49
subsequent notes to Channel 10 would sound like a flute. Sophisticated synthesizers can represent far more than the 128 general-MIDI voices (Sect. 9.3). The additional sounds are invoked by sending the number of an XG (extended general MIDI) bank with the control commands: &hB0+ChanNo &h00 MSB &hB0+ChanNo &h20 LSB
The two numbers represent the most-significant and least-significant byte of the XG bank number. Note that XG voices are not standardized between different manufacturers and may even vary between devices from the same manufacturer. It is best to avoid XG voices if you want to generate output that can be played on all MIDI devices. Up to this point, the discussion has concerned tonal instruments. In other words, the instruments listed in Sect 9.3 create sounds with different pitches – the fundamental frequency is given by the note value of a NoteOn message. We now turn to percussion instruments, which play a large role in modern music. A percussion sound is generally localized in time and has such a broad spectrum that pitch is indistinguishable. There is a wide variety of percussion sounds, depending on the instrument or where is it struck. The signal to a synthesizer that a MIDI channel should represent percussion is an XG MSB control message with a value of &h7F or &h7E: &hB0+ChanNo &h00 &h7F
A following program message sets the drum set: &hC0+ChanNo DrumSetNo
In subsequent NoteOn messages for that channel, the note number (Data1 ) specifies a percussion sound type rather than a pitch. Synthesizers and drum machines may support several drum sets (e.g., symphonic, middle eastern,...), but there is only one standardized set that gives about the same sound on all MIDI devices, DrumSetN o = 0. Section 9.4 shows the correspondence between note number and the percussion sound for the standard set. Finally, a common practice is to use channel &h09 for percussion, although theoretically any channel could be used. 9
3
MIDI files
The standard MIDI binary file (Fig 3) is a collection of MIDI messages. Each message must have an associated timestamp so that programs know when to send it. MIDI files offer more challenges to programers than dealing with real-time messages. Part of the problem is that the format was designed for optimal performance on an Apple II with a 360 kB floppy disk. Several clever but annoying strategies are applied to minimize the file length. A MIDI file has binary format and consists primarily of unsigned bytes (uint8). A file consists of a single header section4 and one or more track sections that contain MIDI messages and other data. The simplest MIDI file (Type 0 ) contains a single track section with MIDI messages arranged in chronological order. To play the file, a program proceeds through the message list, much like a real-time performance. We shall consider this type of file first, and then address Type 1 files which may include several track sections. Files of Type 2 may contain multiple songs and are generally intended for specialized software or hardware sequencers. Table 3 shows the structure of the header section which always appears at the beginning of the file. Section 7 covers how to input header information to a Xojo program. The section length gives the number of following data bytes. The header data always consists of three 16-bit integers (for a total byte length of 6): the MIDI file type, the number of tracks and the number of pulses per quarter note. The final quantity raises the question: what is a pulse? This is a good point to discuss timing in MIDI files. Pulses are a convenient division of a quarter note – the pulse length is short enough to be imperceptible to the listener but long enough so that intervals may be represented by short integer numbers. A typical value is Pq = 120 pulses per quarter note. A tempo command that gives the number of microseconds per quarter note (Usq) occurs in the track section before any notes are played. This command is discussed later in the section. If there are N pulses between two messages, then the time interval in seconds is given by ∆t = N
U sq . (106 ) P q
The track section has the structure shown in Table 4. The byte length is the sum of bytes for intervals, messages and the end-of-track marker. Note that when writing a MIDI file, the byte length must be known in advance. This feature makes it difficult to record MIDI sequences on the fly or to edit existing MIDI files. To minimize the length of the file, time quantities are given as the elapsed number of pulses from the preceding message rather than as the absolute time. For even more reduction, intervals are written as variable-length quantities. This method uses only a single byte to represent a small interval (<&h80 pulses), but may use up to four bytes to represent pulse numbers to &hFFFFFFF. Section 9.1 reviews the mathematics. The main feature to note is that the program knows when it has reached the end of a variable-length quantity. 4
File sections are often called chunks
10
Figure 3: Type 0 MIDI file – content display of header and first part of the track chunk. Table 3: Header section of a MIDI file Data type 4 × uint8 uint32 uint16 uint16 uint16
Function Section marker (string MThd or &h4D &h54 &h68 &h64) Section data byte length (always 6) MIDI file type (0,1 or 2) Number of tracks (1 for Type 0 ) Pulses per quarter note, Pq
Table 4: Track section of a MIDI file Data type 4 × uint8 uint32 1-4 bytes 2 or more bytes 1-4 bytes 2 or more bytes 1-4 bytes 2 or more bytes ... End of track
Function Section marker (string MTrk or &h4D &h54 &h72 &h6B) Section data byte length Interval to Message 1 Message 1 Interval to Message 2 Message 2 Interval to Message 3 Message 3 ... &hFF &h2F &h00
11
Table 5: System exclusive message in a MIDI file Data type Variable quantity &hF0 Variable quantity (Nb -1) bytes &hF7
Function Pulse interval from previous message Status byte Nb , byte length of message including termination The message Termination
A MIDI message follows the interval. At the start of the message, a program would expect to encounter a status byte with a value ≥&h80 followed by data bytes. The number of data bytes depends on the value of the StatusBase. A &hC0 message is followed by one data byte, and messages of type &hA0, &hB0, &hD0 and &hE0 have two data bytes. Messages with status &hF0-&hFF have a variety of lengths and must be treated as special cases. After reading the data bytes, a program expects to start reading the next variable-quantity interval. Lest programers become complacent, there is an exception: the running status. Because MIDI files consist mainly of NoteOn signals, continually rewriting &h90+ChanNo would be redundant. The following rule applies. If a program encounters a byte with value <&h80 after reading the time interval, it should interpret it as the first data byte of a message and use the status of the previous message. We can now understand why a NoteOn message with a volume of zero is used in preference to NoteOff – a string of identical status bytes reduces the file length. And lest progamers become reasonably comfortable, there is an exception to the exception. System exclusive message cannot invoke running status. System common messages (&hF0-&hFF) represent about 90% of the programer’s work for reading and interpreting MIDI files. The functions are shown in Table 2. Again, it is impossible to cover all options in a short report, so I refer you to the references of Chap. 8. We shall concentrate instead on two important cases: system-exclusive messages (&hF0) and non-MIDI messages (&hFF). System-exclusive messages in a file have a modified format from that discussed in Sect. 2.4. Table 5 shows the format. As with all file messages, a variable-length quantity occurs before the status byte to give the pulse-interval from the preceding message. A variable-length quantity after the status byte gives the number of bytes that will follow (including the terminating &hF7). Because there is no instance where a file would call for a synthesizer to reset itself, the status byte &hFF is used for a different purpose: non-MIDI messages. The term implies that the messages contain non-musical information, such as copyright data or the lyrics to a song. The first data byte following the status byte gives the type of message. Table 2 lists the options. Each type has a unique format that must be handled by a program as a special case. To illustrate, we shall consider two instances. A lyric message has the form &FF &05 Length Text
where Length represents a variable-length quantity equal to the text bytes length. The quantity Text is a series of bytes, the ASCII values of characters. Special characters may be included to designate carriage returns or new paragraphs. Lyric messages are timed events, synchronized 12
Table 6: Types of non-MIDI messages MarkerByte &h00 &h01 &h02 &h03 &h04 &h05 &h06 &h07 &h20 &h21 &h2F &h51 &h54 &h58 &h59 &h7F
Function Sequence number Text Copyright Sequence/track name Instrument Lyric Marker Cue point MIDI channel MIDI port End of track Tempo change SMPTE offset Time signature Key signature Proprietary event
with the musical notes. It is straightforward to create a program that displays the lyrics at the correct time – this is how karaoke programs work. A MIDI file with embedded lyrics usually has the suffix kar. Messages of the type text, copyright, sequence/track name, instrument, marker and cue point contain text for various purposes and follow the same format. The most important non-MIDI message is the tempo change. Every MIDI file must contain at least one such message to define how fast to play the content. The message always occupies six bytes and has the form &FF &51 &h03 T1 T2 T3
The value &h03 redundantly states that there are three following data bytes. In contrast to other data bytes, the values T1, T2 and T3 are 8-bit numbers. The number of microseconds per quarter note is given by MuQ = (&h100)(&h100) T1 + (&h100) T2 + T3.
For example, the byte values [&h07 &hA1 &h20] translate to 500,000 µs per quarter note.
13
4
Opening a MIDI port
This section begins our consideration of how to create MIDI programs with Xojo The MIDI functions of the operating system can control a complex array of software programs and hardware devices. For example, a computer may have both input and output communication with a keyboard through a USB cable and also send information to devices like drum machines via an I/O box and standard MIDI cables. Xojo can communicate with the MIDI functions of the operating system through routines in the Monkeybread Software Audio Plugin. The plugin may seem intimidating at first – it includes the following ten classes, the description of which occupies 46 pages in the instruction manual: 4.1. class PortMidiStreamMBS 4.2. class PortMidiDeviceInfoMBS 4.3. class PortMidiMBS 4.4. class PortMidiEventMBS 4.5. class WindowsMidiOutputMBS 4.6. class WindowsMidiStreamMBS 4.7. class WindowsMidiInputMBS 4.8. class WindowsMidiInputInfoMBS 4.9. class WindowsMidiOutputInfoMBS 4.10. class WindowsMidiMBS
The situation is actually much simpler. Classes 4.5-4.10 are legacy routines that function only in Windows and have no advantage in speed. The capabilities of the PortMidi routines are sufficient to create full-functioned interfaces. Most important, it is possible to build executables for Apple, Linux and Windows computers with the classes. Of the four PortMidi classes, two perform functions and two are structures to hold data for the functional classes: • PortMidiMBS sets up communication with the operating system. This class is used only at the beginning of the program or when there is a change in the connected hardware. The associated data class is PortMidiDeviceInfoMBS. • PortMidiStreamMBS exchanges data through connected MIDI ports (devices). The associated data class is PortMidiEventMBS. This section discusses the first function, establishing communications. For the examples, assume that the following global variables are available: dim dim dim dim
MIDIPort as new PortMidiMBS MIDIIn as new PortMidiDeviceInfoMBS MIDIOut as new PortMidiDeviceInfoMBS LastInPortOpened,LastOutPortOpened as string
The string variables record the name of the last device opened. Suppose that port setup is performed in the dialog of Fig. 4. Two listboxes show the available input and output devices. 14
Figure 4: Dialog to choose the MIDI input and output ports. Table 7 shows the code to fill the list boxes. The first group of commands checks that the operating system has MIDI support. If so, the variable NCount is set equal to the total number of MIDI input/output devices connected to the computer. In the second group, the program loops through all devices. For each one, the DeviceInfo property fills the PortMidiDeviceInfoMBS data structure MIDIIn. If the device has input, its name is included in the list. A similar procedure is used for the output devices. The device entries in Figure 4 illustrate a typical setup for a Windows computer with a digital keyboard attached. The entry Microsoft GS Wavetable Synth is a rudimentary virtual instrument included with Windows. Here, the term virtual instrument denotes a software utility that converts MIDI numbers into an audio waveform resembling GM instruments. The output is usually available on the sound card. The entry Coolsoft VirtualMIDISoft is a better utility with lower latency. The action event of the dialog OK button checks which entries in the input and output tables are highlighted to set the string variables LastInPortOpened and LastOutPortOpened. The program then calls the subroutine CheckMIDIPorts(LastInPortOpened,LastOutPortOpened) listed in Table 8 (for brevity, only the input section is shown). The program loops through all MIDI devices and singles out those that have input. The integer variable MidiPortIn is set equal to the number of the device that has the name LastInPortOpened or to 0 if the device is not present. This number is used to open an input data stream, the subject of the next section. To conclude, the examples in this report apply to a keyboard application with one input and one output. MIDI messages resulting from key presses on the input device come in and modified information is sent back to a synthesizer to generate appropriate audio signals. It is possible to open multiple streams to control several MIDI devices. Here, the program decides where to direct the data. For this case, the dialog routines could be modified so that the user can make multiple selection in the listboxes of Fig. 4.
15
Table 7: Open event code for the window of Fig. 4. The default selections are LastInPortOpened and LastOutPortOpened dim NCount as integer dim n as integer n = MIDIPort.ReInitialize if (n <> 0) then MsgBox "MIDI port error" self.close end if NCount=MIDIPort.CountDevices - 1 // Fill the input list box ChooseInPortListBox.SelectionType = ListBox.SelectionSingle if (NCount > -1) then for n = 0 to NCount MIDIIn = MIDIPort.DeviceInfo(n) if MIDIIn <> Nil then if MIDIIn.HasInput then ChooseInPortListbox.AddRow MIDIIn.Name if (MIDIIn.Name = LastInPortOpened) then ChooseInPortListBox.Selected(n) = True end if end if end if next end if // Fill the output list box ChooseOutPortListBox.SelectionType = 0 if (NCount > -1) then for n = 0 to NCount MIDIOut = MIDIPort.DeviceInfo(n) if MIDIOut <> Nil then if MIDIOut.HasOutput then ChooseOutPortListbox.AddRow MIDIOut.Name if (MIDIOut.Name = LastOutPortOpened) then ChooseOutPortListBox.Selected(n) = True end if end if end if next end if
16
Table 8: Subroutine CheckMIDIPorts. Opens either the first available ports or those with the names LastInPortOpened and LastOutPortOpened. dim NCount as integer dim n as integer dim FoundPort as Boolean NCount = MIDIPort.CountDevices-1 if (NCount < 0) then exit end if // Input FoundPort = False MidiPortIn = 0 for n = 0 to NCount MIDIIn = MIDIPort.DeviceInfo(n) if (MIDIIn <> Nil) then if MIDIIn.HasInput then FoundPort = True if (MIDIIn.Name = CheckInPort) then MidiPortIn = n end if end if end if next if (FoundPort) then MIDIIn = MIDIPort.DeviceInfo(MidiPortIn) LastInPortOpened = MIDIIn.Name InPortPresent = True else InPortPresent = False end if ...
17
5
Receiving MIDI data
A computer can read and analyze a MIDI data stream from an attached keyboard, drum pad or other transducer if an input port has been opened (as discussed in the previous section). Applications include recording a performance as a MIDI stream or computing an accompaniment based on incoming information. The PortMidiStreamMBS class is used for real-time communication. In the following discussion, assume that we have defined global variables: dim MIDIInput as new PortMidiStreamMBS dim MIDIEvent as new PortMidiEventMBS
The first step is to open the input stream: dim NCheck as integer NCheck = MIDIInput.OpenInput(MIDIPortIn,1000) if (NCheck <> 0) then MsgBox "Error opening MIDI input device" return end if NCheck = MIDIInput.Setfilter(&h1D00 + &h4000)
The integer parameter MIDIPortIn is the input device identification number returned by the setup routines of the previous section. The number 1000 is the byte size of the input buffer. When activated, the MIDIInput stream accumulates MIDI messages arriving from the device in the buffer. This data is generally not available to your program until it has been fetched from the buffer. We’ll discuss this operation below. Note the statement with the SetFilter method. The parameters specify 1) make filters active (&h4000) and 2) do not store clock messages (&h1D00). A device like a keyboard sends out a continuous stream of synchronization signals (status bytes &hF8, &hFA, &hFB and &hFC) in addition to musical information. Unless you are writing a high level application to synchronize slave devices, it is unlikely you will need this information. With the filter set, messages are not recorded in the buffer. In order to use incoming information, it must be transferred from the buffer to your program variables. This is accomplished with a timer operating in ModeMultiple that performs the following action: dim Status,Data1,Data2 as integer while MIDIInput.Poll<>0 NCheck = MidiInput.Read(MIDIEvent) Status = MIDIEvent.Status Data1 = MIDIEvent.Data1 Data2 = MIDIEvent.Data2 if NCheck = 1 then // Perform actions end if wend
18
On each cycle, the program transfers the accumulated buffer messages to the program variables and takes appropriate actions. A typical action is to echo the messages back to the synthesizer of the keyboard after processing or recording the MIDI stream. In the latter case, the program must also store the reception time of the messages in order to create a MIDI file or to play the song. For real-time applications, the timer period should be short. A period of a few milliseconds is usually sufficient for musical applications because the threshold for distinguishing different notes is about 25 ms. The Read method transfers data in chunks of three bytes, sufficient for most MIDI messages. In the case of the two-byte program message (&hC0), the method returns Data1 = GM P rogN um and Data2 = &h00. If the input device sends a system exclusive message of arbitrary length, the Read method delivers it in chunks of three bytes starting with Status = &hF0. The process continues until the end of the message, with padding bytes (if necessary) of value &h00 after the termination &hF7. Section 9.2 illustrates how to read regular and system exclusive messages in a stream. If your program does not use system exclusive information, the messages may be ignored. Simply take no action if Status = &hF0 or &hF7 or if Status <&h80.
19
6
Sending real-time MIDI data
Next, we turn to MIDI output from a computer to a keyboard, synthesizer, virtual instrument or other MIDI device. There are two types of data output operations: • Sending real-time MIDI messages • Playing MIDI files In the first case, information is simply sent at the time it becomes available. Examples include an electronic piano demo, echoing input from a performer or output from your own electronic music program. In contrast, the second application involves analysis of the timing information associated with the messages. The next section covers this topic. Here, the assumption is that MIDI messages become available at the time they are needed. Examples in this section use the following global variables: MIDIOutput = new PortMidiStreamMBS MIDIEvent = new PortMidiEventMBS MBlock = new MemoryBlock(1000)
To send output, open one or more streams with the command dim NCheck as integer NCheck = MIDIOutput.OpenOutput(MidiPortOut,1000,0) if (NCheck <> 0) then MsgBox "MIDI output error" exit end if
The first integer parameter (1000) is the byte size of the output buffer, while the second (0) is the latency is milliseconds. The zero value indicates that the data should be sent immediately. The latency setting may or may not be useful for synchronizing MIDI with audio and may or may not work in Windows, so it is probably best not to use any value but zero. If necessary, you can handle latency in your software. Once the stream has been opened, a simple three-byte MIDI message is sent with code like the following: dim Status,Data1,Data2,StatusBase as integer double precision VTemp constant StatusBaseMask = &hF0 StatusBase = Status and StatusBaseMask if (StatusBase = &h90) then VTemp = VolumeLevel*Data2 Data2 = VVInt end MIDIEvent.set Status, Data1, Data2 NCheck = MIDIOutput.write(MIDIEvent)
20
The last two lines appear in any output transfer. The initial lines check whether the output message is of type NoteOut. If so, the volume level is adjusted by a parameter set by the user, 0.0 ≤ V olumeLevel ≤ 1.0. Note that in the Monkeybread routines, all messages have three-byte length. For a two byte program command, set Status = &hC0+ChanNo, Data1 = GM P rogN o and Data2 = &h00. As an example, here is a code template for setting the voice (instrument sound) of a channel, complete with XG parameters: SendMIDIMessage(&hB0+ChanOut, SendMIDIMessage(&hB0+ChanOut, SendMIDIMessage(&hB0+ChanOut, SendMIDIMessage(&hC0+ChanOut,
&h79, &h00) &h00, XGMSB) &h20, XGLSB) ProgNo, &h00)
The subroutine has the content sub SendMIDIMessage(Status as integer,Data1 as integer,Data2 as integer) dim NCheck as integer MIDIEvent.set status,data1,data2 NCheck = MIDIOutput.write(MIDIEvent) end sub
The first call to the subroutine sends a channel reset message. Although this is not absolutely necessary, it is always a good idea to include such safety valves. Remember that your program may be interacting with a keyboard, an intelligent device that remembers things. The keyboard may accumulate data that puts it in an unanticipated state, causing peculiar behavior that is difficult to track down. Note that the order of calls in the example is important – always set the XG parameters before sending the program message. A different procedure is necessary for system exclusive message of variable length. To illustrate, suppose the message (complete with initial &hF0 and final &hF7) is stored in the string SysExData in the form of ASCII characters. The following code fills a memory block and then uses the method WriteSysEx of the class PortMIDIStreamMBS : dim m as integer dim NLength as integer dim NCheck as integer NLength = len(SysExData) for m=1 to NLength MBlock.Byte(m-1)= asc(mid(SysExData,m,1)) next MBlock.Byte(NLength) = &h00 // 0 termination NCheck = MIDIOutput.WriteSysEx(0,MBlock)
The additional byte &h00 to terminate the memory block is required. This is an undocumented quirk of the Monkeybread routine. The first parameter for the WriteSysEx method is the latency – again, use a value of zero to send the data immediately. Echoing is a process that combines input and output operations. Here, a program may receive input MIDI messages from a keyboard and return them to the same device after processing. In this case, the program would send an initial LocalOff message to the keyboard so that only the computer messages reach the output synthesizer. To illustrate, consider how to implement Irving Berlin’s piano. The famous composer was entirely self-taught and never 21
Table 9: Irving Berlin’s piano routine constant StatusBaseMask = &hF0 dim Status,Data1,Data2 as integer dim NCheck as integer StatusBase = Status and StatusBaseMask while MIDIInput.Poll<>0 NCheck = MidiInput.Read(MIDIEvent) Status = MIDIEvent.Status Data1 = MIDIEvent.Data1 Data2 = MIDIEvent.Data2 if NCheck = 1 then StatusBase = Status and StatusBaseMask if (StatusBase = &h90) or (StatusBase = &h80) then Data1 = Data1 + KeyOffset end if MIDIEvent.set Status, Data1, Data2 NCheck = MIDIOutput.write(MIDIEvent) end if wend
learned to play the piano in more than one key. To play in different keys, he had a custom piano built with a sliding keyboard. It is easier to accomplish this with MIDI. Playing a CMajor piece in the key of E♯Major is simply a matter of setting up an echo and adding 3 to the note value of outgoing NoteOn and NoteOff messages. Table 9 shows the core of a transposition program.
22
7
Playing MIDI files
A Xojo program to play MIDI files must address three tasks: • Read the file, converting the data to a form usable in a program. • If the file contains multiple tracks, combine the information into a single timeline of events. • Output the events with the correct timing. We shall start by discussing how to read the file. The following code opens a binary MIDI file identified as folder item InputFItem: dim bstream as BinaryStream bstream = BinaryStream.open(InputFItem,False) // Read only
The file of bytes can be read sequentially with statements of the type dim binput as uint8 binput = bstream.readuint8
As an example, the following function returns the first four characters of a chunk as a string. The calling program then determines whether it corresponds to MThd or MTrk : function ReadChunkName as string dim CName as string dim binput as uint8 dim NCount as integer CName = "" for NCount = 0 to 3 binput = bstream.readuint8 CName = CName + chr(binput) next return CName
By good fortune, the byte storage order for longer integers in MIDI is the same as that in Xojo. Therefore, there are options for reading such numbers. As an example, consider input of a track section length, a 32-bit unsigned integer that starts after the MTrk designator (Table 4). You could use either dim NSLength as uint32 dim n as integer dim binput as uint8 NSLength = 0 for n = 0 to 3 binput = bstream.readuint8 NSLength = &h100*NSLength + binput next
23
Figure 5: Utility to play MIDI files. or dim NSLength as uint32 NSLength = bstream.readuint32
The primary task in reading a a MIDI file is to transform the data to a form useful for the program function. There are many options – we shall consider one possible approach. The code excerpt of Sect. 9.5 employs the following data arrays: dim T0Type() as integer dim T0Time() as integer dim T0Message() as string
The first array records the type of message (e.g., standard MIDI messages, system exclusive messages, tempo changes,...). The second array records the absolute time, the total number of pulses that elapsed since the file start. The quantity equals the sum of all previous intervals in the track. The third array holds the message itself, stored as a string of characters to accommodate messages of different lengths. The code of Section 9.5 illustrates how to transfer the data of track section to the arrays. When the arrays have been filled, we can play the file data. Here, the term play implies that the MIDI messages are sent to an output device in the proper sequence and with the proper timing to sound like music. The following routines form the core of the MIDI player of Fig. 5. The quantities MuQ and Pq (defined in Sect. 3) give the number of microseconds per 24
Table 10: Run event of the thread PlayThread to output a stored MIDI sequence. dim NCheck,MaxIndex,NPTimeMax,NPTimeMin,NextEventIndex as integer dim TimeNow,TimePrev,DTime,NPTime as double MaxIndex = UBound(T0Time) NPTimeMin = T0Time(0) NPTimeMax = T0Time(MaxIndex) NextEventPTime = 0 NextEventPTimeD = 0.0 NextEventIndex = 0 NPTime = NPTimeMin DTime = 1.0/(NPTimeMax-NPTimeMin) TimePrev = Microseconds while (NPTime <= NPTimeMax) TimeNow = Microseconds NPTime = NPTime + (TimeNow-TimePrev)/UsPerPulse if (NPTime >= NextEventPTimeD) then if (NextEventPTime < NPTimeMax) then VolumeLevel = PassVolumeLevel SendMIDIEvents end ProgressValue = 500.0*(NPTime-NPTimeMin)*DTime end TimePrev = TimeNow wend ProgressValue = 0 DisplayTimer.Mode = Timer.ModeSingle PlayThread.kill
pulse, UsPerPulse = MuQ/Pq. Sequencing is performed by the thread PlayThread with the run event listed in Table 10. In keeping with the new Xojo convention, the thread receives and transfers information to the main window by updating the global variables PassVolumeLevel and ProgressValue. A timer (DisplayTimer ) operating in ModeMultiple updates the main window progress bar from ProgressValue and determines the current relative volume level from the position of the main window slider. The Start button activates PlayThread. After initialization, the thread enters a loop where it calculates the elapsed number of pulses, NPTime. The thread exits the loop when NPTime reaches NPTimeMax, the maximum absolute time in the file. On each cycle, the thread checks whether NPTime equals or exceeds the absolute time of the next entry in the message list. If so, it calls the subroutine SendMIDIEvents (Table 11). This routine outputs all pending MIDI messages with absolute time less than or equal to NPTime, and then updates the list order parameters. Note that the output volume depends on the current value of VolumeLevel, so that the user actively controls the output volume. With regard to other controls, the Pause button action calls the methods PlayThread.Suspend and PlayThread.Resume, while the Stop button action calls PlayThread.Kill.
25
Table 11: Subroutine SendMIDIEvents called by PlayThread (Table 10). sub dim dim dim dim dim dim dim dim
SendMIDIEvents n as integer NType as uint8 StatusByte as uint8 DataByte1 as uint8 DataByte2 as uint8 StatusBase as uint8 VVInt as integer NCheck as integer
n = NextEventIndex while(T0Time(n) <= NextEventPTime) NType = T0Type(n) select case NType case MidiMessage StatusByte = asc(mid(T0Data(n),1,1)) DataByte1 = asc(mid(T0Data(n),2,1)) StatusBase = StatusByte and StatusBaseMask DataByte2 = asc(mid(T0Data(n),3,1)) if (StatusBase = &h90) then VVInt = DataByte2 VVInt = (VVInt*VolumeLevel)/99 DataByte2 = VVInt end MIDIEvent.set StatusByte, DataByte1, DataByte2 NCheck = MIDIOutput.write(MIDIEvent) case TempChange UsPerQNote = val(T0Data(n)) UsPerPulse = UsPerQNote/PPQuat end select n = n + 1 NextEventIndex = n wend NextEventPTime = T0Time(NextEventIndex) NextEventPTimeD = NextEventPTime end sub
26
MIDI files of Type 1 may contain any number of tracks. Multiple tracks may be used to organize data. For example, voice information might be collected in one track, tempo change information in another, and the notes for each MIDI channel in individual tracks. The difference between Type 0 and Type 1 files is solely one of organization, not content. A Type 1 file can always be converted to a Type 0 file that produces the same sounds. A potential problem interpreting multiple tracks is that the stored time intervals relate only the events in a particular track. The issue is easily resolved by storing the absolute pulse time of events in the T0Time array. The content of multiple tracks is accumulated in the data arrays, and then the Xojo sort method is used to order the data according to T0Time: redim T0Time(-1) redim T0Type(-1) redim T0Data(-1) for nt = 1 to NTracks ChunkName = ReadChunkName() if (ChunkName = TrackChunk) then if (not AddTrackChunk()) then MsgBox "Invalid track in MIDI file" bstream.close exit end end next T0Time.sortwith(T0Type,T0Data)
The sorted arrays are played the same way as those of a Type 0 file. As final topic, consider how to write a MIDI file from the filled data arrays T0Type, T0Time and T0Data. In principle, the task should be easy. Simply reverse the read process, converting values of T0Time to intervals expressed as variable quantities (Sect. 9.1). The one complicating factor is that the total byte length of the track must be known in advance (Sect. 4). One solution is given by the following steps: 1) append the stored data to an array of unsigned bytes, 2) use the resulting array size to write the data byte length and 3) transfer the data bytes to the file: dim RawByte() as uint8 bstream.write("MTrk") AssembleTrackBytes MaxIndex = UBound(RawByte) bstream.writeuint32(MaxIndex+1) // Length for n=0 to MaxIndex bstream.writeuint8(RawByte(n)) next
Here, the subroutine AssembleTrackBytes initializes RawByte and then appends the intervals and messages stored in the data arrays.
27
8
MIDI programing – challenges and resources
When MIDI was created in the 1980s, its sole purpose was real-time communication between electronic instruments. In this application, the interaction between the performer and the hardware ensured the logic of the signal stream. If a performer pressed a key, then (barring a heart attack) he/she would eventually release the key. In other words, a NoteOn signal was always followed by a corresponding NoteOff signal. The relationship between signals was determined solely by their position in time. The nature of the hardware guaranteed that an aftertouch signal would always occur between the NoteOn and NoteOff signals of the note to which it should be applied. With the development of personal computers, MIDI became the de facto standard for anything having to do with electronic instruments, expanding far beyond its intended role. (In fact, many current references refer to a MIDI file as a digital score, an electronic version of musical notation.) We can divide computer programs that interact with MIDI devices into two categories: • Creative applications • Performance assistance To start, consider creative applications. Program examples include composition software, MIDI sequencers, digital-audio-workstations and musical notation converters. The distinguishing feature of these applications is that the duration and other attributes of notes are known in advance. This may seem like an obvious point, but it is important enough to repeat: in a creative application, the note duration is a known quantity. Contrast this with the situation in a real-time application where the program does not know when the performer wants the note to end. In the real-time world, the NoteOn/NoteOff paradigm is unavoidable. On the other hand, it is unsuitable for creative applications. For example, suppose we stretch or move a note in a composition program. If we follow the MIDI convention where independent events are connected only by their position it time, there is no guarantee that a pitch bend will occur within its intended note. Even worse, a NoteOff signal may precede its NoteOn counterpart, The implication is that no creative program can actually work in MIDI. The MIDI message stream must be converted to an internal note-based representation. Here, the notes carry attributes such as duration, pitch, volume, aftertouch, and pitch-bend profiles. Any change of the note makes a corresponding change of the attributes. After an operation, the internal representation must be transformed back to a consistent MIDI format for display or playback. The unfortunate circumstance is that there are hundreds of electronic music programs, and each one has resolved the MIDI paradox in its own unique way. In an ideal world, there would two MIDIs: the standard real-time version and a note-based creative version. Furthermore, the brave new world would feature standardized, documented algorithms to move between the two representations. Until that day arrives, you will have to create yet another unique system if you hope to write a MIDI editor.
28
Figure 6: MIDI metronome project set to make a drum sound. The challenges to real-time performance programs are even greater because of two potential pitfalls: • Hanging notes (a NoteOn signal not balanced by a NoteOff signal). • Overlapping identical notes on the same MIDI channel. Hanging notes are so fearsome that the MIDI standard includes a dead-man switch, Active Sensing. When it is turned on, a MIDI device extinguishes all notes if the sender does not check in every 0.3 seconds. An example will illustrate how hanging notes may occur in a program. Suppose you want to develop accompaniment software to play MIDI loops where the key and chord type are determined by real-time signals from the performer. In an accompaniment, several notes on different channels may be active at any time. The question is what to with those notes when the performer signals a chord change. If the program takes no action, subsequent NoteOff signals for the active notes may have different note values, leaving hanging notes. If the program simply turns off all active notes, there will be a gap in the accompaniment. A solution requires some effort. The program must independently keep track of active notes. At a chord shift, the program turns off present notes and immediately restarts replacements to reflect the new chord. As a result, any following NoteOff signals will be are matched to the active note values. There is a complicating problem for instrument sounds with sharp attack like guitars and pianos. In this case, restarting held notes to reflect a chord change may have a jarring effect. Although some MIDI output devices handle overlapping notes on the same channel gracefully, others crash. To illustrate sources of problems, consider generating an automatic harmonic accompaniment to a melody line based on chord signals from the performer. The harmony note represents a best choice based on the chord and melody note. For example, in CMaj this choice might be G3 if the performer plays C4-D4-E4. There would be no problem if the performer plays the notes marcato, but there would be overlapping notes if the phrase were performed legato. Solutions are surprisingly complex – in addition to a list of active notes, the program must record how many times the performer attempted to start the note. The bottom line for coping with MIDI in real-time programs is vigilance and testing. 29
To learn more about MIDI, there are two essential references for programers: • David Miles Huber, The MIDI Manual, Third Edition (Focal Press, New York, 2007) • Robert Gu´erin, MIDI Power, Second Edition (Course Technology, Boston, 2008). There is an extensive array of reports on MIDI and downloadable MIDI files in all genres available on the Internet. I have set up a resource site containing the code examples and tables discussed in this report. The URL is http://www.kbd-infinity.com/xojo_resource.html
On the site, you can download a complete Xojo project for the MIDI metronome shown in Fig. 6. The metronome program is useful, even for non-musicians. Any clock sound may be created by setting the GM or XG parameters, so you can check out the voice settings discussed in Sect. 2.5. Alternatively, the metronome is a good way to irritate the person in the next cubicle. If you have questions or comments, please contact me at
[email protected].
30
9
Appendix
9.1
Variable length quantities
Time offsets and the lengths of system exclusive messages in MIDI files are stored as a series of bytes in a form called a variable-length quantity. The idea is to represent a assortment of small and large integer numbers with the minimum number of bytes. A variable-length quantity may contain from one to four bytes. The eighth bit of each byte is used as a marker, so each byte can represent the range 0-127 (&h00-&h7F). If the eighth bit is set, additional bytes follow. The eighth bit is unset in the final byte to mark the end. As an example, the three bytes [&h81 &h80 &h00] represent the number &h4000. The largest number that can be represented by four bytes is [&hFF &hFF &hFF &h7F] → &FFFFFFF. It is probably easier to understand the rules from a code example rather than a verbal description. Here is a Xojo function that reads as many bytes as necessary from a MIDI file to fill out a variable quantity: function GetVariableQuantity as integer const NMask = &h7F const NMult = &h80 dim NSum as integer dim NIn as uint8 NSum = 0 NIn = bstream.readuint8 if (NIn < &h80) then return NIn exit else NSum = (NIn and NMask) do NSum = NSum*NMult NIn = bstream.readuint8 NSum = NSum + (NIn and NMask) loop until (NIn < &h80) end return NSum end function
31
The next example shows how to go the other way, converting an integer number in the range &h0000000-&hFFFFFFF to a set of 1-4 bytes stored as characters in a string: function CreateVarQuant(NIn as integer) as string dim RString as string dim b1,b2,b3,b4,BShift as uint32 if (NIn < &h80) then // One byte RString = chr(NIn) elseif (NIn < &h4000) then // Two byte b2 = NIn and &h7F b1 = (NIn - b2)/&h80 + &h80 RString = chr(b1) + chr(b2) elseif (NIn < &h200000) then // Three byte b3 = NIn and &h7F BShift = (NIn - b3)/&h80 b2 = BShift and &h7F b1 = (BShift - b2)/&h80 RString = chr(b1+&h80) + chr(b2+&h80) + chr(b3) else // Four byte b4 = NIn and &h7F BShift = (NIn - b4)/&h80 b3 = BShift and &h7F BShift = (BShift - b3)/&h80 b2 = BShift and &h7F b1 = (BShift - b2)/&h80 RString = chr(b1+&h80) + chr(b2+&h80) + chr(b3+&h80) + chr(b4) end function
end if
9.2
Receiving System Exclusive data
The Read method for the PortMidiStreamMBS class transfers data from the input buffer to the program variables in three-byte chunks. A system-exclusive message begins with the status byte &hF0 and ends with &hF7 after an arbitrary number of data bytes. The following example illustrates how to record both standard and system exclusive messages from a MIDI device. The array T0Data contains the message as a string of characters and the T0Type array designates the type of data.
32
dim NCheck as integer dim Status,Data1,Data2 as integer dim NNote as integer dim CurrentTime as double while MIDIInput.Poll<>0 NCheck = MidiInput.Read(MIDIEvent) if NCheck = 1 then Status = MIDIEvent.Status Data1 = MIDIEvent.Data1 Data2 = MIDIEvent.Data2 if (ReadingSysEx) then if (Status = &hF7) then SysExString = SysExString + chr(Status) T0Type.Append SysEx T0Data.Append SysExString ReadingSysEx = False elseif (Data1 = &hF7) then SysExString = SysExString + chr(Status) + chr(Data1) T0Type.Append SysEx T0Data.Append SysExString ReadingSysEx = False elseif (Data2 = &hF7) then SysExString = SysExString + chr(Status) + chr(Data1) + chr(Data2) T0Type.Append SysEx T0Data.Append SysExString ReadingSysEx = False else SysExString = SysExString + chr(Status) + chr(Data1) + chr(Data2) end if else if (Status = &hC0) or (Status = &hB0) then CurrentTime = 0.0 T0Type.Append MidiMessage T0Data.Append chr(Status) + chr(Data1) + chr(Data2) elseif (Status = &hF0) then // SysEx start SysExString = chr(Status) if (Data1 = &hF7) then SysExString = SysExString + chr(Data1) T0Type.Append SysEx T0Data.Append SysExString elseif (Data2 = &hF7) then SysExString = SysExString + chr(Data1) + chr(Data2) T0Type.Append SysEx T0Data.Append SysExString else SysExString = SysExString + chr(Data1) + chr(Data2) ReadingSysEx = True end if end if end if end if wend
33
9.3
General MIDI voices (program numbers)
The numbers (which appear as the Data1 value in a program change command &hC0) designate the instrument listed. A GM compliant device will produce a sound like the instrument, but no two synthesizers sound exactly the same. The final entries are interesting. If you had 128 opportunities to represent every musical instrument on earth, would you include telephone ring, helicopter and gunshot? 000 001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042
Acoustic Grand Piano Bright Acoustic Piano Electric Grand Piano Honky-tonk Piano Electric Piano 1 Electric Piano 2 Harpsichord Clavinet Celesta Glockenspiel Music Box Vibraphone Marimba Xylophone Tubular Bells Dulcimer Drawbar Organ Percussive Organ Rock Organ Church Organ Reed Organ Accordion Harmonica Bandoneon Acoustic Guitar (nylon) Acoustic Guitar (steel) Electric Guitar (jazz) Electric Guitar (clean) Electric Guitar (muted) Overdriven Guitar Distortion Guitar Guitar Harmonics Acoustic Bass Electric Bass (finger) Electric Bass (pick) Fretless Bass Slap Bass 1 Slap Bass 2 Synth Bass 1 Synth Bass 2 Violin Viola Cello
043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085
Contrabass Tremolo Strings Pizzicato Strings Orchestral Harp Timpani String Ensemble 1 String Ensemble 2 Synth Strings 1 Synth Strings 2 Choir Aahs Voice Oohs Synth Choir Orchestra Hit Trumpet Trombone Tuba Muted Trumpet French Horn Brass Section Synth Brass 1 Synth Brass 2 Soprano Sax Alto Sax Tenor Sax Baritone Sax Oboe English Horn Bassoon Clarinet Piccolo Flute Recorder Pan Flute Blown Bottle Shakuhachi Whistle Ocarina Lead 1 (square) Lead 2 (sawtooth) Lead 3 (calliope) Lead 4 (chiff) Lead 5 (charang) Lead 6 (voice)
34
086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
Lead 7 (fifths) Lead 8 (bass + lead) Pad 1 (new age) Pad 2 (warm) Pad 3 (polysynth) Pad 4 (choir) Pad 5 (bowed) Pad 6 (metallic) Pad 7 (halo) Pad 8 (sweep) FX 1 (rain) FX 2 (soundtrack) FX 3 (crystal) FX 4 (atmosphere) FX 5 (brightness) FX 6 (goblins) FX 7 (echoes) FX 8 (sci-fi) Sitar Banjo Shamisen Koto Kalimba Bagpipe Fiddle Shanai Tinkle Bell Agogo Steel Drums Woodblock Taiko Drum Melodic Tom Synth Drum Reverse Cymbal Guitar Fret Noise Breath Noise Seashore Bird Tweet Telephone Ring Helicopter Applause Gunshot
9.4
Standard drum set
Specify a standard percussion channel by sending the messages &hB0+ChanNo &h00 &h7F &hC0+ChanNo &h00
Subsequent NoteOn signals of the form &h90+ChanNo InstNo
Volume
generate the sounds listed below. InstNo Instrument InstNo Instrument --------------------------------------------------------------035 Bass Drum 2 059 Ride Cymbal 2 036 Bass Drum 1 060 High Bongo 037 Side Stick/Rimshot 061 Low Bongo 038 Snare Drum 1 062 Mute High Conga 039 Hand Clap 063 Open High Conga 040 Snare Drum 2 064 Low Conga 041 Low Tom 2 065 High Timbale 042 Closed Hi-hat 066 Low Timbale 043 Low Tom 1 067 High Agogo 044 Pedal Hi-hat 068 Low Agogo 045 Mid Tom 2 069 Cabasa 046 Open Hi-hat 070 Maracas 047 Mid Tom 1 071 Short Whistle 048 High Tom 2 072 Long Whistle 049 Crash Cymbal 1 073 Short Guiro 050 High Tom 1 074 Long Guiro 051 Ride Cymbal 1 075 Claves 052 Chinese Cymbal 076 High Wood Block 053 Ride Bell 077 Low Wood Block 054 Tambourine 078 Mute Cuica 055 Splash Cymbal 079 Open Cuica 056 Cowbell 080 Mute Triangle 057 Crash Cymbal 2 081 Open Triangle 058 Vibra Slap
35
9.5
Reading a MIDI file
The code extracts illustrate how to transfer MIDI file messages to the Xojo data arrays T0Type, T0Time and T0Data discussed in the report. Note that the routines can handle the running status condition. function AddTrackChunk as Boolean dim ChunkLength as uint32 dim EventStatus as uint8 dim PreviousStatus as uint8 // Initialize NbRead = 0 PreviousStatus = 0 TAbsolute = 0 // Program has read chunk name // Length of chunk ChunkLength = bstream.readuint32 // Process the track chunk do // Bail out if end of file reached if (bstream.EOF) then return False exit end EventTime = GetVariableQuantity() TAbsolute = TAbsolute + EventTime EventStatus = bstream.readuint8 NbRead = NbRead + 1 NbTotal = NbTotal + 1 // Running status if (EventStatus < &h80) then if (PreviousStatus = &hF0) then // SysEx cannot use running status return False exit end FirstByte = EventStatus RunningStatus = True ProcessStatus(PreviousStatus) // New status else RunningStatus = False ProcessStatus(EventStatus) PreviousStatus = EventStatus end loop until (NbRead = ChunkLength) return True end function
36
subroutine ProcessStatus(Status as uint8) dim StatusBase as uint8 dim ChanNo as uint8 StatusBase = Status and StatusBaseMask // Status with channel number appended Select Case StatusBase Case &h80 // Note off T0Time.append TAbsolute T0Type.append MidiMessage DataString = chr(Status) ProcessNoteOff Case &h90 // Note on T0Time.append TAbsolute T0Type.append MidiMessage DataString = chr(Status) ProcessNoteOn End Select end subroutine
sub ProcessNoteOn dim NoteValue as uint8 dim VelocityValue as uint8 if (RunningStatus) then NoteValue = FirstByte else NoteValue = bstream.readuint8 NbRead = NbRead + 1 NbTotal = NbTotal + 1 end VelocityValue = bstream.readuint8 NbRead = NbRead + 1 NbTotal = NbTotal + 1 DataString = DataString + chr(NoteValue) + chr(VelocityValue) T0Data.append(DataString) end sub
37