From b06fd67fc28d7500b50364d90c30d924db9f9376 Mon Sep 17 00:00:00 2001 From: Ross Duggan Date: Sat, 14 Apr 2012 02:43:53 +0100 Subject: [PATCH] Drop of 0.87 release. --- .gitignore | 2 + CHANGELOG | 30 + License.txt | 26 + MANIFEST | 12 + PKG-INFO | 12 + README.txt | 118 +++ VERSION | 2 + build/documentation/ClassReference.txt | 229 ++++++ build/documentation/Extending.txt | 169 ++++ build/lib/midiutil/MidiFile.py | 993 +++++++++++++++++++++++ build/lib/midiutil/__init__.py | 0 build/scripts-2.7/single-note-example.py | 32 + documentation/ClassReference.txt | 229 ++++++ documentation/Extending.txt | 169 ++++ examples/single-note-example.py | 32 + setup.py | 18 + src/midiutil/MidiFile.py | 993 +++++++++++++++++++++++ src/midiutil/__init__.py | 0 src/unittests/miditest.py | 235 ++++++ 19 files changed, 3301 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG create mode 100644 License.txt create mode 100644 MANIFEST create mode 100644 PKG-INFO create mode 100644 README.txt create mode 100644 VERSION create mode 100644 build/documentation/ClassReference.txt create mode 100644 build/documentation/Extending.txt create mode 100644 build/lib/midiutil/MidiFile.py create mode 100644 build/lib/midiutil/__init__.py create mode 100755 build/scripts-2.7/single-note-example.py create mode 100644 documentation/ClassReference.txt create mode 100644 documentation/Extending.txt create mode 100644 examples/single-note-example.py create mode 100644 setup.py create mode 100644 src/midiutil/MidiFile.py create mode 100644 src/midiutil/__init__.py create mode 100644 src/unittests/miditest.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..baade12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.mid +*.swp diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..c2611c3 --- /dev/null +++ b/CHANGELOG @@ -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. + diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..0db0915 --- /dev/null +++ b/License.txt @@ -0,0 +1,26 @@ +-------------------------------------------------------------------------- +MIDUTIL, Copyright (c) 2009, Mark Conway Wirt + + +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. +-------------------------------------------------------------------------- + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..b62a96b --- /dev/null +++ b/MANIFEST @@ -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 diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..fb9beea --- /dev/null +++ b/PKG-INFO @@ -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 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..3518483 --- /dev/null +++ b/README.txt @@ -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! + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..c58d90b --- /dev/null +++ b/VERSION @@ -0,0 +1,2 @@ +This is version 0.87. + diff --git a/build/documentation/ClassReference.txt b/build/documentation/ClassReference.txt new file mode 100644 index 0000000..adf5db7 --- /dev/null +++ b/build/documentation/ClassReference.txt @@ -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. + diff --git a/build/documentation/Extending.txt b/build/documentation/Extending.txt new file mode 100644 index 0000000..9bee537 --- /dev/null +++ b/build/documentation/Extending.txt @@ -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! diff --git a/build/lib/midiutil/MidiFile.py b/build/lib/midiutil/MidiFile.py new file mode 100644 index 0000000..3a65846 --- /dev/null +++ b/build/lib/midiutil/MidiFile.py @@ -0,0 +1,993 @@ +#----------------------------------------------------------------------------- +# Name: MidiFile.py +# Purpose: MIDI file manipulation utilities +# +# Author: Mark Conway Wirt +# +# 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 diff --git a/build/lib/midiutil/__init__.py b/build/lib/midiutil/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/scripts-2.7/single-note-example.py b/build/scripts-2.7/single-note-example.py new file mode 100755 index 0000000..07b4445 --- /dev/null +++ b/build/scripts-2.7/single-note-example.py @@ -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() + diff --git a/documentation/ClassReference.txt b/documentation/ClassReference.txt new file mode 100644 index 0000000..adf5db7 --- /dev/null +++ b/documentation/ClassReference.txt @@ -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. + diff --git a/documentation/Extending.txt b/documentation/Extending.txt new file mode 100644 index 0000000..9bee537 --- /dev/null +++ b/documentation/Extending.txt @@ -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! diff --git a/examples/single-note-example.py b/examples/single-note-example.py new file mode 100644 index 0000000..07b4445 --- /dev/null +++ b/examples/single-note-example.py @@ -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() + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fa505f8 --- /dev/null +++ b/setup.py @@ -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.''' + ) diff --git a/src/midiutil/MidiFile.py b/src/midiutil/MidiFile.py new file mode 100644 index 0000000..3a65846 --- /dev/null +++ b/src/midiutil/MidiFile.py @@ -0,0 +1,993 @@ +#----------------------------------------------------------------------------- +# Name: MidiFile.py +# Purpose: MIDI file manipulation utilities +# +# Author: Mark Conway Wirt +# +# 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 diff --git a/src/midiutil/__init__.py b/src/midiutil/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/unittests/miditest.py b/src/unittests/miditest.py new file mode 100644 index 0000000..2ccc81a --- /dev/null +++ b/src/unittests/miditest.py @@ -0,0 +1,235 @@ +#----------------------------------------------------------------------------- +# Name: miditest.py +# Purpose: Unit testing harness for midiutil +# +# Author: Mark Conway Wirt +# +# 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) +