Drop of 0.87 release.
This commit is contained in:
commit
b06fd67fc2
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.mid
|
||||
*.swp
|
30
CHANGELOG
Normal file
30
CHANGELOG
Normal file
@ -0,0 +1,30 @@
|
||||
Date: 20 October 2009
|
||||
Version: 0.87
|
||||
|
||||
First public release.
|
||||
|
||||
* Tweaked email address in contact information.
|
||||
* Added/updated documentation.
|
||||
* Tweaked the setup.py file to produce better distributions.
|
||||
|
||||
Date: 9 October 2009
|
||||
Version: 0.86
|
||||
|
||||
* added addNote as main interface into package (not
|
||||
addNoteByNumber). It's been a while since I've cut a release,
|
||||
so there may be other things that have happened.
|
||||
|
||||
* Created distutils package.
|
||||
|
||||
* Minor code clean-up.
|
||||
|
||||
* Added documentation in-line and in text (MIDIFile.txt).
|
||||
|
||||
* All public functions should now be accessed thought
|
||||
MIDIFile directly, and not the component tracks.
|
||||
|
||||
Date: 15 January 2009
|
||||
Version: 0.85
|
||||
|
||||
* Split out from existing work as a separate project.
|
||||
|
26
License.txt
Normal file
26
License.txt
Normal file
@ -0,0 +1,26 @@
|
||||
--------------------------------------------------------------------------
|
||||
MIDUTIL, Copyright (c) 2009, Mark Conway Wirt
|
||||
<emergentmusics) at (gmail . com>
|
||||
|
||||
This software is distributed under an Open Source license, the
|
||||
details of which follow.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
--------------------------------------------------------------------------
|
||||
|
12
MANIFEST
Normal file
12
MANIFEST
Normal file
@ -0,0 +1,12 @@
|
||||
README.txt
|
||||
setup.py
|
||||
License.txt
|
||||
CHANGELOG
|
||||
VERSION
|
||||
MANIFEST
|
||||
src/midiutil/MidiFile.py
|
||||
src/midiutil/__init__.py
|
||||
examples/single-note-example.py
|
||||
documentation/Extending.txt
|
||||
documentation/ClassReference.txt
|
||||
src/unittests/miditest.py
|
12
PKG-INFO
Normal file
12
PKG-INFO
Normal file
@ -0,0 +1,12 @@
|
||||
Metadata-Version: 1.0
|
||||
Name: MIDIUtil
|
||||
Version: 0.87
|
||||
Summary: MIDIUtil, a MIDI Interface for Python
|
||||
Home-page: http://www.emergentmusics.org/midiutil/
|
||||
Author: Mark Conway Wirt
|
||||
Author-email: emergentmusics) at (gmail . com
|
||||
License: Copyright (C) 2009, Mark Conway Wirt. See License.txt for details.
|
||||
Description:
|
||||
This package provides a simple interface to allow Python programs to
|
||||
write multi-track MIDI files.
|
||||
Platform: Platform Independent
|
118
README.txt
Normal file
118
README.txt
Normal file
@ -0,0 +1,118 @@
|
||||
========
|
||||
MIDIUtil
|
||||
========
|
||||
|
||||
------------
|
||||
Introduction
|
||||
------------
|
||||
|
||||
MIDIUtil is a pure Python library that allows one to write muti-track
|
||||
Musical Instrument Digital Interface (MIDI) files from within Python
|
||||
programs. It is object-oriented and allows one to create and write these
|
||||
files with a minimum of fuss.
|
||||
|
||||
MIDIUtil isn't a full implementation of the MIDI specification. The actual
|
||||
specification is a large, sprawling document which has organically grown
|
||||
over the course of decades. I have selectively implemented some of the
|
||||
more useful and common aspects of the specification. The choices have
|
||||
been somewhat idiosyncratic; I largely implemented what I needed. When
|
||||
I decided that it could be of use to other people I fleshed it out a bit,
|
||||
but there are still things missing. Regardless, the code is fairly easy to
|
||||
understand and well structured. Additions can be made to the library by
|
||||
anyone with a good working knowledge of the MIDI file format and a good,
|
||||
working knowledge of Python. Documentation for extending the library
|
||||
is provided.
|
||||
|
||||
This software was originally developed with Python 2.5.2 and it makes use
|
||||
of some features that were introduced in 2.5. I have used it extensively
|
||||
in Python 2.6, so it should work in this or any later versions (but I
|
||||
have not tested it on Python 3).
|
||||
|
||||
This software is distributed under an Open Source license and you are
|
||||
free to use it as you see fit, provided that attribution is maintained.
|
||||
See License.txt in the source distribution for details.
|
||||
|
||||
------------
|
||||
Installation
|
||||
------------
|
||||
|
||||
To use the library one can either install it on one's system or
|
||||
copy the midiutil directory of the source distribution to your
|
||||
project's directory (or to any directory pointed to by the PYTHONPATH
|
||||
environment variable). For the Windows platforms an executable installer
|
||||
is provided. Alternately the source distribution can be downloaded,
|
||||
un-zipped (or un-tarred), and installed in the standard way:
|
||||
|
||||
python setup.py install
|
||||
|
||||
On non-Windows platforms (Linux, MacOS, etc.) the software should be
|
||||
installed in this way. MIDIUtil is pure Python and should work on any
|
||||
platform to which Python has been ported.
|
||||
|
||||
If you do not wish to install in on your system, just copy the
|
||||
src/midiutil directory to your project's directory or elsewhere on
|
||||
your PYTHONPATH. If you're using this software in your own projects
|
||||
you may want to consider distributing the library bundled with yours;
|
||||
the library is small and self-contained, and such bundling makes things
|
||||
more convenient for your users. The best way of doing this is probably
|
||||
to copy the midiutil directory directly to your package directory and
|
||||
then refer to it with a fully qualified name. This will prevent it from
|
||||
conflicting with any version of the software that may be installed on
|
||||
the target system.
|
||||
|
||||
-----------
|
||||
Quick Start
|
||||
-----------
|
||||
|
||||
Using the software is easy:
|
||||
|
||||
o The package must be imported into your namespace
|
||||
o A MIDIFile object is created
|
||||
o Events (notes, tempo-changes, etc.) are added to the object
|
||||
o The MIDI file is written to disk.
|
||||
|
||||
Detailed documentation is provided; what follows is a simple example
|
||||
to get you going quickly. In this example we'll create a one track MIDI
|
||||
File, assign a name and tempo to the track, add a one beat middle-C to
|
||||
the track, and write it to disk.
|
||||
|
||||
#Import the library
|
||||
from midiutil.MidiFile import MIDIFile
|
||||
|
||||
# Create the MIDIFile Object with 1 track
|
||||
MyMIDI = MIDIFile(1)
|
||||
|
||||
# Tracks are numbered from zero. Times are measured in beats.
|
||||
track = 0
|
||||
time = 0
|
||||
|
||||
# Add track name and tempo.
|
||||
MyMIDI.addTrackName(track,time,"Sample Track")
|
||||
MyMIDI.addTempo(track,time,120)
|
||||
|
||||
# Add a note. addNote expects the following information:
|
||||
track = 0
|
||||
channel = 0
|
||||
pitch = 60
|
||||
time = 0
|
||||
duration = 1
|
||||
volume = 100
|
||||
|
||||
# Now add the note.
|
||||
MyMIDI.addNote(track,channel,pitch,time,duration,volume)
|
||||
|
||||
# And write it to disk.
|
||||
binfile = open("output.mid", 'wb')
|
||||
MyMIDI.writeFile(binfile)
|
||||
binfile.close()
|
||||
|
||||
There are several additional event types that can be added and there are
|
||||
various options available for creating the MIDIFile object, but the above
|
||||
is sufficient to begin using the library and creating note sequences.
|
||||
|
||||
The above code is found in machine-readable form in the examples directory.
|
||||
A detailed class reference and documentation describing how to extend
|
||||
the library is provided in the documentation directory.
|
||||
|
||||
Have fun!
|
||||
|
229
build/documentation/ClassReference.txt
Normal file
229
build/documentation/ClassReference.txt
Normal file
@ -0,0 +1,229 @@
|
||||
========================
|
||||
MIDIUtil Class Reference
|
||||
========================
|
||||
|
||||
--------------
|
||||
class MIDIFile
|
||||
--------------
|
||||
|
||||
A class that represents a full, well-formed MIDI pattern.
|
||||
|
||||
This is a container object that contains a header, one or more
|
||||
tracks, and the data associated with a proper and well-formed
|
||||
MIDI pattern.
|
||||
|
||||
Calling
|
||||
|
||||
MyMIDI = MidiFile(tracks, removeDuplicates=True, deinterleave=True)
|
||||
|
||||
normally
|
||||
|
||||
MyMIDI = MidiFile(tracks)
|
||||
|
||||
Arguments
|
||||
|
||||
o tracks: The number of tracks this object contains
|
||||
|
||||
o removeDuplicates: If true (the default), the software will
|
||||
remove duplicate events which have been added. For example,
|
||||
two notes at the same channel, time, pitch, and duration would
|
||||
be considered duplicate.
|
||||
|
||||
o deinterleave: If True (the default), overlapping notes
|
||||
(same pitch, same channel) will be modified so that they do
|
||||
not overlap. Otherwise the sequencing software will need to
|
||||
figure out how to interpret NoteOff events upon playback.
|
||||
|
||||
================
|
||||
Public Functions
|
||||
================
|
||||
|
||||
---------------------------------------------------
|
||||
addNote(track, channel, pitch,time,duration,volume)
|
||||
---------------------------------------------------
|
||||
|
||||
Add notes to the MIDIFile object
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addNotes(track,channel,pitch,time, duration, volume)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the note is added.
|
||||
o channel: the MIDI channel to assign to the note. [Integer, 0-15]
|
||||
o pitch: the MIDI pitch number [Integer, 0-127].
|
||||
o time: the time (in beats) at which the note sounds [Float].
|
||||
o duration: the duration of the note (in beats) [Float].
|
||||
o lume: the volume (velocity) of the note. [Integer, 0-127].
|
||||
|
||||
|
||||
----------------------------------
|
||||
addTrackName(track, time,trackName)
|
||||
----------------------------------
|
||||
|
||||
Add a track name to a MIDI track.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addTrackName(track,time,trackName)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the name is added. [Integer, 0-127].
|
||||
o time: The time at which the track name is added, in beats
|
||||
[Float].
|
||||
o trackName: The track name. [String].
|
||||
|
||||
---------------------------
|
||||
addTempo(track, time,tempo)
|
||||
---------------------------
|
||||
|
||||
Add a tempo event.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addTempo(track, time, tempo)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127]
|
||||
o time: The time at which the event is added, in beats. [Float]
|
||||
o tempo: The tempo, in Beats per Minute. [Integer]
|
||||
|
||||
|
||||
-----------------------------------------------
|
||||
addProgramChange(track, channel, time, program)
|
||||
-----------------------------------------------
|
||||
|
||||
Add a MIDI program change event.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addProgramChange(track,channel, time, program)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127]
|
||||
o channel: The channel the event is assigned to. [Integer, 0-15]
|
||||
o time: The time at which the event is added, in beats. [Float]
|
||||
o program: the program number. [Integer, 0-127]
|
||||
|
||||
|
||||
--------------------------------------------------------------
|
||||
addControllerEvent(track, channel,time,eventType, paramerter1)
|
||||
--------------------------------------------------------------
|
||||
|
||||
Add a MIDI controller event.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addControllerEvent(track, channel, time, eventType, \
|
||||
parameter1)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127]
|
||||
o channel: The channel the event is assigned to. [Integer, 0-15]
|
||||
o time: The time at which the event is added, in beats. [Float]
|
||||
o eventType: the controller event type.
|
||||
o parameter1: The event's parameter. The meaning of which varies
|
||||
by event type.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
changeNoteTuning(track, tunings, sysExChannel=0x7F, realTime=False, \
|
||||
tuningProgam=0)
|
||||
---------------------------------------------------------------------
|
||||
|
||||
Change a note's tuning using sysEx change tuning program.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.changeNoteTuning(track,[tunings],realTime=False, \
|
||||
tuningProgram=0)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127].
|
||||
o tunings: A list of tuples in the form (pitchNumber,
|
||||
frequency). [[(Integer,Float]]
|
||||
o realTime: Boolean which sets the real-time flag. Defaults to false.
|
||||
o sysExChannel: do note use (see below).
|
||||
o tuningProgram: Tuning program to assign. Defaults to
|
||||
zero. [Integer, 0-127]
|
||||
|
||||
In general the sysExChannel should not be changed (parameter will
|
||||
be depreciated).
|
||||
|
||||
Also note that many software packages and hardware packages do not
|
||||
implement this standard!
|
||||
|
||||
|
||||
---------------------
|
||||
writeFile(fileHandle)
|
||||
---------------------
|
||||
|
||||
Write the MIDI File.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.writeFile(filehandle)
|
||||
|
||||
Arguments
|
||||
|
||||
o filehandle: a file handle that has been opened for binary
|
||||
writing.
|
||||
|
||||
|
||||
-------------------------------------
|
||||
addSysEx(track, time, manID, payload)
|
||||
-------------------------------------
|
||||
|
||||
Add a SysEx event
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addSysEx(track,time,ID,payload)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127].
|
||||
o time: The time at which the event is added, in beats. [Float].
|
||||
o ID: The SysEx ID number
|
||||
o payload: the event payload.
|
||||
|
||||
Note: This is a low-level MIDI function, so care must be used in
|
||||
constructing the payload. It is recommended that higher-level helper
|
||||
functions be written to wrap this function and construct the payload
|
||||
if a developer finds him or herself using the function heavily.
|
||||
|
||||
|
||||
---------------------------------------------------------
|
||||
addUniversalSysEx(track, time,code, subcode, payload, \
|
||||
sysExChannel=0x7F, realTime=False)}f
|
||||
---------------------------------------------------------
|
||||
|
||||
Add a Universal SysEx event.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addUniversalSysEx(track, time, code, subcode, payload, \
|
||||
sysExChannel=0x7f, realTime=False)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127].
|
||||
o time: The time at which the event is added, in beats. [Float].
|
||||
o code: The event code. [Integer]
|
||||
o subcode The event sub-code [Integer]
|
||||
o payload: The event payload. [Binary string]
|
||||
o sysExChannel: The SysEx channel.
|
||||
o realTime: Sets the real-time flag. Defaults to zero.
|
||||
|
||||
Note: This is a low-level MIDI function, so care must be used in
|
||||
constructing the payload. It is recommended that higher-level helper
|
||||
functions be written to wrap this function and construct the payload
|
||||
if a developer finds him or herself using the function heavily. As an
|
||||
example of such a helper function, see the changeNoteTuning function,
|
||||
both here and in MIDITrack.
|
||||
|
169
build/documentation/Extending.txt
Normal file
169
build/documentation/Extending.txt
Normal file
@ -0,0 +1,169 @@
|
||||
=====================
|
||||
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!
|
993
build/lib/midiutil/MidiFile.py
Normal file
993
build/lib/midiutil/MidiFile.py
Normal file
@ -0,0 +1,993 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# Name: MidiFile.py
|
||||
# Purpose: MIDI file manipulation utilities
|
||||
#
|
||||
# Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
|
||||
#
|
||||
# Created: 2008/04/17
|
||||
# Copyright: (c) 2009 Mark Conway Wirt
|
||||
# License: Please see License.txt for the terms under which this
|
||||
# software is distributed.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import struct, sys, math
|
||||
|
||||
# TICKSPERBEAT is the number of "ticks" (time measurement in the MIDI file) that
|
||||
# corresponds to one beat. This number is somewhat arbitrary, but should be chosen
|
||||
# to provide adequate temporal resolution.
|
||||
|
||||
TICKSPERBEAT = 128
|
||||
|
||||
controllerEventTypes = {
|
||||
'pan' : 0x0a
|
||||
}
|
||||
class MIDIEvent:
|
||||
'''
|
||||
The class to contain the MIDI Event (placed on MIDIEventList.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.type='unknown'
|
||||
self.time=0
|
||||
self.ord = 0
|
||||
|
||||
def __cmp__(self, other):
|
||||
''' Sorting function for events.'''
|
||||
if self.time < other.time:
|
||||
return -1
|
||||
elif self.time > other.time:
|
||||
return 1
|
||||
else:
|
||||
if self.ord < other.ord:
|
||||
return -1
|
||||
elif self.ord > other.ord:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
class GenericEvent():
|
||||
'''The event class from which specific events are derived
|
||||
'''
|
||||
def __init__(self,time):
|
||||
self.time = time
|
||||
self.type = 'Unknown'
|
||||
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
'''
|
||||
Equality operator for Generic Events and derived classes.
|
||||
|
||||
In the processing of the event list, we have need to remove duplicates. To do this
|
||||
we rely on the fact that the classes are hashable, and must therefore have an
|
||||
equality operator (__hash__() and __eq__() must both be defined).
|
||||
|
||||
This is the most embarrassing portion of the code, and anyone who knows about OO
|
||||
programming would find this almost unbelievable. Here we have a base class that
|
||||
knows specifics about derived classes, thus breaking the very spirit of
|
||||
OO programming.
|
||||
|
||||
I suppose I should go back and restructure the code, perhaps removing the derived
|
||||
classes altogether. At some point perhaps I will.
|
||||
'''
|
||||
if self.time != other.time or self.type != other.type:
|
||||
return False
|
||||
|
||||
# What follows is code that encodes the concept of equality for each derived
|
||||
# class. Believe it f you dare.
|
||||
|
||||
if self.type == 'note':
|
||||
if self.pitch != other.pitch or self.channel != other.channel:
|
||||
return False
|
||||
if self.type == 'tempo':
|
||||
if self.tempo != other.tempo:
|
||||
return False
|
||||
if self.type == 'programChange':
|
||||
if self.programNumber != other.programNumber or self.channel != other.channel:
|
||||
return False
|
||||
if self.type == 'trackName':
|
||||
if self.trackName != other.trackName:
|
||||
return False
|
||||
if self.type == 'controllerEvent':
|
||||
if self.parameter1 != other.parameter1 or \
|
||||
self.parameter2 != other.parameter2 or \
|
||||
self.channel != other.channel or \
|
||||
self.eventType != other.eventType:
|
||||
return False
|
||||
|
||||
if self.type == 'SysEx':
|
||||
if self.manID != other.manID:
|
||||
return False
|
||||
|
||||
if self.type == 'UniversalSysEx':
|
||||
if self.code != other.code or\
|
||||
self.subcode != other.subcode or \
|
||||
self.sysExChannel != other.sysExChannel:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
'''
|
||||
Return a hash code for the object.
|
||||
|
||||
This is needed for the removal of duplicate objects from the event list. The only
|
||||
real requirement for the algorithm is that the hash of equal objects must be equal.
|
||||
There is probably great opportunity for improvements in the hashing function.
|
||||
'''
|
||||
# Robert Jenkin's 32 bit hash.
|
||||
a = int(self.time)
|
||||
a = (a+0x7ed55d16) + (a<<12)
|
||||
a = (a^0xc761c23c) ^ (a>>19)
|
||||
a = (a+0x165667b1) + (a<<5)
|
||||
a = (a+0xd3a2646c) ^ (a<<9)
|
||||
a = (a+0xfd7046c5) + (a<<3)
|
||||
a = (a^0xb55a4f09) ^ (a>>16)
|
||||
return a
|
||||
|
||||
class MIDITrack:
|
||||
'''A class that encapsulates a MIDI track
|
||||
'''
|
||||
# Nested class definitions.
|
||||
|
||||
class note(GenericEvent):
|
||||
'''A class that encapsulates a note
|
||||
'''
|
||||
def __init__(self,channel, pitch,time,duration,volume):
|
||||
|
||||
GenericEvent.__init__(self,time)
|
||||
self.pitch = pitch
|
||||
self.duration = duration
|
||||
self.volume = volume
|
||||
self.type = 'note'
|
||||
self.channel = channel
|
||||
|
||||
def compare(self, other):
|
||||
'''Compare two notes for equality.
|
||||
'''
|
||||
if self.pitch == other.pitch and \
|
||||
self.time == other.time and \
|
||||
self.duration == other.duration and \
|
||||
self.volume == other.volume and \
|
||||
self.type == other.type and \
|
||||
self.channel == other.channel:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class tempo(GenericEvent):
|
||||
'''A class that encapsulates a tempo meta-event
|
||||
'''
|
||||
def __init__(self,time,tempo):
|
||||
|
||||
GenericEvent.__init__(self,time)
|
||||
self.type = 'tempo'
|
||||
self.tempo = int(60000000 / tempo)
|
||||
|
||||
class programChange(GenericEvent):
|
||||
'''A class that encapsulates a program change event.
|
||||
'''
|
||||
|
||||
def __init__(self, channel, time, programNumber):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'programChange'
|
||||
self.programNumber = programNumber
|
||||
self.channel = channel
|
||||
|
||||
class SysExEvent(GenericEvent):
|
||||
'''A class that encapsulates a System Exclusive event.
|
||||
'''
|
||||
|
||||
def __init__(self, time, manID, payload):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'SysEx'
|
||||
self.manID = manID
|
||||
self.payload = payload
|
||||
|
||||
class UniversalSysExEvent(GenericEvent):
|
||||
'''A class that encapsulates a Universal System Exclusive event.
|
||||
'''
|
||||
|
||||
def __init__(self, time, realTime, sysExChannel, code, subcode, payload):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'UniversalSysEx'
|
||||
self.realTime = realTime
|
||||
self.sysExChannel = sysExChannel
|
||||
self.code = code
|
||||
self.subcode = subcode
|
||||
self.payload = payload
|
||||
|
||||
class ControllerEvent(GenericEvent):
|
||||
'''A class that encapsulates a program change event.
|
||||
'''
|
||||
|
||||
def __init__(self, channel, time, eventType, parameter1,):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'controllerEvent'
|
||||
self.parameter1 = parameter1
|
||||
self.channel = channel
|
||||
self.eventType = eventType
|
||||
|
||||
class trackName(GenericEvent):
|
||||
'''A class that encapsulates a program change event.
|
||||
'''
|
||||
|
||||
def __init__(self, time, trackName):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'trackName'
|
||||
self.trackName = trackName
|
||||
|
||||
|
||||
def __init__(self, removeDuplicates, deinterleave):
|
||||
'''Initialize the MIDITrack object.
|
||||
'''
|
||||
self.headerString = struct.pack('cccc','M','T','r','k')
|
||||
self.dataLength = 0 # Is calculated after the data is in place
|
||||
self.MIDIdata = ""
|
||||
self.closed = False
|
||||
self.eventList = []
|
||||
self.MIDIEventList = []
|
||||
self.remdep = removeDuplicates
|
||||
self.deinterleave = deinterleave
|
||||
|
||||
def addNoteByNumber(self,channel, pitch,time,duration,volume):
|
||||
'''Add a note by chromatic MIDI number
|
||||
'''
|
||||
self.eventList.append(MIDITrack.note(channel, pitch,time,duration,volume))
|
||||
|
||||
def addControllerEvent(self,channel,time,eventType, paramerter1):
|
||||
'''
|
||||
Add a controller event.
|
||||
'''
|
||||
|
||||
self.eventList.append(MIDITrack.ControllerEvent(channel,time,eventType, \
|
||||
paramerter1))
|
||||
|
||||
def addTempo(self,time,tempo):
|
||||
'''
|
||||
Add a tempo change (or set) event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.tempo(time,tempo))
|
||||
|
||||
def addSysEx(self,time,manID, payload):
|
||||
'''
|
||||
Add a SysEx event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.SysExEvent(time, manID, payload))
|
||||
|
||||
def addUniversalSysEx(self,time,code, subcode, payload, sysExChannel=0x7F, \
|
||||
realTime=False):
|
||||
'''
|
||||
Add a Universal SysEx event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.UniversalSysExEvent(time, realTime, \
|
||||
sysExChannel, code, subcode, payload))
|
||||
|
||||
def addProgramChange(self,channel, time, program):
|
||||
'''
|
||||
Add a program change event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.programChange(channel, time, program))
|
||||
|
||||
def addTrackName(self,time,trackName):
|
||||
'''
|
||||
Add a track name event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.trackName(time,trackName))
|
||||
|
||||
def changeNoteTuning(self, tunings, sysExChannel=0x7F, realTime=False, \
|
||||
tuningProgam=0):
|
||||
'''Change the tuning of MIDI notes
|
||||
'''
|
||||
payload = struct.pack('>B', tuningProgam)
|
||||
payload = payload + struct.pack('>B', len(tunings))
|
||||
for (noteNumber, frequency) in tunings:
|
||||
payload = payload + struct.pack('>B', noteNumber)
|
||||
MIDIFreqency = frequencyTransform(frequency)
|
||||
for byte in MIDIFreqency:
|
||||
payload = payload + struct.pack('>B', byte)
|
||||
|
||||
self.eventList.append(MIDITrack.UniversalSysExEvent(0, realTime, sysExChannel,\
|
||||
8, 2, payload))
|
||||
|
||||
def processEventList(self):
|
||||
'''
|
||||
Process the event list, creating a MIDIEventList
|
||||
|
||||
For each item in the event list, one or more events in the MIDIEvent
|
||||
list are created.
|
||||
'''
|
||||
|
||||
# Loop over all items in the eventList
|
||||
|
||||
for thing in self.eventList:
|
||||
if thing.type == 'note':
|
||||
event = MIDIEvent()
|
||||
event.type = "NoteOn"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.pitch = thing.pitch
|
||||
event.volume = thing.volume
|
||||
event.channel = thing.channel
|
||||
event.ord = 3
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
event = MIDIEvent()
|
||||
event.type = "NoteOff"
|
||||
event.time = (thing.time + thing.duration) * TICKSPERBEAT
|
||||
event.pitch = thing.pitch
|
||||
event.volume = thing.volume
|
||||
event.channel = thing.channel
|
||||
event.ord = 2
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
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)
|
||||
|
||||
elif thing.type == 'programChange':
|
||||
event = MIDIEvent()
|
||||
event.type = "ProgramChange"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.programNumber = thing.programNumber
|
||||
event.channel = thing.channel
|
||||
event.ord = 1
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
elif thing.type == 'trackName':
|
||||
event = MIDIEvent()
|
||||
event.type = "TrackName"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.trackName = thing.trackName
|
||||
event.ord = 0
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
elif thing.type == 'controllerEvent':
|
||||
event = MIDIEvent()
|
||||
event.type = "ControllerEvent"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.eventType = thing.eventType
|
||||
event.channel = thing.channel
|
||||
event.paramerter1 = thing.parameter1
|
||||
event.ord = 1
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
elif thing.type == 'SysEx':
|
||||
event = MIDIEvent()
|
||||
event.type = "SysEx"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.manID = thing.manID
|
||||
event.payload = thing.payload
|
||||
event.ord = 1
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
elif thing.type == 'UniversalSysEx':
|
||||
event = MIDIEvent()
|
||||
event.type = "UniversalSysEx"
|
||||
event.realTime = thing.realTime
|
||||
event.sysExChannel = thing.sysExChannel
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.code = thing.code
|
||||
event.subcode = thing.subcode
|
||||
event.payload = thing.payload
|
||||
event.ord = 1
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
else:
|
||||
print "Error in MIDITrack: Unknown event type"
|
||||
sys.exit(2)
|
||||
|
||||
# Assumptions in the code expect the list to be time-sorted.
|
||||
# self.MIDIEventList.sort(lambda x, y: x.time - y.time)
|
||||
|
||||
self.MIDIEventList.sort(lambda x, y: int( 1000 * (x.time - y.time)))
|
||||
|
||||
if self.deinterleave:
|
||||
self.deInterleaveNotes()
|
||||
|
||||
def removeDuplicates(self):
|
||||
'''
|
||||
Remove duplicates from the eventList.
|
||||
|
||||
This function will remove duplicates from the eventList. This is necessary
|
||||
because we the MIDI event stream can become confused otherwise.
|
||||
'''
|
||||
|
||||
# For this algorithm to work, the events in the eventList must be hashable
|
||||
# (that is, they must have a __hash__() and __eq__() function defined).
|
||||
|
||||
tempDict = {}
|
||||
for item in self.eventList:
|
||||
tempDict[item] = 1
|
||||
|
||||
self.eventList = tempDict.keys()
|
||||
|
||||
# Sort on type, them on time. Necessary because keys() has no requirement to return
|
||||
# things in any order.
|
||||
|
||||
self.eventList.sort(lambda x, y: cmp(x.type , y.type))
|
||||
self.eventList.sort(lambda x, y: int( 1000 * (x.time - y.time))) #A bit of a hack.
|
||||
|
||||
def closeTrack(self):
|
||||
'''Called to close a track before writing
|
||||
|
||||
This function should be called to "close a track," that is to
|
||||
prepare the actual data stream for writing. Duplicate events are
|
||||
removed from the eventList, and the MIDIEventList is created.
|
||||
|
||||
Called by the parent MIDIFile object.
|
||||
'''
|
||||
|
||||
if self.closed == True:
|
||||
return
|
||||
self.closed = True
|
||||
|
||||
if self.remdep:
|
||||
self.removeDuplicates()
|
||||
|
||||
|
||||
self.processEventList()
|
||||
|
||||
def writeMIDIStream(self):
|
||||
'''
|
||||
Write the meta data and note data to the packed MIDI stream.
|
||||
'''
|
||||
|
||||
#Process the events in the eventList
|
||||
|
||||
self.writeEventsToStream()
|
||||
|
||||
# Write MIDI close event.
|
||||
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('BBBB',0x00,0xFF, \
|
||||
0x2F,0x00)
|
||||
|
||||
# Calculate the entire length of the data and write to the header
|
||||
|
||||
self.dataLength = struct.pack('>L',len(self.MIDIdata))
|
||||
|
||||
def writeEventsToStream(self):
|
||||
'''
|
||||
Write the events in MIDIEvents to the MIDI stream.
|
||||
'''
|
||||
|
||||
for event in self.MIDIEventList:
|
||||
if event.type == "NoteOn":
|
||||
code = 0x9 << 4 | event.channel
|
||||
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',event.pitch)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume)
|
||||
elif event.type == "NoteOff":
|
||||
code = 0x8 << 4 | event.channel
|
||||
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',event.pitch)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume)
|
||||
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) # Data length: 3
|
||||
self.MIDIdata = self.MIDIdata + threebite
|
||||
elif event.type == 'ProgramChange':
|
||||
code = 0xC << 4 | event.channel
|
||||
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',event.programNumber)
|
||||
elif event.type == 'TrackName':
|
||||
varTime = writeVarLength(event.time)
|
||||
for timeByte in varTime:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('B',0xFF) # Meta-event
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('B',0X03) # Event Type
|
||||
dataLength = len(event.trackName)
|
||||
dataLenghtVar = writeVarLength(dataLength)
|
||||
for i in range(0,len(dataLenghtVar)):
|
||||
self.MIDIdata = self.MIDIdata + struct.pack("b",dataLenghtVar[i])
|
||||
self.MIDIdata = self.MIDIdata + event.trackName
|
||||
elif event.type == "ControllerEvent":
|
||||
code = 0xB << 4 | event.channel
|
||||
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',event.eventType)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',event.paramerter1)
|
||||
elif event.type == "SysEx":
|
||||
code = 0xF0
|
||||
varTime = writeVarLength(event.time)
|
||||
for timeByte in varTime:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', code)
|
||||
|
||||
payloadLength = writeVarLength(len(event.payload)+2)
|
||||
for lenByte in payloadLength:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte)
|
||||
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', event.manID)
|
||||
self.MIDIdata = self.MIDIdata + event.payload
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7)
|
||||
elif event.type == "UniversalSysEx":
|
||||
code = 0xF0
|
||||
varTime = writeVarLength(event.time)
|
||||
for timeByte in varTime:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', code)
|
||||
|
||||
# Do we need to add a length?
|
||||
payloadLength = writeVarLength(len(event.payload)+5)
|
||||
for lenByte in payloadLength:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte)
|
||||
|
||||
if event.realTime :
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7F)
|
||||
else:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7E)
|
||||
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', event.sysExChannel)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', event.code)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', event.subcode)
|
||||
self.MIDIdata = self.MIDIdata + event.payload
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7)
|
||||
|
||||
def deInterleaveNotes(self):
|
||||
'''Correct Interleaved notes.
|
||||
|
||||
Because we are writing multiple notes in no particular order, we
|
||||
can have notes which are interleaved with respect to their start
|
||||
and stop times. This method will correct that. It expects that the
|
||||
MIDIEventList has been time-ordered.
|
||||
'''
|
||||
|
||||
tempEventList = []
|
||||
stack = {}
|
||||
|
||||
for event in self.MIDIEventList:
|
||||
|
||||
if event.type == 'NoteOn':
|
||||
if stack.has_key(str(event.pitch)+str(event.channel)):
|
||||
stack[str(event.pitch)+str(event.channel)].append(event.time)
|
||||
else:
|
||||
stack[str(event.pitch)+str(event.channel)] = [event.time]
|
||||
tempEventList.append(event)
|
||||
elif event.type == 'NoteOff':
|
||||
if len(stack[str(event.pitch)+str(event.channel)]) > 1:
|
||||
event.time = stack[str(event.pitch)+str(event.channel)].pop()
|
||||
tempEventList.append(event)
|
||||
else:
|
||||
stack[str(event.pitch)+str(event.channel)].pop()
|
||||
tempEventList.append(event)
|
||||
else:
|
||||
tempEventList.append(event)
|
||||
|
||||
self.MIDIEventList = tempEventList
|
||||
|
||||
# A little trickery here. We want to make sure that NoteOff events appear
|
||||
# before NoteOn events, so we'll do two sorts -- on on type, one on time.
|
||||
# This may have to be revisited, as it makes assumptions about how
|
||||
# the internal sort works, and is in essence creating a sort on a primary
|
||||
# and secondary key.
|
||||
|
||||
self.MIDIEventList.sort(lambda x, y: cmp(x.type , y.type))
|
||||
self.MIDIEventList.sort(lambda x, y: int( 1000 * (x.time - y.time)))
|
||||
|
||||
def adjustTime(self,origin):
|
||||
'''
|
||||
Adjust Times to be relative, and zero-origined
|
||||
'''
|
||||
|
||||
if len(self.MIDIEventList) == 0:
|
||||
return
|
||||
tempEventList = []
|
||||
|
||||
runningTime = 0
|
||||
|
||||
for event in self.MIDIEventList:
|
||||
adjustedTime = event.time - origin
|
||||
event.time = adjustedTime - runningTime
|
||||
runningTime = adjustedTime
|
||||
tempEventList.append(event)
|
||||
|
||||
self.MIDIEventList = tempEventList
|
||||
|
||||
def writeTrack(self,fileHandle):
|
||||
'''
|
||||
Write track to disk.
|
||||
'''
|
||||
|
||||
if not self.closed:
|
||||
self.closeTrack()
|
||||
|
||||
fileHandle.write(self.headerString)
|
||||
fileHandle.write(self.dataLength)
|
||||
fileHandle.write(self.MIDIdata)
|
||||
|
||||
|
||||
class MIDIHeader:
|
||||
'''
|
||||
Class to encapsulate the MIDI header structure.
|
||||
|
||||
This class encapsulates a MIDI header structure. It isn't used for much,
|
||||
but it will create the appropriately packed identifier string that all
|
||||
MIDI files should contain. It is used by the MIDIFile class to create a
|
||||
complete and well formed MIDI pattern.
|
||||
|
||||
'''
|
||||
def __init__(self,numTracks):
|
||||
''' Initialize the data structures
|
||||
'''
|
||||
self.headerString = struct.pack('cccc','M','T','h','d')
|
||||
self.headerSize = struct.pack('>L',6)
|
||||
# Format 1 = multi-track file
|
||||
self.format = struct.pack('>H',1)
|
||||
self.numTracks = struct.pack('>H',numTracks)
|
||||
self.ticksPerBeat = struct.pack('>H',TICKSPERBEAT)
|
||||
|
||||
|
||||
def writeFile(self,fileHandle):
|
||||
fileHandle.write(self.headerString)
|
||||
fileHandle.write(self.headerSize)
|
||||
fileHandle.write(self.format)
|
||||
fileHandle.write(self.numTracks)
|
||||
fileHandle.write(self.ticksPerBeat)
|
||||
|
||||
class MIDIFile:
|
||||
'''Class that represents a full, well-formed MIDI pattern.
|
||||
|
||||
This is a container object that contains a header, one or more tracks,
|
||||
and the data associated with a proper and well-formed MIDI pattern.
|
||||
|
||||
Calling:
|
||||
|
||||
MyMIDI = MidiFile(tracks, removeDuplicates=True, deinterleave=True)
|
||||
|
||||
normally
|
||||
|
||||
MyMIDI = MidiFile(tracks)
|
||||
|
||||
Arguments:
|
||||
|
||||
tracks: The number of tracks this object contains
|
||||
|
||||
removeDuplicates: If true (the default), the software will remove duplicate
|
||||
events which have been added. For example, two notes at the same channel,
|
||||
time, pitch, and duration would be considered duplicate.
|
||||
|
||||
deinterleave: If True (the default), overlapping notes (same pitch, same
|
||||
channel) will be modified so that they do not overlap. Otherwise the sequencing
|
||||
software will need to figure out how to interpret NoteOff events upon playback.
|
||||
'''
|
||||
|
||||
def __init__(self, numTracks, removeDuplicates=True, deinterleave=True):
|
||||
'''
|
||||
Initialize the class
|
||||
'''
|
||||
self.header = MIDIHeader(numTracks)
|
||||
|
||||
self.tracks = list()
|
||||
self.numTracks = numTracks
|
||||
self.closed = False
|
||||
|
||||
for i in range(0,numTracks):
|
||||
self.tracks.append(MIDITrack(removeDuplicates, deinterleave))
|
||||
|
||||
|
||||
# Public Functions. These (for the most part) wrap the MIDITrack functions, where most
|
||||
# Processing takes place.
|
||||
|
||||
def addNote(self,track, channel, pitch,time,duration,volume):
|
||||
"""
|
||||
Add notes to the MIDIFile object
|
||||
|
||||
Use:
|
||||
MyMIDI.addNotes(track,channel,pitch,time, duration, volume)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the note is added.
|
||||
channel: the MIDI channel to assign to the note. [Integer, 0-15]
|
||||
pitch: the MIDI pitch number [Integer, 0-127].
|
||||
time: the time (in beats) at which the note sounds [Float].
|
||||
duration: the duration of the note (in beats) [Float].
|
||||
volume: the volume (velocity) of the note. [Integer, 0-127].
|
||||
"""
|
||||
self.tracks[track].addNoteByNumber(channel, pitch, time, duration, volume)
|
||||
|
||||
def addTrackName(self,track, time,trackName):
|
||||
"""
|
||||
Add a track name to a MIDI track.
|
||||
|
||||
Use:
|
||||
MyMIDI.addTrackName(track,time,trackName)
|
||||
|
||||
Argument:
|
||||
track: The track to which the name is added. [Integer, 0-127].
|
||||
time: The time at which the track name is added, in beats [Float].
|
||||
trackName: The track name. [String].
|
||||
"""
|
||||
self.tracks[track].addTrackName(time,trackName)
|
||||
|
||||
def addTempo(self,track, time,tempo):
|
||||
"""
|
||||
Add a tempo event.
|
||||
|
||||
Use:
|
||||
MyMIDI.addTempo(track, time, tempo)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
tempo: The tempo, in Beats per Minute. [Integer]
|
||||
"""
|
||||
self.tracks[track].addTempo(time,tempo)
|
||||
|
||||
def addProgramChange(self,track, channel, time, program):
|
||||
"""
|
||||
Add a MIDI program change event.
|
||||
|
||||
Use:
|
||||
MyMIDI.addProgramChange(track,channel, time, program)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
channel: The channel the event is assigned to. [Integer, 0-15].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
program: the program number. [Integer, 0-127].
|
||||
"""
|
||||
self.tracks[track].addProgramChange(channel, time, program)
|
||||
|
||||
def addControllerEvent(self,track, channel,time,eventType, paramerter1):
|
||||
"""
|
||||
Add a MIDI controller event.
|
||||
|
||||
Use:
|
||||
MyMIDI.addControllerEvent(track, channel, time, eventType, parameter1)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
channel: The channel the event is assigned to. [Integer, 0-15].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
eventType: the controller event type.
|
||||
parameter1: The event's parameter. The meaning of which varies by event type.
|
||||
"""
|
||||
self.tracks[track].addControllerEvent(channel,time,eventType, paramerter1)
|
||||
|
||||
def changeNoteTuning(self, track, tunings, sysExChannel=0x7F, \
|
||||
realTime=False, tuningProgam=0):
|
||||
"""
|
||||
Change a note's tuning using SysEx change tuning program.
|
||||
|
||||
Use:
|
||||
MyMIDI.changeNoteTuning(track,[tunings],realTime=False, tuningProgram=0)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
tunings: A list of tuples in the form (pitchNumber, frequency).
|
||||
[[(Integer,Float]]
|
||||
realTime: Boolean which sets the real-time flag. Defaults to false.
|
||||
sysExChannel: do note use (see below).
|
||||
tuningProgram: Tuning program to assign. Defaults to zero. [Integer, 0-127]
|
||||
|
||||
In general the sysExChannel should not be changed (parameter will be depreciated).
|
||||
|
||||
Also note that many software packages and hardware packages do not implement
|
||||
this standard!
|
||||
"""
|
||||
self.tracks[track].changeNoteTuning(tunings, sysExChannel, realTime,\
|
||||
tuningProgam)
|
||||
|
||||
def writeFile(self,fileHandle):
|
||||
'''
|
||||
Write the MIDI File.
|
||||
|
||||
Use:
|
||||
MyMIDI.writeFile(filehandle)
|
||||
|
||||
Arguments:
|
||||
filehandle: a file handle that has been opened for binary writing.
|
||||
'''
|
||||
|
||||
self.header.writeFile(fileHandle)
|
||||
|
||||
#Close the tracks and have them create the MIDI event data structures.
|
||||
self.close()
|
||||
|
||||
#Write the MIDI Events to file.
|
||||
for i in range(0,self.numTracks):
|
||||
self.tracks[i].writeTrack(fileHandle)
|
||||
|
||||
def addSysEx(self,track, time, manID, payload):
|
||||
"""
|
||||
Add a SysEx event
|
||||
|
||||
Use:
|
||||
MyMIDI.addSysEx(track,time,ID,payload)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
ID: The SysEx ID number
|
||||
payload: the event payload.
|
||||
|
||||
Note: This is a low-level MIDI function, so care must be used in
|
||||
constructing the payload. It is recommended that higher-level helper
|
||||
functions be written to wrap this function and construct the payload if
|
||||
a developer finds him or herself using the function heavily.
|
||||
"""
|
||||
self.tracks[track].addSysEx(time,manID, payload)
|
||||
|
||||
def addUniversalSysEx(self,track, time,code, subcode, payload, \
|
||||
sysExChannel=0x7F, realTime=False):
|
||||
"""
|
||||
Add a Universal SysEx event.
|
||||
|
||||
Use:
|
||||
MyMIDI.addUniversalSysEx(track, time, code, subcode, payload,\
|
||||
sysExChannel=0x7f, realTime=False)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
code: The even code. [Integer]
|
||||
subcode The event sub-code [Integer]
|
||||
payload: The event payload. [Binary string]
|
||||
sysExChannel: The SysEx channel.
|
||||
realTime: Sets the real-time flag. Defaults to zero.
|
||||
|
||||
Note: This is a low-level MIDI function, so care must be used in
|
||||
constructing the payload. It is recommended that higher-level helper
|
||||
functions be written to wrap this function and construct the payload if
|
||||
a developer finds him or herself using the function heavily. As an example
|
||||
of such a helper function, see the changeNoteTuning function, both here and
|
||||
in MIDITrack.
|
||||
"""
|
||||
|
||||
self.tracks[track].addUniversalSysEx(time,code, subcode, payload, sysExChannel,\
|
||||
realTime)
|
||||
|
||||
def shiftTracks(self, offset=0):
|
||||
"""Shift tracks to be zero-origined, or origined at offset.
|
||||
|
||||
Note that the shifting of the time in the tracks uses the MIDIEventList -- in other
|
||||
words it is assumed to be called in the stage where the MIDIEventList has been
|
||||
created. This function, however, it meant to operate on the eventList itself.
|
||||
"""
|
||||
origin = 1000000 # A little silly, but we'll assume big enough
|
||||
|
||||
for track in self.tracks:
|
||||
if len(track.eventList) > 0:
|
||||
for event in track.eventList:
|
||||
if event.time < origin:
|
||||
origin = event.time
|
||||
|
||||
for track in self.tracks:
|
||||
tempEventList = []
|
||||
#runningTime = 0
|
||||
|
||||
for event in track.eventList:
|
||||
adjustedTime = event.time - origin
|
||||
#event.time = adjustedTime - runningTime + offset
|
||||
event.time = adjustedTime + offset
|
||||
#runningTime = adjustedTime
|
||||
tempEventList.append(event)
|
||||
|
||||
track.eventList = tempEventList
|
||||
|
||||
#End Public Functions ########################
|
||||
|
||||
def close(self):
|
||||
'''Close the MIDIFile for further writing.
|
||||
|
||||
To close the File for events, we must close the tracks, adjust the time to be
|
||||
zero-origined, and have the tracks write to their MIDI Stream data structure.
|
||||
'''
|
||||
|
||||
if self.closed == True:
|
||||
return
|
||||
|
||||
for i in range(0,self.numTracks):
|
||||
self.tracks[i].closeTrack()
|
||||
# We want things like program changes to come before notes when they are at the
|
||||
# same time, so we sort the MIDI events by their ordinality
|
||||
self.tracks[i].MIDIEventList.sort()
|
||||
|
||||
origin = self.findOrigin()
|
||||
|
||||
for i in range(0,self.numTracks):
|
||||
self.tracks[i].adjustTime(origin)
|
||||
self.tracks[i].writeMIDIStream()
|
||||
|
||||
self.closed = True
|
||||
|
||||
|
||||
def findOrigin(self):
|
||||
'''Find the earliest time in the file's tracks.append.
|
||||
'''
|
||||
origin = 1000000 # A little silly, but we'll assume big enough
|
||||
|
||||
# Note: This code assumes that the MIDIEventList has been sorted, so this should be insured
|
||||
# before it is called. It is probably a poor design to do this.
|
||||
# TODO: -- Consider making this less efficient but more robust by not assuming the list to be sorted.
|
||||
|
||||
for track in self.tracks:
|
||||
if len(track.MIDIEventList) > 0:
|
||||
if track.MIDIEventList[0].time < origin:
|
||||
origin = track.MIDIEventList[0].time
|
||||
|
||||
|
||||
return origin
|
||||
|
||||
def writeVarLength(i):
|
||||
'''Accept an input, and write a MIDI-compatible variable length stream
|
||||
|
||||
The MIDI format is a little strange, and makes use of so-called variable
|
||||
length quantities. These quantities are a stream of bytes. If the most
|
||||
significant bit is 1, then more bytes follow. If it is zero, then the
|
||||
byte in question is the last in the stream
|
||||
'''
|
||||
input = int(i)
|
||||
output = [0,0,0,0]
|
||||
reversed = [0,0,0,0]
|
||||
count = 0
|
||||
result = input & 0x7F
|
||||
output[count] = result
|
||||
count = count + 1
|
||||
input = input >> 7
|
||||
while input > 0:
|
||||
result = input & 0x7F
|
||||
result = result | 0x80
|
||||
output[count] = result
|
||||
count = count + 1
|
||||
input = input >> 7
|
||||
|
||||
reversed[0] = output[3]
|
||||
reversed[1] = output[2]
|
||||
reversed[2] = output[1]
|
||||
reversed[3] = output[0]
|
||||
return reversed[4-count:4]
|
||||
|
||||
def frequencyTransform(freq):
|
||||
'''Returns a three-byte transform of a frequencyTransform
|
||||
'''
|
||||
resolution = 16384
|
||||
freq = float(freq)
|
||||
dollars = 69 + 12 * math.log(freq/(float(440)), 2)
|
||||
firstByte = int(dollars)
|
||||
lowerFreq = 440 * pow(2.0, ((float(firstByte) - 69.0)/12.0))
|
||||
if freq != lowerFreq:
|
||||
centDif = 1200 * math.log( (freq/lowerFreq), 2)
|
||||
else:
|
||||
centDif = 0
|
||||
cents = round(centDif/100 * resolution) # round?
|
||||
secondByte = min([int(cents)>>7, 0x7F])
|
||||
thirdByte = cents - (secondByte << 7)
|
||||
thirdByte = min([thirdByte, 0x7f])
|
||||
if thirdByte == 0x7f and secondByte == 0x7F and firstByte == 0x7F:
|
||||
thirdByte = 0x7e
|
||||
|
||||
thirdByte = int(thirdByte)
|
||||
return [firstByte, secondByte, thirdByte]
|
||||
|
||||
def returnFrequency(freqBytes):
|
||||
'''The reverse of frequencyTransform. Given a byte stream, return a frequency.
|
||||
'''
|
||||
resolution = 16384.0
|
||||
baseFrequency = 440 * pow(2.0, (float(freqBytes[0]-69.0)/12.0))
|
||||
frac = (float((int(freqBytes[1]) << 7) + int(freqBytes[2])) * 100.0) / resolution
|
||||
frequency = baseFrequency * pow(2.0, frac/1200.0)
|
||||
return frequency
|
0
build/lib/midiutil/__init__.py
Normal file
0
build/lib/midiutil/__init__.py
Normal file
32
build/scripts-2.7/single-note-example.py
Executable file
32
build/scripts-2.7/single-note-example.py
Executable file
@ -0,0 +1,32 @@
|
||||
############################################################################
|
||||
# A sample program to create a single-track MIDI file, add a note,
|
||||
# and write to disk.
|
||||
############################################################################
|
||||
|
||||
#Import the library
|
||||
from midiutil.MidiFile import MIDIFile
|
||||
|
||||
# Create the MIDIFile Object
|
||||
MyMIDI = MIDIFile(1)
|
||||
|
||||
# Add track name and tempo. The first argument to addTrackName and
|
||||
# addTempo is the time to write the event.
|
||||
track = 0
|
||||
time = 0
|
||||
MyMIDI.addTrackName(track,time,"Sample Track")
|
||||
MyMIDI.addTempo(track,time, 120)
|
||||
|
||||
# Add a note. addNote expects the following information:
|
||||
channel = 0
|
||||
pitch = 60
|
||||
duration = 1
|
||||
volume = 100
|
||||
|
||||
# Now add the note.
|
||||
MyMIDI.addNote(track,channel,pitch,time,duration,volume)
|
||||
|
||||
# And write it to disk.
|
||||
binfile = open("output.mid", 'wb')
|
||||
MyMIDI.writeFile(binfile)
|
||||
binfile.close()
|
||||
|
229
documentation/ClassReference.txt
Normal file
229
documentation/ClassReference.txt
Normal file
@ -0,0 +1,229 @@
|
||||
========================
|
||||
MIDIUtil Class Reference
|
||||
========================
|
||||
|
||||
--------------
|
||||
class MIDIFile
|
||||
--------------
|
||||
|
||||
A class that represents a full, well-formed MIDI pattern.
|
||||
|
||||
This is a container object that contains a header, one or more
|
||||
tracks, and the data associated with a proper and well-formed
|
||||
MIDI pattern.
|
||||
|
||||
Calling
|
||||
|
||||
MyMIDI = MidiFile(tracks, removeDuplicates=True, deinterleave=True)
|
||||
|
||||
normally
|
||||
|
||||
MyMIDI = MidiFile(tracks)
|
||||
|
||||
Arguments
|
||||
|
||||
o tracks: The number of tracks this object contains
|
||||
|
||||
o removeDuplicates: If true (the default), the software will
|
||||
remove duplicate events which have been added. For example,
|
||||
two notes at the same channel, time, pitch, and duration would
|
||||
be considered duplicate.
|
||||
|
||||
o deinterleave: If True (the default), overlapping notes
|
||||
(same pitch, same channel) will be modified so that they do
|
||||
not overlap. Otherwise the sequencing software will need to
|
||||
figure out how to interpret NoteOff events upon playback.
|
||||
|
||||
================
|
||||
Public Functions
|
||||
================
|
||||
|
||||
---------------------------------------------------
|
||||
addNote(track, channel, pitch,time,duration,volume)
|
||||
---------------------------------------------------
|
||||
|
||||
Add notes to the MIDIFile object
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addNotes(track,channel,pitch,time, duration, volume)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the note is added.
|
||||
o channel: the MIDI channel to assign to the note. [Integer, 0-15]
|
||||
o pitch: the MIDI pitch number [Integer, 0-127].
|
||||
o time: the time (in beats) at which the note sounds [Float].
|
||||
o duration: the duration of the note (in beats) [Float].
|
||||
o lume: the volume (velocity) of the note. [Integer, 0-127].
|
||||
|
||||
|
||||
----------------------------------
|
||||
addTrackName(track, time,trackName)
|
||||
----------------------------------
|
||||
|
||||
Add a track name to a MIDI track.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addTrackName(track,time,trackName)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the name is added. [Integer, 0-127].
|
||||
o time: The time at which the track name is added, in beats
|
||||
[Float].
|
||||
o trackName: The track name. [String].
|
||||
|
||||
---------------------------
|
||||
addTempo(track, time,tempo)
|
||||
---------------------------
|
||||
|
||||
Add a tempo event.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addTempo(track, time, tempo)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127]
|
||||
o time: The time at which the event is added, in beats. [Float]
|
||||
o tempo: The tempo, in Beats per Minute. [Integer]
|
||||
|
||||
|
||||
-----------------------------------------------
|
||||
addProgramChange(track, channel, time, program)
|
||||
-----------------------------------------------
|
||||
|
||||
Add a MIDI program change event.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addProgramChange(track,channel, time, program)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127]
|
||||
o channel: The channel the event is assigned to. [Integer, 0-15]
|
||||
o time: The time at which the event is added, in beats. [Float]
|
||||
o program: the program number. [Integer, 0-127]
|
||||
|
||||
|
||||
--------------------------------------------------------------
|
||||
addControllerEvent(track, channel,time,eventType, paramerter1)
|
||||
--------------------------------------------------------------
|
||||
|
||||
Add a MIDI controller event.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addControllerEvent(track, channel, time, eventType, \
|
||||
parameter1)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127]
|
||||
o channel: The channel the event is assigned to. [Integer, 0-15]
|
||||
o time: The time at which the event is added, in beats. [Float]
|
||||
o eventType: the controller event type.
|
||||
o parameter1: The event's parameter. The meaning of which varies
|
||||
by event type.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
changeNoteTuning(track, tunings, sysExChannel=0x7F, realTime=False, \
|
||||
tuningProgam=0)
|
||||
---------------------------------------------------------------------
|
||||
|
||||
Change a note's tuning using sysEx change tuning program.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.changeNoteTuning(track,[tunings],realTime=False, \
|
||||
tuningProgram=0)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127].
|
||||
o tunings: A list of tuples in the form (pitchNumber,
|
||||
frequency). [[(Integer,Float]]
|
||||
o realTime: Boolean which sets the real-time flag. Defaults to false.
|
||||
o sysExChannel: do note use (see below).
|
||||
o tuningProgram: Tuning program to assign. Defaults to
|
||||
zero. [Integer, 0-127]
|
||||
|
||||
In general the sysExChannel should not be changed (parameter will
|
||||
be depreciated).
|
||||
|
||||
Also note that many software packages and hardware packages do not
|
||||
implement this standard!
|
||||
|
||||
|
||||
---------------------
|
||||
writeFile(fileHandle)
|
||||
---------------------
|
||||
|
||||
Write the MIDI File.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.writeFile(filehandle)
|
||||
|
||||
Arguments
|
||||
|
||||
o filehandle: a file handle that has been opened for binary
|
||||
writing.
|
||||
|
||||
|
||||
-------------------------------------
|
||||
addSysEx(track, time, manID, payload)
|
||||
-------------------------------------
|
||||
|
||||
Add a SysEx event
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addSysEx(track,time,ID,payload)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127].
|
||||
o time: The time at which the event is added, in beats. [Float].
|
||||
o ID: The SysEx ID number
|
||||
o payload: the event payload.
|
||||
|
||||
Note: This is a low-level MIDI function, so care must be used in
|
||||
constructing the payload. It is recommended that higher-level helper
|
||||
functions be written to wrap this function and construct the payload
|
||||
if a developer finds him or herself using the function heavily.
|
||||
|
||||
|
||||
---------------------------------------------------------
|
||||
addUniversalSysEx(track, time,code, subcode, payload, \
|
||||
sysExChannel=0x7F, realTime=False)}f
|
||||
---------------------------------------------------------
|
||||
|
||||
Add a Universal SysEx event.
|
||||
|
||||
Use
|
||||
|
||||
MyMIDI.addUniversalSysEx(track, time, code, subcode, payload, \
|
||||
sysExChannel=0x7f, realTime=False)
|
||||
|
||||
Arguments
|
||||
|
||||
o track: The track to which the event is added. [Integer, 0-127].
|
||||
o time: The time at which the event is added, in beats. [Float].
|
||||
o code: The event code. [Integer]
|
||||
o subcode The event sub-code [Integer]
|
||||
o payload: The event payload. [Binary string]
|
||||
o sysExChannel: The SysEx channel.
|
||||
o realTime: Sets the real-time flag. Defaults to zero.
|
||||
|
||||
Note: This is a low-level MIDI function, so care must be used in
|
||||
constructing the payload. It is recommended that higher-level helper
|
||||
functions be written to wrap this function and construct the payload
|
||||
if a developer finds him or herself using the function heavily. As an
|
||||
example of such a helper function, see the changeNoteTuning function,
|
||||
both here and in MIDITrack.
|
||||
|
169
documentation/Extending.txt
Normal file
169
documentation/Extending.txt
Normal file
@ -0,0 +1,169 @@
|
||||
=====================
|
||||
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!
|
32
examples/single-note-example.py
Normal file
32
examples/single-note-example.py
Normal file
@ -0,0 +1,32 @@
|
||||
############################################################################
|
||||
# A sample program to create a single-track MIDI file, add a note,
|
||||
# and write to disk.
|
||||
############################################################################
|
||||
|
||||
#Import the library
|
||||
from midiutil.MidiFile import MIDIFile
|
||||
|
||||
# Create the MIDIFile Object
|
||||
MyMIDI = MIDIFile(1)
|
||||
|
||||
# Add track name and tempo. The first argument to addTrackName and
|
||||
# addTempo is the time to write the event.
|
||||
track = 0
|
||||
time = 0
|
||||
MyMIDI.addTrackName(track,time,"Sample Track")
|
||||
MyMIDI.addTempo(track,time, 120)
|
||||
|
||||
# Add a note. addNote expects the following information:
|
||||
channel = 0
|
||||
pitch = 60
|
||||
duration = 1
|
||||
volume = 100
|
||||
|
||||
# Now add the note.
|
||||
MyMIDI.addNote(track,channel,pitch,time,duration,volume)
|
||||
|
||||
# And write it to disk.
|
||||
binfile = open("output.mid", 'wb')
|
||||
MyMIDI.writeFile(binfile)
|
||||
binfile.close()
|
||||
|
18
setup.py
Normal file
18
setup.py
Normal file
@ -0,0 +1,18 @@
|
||||
from distutils.core import setup
|
||||
|
||||
setup(name='MIDIUtil',
|
||||
version='0.87',
|
||||
description='MIDIUtil, a MIDI Interface for Python',
|
||||
author='Mark Conway Wirt',
|
||||
author_email='emergentmusics) at (gmail . com',
|
||||
license='Copyright (C) 2009, Mark Conway Wirt. See License.txt for details.',
|
||||
url='http://www.emergentmusics.org/midiutil/',
|
||||
packages=["midiutil"],
|
||||
package_dir = {'midiutil': 'src/midiutil'},
|
||||
package_data={'midiutil' : ['../../documentation/*']},
|
||||
scripts=['examples/single-note-example.py'],
|
||||
platforms='Platform Independent',
|
||||
long_description='''
|
||||
This package provides a simple interface to allow Python programs to
|
||||
write multi-track MIDI files.'''
|
||||
)
|
993
src/midiutil/MidiFile.py
Normal file
993
src/midiutil/MidiFile.py
Normal file
@ -0,0 +1,993 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# Name: MidiFile.py
|
||||
# Purpose: MIDI file manipulation utilities
|
||||
#
|
||||
# Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
|
||||
#
|
||||
# Created: 2008/04/17
|
||||
# Copyright: (c) 2009 Mark Conway Wirt
|
||||
# License: Please see License.txt for the terms under which this
|
||||
# software is distributed.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import struct, sys, math
|
||||
|
||||
# TICKSPERBEAT is the number of "ticks" (time measurement in the MIDI file) that
|
||||
# corresponds to one beat. This number is somewhat arbitrary, but should be chosen
|
||||
# to provide adequate temporal resolution.
|
||||
|
||||
TICKSPERBEAT = 128
|
||||
|
||||
controllerEventTypes = {
|
||||
'pan' : 0x0a
|
||||
}
|
||||
class MIDIEvent:
|
||||
'''
|
||||
The class to contain the MIDI Event (placed on MIDIEventList.
|
||||
'''
|
||||
def __init__(self):
|
||||
self.type='unknown'
|
||||
self.time=0
|
||||
self.ord = 0
|
||||
|
||||
def __cmp__(self, other):
|
||||
''' Sorting function for events.'''
|
||||
if self.time < other.time:
|
||||
return -1
|
||||
elif self.time > other.time:
|
||||
return 1
|
||||
else:
|
||||
if self.ord < other.ord:
|
||||
return -1
|
||||
elif self.ord > other.ord:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
class GenericEvent():
|
||||
'''The event class from which specific events are derived
|
||||
'''
|
||||
def __init__(self,time):
|
||||
self.time = time
|
||||
self.type = 'Unknown'
|
||||
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
'''
|
||||
Equality operator for Generic Events and derived classes.
|
||||
|
||||
In the processing of the event list, we have need to remove duplicates. To do this
|
||||
we rely on the fact that the classes are hashable, and must therefore have an
|
||||
equality operator (__hash__() and __eq__() must both be defined).
|
||||
|
||||
This is the most embarrassing portion of the code, and anyone who knows about OO
|
||||
programming would find this almost unbelievable. Here we have a base class that
|
||||
knows specifics about derived classes, thus breaking the very spirit of
|
||||
OO programming.
|
||||
|
||||
I suppose I should go back and restructure the code, perhaps removing the derived
|
||||
classes altogether. At some point perhaps I will.
|
||||
'''
|
||||
if self.time != other.time or self.type != other.type:
|
||||
return False
|
||||
|
||||
# What follows is code that encodes the concept of equality for each derived
|
||||
# class. Believe it f you dare.
|
||||
|
||||
if self.type == 'note':
|
||||
if self.pitch != other.pitch or self.channel != other.channel:
|
||||
return False
|
||||
if self.type == 'tempo':
|
||||
if self.tempo != other.tempo:
|
||||
return False
|
||||
if self.type == 'programChange':
|
||||
if self.programNumber != other.programNumber or self.channel != other.channel:
|
||||
return False
|
||||
if self.type == 'trackName':
|
||||
if self.trackName != other.trackName:
|
||||
return False
|
||||
if self.type == 'controllerEvent':
|
||||
if self.parameter1 != other.parameter1 or \
|
||||
self.parameter2 != other.parameter2 or \
|
||||
self.channel != other.channel or \
|
||||
self.eventType != other.eventType:
|
||||
return False
|
||||
|
||||
if self.type == 'SysEx':
|
||||
if self.manID != other.manID:
|
||||
return False
|
||||
|
||||
if self.type == 'UniversalSysEx':
|
||||
if self.code != other.code or\
|
||||
self.subcode != other.subcode or \
|
||||
self.sysExChannel != other.sysExChannel:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
'''
|
||||
Return a hash code for the object.
|
||||
|
||||
This is needed for the removal of duplicate objects from the event list. The only
|
||||
real requirement for the algorithm is that the hash of equal objects must be equal.
|
||||
There is probably great opportunity for improvements in the hashing function.
|
||||
'''
|
||||
# Robert Jenkin's 32 bit hash.
|
||||
a = int(self.time)
|
||||
a = (a+0x7ed55d16) + (a<<12)
|
||||
a = (a^0xc761c23c) ^ (a>>19)
|
||||
a = (a+0x165667b1) + (a<<5)
|
||||
a = (a+0xd3a2646c) ^ (a<<9)
|
||||
a = (a+0xfd7046c5) + (a<<3)
|
||||
a = (a^0xb55a4f09) ^ (a>>16)
|
||||
return a
|
||||
|
||||
class MIDITrack:
|
||||
'''A class that encapsulates a MIDI track
|
||||
'''
|
||||
# Nested class definitions.
|
||||
|
||||
class note(GenericEvent):
|
||||
'''A class that encapsulates a note
|
||||
'''
|
||||
def __init__(self,channel, pitch,time,duration,volume):
|
||||
|
||||
GenericEvent.__init__(self,time)
|
||||
self.pitch = pitch
|
||||
self.duration = duration
|
||||
self.volume = volume
|
||||
self.type = 'note'
|
||||
self.channel = channel
|
||||
|
||||
def compare(self, other):
|
||||
'''Compare two notes for equality.
|
||||
'''
|
||||
if self.pitch == other.pitch and \
|
||||
self.time == other.time and \
|
||||
self.duration == other.duration and \
|
||||
self.volume == other.volume and \
|
||||
self.type == other.type and \
|
||||
self.channel == other.channel:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class tempo(GenericEvent):
|
||||
'''A class that encapsulates a tempo meta-event
|
||||
'''
|
||||
def __init__(self,time,tempo):
|
||||
|
||||
GenericEvent.__init__(self,time)
|
||||
self.type = 'tempo'
|
||||
self.tempo = int(60000000 / tempo)
|
||||
|
||||
class programChange(GenericEvent):
|
||||
'''A class that encapsulates a program change event.
|
||||
'''
|
||||
|
||||
def __init__(self, channel, time, programNumber):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'programChange'
|
||||
self.programNumber = programNumber
|
||||
self.channel = channel
|
||||
|
||||
class SysExEvent(GenericEvent):
|
||||
'''A class that encapsulates a System Exclusive event.
|
||||
'''
|
||||
|
||||
def __init__(self, time, manID, payload):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'SysEx'
|
||||
self.manID = manID
|
||||
self.payload = payload
|
||||
|
||||
class UniversalSysExEvent(GenericEvent):
|
||||
'''A class that encapsulates a Universal System Exclusive event.
|
||||
'''
|
||||
|
||||
def __init__(self, time, realTime, sysExChannel, code, subcode, payload):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'UniversalSysEx'
|
||||
self.realTime = realTime
|
||||
self.sysExChannel = sysExChannel
|
||||
self.code = code
|
||||
self.subcode = subcode
|
||||
self.payload = payload
|
||||
|
||||
class ControllerEvent(GenericEvent):
|
||||
'''A class that encapsulates a program change event.
|
||||
'''
|
||||
|
||||
def __init__(self, channel, time, eventType, parameter1,):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'controllerEvent'
|
||||
self.parameter1 = parameter1
|
||||
self.channel = channel
|
||||
self.eventType = eventType
|
||||
|
||||
class trackName(GenericEvent):
|
||||
'''A class that encapsulates a program change event.
|
||||
'''
|
||||
|
||||
def __init__(self, time, trackName):
|
||||
GenericEvent.__init__(self, time,)
|
||||
self.type = 'trackName'
|
||||
self.trackName = trackName
|
||||
|
||||
|
||||
def __init__(self, removeDuplicates, deinterleave):
|
||||
'''Initialize the MIDITrack object.
|
||||
'''
|
||||
self.headerString = struct.pack('cccc','M','T','r','k')
|
||||
self.dataLength = 0 # Is calculated after the data is in place
|
||||
self.MIDIdata = ""
|
||||
self.closed = False
|
||||
self.eventList = []
|
||||
self.MIDIEventList = []
|
||||
self.remdep = removeDuplicates
|
||||
self.deinterleave = deinterleave
|
||||
|
||||
def addNoteByNumber(self,channel, pitch,time,duration,volume):
|
||||
'''Add a note by chromatic MIDI number
|
||||
'''
|
||||
self.eventList.append(MIDITrack.note(channel, pitch,time,duration,volume))
|
||||
|
||||
def addControllerEvent(self,channel,time,eventType, paramerter1):
|
||||
'''
|
||||
Add a controller event.
|
||||
'''
|
||||
|
||||
self.eventList.append(MIDITrack.ControllerEvent(channel,time,eventType, \
|
||||
paramerter1))
|
||||
|
||||
def addTempo(self,time,tempo):
|
||||
'''
|
||||
Add a tempo change (or set) event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.tempo(time,tempo))
|
||||
|
||||
def addSysEx(self,time,manID, payload):
|
||||
'''
|
||||
Add a SysEx event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.SysExEvent(time, manID, payload))
|
||||
|
||||
def addUniversalSysEx(self,time,code, subcode, payload, sysExChannel=0x7F, \
|
||||
realTime=False):
|
||||
'''
|
||||
Add a Universal SysEx event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.UniversalSysExEvent(time, realTime, \
|
||||
sysExChannel, code, subcode, payload))
|
||||
|
||||
def addProgramChange(self,channel, time, program):
|
||||
'''
|
||||
Add a program change event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.programChange(channel, time, program))
|
||||
|
||||
def addTrackName(self,time,trackName):
|
||||
'''
|
||||
Add a track name event.
|
||||
'''
|
||||
self.eventList.append(MIDITrack.trackName(time,trackName))
|
||||
|
||||
def changeNoteTuning(self, tunings, sysExChannel=0x7F, realTime=False, \
|
||||
tuningProgam=0):
|
||||
'''Change the tuning of MIDI notes
|
||||
'''
|
||||
payload = struct.pack('>B', tuningProgam)
|
||||
payload = payload + struct.pack('>B', len(tunings))
|
||||
for (noteNumber, frequency) in tunings:
|
||||
payload = payload + struct.pack('>B', noteNumber)
|
||||
MIDIFreqency = frequencyTransform(frequency)
|
||||
for byte in MIDIFreqency:
|
||||
payload = payload + struct.pack('>B', byte)
|
||||
|
||||
self.eventList.append(MIDITrack.UniversalSysExEvent(0, realTime, sysExChannel,\
|
||||
8, 2, payload))
|
||||
|
||||
def processEventList(self):
|
||||
'''
|
||||
Process the event list, creating a MIDIEventList
|
||||
|
||||
For each item in the event list, one or more events in the MIDIEvent
|
||||
list are created.
|
||||
'''
|
||||
|
||||
# Loop over all items in the eventList
|
||||
|
||||
for thing in self.eventList:
|
||||
if thing.type == 'note':
|
||||
event = MIDIEvent()
|
||||
event.type = "NoteOn"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.pitch = thing.pitch
|
||||
event.volume = thing.volume
|
||||
event.channel = thing.channel
|
||||
event.ord = 3
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
event = MIDIEvent()
|
||||
event.type = "NoteOff"
|
||||
event.time = (thing.time + thing.duration) * TICKSPERBEAT
|
||||
event.pitch = thing.pitch
|
||||
event.volume = thing.volume
|
||||
event.channel = thing.channel
|
||||
event.ord = 2
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
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)
|
||||
|
||||
elif thing.type == 'programChange':
|
||||
event = MIDIEvent()
|
||||
event.type = "ProgramChange"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.programNumber = thing.programNumber
|
||||
event.channel = thing.channel
|
||||
event.ord = 1
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
elif thing.type == 'trackName':
|
||||
event = MIDIEvent()
|
||||
event.type = "TrackName"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.trackName = thing.trackName
|
||||
event.ord = 0
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
elif thing.type == 'controllerEvent':
|
||||
event = MIDIEvent()
|
||||
event.type = "ControllerEvent"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.eventType = thing.eventType
|
||||
event.channel = thing.channel
|
||||
event.paramerter1 = thing.parameter1
|
||||
event.ord = 1
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
elif thing.type == 'SysEx':
|
||||
event = MIDIEvent()
|
||||
event.type = "SysEx"
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.manID = thing.manID
|
||||
event.payload = thing.payload
|
||||
event.ord = 1
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
elif thing.type == 'UniversalSysEx':
|
||||
event = MIDIEvent()
|
||||
event.type = "UniversalSysEx"
|
||||
event.realTime = thing.realTime
|
||||
event.sysExChannel = thing.sysExChannel
|
||||
event.time = thing.time * TICKSPERBEAT
|
||||
event.code = thing.code
|
||||
event.subcode = thing.subcode
|
||||
event.payload = thing.payload
|
||||
event.ord = 1
|
||||
self.MIDIEventList.append(event)
|
||||
|
||||
else:
|
||||
print "Error in MIDITrack: Unknown event type"
|
||||
sys.exit(2)
|
||||
|
||||
# Assumptions in the code expect the list to be time-sorted.
|
||||
# self.MIDIEventList.sort(lambda x, y: x.time - y.time)
|
||||
|
||||
self.MIDIEventList.sort(lambda x, y: int( 1000 * (x.time - y.time)))
|
||||
|
||||
if self.deinterleave:
|
||||
self.deInterleaveNotes()
|
||||
|
||||
def removeDuplicates(self):
|
||||
'''
|
||||
Remove duplicates from the eventList.
|
||||
|
||||
This function will remove duplicates from the eventList. This is necessary
|
||||
because we the MIDI event stream can become confused otherwise.
|
||||
'''
|
||||
|
||||
# For this algorithm to work, the events in the eventList must be hashable
|
||||
# (that is, they must have a __hash__() and __eq__() function defined).
|
||||
|
||||
tempDict = {}
|
||||
for item in self.eventList:
|
||||
tempDict[item] = 1
|
||||
|
||||
self.eventList = tempDict.keys()
|
||||
|
||||
# Sort on type, them on time. Necessary because keys() has no requirement to return
|
||||
# things in any order.
|
||||
|
||||
self.eventList.sort(lambda x, y: cmp(x.type , y.type))
|
||||
self.eventList.sort(lambda x, y: int( 1000 * (x.time - y.time))) #A bit of a hack.
|
||||
|
||||
def closeTrack(self):
|
||||
'''Called to close a track before writing
|
||||
|
||||
This function should be called to "close a track," that is to
|
||||
prepare the actual data stream for writing. Duplicate events are
|
||||
removed from the eventList, and the MIDIEventList is created.
|
||||
|
||||
Called by the parent MIDIFile object.
|
||||
'''
|
||||
|
||||
if self.closed == True:
|
||||
return
|
||||
self.closed = True
|
||||
|
||||
if self.remdep:
|
||||
self.removeDuplicates()
|
||||
|
||||
|
||||
self.processEventList()
|
||||
|
||||
def writeMIDIStream(self):
|
||||
'''
|
||||
Write the meta data and note data to the packed MIDI stream.
|
||||
'''
|
||||
|
||||
#Process the events in the eventList
|
||||
|
||||
self.writeEventsToStream()
|
||||
|
||||
# Write MIDI close event.
|
||||
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('BBBB',0x00,0xFF, \
|
||||
0x2F,0x00)
|
||||
|
||||
# Calculate the entire length of the data and write to the header
|
||||
|
||||
self.dataLength = struct.pack('>L',len(self.MIDIdata))
|
||||
|
||||
def writeEventsToStream(self):
|
||||
'''
|
||||
Write the events in MIDIEvents to the MIDI stream.
|
||||
'''
|
||||
|
||||
for event in self.MIDIEventList:
|
||||
if event.type == "NoteOn":
|
||||
code = 0x9 << 4 | event.channel
|
||||
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',event.pitch)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume)
|
||||
elif event.type == "NoteOff":
|
||||
code = 0x8 << 4 | event.channel
|
||||
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',event.pitch)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume)
|
||||
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) # Data length: 3
|
||||
self.MIDIdata = self.MIDIdata + threebite
|
||||
elif event.type == 'ProgramChange':
|
||||
code = 0xC << 4 | event.channel
|
||||
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',event.programNumber)
|
||||
elif event.type == 'TrackName':
|
||||
varTime = writeVarLength(event.time)
|
||||
for timeByte in varTime:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('B',0xFF) # Meta-event
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('B',0X03) # Event Type
|
||||
dataLength = len(event.trackName)
|
||||
dataLenghtVar = writeVarLength(dataLength)
|
||||
for i in range(0,len(dataLenghtVar)):
|
||||
self.MIDIdata = self.MIDIdata + struct.pack("b",dataLenghtVar[i])
|
||||
self.MIDIdata = self.MIDIdata + event.trackName
|
||||
elif event.type == "ControllerEvent":
|
||||
code = 0xB << 4 | event.channel
|
||||
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',event.eventType)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',event.paramerter1)
|
||||
elif event.type == "SysEx":
|
||||
code = 0xF0
|
||||
varTime = writeVarLength(event.time)
|
||||
for timeByte in varTime:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', code)
|
||||
|
||||
payloadLength = writeVarLength(len(event.payload)+2)
|
||||
for lenByte in payloadLength:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte)
|
||||
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', event.manID)
|
||||
self.MIDIdata = self.MIDIdata + event.payload
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7)
|
||||
elif event.type == "UniversalSysEx":
|
||||
code = 0xF0
|
||||
varTime = writeVarLength(event.time)
|
||||
for timeByte in varTime:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', code)
|
||||
|
||||
# Do we need to add a length?
|
||||
payloadLength = writeVarLength(len(event.payload)+5)
|
||||
for lenByte in payloadLength:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte)
|
||||
|
||||
if event.realTime :
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7F)
|
||||
else:
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7E)
|
||||
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', event.sysExChannel)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', event.code)
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B', event.subcode)
|
||||
self.MIDIdata = self.MIDIdata + event.payload
|
||||
self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7)
|
||||
|
||||
def deInterleaveNotes(self):
|
||||
'''Correct Interleaved notes.
|
||||
|
||||
Because we are writing multiple notes in no particular order, we
|
||||
can have notes which are interleaved with respect to their start
|
||||
and stop times. This method will correct that. It expects that the
|
||||
MIDIEventList has been time-ordered.
|
||||
'''
|
||||
|
||||
tempEventList = []
|
||||
stack = {}
|
||||
|
||||
for event in self.MIDIEventList:
|
||||
|
||||
if event.type == 'NoteOn':
|
||||
if stack.has_key(str(event.pitch)+str(event.channel)):
|
||||
stack[str(event.pitch)+str(event.channel)].append(event.time)
|
||||
else:
|
||||
stack[str(event.pitch)+str(event.channel)] = [event.time]
|
||||
tempEventList.append(event)
|
||||
elif event.type == 'NoteOff':
|
||||
if len(stack[str(event.pitch)+str(event.channel)]) > 1:
|
||||
event.time = stack[str(event.pitch)+str(event.channel)].pop()
|
||||
tempEventList.append(event)
|
||||
else:
|
||||
stack[str(event.pitch)+str(event.channel)].pop()
|
||||
tempEventList.append(event)
|
||||
else:
|
||||
tempEventList.append(event)
|
||||
|
||||
self.MIDIEventList = tempEventList
|
||||
|
||||
# A little trickery here. We want to make sure that NoteOff events appear
|
||||
# before NoteOn events, so we'll do two sorts -- on on type, one on time.
|
||||
# This may have to be revisited, as it makes assumptions about how
|
||||
# the internal sort works, and is in essence creating a sort on a primary
|
||||
# and secondary key.
|
||||
|
||||
self.MIDIEventList.sort(lambda x, y: cmp(x.type , y.type))
|
||||
self.MIDIEventList.sort(lambda x, y: int( 1000 * (x.time - y.time)))
|
||||
|
||||
def adjustTime(self,origin):
|
||||
'''
|
||||
Adjust Times to be relative, and zero-origined
|
||||
'''
|
||||
|
||||
if len(self.MIDIEventList) == 0:
|
||||
return
|
||||
tempEventList = []
|
||||
|
||||
runningTime = 0
|
||||
|
||||
for event in self.MIDIEventList:
|
||||
adjustedTime = event.time - origin
|
||||
event.time = adjustedTime - runningTime
|
||||
runningTime = adjustedTime
|
||||
tempEventList.append(event)
|
||||
|
||||
self.MIDIEventList = tempEventList
|
||||
|
||||
def writeTrack(self,fileHandle):
|
||||
'''
|
||||
Write track to disk.
|
||||
'''
|
||||
|
||||
if not self.closed:
|
||||
self.closeTrack()
|
||||
|
||||
fileHandle.write(self.headerString)
|
||||
fileHandle.write(self.dataLength)
|
||||
fileHandle.write(self.MIDIdata)
|
||||
|
||||
|
||||
class MIDIHeader:
|
||||
'''
|
||||
Class to encapsulate the MIDI header structure.
|
||||
|
||||
This class encapsulates a MIDI header structure. It isn't used for much,
|
||||
but it will create the appropriately packed identifier string that all
|
||||
MIDI files should contain. It is used by the MIDIFile class to create a
|
||||
complete and well formed MIDI pattern.
|
||||
|
||||
'''
|
||||
def __init__(self,numTracks):
|
||||
''' Initialize the data structures
|
||||
'''
|
||||
self.headerString = struct.pack('cccc','M','T','h','d')
|
||||
self.headerSize = struct.pack('>L',6)
|
||||
# Format 1 = multi-track file
|
||||
self.format = struct.pack('>H',1)
|
||||
self.numTracks = struct.pack('>H',numTracks)
|
||||
self.ticksPerBeat = struct.pack('>H',TICKSPERBEAT)
|
||||
|
||||
|
||||
def writeFile(self,fileHandle):
|
||||
fileHandle.write(self.headerString)
|
||||
fileHandle.write(self.headerSize)
|
||||
fileHandle.write(self.format)
|
||||
fileHandle.write(self.numTracks)
|
||||
fileHandle.write(self.ticksPerBeat)
|
||||
|
||||
class MIDIFile:
|
||||
'''Class that represents a full, well-formed MIDI pattern.
|
||||
|
||||
This is a container object that contains a header, one or more tracks,
|
||||
and the data associated with a proper and well-formed MIDI pattern.
|
||||
|
||||
Calling:
|
||||
|
||||
MyMIDI = MidiFile(tracks, removeDuplicates=True, deinterleave=True)
|
||||
|
||||
normally
|
||||
|
||||
MyMIDI = MidiFile(tracks)
|
||||
|
||||
Arguments:
|
||||
|
||||
tracks: The number of tracks this object contains
|
||||
|
||||
removeDuplicates: If true (the default), the software will remove duplicate
|
||||
events which have been added. For example, two notes at the same channel,
|
||||
time, pitch, and duration would be considered duplicate.
|
||||
|
||||
deinterleave: If True (the default), overlapping notes (same pitch, same
|
||||
channel) will be modified so that they do not overlap. Otherwise the sequencing
|
||||
software will need to figure out how to interpret NoteOff events upon playback.
|
||||
'''
|
||||
|
||||
def __init__(self, numTracks, removeDuplicates=True, deinterleave=True):
|
||||
'''
|
||||
Initialize the class
|
||||
'''
|
||||
self.header = MIDIHeader(numTracks)
|
||||
|
||||
self.tracks = list()
|
||||
self.numTracks = numTracks
|
||||
self.closed = False
|
||||
|
||||
for i in range(0,numTracks):
|
||||
self.tracks.append(MIDITrack(removeDuplicates, deinterleave))
|
||||
|
||||
|
||||
# Public Functions. These (for the most part) wrap the MIDITrack functions, where most
|
||||
# Processing takes place.
|
||||
|
||||
def addNote(self,track, channel, pitch,time,duration,volume):
|
||||
"""
|
||||
Add notes to the MIDIFile object
|
||||
|
||||
Use:
|
||||
MyMIDI.addNotes(track,channel,pitch,time, duration, volume)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the note is added.
|
||||
channel: the MIDI channel to assign to the note. [Integer, 0-15]
|
||||
pitch: the MIDI pitch number [Integer, 0-127].
|
||||
time: the time (in beats) at which the note sounds [Float].
|
||||
duration: the duration of the note (in beats) [Float].
|
||||
volume: the volume (velocity) of the note. [Integer, 0-127].
|
||||
"""
|
||||
self.tracks[track].addNoteByNumber(channel, pitch, time, duration, volume)
|
||||
|
||||
def addTrackName(self,track, time,trackName):
|
||||
"""
|
||||
Add a track name to a MIDI track.
|
||||
|
||||
Use:
|
||||
MyMIDI.addTrackName(track,time,trackName)
|
||||
|
||||
Argument:
|
||||
track: The track to which the name is added. [Integer, 0-127].
|
||||
time: The time at which the track name is added, in beats [Float].
|
||||
trackName: The track name. [String].
|
||||
"""
|
||||
self.tracks[track].addTrackName(time,trackName)
|
||||
|
||||
def addTempo(self,track, time,tempo):
|
||||
"""
|
||||
Add a tempo event.
|
||||
|
||||
Use:
|
||||
MyMIDI.addTempo(track, time, tempo)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
tempo: The tempo, in Beats per Minute. [Integer]
|
||||
"""
|
||||
self.tracks[track].addTempo(time,tempo)
|
||||
|
||||
def addProgramChange(self,track, channel, time, program):
|
||||
"""
|
||||
Add a MIDI program change event.
|
||||
|
||||
Use:
|
||||
MyMIDI.addProgramChange(track,channel, time, program)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
channel: The channel the event is assigned to. [Integer, 0-15].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
program: the program number. [Integer, 0-127].
|
||||
"""
|
||||
self.tracks[track].addProgramChange(channel, time, program)
|
||||
|
||||
def addControllerEvent(self,track, channel,time,eventType, paramerter1):
|
||||
"""
|
||||
Add a MIDI controller event.
|
||||
|
||||
Use:
|
||||
MyMIDI.addControllerEvent(track, channel, time, eventType, parameter1)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
channel: The channel the event is assigned to. [Integer, 0-15].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
eventType: the controller event type.
|
||||
parameter1: The event's parameter. The meaning of which varies by event type.
|
||||
"""
|
||||
self.tracks[track].addControllerEvent(channel,time,eventType, paramerter1)
|
||||
|
||||
def changeNoteTuning(self, track, tunings, sysExChannel=0x7F, \
|
||||
realTime=False, tuningProgam=0):
|
||||
"""
|
||||
Change a note's tuning using SysEx change tuning program.
|
||||
|
||||
Use:
|
||||
MyMIDI.changeNoteTuning(track,[tunings],realTime=False, tuningProgram=0)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
tunings: A list of tuples in the form (pitchNumber, frequency).
|
||||
[[(Integer,Float]]
|
||||
realTime: Boolean which sets the real-time flag. Defaults to false.
|
||||
sysExChannel: do note use (see below).
|
||||
tuningProgram: Tuning program to assign. Defaults to zero. [Integer, 0-127]
|
||||
|
||||
In general the sysExChannel should not be changed (parameter will be depreciated).
|
||||
|
||||
Also note that many software packages and hardware packages do not implement
|
||||
this standard!
|
||||
"""
|
||||
self.tracks[track].changeNoteTuning(tunings, sysExChannel, realTime,\
|
||||
tuningProgam)
|
||||
|
||||
def writeFile(self,fileHandle):
|
||||
'''
|
||||
Write the MIDI File.
|
||||
|
||||
Use:
|
||||
MyMIDI.writeFile(filehandle)
|
||||
|
||||
Arguments:
|
||||
filehandle: a file handle that has been opened for binary writing.
|
||||
'''
|
||||
|
||||
self.header.writeFile(fileHandle)
|
||||
|
||||
#Close the tracks and have them create the MIDI event data structures.
|
||||
self.close()
|
||||
|
||||
#Write the MIDI Events to file.
|
||||
for i in range(0,self.numTracks):
|
||||
self.tracks[i].writeTrack(fileHandle)
|
||||
|
||||
def addSysEx(self,track, time, manID, payload):
|
||||
"""
|
||||
Add a SysEx event
|
||||
|
||||
Use:
|
||||
MyMIDI.addSysEx(track,time,ID,payload)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
ID: The SysEx ID number
|
||||
payload: the event payload.
|
||||
|
||||
Note: This is a low-level MIDI function, so care must be used in
|
||||
constructing the payload. It is recommended that higher-level helper
|
||||
functions be written to wrap this function and construct the payload if
|
||||
a developer finds him or herself using the function heavily.
|
||||
"""
|
||||
self.tracks[track].addSysEx(time,manID, payload)
|
||||
|
||||
def addUniversalSysEx(self,track, time,code, subcode, payload, \
|
||||
sysExChannel=0x7F, realTime=False):
|
||||
"""
|
||||
Add a Universal SysEx event.
|
||||
|
||||
Use:
|
||||
MyMIDI.addUniversalSysEx(track, time, code, subcode, payload,\
|
||||
sysExChannel=0x7f, realTime=False)
|
||||
|
||||
Arguments:
|
||||
track: The track to which the event is added. [Integer, 0-127].
|
||||
time: The time at which the event is added, in beats. [Float].
|
||||
code: The even code. [Integer]
|
||||
subcode The event sub-code [Integer]
|
||||
payload: The event payload. [Binary string]
|
||||
sysExChannel: The SysEx channel.
|
||||
realTime: Sets the real-time flag. Defaults to zero.
|
||||
|
||||
Note: This is a low-level MIDI function, so care must be used in
|
||||
constructing the payload. It is recommended that higher-level helper
|
||||
functions be written to wrap this function and construct the payload if
|
||||
a developer finds him or herself using the function heavily. As an example
|
||||
of such a helper function, see the changeNoteTuning function, both here and
|
||||
in MIDITrack.
|
||||
"""
|
||||
|
||||
self.tracks[track].addUniversalSysEx(time,code, subcode, payload, sysExChannel,\
|
||||
realTime)
|
||||
|
||||
def shiftTracks(self, offset=0):
|
||||
"""Shift tracks to be zero-origined, or origined at offset.
|
||||
|
||||
Note that the shifting of the time in the tracks uses the MIDIEventList -- in other
|
||||
words it is assumed to be called in the stage where the MIDIEventList has been
|
||||
created. This function, however, it meant to operate on the eventList itself.
|
||||
"""
|
||||
origin = 1000000 # A little silly, but we'll assume big enough
|
||||
|
||||
for track in self.tracks:
|
||||
if len(track.eventList) > 0:
|
||||
for event in track.eventList:
|
||||
if event.time < origin:
|
||||
origin = event.time
|
||||
|
||||
for track in self.tracks:
|
||||
tempEventList = []
|
||||
#runningTime = 0
|
||||
|
||||
for event in track.eventList:
|
||||
adjustedTime = event.time - origin
|
||||
#event.time = adjustedTime - runningTime + offset
|
||||
event.time = adjustedTime + offset
|
||||
#runningTime = adjustedTime
|
||||
tempEventList.append(event)
|
||||
|
||||
track.eventList = tempEventList
|
||||
|
||||
#End Public Functions ########################
|
||||
|
||||
def close(self):
|
||||
'''Close the MIDIFile for further writing.
|
||||
|
||||
To close the File for events, we must close the tracks, adjust the time to be
|
||||
zero-origined, and have the tracks write to their MIDI Stream data structure.
|
||||
'''
|
||||
|
||||
if self.closed == True:
|
||||
return
|
||||
|
||||
for i in range(0,self.numTracks):
|
||||
self.tracks[i].closeTrack()
|
||||
# We want things like program changes to come before notes when they are at the
|
||||
# same time, so we sort the MIDI events by their ordinality
|
||||
self.tracks[i].MIDIEventList.sort()
|
||||
|
||||
origin = self.findOrigin()
|
||||
|
||||
for i in range(0,self.numTracks):
|
||||
self.tracks[i].adjustTime(origin)
|
||||
self.tracks[i].writeMIDIStream()
|
||||
|
||||
self.closed = True
|
||||
|
||||
|
||||
def findOrigin(self):
|
||||
'''Find the earliest time in the file's tracks.append.
|
||||
'''
|
||||
origin = 1000000 # A little silly, but we'll assume big enough
|
||||
|
||||
# Note: This code assumes that the MIDIEventList has been sorted, so this should be insured
|
||||
# before it is called. It is probably a poor design to do this.
|
||||
# TODO: -- Consider making this less efficient but more robust by not assuming the list to be sorted.
|
||||
|
||||
for track in self.tracks:
|
||||
if len(track.MIDIEventList) > 0:
|
||||
if track.MIDIEventList[0].time < origin:
|
||||
origin = track.MIDIEventList[0].time
|
||||
|
||||
|
||||
return origin
|
||||
|
||||
def writeVarLength(i):
|
||||
'''Accept an input, and write a MIDI-compatible variable length stream
|
||||
|
||||
The MIDI format is a little strange, and makes use of so-called variable
|
||||
length quantities. These quantities are a stream of bytes. If the most
|
||||
significant bit is 1, then more bytes follow. If it is zero, then the
|
||||
byte in question is the last in the stream
|
||||
'''
|
||||
input = int(i)
|
||||
output = [0,0,0,0]
|
||||
reversed = [0,0,0,0]
|
||||
count = 0
|
||||
result = input & 0x7F
|
||||
output[count] = result
|
||||
count = count + 1
|
||||
input = input >> 7
|
||||
while input > 0:
|
||||
result = input & 0x7F
|
||||
result = result | 0x80
|
||||
output[count] = result
|
||||
count = count + 1
|
||||
input = input >> 7
|
||||
|
||||
reversed[0] = output[3]
|
||||
reversed[1] = output[2]
|
||||
reversed[2] = output[1]
|
||||
reversed[3] = output[0]
|
||||
return reversed[4-count:4]
|
||||
|
||||
def frequencyTransform(freq):
|
||||
'''Returns a three-byte transform of a frequencyTransform
|
||||
'''
|
||||
resolution = 16384
|
||||
freq = float(freq)
|
||||
dollars = 69 + 12 * math.log(freq/(float(440)), 2)
|
||||
firstByte = int(dollars)
|
||||
lowerFreq = 440 * pow(2.0, ((float(firstByte) - 69.0)/12.0))
|
||||
if freq != lowerFreq:
|
||||
centDif = 1200 * math.log( (freq/lowerFreq), 2)
|
||||
else:
|
||||
centDif = 0
|
||||
cents = round(centDif/100 * resolution) # round?
|
||||
secondByte = min([int(cents)>>7, 0x7F])
|
||||
thirdByte = cents - (secondByte << 7)
|
||||
thirdByte = min([thirdByte, 0x7f])
|
||||
if thirdByte == 0x7f and secondByte == 0x7F and firstByte == 0x7F:
|
||||
thirdByte = 0x7e
|
||||
|
||||
thirdByte = int(thirdByte)
|
||||
return [firstByte, secondByte, thirdByte]
|
||||
|
||||
def returnFrequency(freqBytes):
|
||||
'''The reverse of frequencyTransform. Given a byte stream, return a frequency.
|
||||
'''
|
||||
resolution = 16384.0
|
||||
baseFrequency = 440 * pow(2.0, (float(freqBytes[0]-69.0)/12.0))
|
||||
frac = (float((int(freqBytes[1]) << 7) + int(freqBytes[2])) * 100.0) / resolution
|
||||
frequency = baseFrequency * pow(2.0, frac/1200.0)
|
||||
return frequency
|
0
src/midiutil/__init__.py
Normal file
0
src/midiutil/__init__.py
Normal file
235
src/unittests/miditest.py
Normal file
235
src/unittests/miditest.py
Normal file
@ -0,0 +1,235 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# Name: miditest.py
|
||||
# Purpose: Unit testing harness for midiutil
|
||||
#
|
||||
# Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
|
||||
#
|
||||
# Created: 2008/04/17
|
||||
# Copyright: (c) 2009, Mark Conway Wirt
|
||||
# License: Please see License.txt for the terms under which this
|
||||
# software is distributed.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
# Next few lines are necessary owing to limitations of the IDE and the
|
||||
# directory structure of the project.
|
||||
|
||||
import sys, struct
|
||||
sys.path.append('..')
|
||||
|
||||
import unittest
|
||||
from midiutil.MidiFile import MIDIFile, MIDIHeader, MIDITrack, writeVarLength, \
|
||||
frequencyTransform, returnFrequency
|
||||
import sys
|
||||
|
||||
class TestMIDIUtils(unittest.TestCase):
|
||||
|
||||
def testWriteVarLength(self):
|
||||
self.assertEquals(writeVarLength(0x70), [0x70])
|
||||
self.assertEquals(writeVarLength(0x80), [0x81, 0x00])
|
||||
self.assertEquals(writeVarLength(0x1FFFFF), [0xFF, 0xFF, 0x7F])
|
||||
self.assertEquals(writeVarLength(0x08000000), [0xC0, 0x80, 0x80, 0x00])
|
||||
|
||||
def testAddNote(self):
|
||||
MyMIDI = MIDIFile(1)
|
||||
MyMIDI.addNote(0, 0, 100,0,1,100)
|
||||
self.assertEquals(MyMIDI.tracks[0].eventList[0].type, "note")
|
||||
self.assertEquals(MyMIDI.tracks[0].eventList[0].pitch, 100)
|
||||
self.assertEquals(MyMIDI.tracks[0].eventList[0].time, 0)
|
||||
self.assertEquals(MyMIDI.tracks[0].eventList[0].duration, 1)
|
||||
self.assertEquals(MyMIDI.tracks[0].eventList[0].volume, 100)
|
||||
|
||||
def testDeinterleaveNotes(self):
|
||||
MyMIDI = MIDIFile(1)
|
||||
MyMIDI.addNote(0, 0, 100, 0, 2, 100)
|
||||
MyMIDI.addNote(0, 0, 100, 1, 2, 100)
|
||||
MyMIDI.close()
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0)
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 128)
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[2].type, 'NoteOn')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[2].time, 0)
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[3].type, 'NoteOff')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[3].time, 256)
|
||||
|
||||
def testTimeShift(self):
|
||||
|
||||
# With one track
|
||||
MyMIDI = MIDIFile(1)
|
||||
MyMIDI.addNote(0, 0, 100, 5, 1, 100)
|
||||
MyMIDI.close()
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0)
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 128)
|
||||
|
||||
# With two tracks
|
||||
MyMIDI = MIDIFile(2)
|
||||
MyMIDI.addNote(0, 0, 100, 5, 1, 100)
|
||||
MyMIDI.addNote(1, 0, 100, 6, 1, 100)
|
||||
MyMIDI.close()
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0)
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 128)
|
||||
self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
|
||||
self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 128)
|
||||
self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
|
||||
self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 128)
|
||||
|
||||
# Negative Time
|
||||
MyMIDI = MIDIFile(1)
|
||||
MyMIDI.addNote(0, 0, 100, -5, 1, 100)
|
||||
MyMIDI.close()
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0)
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 128)
|
||||
|
||||
# Negative time, two tracks
|
||||
|
||||
MyMIDI = MIDIFile(2)
|
||||
MyMIDI.addNote(0, 0, 100, -1, 1, 100)
|
||||
MyMIDI.addNote(1, 0, 100, 0, 1, 100)
|
||||
MyMIDI.close()
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0)
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff')
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 128)
|
||||
self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
|
||||
self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 128)
|
||||
self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
|
||||
self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 128)
|
||||
|
||||
def testFrequency(self):
|
||||
freq = frequencyTransform(8.1758)
|
||||
self.assertEquals(freq[0], 0x00)
|
||||
self.assertEquals(freq[1], 0x00)
|
||||
self.assertEquals(freq[2], 0x00)
|
||||
freq = frequencyTransform(8.66196) # 8.6620 in MIDI documentation
|
||||
self.assertEquals(freq[0], 0x01)
|
||||
self.assertEquals(freq[1], 0x00)
|
||||
self.assertEquals(freq[2], 0x00)
|
||||
freq = frequencyTransform(440.00)
|
||||
self.assertEquals(freq[0], 0x45)
|
||||
self.assertEquals(freq[1], 0x00)
|
||||
self.assertEquals(freq[2], 0x00)
|
||||
freq = frequencyTransform(440.0016)
|
||||
self.assertEquals(freq[0], 0x45)
|
||||
self.assertEquals(freq[1], 0x00)
|
||||
self.assertEquals(freq[2], 0x01)
|
||||
freq = frequencyTransform(439.9984)
|
||||
self.assertEquals(freq[0], 0x44)
|
||||
self.assertEquals(freq[1], 0x7f)
|
||||
self.assertEquals(freq[2], 0x7f)
|
||||
freq = frequencyTransform(8372.0190)
|
||||
self.assertEquals(freq[0], 0x78)
|
||||
self.assertEquals(freq[1], 0x00)
|
||||
self.assertEquals(freq[2], 0x00)
|
||||
freq = frequencyTransform(8372.062) #8372.0630 in MIDI documentation
|
||||
self.assertEquals(freq[0], 0x78)
|
||||
self.assertEquals(freq[1], 0x00)
|
||||
self.assertEquals(freq[2], 0x01)
|
||||
freq = frequencyTransform(13289.7300)
|
||||
self.assertEquals(freq[0], 0x7F)
|
||||
self.assertEquals(freq[1], 0x7F)
|
||||
self.assertEquals(freq[2], 0x7E)
|
||||
freq = frequencyTransform(12543.8760)
|
||||
self.assertEquals(freq[0], 0x7F)
|
||||
self.assertEquals(freq[1], 0x00)
|
||||
self.assertEquals(freq[2], 0x00)
|
||||
freq = frequencyTransform(8.2104) # Just plain wrong in documentation, as far as I can tell.
|
||||
#self.assertEquals(freq[0], 0x0)
|
||||
#self.assertEquals(freq[1], 0x0)
|
||||
#self.assertEquals(freq[2], 0x1)
|
||||
|
||||
# Test the inverse
|
||||
testFreq = 15.0
|
||||
accuracy = 0.00001
|
||||
x = returnFrequency(frequencyTransform(testFreq))
|
||||
delta = abs(testFreq - x)
|
||||
self.assertEquals(delta < (accuracy*testFreq), True)
|
||||
testFreq = 200.0
|
||||
x = returnFrequency(frequencyTransform(testFreq))
|
||||
delta = abs(testFreq - x)
|
||||
self.assertEquals(delta < (accuracy*testFreq), True)
|
||||
testFreq = 400.0
|
||||
x = returnFrequency(frequencyTransform(testFreq))
|
||||
delta = abs(testFreq - x)
|
||||
self.assertEquals(delta < (accuracy*testFreq), True)
|
||||
testFreq = 440.0
|
||||
x = returnFrequency(frequencyTransform(testFreq))
|
||||
delta = abs(testFreq - x)
|
||||
self.assertEquals(delta < (accuracy*testFreq), True)
|
||||
testFreq = 1200.0
|
||||
x = returnFrequency(frequencyTransform(testFreq))
|
||||
delta = abs(testFreq - x)
|
||||
self.assertEquals(delta < (accuracy*testFreq), True)
|
||||
testFreq = 5000.0
|
||||
x = returnFrequency(frequencyTransform(testFreq))
|
||||
delta = abs(testFreq - x)
|
||||
self.assertEquals(delta < (accuracy*testFreq), True)
|
||||
testFreq = 12000.0
|
||||
x = returnFrequency(frequencyTransform(testFreq))
|
||||
delta = abs(testFreq - x)
|
||||
self.assertEquals(delta < (accuracy*testFreq), True)
|
||||
|
||||
|
||||
def testSysEx(self):
|
||||
MyMIDI = MIDIFile(1)
|
||||
MyMIDI.addSysEx(0,0, 0, struct.pack('>B', 0x01))
|
||||
MyMIDI.close()
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'SysEx')
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[0])[0], 0x00)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[1])[0], 0xf0)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[2])[0], 3)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[3])[0], 0x00)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[4])[0], 0x01)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[5])[0], 0xf7)
|
||||
|
||||
def testUniversalSysEx(self):
|
||||
MyMIDI = MIDIFile(1)
|
||||
MyMIDI.addUniversalSysEx(0,0, 1, 2, struct.pack('>B', 0x01))
|
||||
MyMIDI.close()
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'UniversalSysEx')
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[0])[0], 0x00)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[1])[0], 0xf0)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[2])[0], 6)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[3])[0], 0x7E)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[4])[0], 0x7F)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[5])[0], 0x01)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[6])[0], 0x02)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[7])[0], 0x01)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[8])[0], 0xf7)
|
||||
|
||||
def testTuning(self):
|
||||
MyMIDI = MIDIFile(1)
|
||||
MyMIDI.changeNoteTuning(0, [(1, 440), (2, 880)])
|
||||
MyMIDI.close()
|
||||
self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'UniversalSysEx')
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[0])[0], 0x00)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[1])[0], 0xf0)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[2])[0], 15)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[3])[0], 0x7E)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[4])[0], 0x7F)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[5])[0], 0x08)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[6])[0], 0x02)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[7])[0], 0x00)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[8])[0], 0x2)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[9])[0], 0x1)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[10])[0], 69)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[11])[0], 0)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[12])[0], 0)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[13])[0], 0x2)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[14])[0], 81)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[15])[0], 0)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[16])[0], 0)
|
||||
self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[17])[0], 0xf7)
|
||||
|
||||
MIDISuite = unittest.TestLoader().loadTestsFromTestCase(TestMIDIUtils)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.TextTestRunner(verbosity=1).run(MIDISuite)
|
||||
|
Loading…
Reference in New Issue
Block a user