170 lines
6.5 KiB
Plaintext
170 lines
6.5 KiB
Plaintext
|
=====================
|
||
|
Extending the Library
|
||
|
=====================
|
||
|
|
||
|
The choice of MIDI event types included in the library is somewhat
|
||
|
idiosyncratic; I included the events I needed for another software
|
||
|
project I was wrote. You may find that you need additional events in
|
||
|
your work. For this reason I am including some instructions on extending
|
||
|
the library. The process isn't too hard (provided you have a working
|
||
|
knowledge of Python and the MIDI standard), so the task shouldn't present
|
||
|
a competent coder too much difficulty. Alternately (if, for example,
|
||
|
you *don't* have a working knowledge of MIDI and don't desire to gain it),
|
||
|
you can submit new feature requests to me, and I will include them into
|
||
|
the development branch of the code, subject to the constraints of time.
|
||
|
|
||
|
To illustrate the process I show below how the MIDI tempo event is
|
||
|
incorporated into the code. This is a relatively simple event, so while
|
||
|
it may not illustrate some of the subtleties of MIDI programing, it
|
||
|
provides a good, illustrative case.
|
||
|
|
||
|
|
||
|
-----------------------
|
||
|
Create a New Event Type
|
||
|
-----------------------
|
||
|
|
||
|
The first order of business is to create a new subclass of the GnericEvent
|
||
|
object of the MIDIFile module. This subclass initializes any specific
|
||
|
instance data that is needed for the MIDI event to be written. In
|
||
|
the case of the tempo event, it is the actual tempo (which is defined
|
||
|
in the MIDI standard to be 60000000 divided by the tempo in beats per
|
||
|
minute). This class should also call the superclass' initializer with
|
||
|
the event time and set the event type (a unique string used internally by
|
||
|
the software) in the __init__() function. In the case of the tempo event:
|
||
|
|
||
|
class tempo(GenericEvent):
|
||
|
def __init__(self,time,tempo):
|
||
|
GenericEvent.__init__(self,time)
|
||
|
self.type = 'tempo'
|
||
|
self.tempo = int(60000000 / tempo)
|
||
|
|
||
|
Next (and this is an embarrassing break of OO programming) the __eq__()
|
||
|
function of the GenericEvent class should be modified so that equality
|
||
|
of these types of events can be calculated. In calculating equivalence
|
||
|
time is always checked, so two tempo events are considered the same if
|
||
|
the have the same tempo value. Thus the following snippet of code from
|
||
|
GenericEvent's _eq__() function accomplishes this goal:
|
||
|
|
||
|
|
||
|
if self.type == 'tempo':
|
||
|
if self.tempo != other.tempo:
|
||
|
return False
|
||
|
|
||
|
|
||
|
If events are equivalent, the code should return False. If they are not
|
||
|
equivalent no return should be called.
|
||
|
|
||
|
---------------------------
|
||
|
Create an Accessor Function
|
||
|
---------------------------
|
||
|
|
||
|
|
||
|
Next, an accessor function should be added to MIDITrack to create an
|
||
|
event of this type. Continuing the example of the tempo event:
|
||
|
|
||
|
|
||
|
def addTempo(self,time,tempo):
|
||
|
self.eventList.append(MIDITrack.tempo(time,tempo))
|
||
|
|
||
|
|
||
|
The public accessor function is via the MIDIFile object, and must include
|
||
|
the track number to which the event is written:
|
||
|
|
||
|
|
||
|
def addTempo(self,track,time,tempo):
|
||
|
self.tracks[track].addTempo(time,tempo)
|
||
|
|
||
|
|
||
|
This is the function you will use in your code to create an event of
|
||
|
the desired type.
|
||
|
|
||
|
|
||
|
-----------------------
|
||
|
Modify processEventList
|
||
|
-----------------------
|
||
|
|
||
|
Next, the logic pertaining to the new event type should be added to
|
||
|
processEventList function of the MIDITrack class. In general this code
|
||
|
will create a MIDIEvent object and set its type, time, ordinality, and
|
||
|
any specific information that is needed for the event type. This object
|
||
|
is then added to the MIDIEventList.
|
||
|
|
||
|
The ordinality (self.ord) is a number that tells the software how to
|
||
|
sequence MIDI events that occur at the same time. The higher the number,
|
||
|
the later in the sequence the event will be written in comparison to
|
||
|
other, simultaneous events.
|
||
|
|
||
|
The relevant section for the tempo event is:
|
||
|
|
||
|
|
||
|
elif thing.type == 'tempo':
|
||
|
event = MIDIEvent()
|
||
|
event.type = "Tempo"
|
||
|
event.time = thing.time * TICKSPERBEAT
|
||
|
event.tempo = thing.tempo
|
||
|
event.ord = 3
|
||
|
self.MIDIEventList.append(event)
|
||
|
|
||
|
|
||
|
Thus if other events occur at the same time, type which have an ordinality
|
||
|
of 1 or 2 will be written to the stream first.
|
||
|
|
||
|
Time needs to be converted from beats (which the accessor function uses)
|
||
|
and MIDI time by multiplying by the constant TICKSPERBEAT. The value
|
||
|
of thing.type is the unique string you defined above, and event.type
|
||
|
is another unique things (they can--and probably should--be the same,
|
||
|
although the coding here is a little sloppy and changes case of the
|
||
|
string).
|
||
|
|
||
|
|
||
|
----------------------------------------
|
||
|
Write the Event Data to the MIDI Stream
|
||
|
----------------------------------------
|
||
|
|
||
|
|
||
|
The last step is to modify the MIDIFile writeEventsToStream function;
|
||
|
here is where some understanding of the MIDI standard is necessary. The
|
||
|
following code shows the creation of a MIDI tempo event:
|
||
|
|
||
|
|
||
|
elif event.type == "Tempo":
|
||
|
code = 0xFF
|
||
|
subcode = 0x51
|
||
|
fourbite = struct.pack('>L', event.tempo)
|
||
|
threebite = fourbite[1:4] # Just discard the MSB
|
||
|
varTime = writeVarLength(event.time)
|
||
|
for timeByte in varTime:
|
||
|
self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte)
|
||
|
self.MIDIdata = self.MIDIdata + struct.pack('>B',code)
|
||
|
self.MIDIdata = self.MIDIdata + struct.pack('>B',subcode)
|
||
|
self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x03)
|
||
|
self.MIDIdata = self.MIDIdata + threebite
|
||
|
|
||
|
|
||
|
The event.type string ("Tempo") was the one chosen in the processEventList
|
||
|
logic.
|
||
|
|
||
|
The code and subcode are binary values that come from the MIDI
|
||
|
specification.
|
||
|
|
||
|
Next the data is packed into a three byte structure (or a four byte
|
||
|
structure, discarding the most significant byte). Again, the MIDI
|
||
|
specification determines the number of bytes used in the data payload.
|
||
|
|
||
|
The event time should be converted to MIDI variable-length data with the
|
||
|
writeVarLength() function before writing to the stream (as shown above).
|
||
|
The MIDI standard utilizes a slightly bizarre variable length data
|
||
|
record. In it, only seven bits of a word are used to store data; the
|
||
|
eighth bit signifies if more bytes encoding the value follow. The total
|
||
|
length may be 1 to 3 bytes, depending upon the size of the value encoded.
|
||
|
The writeVarLength() function takes care of this conversion for you.
|
||
|
|
||
|
Now the data is written to the binary object self.MIDIdata, which is
|
||
|
the actual MIDI-encoded data stream. As per the MIDI standard, first we
|
||
|
write our variable-length time value. Next we add the event type code and
|
||
|
subcode. Then we write the length of the data payload, which in the case
|
||
|
of the tempo event is three bytes. Lastly, we write the actual payload,
|
||
|
which has been packed into the variable threebite.
|
||
|
|
||
|
Clear as mud!
|