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