[SCM] python-midiutil/master: New upstream version 1.1.3
umlaeute at users.alioth.debian.org
umlaeute at users.alioth.debian.org
Wed Jun 21 18:29:42 UTC 2017
The following commit has been merged in the master branch:
commit 61f207d6a4e7e200091fbd5240f9f2d76dfe7c73
Author: IOhannes m zmölnig <zmoelnig at umlautQ.umlaeute.mur.at>
Date: Wed Jun 21 16:07:49 2017 +0200
New upstream version 1.1.3
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c8bdcc9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+*.pyc
+*.swp
+_build
+dist
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..c659cc2
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,9 @@
+language: python
+python:
+ - "2.7"
+ - "3.5"
+ - "3.6"
+ - "nightly" # currently points to 3.7-dev
+install:
+ - python setup.py -q install
+script: python src/unittests/test_midi.py
diff --git a/CHANGELOG b/CHANGELOG
index 679e052..3111dfd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,82 @@
+Date: 6 March 2017
+Version: 1.1.3
+
+ A code clean-up release. Not much in the way of new features.
+
+ * Documentation changes, typo corrections, etc.
+ * Simplify the importation of the library. It's now possible to just:
+
+ from midiutil import MIDIFile
+
+ which seems somewhat easier.
+ * Some formatting and cleanup work (PEP-ish stuff).
+ * Added Python 3.6 testing/support.
+ * Removed support for Python 2.6. All good things eventually come to an
+ end
+
+Date: 28 September 2016
+Version: 1.1.1
+
+ * Just a couple of typo's corrected (what's a "but fix" anyway?),
+ and the display of README.rst on Pypi.
+
+Version: 1.1.0
+ * Allow for the specification of either MIDI format 1 or 2 files.
+ Note that in previous versions of the code the file being
+ written was format 2, but it was identified as format 1.
+ This error has been corrected. The default format for the file
+ is 1, which is the most widely supported format.
+ * Increased test coverage.
+ * Fixed bug in MIDIFile for `adjust_origin` = `False`
+ * Added ability to order RPC and NRPC sub-events in time, as
+ a work-around for sequencers that aren't good about preserving
+ event ordering for events at the time time.
+ * Updates to documentation.
+ * Added Travis CI to the build process. Verifying operation
+ on python 2.6 - 3 development version.
+ * Functions added:
+ * `addTimeSignature()`
+ * `addCopyright()`
+ * `addText()`
+ * `addKeySignature()`
+
+Date: 23 September 2016
+Version: 1.0.1
+ * Minor updates to build system so that code can be hosted at
+ Pypi (https://pypi.python.org/pypi/MIDIUtil/).
+
+Date: 23 September 2016
+Version: 1.0.0
+ * Code ported to GitHub
+ * Extensive updates to documentation, ported to Sphinx.
+ * Added ability to *not* adjust the MIDIFile's time origin.
+ Default behaviour is maintained, but will change in a future
+ version (by default the origin is currently adjusted).
+ * Changed the controller event parameter names to make them
+ clearer.
+ * Added support for Registered and Non-Registered parameter
+ calls (``makeRPNCall`` and ``makeNRPNCall``).
+ * General refactoring and clean-up.
+ * Added function to select tuning program and bank.
+ Some synthesizers, such as fluidsynth, require that
+ uploaded tunings be explicitly assigned to a channel.
+ This can be used after ``setNoteTuning`` in such a
+ case.
+ * Completed port to Python 3 / Unification of code base.
+ Support for python < 2.6 has been dropped so that the
+ Python 2 and 3 codebases could be unified.
+ * Changes the way that sorting works, simplifying it and making
+ it more expressive. The primary sort is on time; secondary on
+ ordinality for the event (which is user-definable, but defaults
+ to an ordinality for the class); and the
+ third is the order in which the events were added. Thus
+ is becomes easier to, say, make an RPN call, which entails
+ and ordered series of control change events all occurring at the
+ same time and of the same type.
+ * Added 'annotation' as a parameter to note addition function.
+ This can be used to attach an arbitrary python object to the
+ note event. This is useful for extension development.
+
Date: 1 December 2013
Version: 0.89
* Updated MIDIFile to support non-integral note values better.
@@ -5,6 +84,7 @@ Version: 0.89
* Updated Python3 support. It is still somewhat experimental.
* Misc. Bug Fixes.
+
Date: 20 October 2009
Version: 0.87
diff --git a/License.txt b/License.txt
index 0db0915..6714626 100644
--- a/License.txt
+++ b/License.txt
@@ -1,5 +1,5 @@
--------------------------------------------------------------------------
-MIDUTIL, Copyright (c) 2009, Mark Conway Wirt
+MIDUTIL, Copyright (c) 2009-2016, Mark Conway Wirt
<emergentmusics) at (gmail . com>
This software is distributed under an Open Source license, the
diff --git a/MANIFEST b/MANIFEST
deleted file mode 100644
index d062e4c..0000000
--- a/MANIFEST
+++ /dev/null
@@ -1,14 +0,0 @@
-README.txt
-setup.py
-License.txt
-CHANGELOG
-VERSION
-MANIFEST
-src/midiutil/MidiFile.py
-src/midiutil/MidiFile3.py
-src/midiutil/__init__.py
-examples/single-note-example.py
-documentation/Extending.txt
-documentation/ClassReference.txt
-src/unittests/miditest.py
-src/unittests/miditest.py3
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..4570748
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,9 @@
+include *.py *.rst *.txt VERSION CHANGELOG
+recursive-include documentation *
+recursive-include examples *.py
+recursive-exclude build *
+recursive-exclude dist *
+recursive-exclude . *.pyc __pycache__
+recursive-include src *
+
+
diff --git a/PKG-INFO b/PKG-INFO
deleted file mode 100644
index 3cb019d..0000000
--- a/PKG-INFO
+++ /dev/null
@@ -1,12 +0,0 @@
-Metadata-Version: 1.0
-Name: MIDIUtil
-Version: 0.89
-Summary: MIDIUtil, a MIDI Interface for Python
-Home-page: www.emergentmusics.org
-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.rst b/README.rst
new file mode 100644
index 0000000..07d707d
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,165 @@
+MIDIUtil
+========
+
+|build|
+
+This is just a brief adumbration. Full documentation for the development
+version can be found at `Read the Docs
+<http://midiutil.readthedocs.io/en/latest/>`_.
+
+|docs|
+
+The documentation for the latest stable release is `here
+<http://midiutil.readthedocs.io/en/stable/>`_.
+
+Introduction
+------------
+
+MIDIUtil is a pure Python library that allows one to write multi-track
+Musical Instrument Digital Interface (MIDI) files from within Python
+programs (both format 1 and format 2 files are now supported).
+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 made use of
+some features that were introduced in 2.5. More recently Python 2 and 3
+support has been unified, so the code should work in both environments.
+However, support for versions of Python previous to 2.7 has been dropped.
+Any mission-critical music generation systems should probably be updated
+to a version of Python supported and maintained by the Python foundation,
+lest society devolve into lawlessness.
+
+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
+------------
+
+The latest, stable version of MIDIUtil is hosted at the `Python Package
+Index <https://pypi.python.org/pypi/MIDIUtil/>`__ and can be installed
+via the normal channels:
+
+.. code:: bash
+
+ pip install MIDIUtil
+
+Source code is available on `Github <https://github.com/MarkCWirt/MIDIUtil>`__ ,
+and be cloned with one of the following URLS:
+
+.. code:: bash
+
+ git clone git at github.com:MarkCWirt/MIDIUtil.git
+ # or
+ git clone https://github.com/MarkCWirt/MIDIUtil.git
+
+depending on if you want to use SSH or HTTPS. (The source code
+for stable releases can also be downloaded from the
+`Releases <https://github.com/MarkCWirt/MIDIUtil/releases>`__
+page.)
+
+To use the library one can either install it on one's system:
+
+.. code:: bash
+
+ python setup.py install
+
+or point your ``$PYTHONPATH`` environment variable to the directory
+containing ``midiutil`` (i.e., ``src``).
+
+MIDIUtil is pure Python and should work on any platform to which
+Python has been ported.
+
+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:
+
+* The package must be imported into your namespace
+* A MIDIFile object is created
+* Events (notes, tempo-changes, etc.) are added to the object
+* 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 tempo to the track, and write a C-Major scale. Then we
+write it to disk.
+
+.. code:: python
+
+ #!/usr/bin/env python
+
+ from midiutil import MIDIFile
+
+ degrees = [60, 62, 64, 65, 67, 69, 71, 72] # MIDI note number
+ track = 0
+ channel = 0
+ time = 0 # In beats
+ duration = 1 # In beats
+ tempo = 60 # In BPM
+ volume = 100 # 0-127, as per the MIDI standard
+
+ MyMIDI = MIDIFile(1) # One track, defaults to format 1 (tempo track is created
+ # automatically)
+ MyMIDI.addTempo(track, time, tempo)
+
+ for i, pitch in enumerate(degrees):
+ MyMIDI.addNote(track, channel, pitch, time + i, duration, volume)
+
+ with open("major-scale.mid", "wb") as output_file:
+ MyMIDI.writeFile(output_file)
+
+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!
+
+Thank You
+---------
+
+I'd like to mention the following people who have given feedback, bug
+fixes, and suggestions on the library:
+
+* Bram de Jong
+* Mike Reeves-McMillan
+* Egg Syntax
+* Nils Gey
+* Francis G.
+* cclauss (Code formating cleanup and PEP-8 stuff, which I'm not good at following).
+
+I've actually been off email for a few years, so I'm sure there are lots
+of suggestions waiting. Stay tuned for updates and bug fixes!
+
+.. |docs| image:: https://readthedocs.org/projects/midiutil/badge/?version=latest
+ :target: http://midiutil.readthedocs.io/en/latest/?badge=latest
+ :alt: Documentation Status
+
+.. |build| image:: https://travis-ci.org/MarkCWirt/MIDIUtil.svg?branch=master
+ :target: https://travis-ci.org/MarkCWirt/MIDIUtil
diff --git a/README.txt b/README.txt
deleted file mode 100644
index 2b9972a..0000000
--- a/README.txt
+++ /dev/null
@@ -1,138 +0,0 @@
-========
-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.
-
-Included in this version is an intitial port to Python 3 (but which should
-work in 2.6.X also). The file is called MidiFile3.py. To use it, use
-the following import line in your code:
-
- from midiutil.MidiFile3 import MIDIFile
-
-(This assumes that the code has been installed into your system path or that
-the midiutil directory is copied into your script's working directory.)
-
-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!
-
----------
-Thank You
----------
-
-I'd like to mention the following people who have given feedback, but
-fixes, and suggestions on the library:
-
- Bram de Jong
- Mike Reeves-McMillan
- Egg Syntax
- Nils Gey
- Francis G.
diff --git a/VERSION b/VERSION
index ff59572..04f10b7 100644
--- a/VERSION
+++ b/VERSION
@@ -1,2 +1,2 @@
-This is version 0.89.
+This is version HEAD
diff --git a/documentation/ClassReference.txt b/documentation/ClassReference.txt
deleted file mode 100644
index adf5db7..0000000
--- a/documentation/ClassReference.txt
+++ /dev/null
@@ -1,229 +0,0 @@
-========================
-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
deleted file mode 100644
index 9bee537..0000000
--- a/documentation/Extending.txt
+++ /dev/null
@@ -1,169 +0,0 @@
-=====================
-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/documentation/Makefile b/documentation/Makefile
new file mode 100644
index 0000000..298d5c6
--- /dev/null
+++ b/documentation/Makefile
@@ -0,0 +1,225 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " applehelp to make an Apple Help Book"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " epub3 to make an epub3"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo " coverage to run coverage check of the documentation (if enabled)"
+ @echo " dummy to check syntax errors of document sources"
+
+.PHONY: clean
+clean:
+ rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MIDIUtil.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MIDIUtil.qhc"
+
+.PHONY: applehelp
+applehelp:
+ $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+ @echo
+ @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+ @echo "N.B. You won't be able to view it unless you put it in" \
+ "~/Library/Documentation/Help or install it in your application" \
+ "bundle."
+
+.PHONY: devhelp
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/MIDIUtil"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/MIDIUtil"
+ @echo "# devhelp"
+
+.PHONY: epub
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: epub3
+epub3:
+ $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
+ @echo
+ @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
+
+.PHONY: latex
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: doctest
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: coverage
+coverage:
+ $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+ @echo "Testing of coverage in the sources finished, look at the " \
+ "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: xml
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+
+.PHONY: dummy
+dummy:
+ $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
+ @echo
+ @echo "Build finished. Dummy builder generates no files."
diff --git a/documentation/class.rst b/documentation/class.rst
new file mode 100644
index 0000000..417a34e
--- /dev/null
+++ b/documentation/class.rst
@@ -0,0 +1,10 @@
+.. _ClassRef:
+
+Class Reference
+===============
+
+.. currentmodule:: midiutil.MidiFile
+
+.. autoclass:: MIDIFile
+ :members: addNote, addTrackName, addTempo, addProgramChange, addControllerEvent, makeRPNCall, makeNRPNCall, changeTuningBank, changeTuningProgram,
+ changeNoteTuning, addSysEx, addUniversalSysEx, writeFile, __init__ , addTimeSignature, addCopyright, addText, addKeySignature
diff --git a/documentation/common.rst b/documentation/common.rst
new file mode 100644
index 0000000..b8356e4
--- /dev/null
+++ b/documentation/common.rst
@@ -0,0 +1,121 @@
+Common Events and Function
+==========================
+
+.. currentmodule:: midiutil.MidiFile
+
+This page lists some of the more common things that a user is likely to
+do with the MIDI file. It is not exhaustive; see the class reference for a more
+complete list of public functions.
+
+Adding Notes
+------------
+
+As the MIDI standard is all about music, creating notes will probably be
+the lion's share of what you're doing. This is done with the ``addNote()``
+function.
+
+.. automethod:: MIDIFile.addNote
+ :noindex:
+
+As an example, the following code-fragment adds two notes to an (already
+created) MIDIFile object:
+
+.. code:: python
+
+ track = 0 # Track numbers are zero-origined
+ channel = 0 # MIDI channel number
+ pitch = 60 # MIDI note number
+ time = 0 # In beats
+ duration = 1 # In beats
+ volume = 100 # 0-127, 127 being full volume
+
+ MyMIDI.addNote(track,channel,pitch,time,duration,volume)
+ time = 1
+ pitch = 61
+ MyMIDI.addNote(track,channel,pitch,time,duration,volume)
+
+Add a Tempo
+-----------
+
+Every track can have tempos specified (the unit of which is beats per minute).
+
+.. automethod:: MIDIFile.addTempo
+ :noindex:
+
+Example:
+
+.. code:: python
+
+ track = 0
+ time = 0 # beats, beginning of track
+ tempo = 120 # BPM
+ MyMIDI.addTempo(track, time, tempo)
+
+Assign a Name to a Track
+------------------------
+
+.. automethod:: MIDIFile.addTrackName
+ :noindex:
+
+In general, the time should probably be t=0
+
+Example:
+
+.. code:: python
+
+ track = 0
+ time = 0
+ track_name = "Bassline 1"
+ MyMIDI.addTrackName(track, time, track_name)
+
+Adding a Program Change Event
+-----------------------------
+
+The program change event tells the the instrument what voice a
+certain track should sound. As an example, if the instrument you're
+using supports `General MIDI <https://www.midi.org/specifications/item/gm-level-1-sound-set>`_,
+you can use the GM numbers to specify the instrument.
+
+**Important Note:** Within this library program numbers are
+zero-origined (as they are on a byte-level within the MIDI
+standard), but most of the documentation you will see is
+musician-centric, so they are usually given as one-origined. So, for example,
+if you want to sound a Cello, you would use a program number of 42, not the
+43 which is given in the link above.
+
+.. automethod:: MIDIFile.addProgramChange
+ :noindex:
+
+Example:
+
+.. code:: python
+
+ track = 0
+ channel = 0
+ time = 8 # Eight beats into the composition
+ program = 42 # A Cello
+
+ MyMIDI.addProgramChange(track, channel, time, program)
+
+Writing the File to Disk
+------------------------
+
+Ultimately, you'll need to write your data to disk to use it.
+
+.. automethod:: MIDIFile.writeFile
+ :noindex:
+
+Example:
+
+.. code:: python
+
+ with open("mymidifile.midi", 'wb') as output_file:
+ MyMIDI.writeFile(output_file)
+
+Additional Public Function
+--------------------------
+
+The above list is not exhaustive. For example, the library includes methods
+to create arbitrary channel control events, SysEx and Universal SysEx events,
+Registered Parameter calls and Non-Registered Parameter calls, etc. Please see the
+:ref:`ClassRef` for a more complete list of public functions.
diff --git a/documentation/conf.py b/documentation/conf.py
new file mode 100644
index 0000000..79c64e9
--- /dev/null
+++ b/documentation/conf.py
@@ -0,0 +1,343 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# MIDIUtil documentation build configuration file, created by
+# sphinx-quickstart on Thu Sep 22 19:23:00 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.todo',
+ 'sphinx.ext.mathjax',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'MIDIUtil'
+copyright = '2016, Mark Conway Wirt'
+author = 'Mark Conway Wirt'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.1'
+# The full version, including alpha/beta/rc tags.
+release = '1.1.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#
+# today = ''
+#
+# Else, today_fmt is used as the format for a strftime call.
+#
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = True
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents.
+# "<project> v<release> documentation" by default.
+#
+# html_title = 'MIDIUtil v0.919'
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#
+# html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#
+# html_extra_path = []
+
+# If not None, a 'Last updated on:' timestamp is inserted at every page
+# bottom, using the given strftime format.
+# The empty string is equivalent to '%b %d, %Y'.
+#
+# html_last_updated_fmt = None
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+#
+# html_domain_indices = True
+
+# If false, no index is generated.
+#
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
+#
+# html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# 'ja' uses this config value.
+# 'zh' user can custom change `jieba` dictionary path.
+#
+# html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#
+# html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'MIDIUtildoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'MIDIUtil.tex', 'MIDIUtil Documentation',
+ 'Mark Conway Wirt', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+#
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#
+# latex_appendices = []
+
+# It false, will not define \strong, \code, itleref, \crossref ... but only
+# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
+# packages.
+#
+# latex_keep_old_macro_names = True
+
+# If false, no module index is generated.
+#
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'midiutil', 'MIDIUtil Documentation',
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'MIDIUtil', 'MIDIUtil Documentation',
+ author, 'MIDIUtil', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+#
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#
+# texinfo_no_detailmenu = False
diff --git a/documentation/creating.rst b/documentation/creating.rst
new file mode 100644
index 0000000..9da040b
--- /dev/null
+++ b/documentation/creating.rst
@@ -0,0 +1,128 @@
+Creating the MIDIFile Object
+============================
+
+The first step in using the library is creating a ``MIDIFile`` object.
+There are only a few parameters that need be specified, but they affect
+the functioning of the library, so it's good to understand what they do.
+
+The signature of of the ``MIDIFile`` ``__init__()`` function is
+as follows:
+
+.. code:: python
+
+ def __init__(self, numTracks=1, removeDuplicates=True,
+ deinterleave=True, adjust_origin=None, file_format=1):
+
+where the parameters do the following:
+
+numTracks
+---------
+
+``numTracks`` specifies the number of tracks the MIDI file should have.
+It should be set to at least 1 (after all, a MIDI file without tracks isn't
+very useful), but it may be set higher for a multi-track file.
+
+This parameter defaults to ``1``.
+
+removeDuplicates
+----------------
+
+If set to ``True`` (the default), duplicate notes will be removed from
+the file. This is done on a track-by-track basis.
+
+Notes are considered duplicates if they occur at the same time, and have
+equivalent pitch, and MIDI channel. If set to ``False`` no attempt is made
+to remove notes which appear to be duplicates.
+
+``removeDuplicates()`` also attempts to remove other kinds of duplicates. For
+example, if there are two tempo events at the same time and same tempo, they
+are considered duplicates.
+
+Of course, it's best not to insert duplicate events in the first place,
+but this could be unavoidable in some instances -- for example, if the software
+is used in the creation of `Generative Music <https://en.wikipedia.org/wiki/Generative_music>`_
+using an algorithm that can create duplication of events.
+
+deinterleave
+------------
+
+If ``deinterleave`` is set to ``True`` (the default), an attempt will be made
+to remove interleaved notes.
+
+To understand what an *interleaved* note is, it is useful to have some understanding
+of the MIDI standard.
+
+To make this library more human-centric, one of the fundamental concepts used is
+that of the **note**. But the MIDI standard doesn't have notes; instead, it has
+**note on** and **note off** events. These are correlated by channel and pitch.
+
+So if, for example, you create two notes of duration 1 and separated by 1/2 of
+a beat, ie:
+
+.. code:: python
+
+ time = 0
+ duration = 1
+ MyMIDI.addNote(track,channel,pitch,time,duration,volume)
+ time = 0.5
+ MyMIDI.addNote(track,channel,pitch,time,duration,volume)
+
+you end up with a note on event at 0, another note on event a 0.5, and
+two note off events, one at 1.0 and one at 1.5. So when the first note off
+event is processed it raises the question: which note on event does it correspond to?
+The channel and pitch are the same, so there is some ambiguity in the
+way that a hardware or software instrument will respond.
+
+if ``deinterleave`` is ``True`` the library tries to disambiguate the situation
+by shifting the first note's off event to be immediately before the second
+note's on event. Thus in the example above the first note on would be at 0,
+the first note off would be at 0.5, the second note on would also be at
+0.5 (but would be processed after the note off at that time), and the last
+note off would be at 1.5.
+
+If this parameter is set to ``False`` no events will be shifted.
+
+adjust_origin
+-------------
+
+If ``adjust_origin`` is ``True`` the library will find the earliest
+event in all the tracks and shift all events so that that time is t=0.
+If it is ``False`` no time-shifting will occur.
+
+If it is left at it's default value, ``None``, ``adjust_origin`` will be
+set to ``True`` and a ``FutureWarning`` will be displayed. This is because in
+the next release the default behavior will change and no adjustment will be
+performed by default.
+
+file_format
+-----------
+
+This specifies the format of the file to be written. Both format 1 (the default)
+and format 2 files are supported.
+
+In the format 1 file there is a separate "tempo" track to which tempo and
+time signature events are written. The calls to create these events --
+``addTemo()`` and ``addTimeSignature()`` accept a track parameter, but in
+a format 1 file these are ignored. In format 2 files they are interpreted
+literally (and zero-origined, so that a two track file has indices ``0`` and
+``1``).
+
+Track indexing is always zero-based, but with the format 1 file the tempo track
+is not indexed. Thus if you create a one track file:
+
+.. code:: python
+
+ MyMIDI = MIDIFile(1, file_format=1)
+
+you would only have ``0`` as a valid index; the tempo track is managed independently
+for you. Thus:
+
+.. code:: python
+
+ track = 0
+ big_track = 1000
+ MyMIDI.addTempo(big_track, 0, 120)
+ MyMIDI.addNote(track, 0, 69, 0, 1, 100)
+
+works, even though "track 0" is really the second track in the file, and there is
+no track 1000.
diff --git a/documentation/extending.rst b/documentation/extending.rst
new file mode 100644
index 0000000..ca3bbdf
--- /dev/null
+++ b/documentation/extending.rst
@@ -0,0 +1,224 @@
+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, ordinal, and insertion order, and set the event type
+(a unique string used internally by the software).
+In the case of the tempo event:
+
+.. code:: python
+
+ class Tempo(GenericEvent):
+ '''A class that encapsulates a tempo meta-event
+ '''
+ def __init__(self,time,tempo, ordinal=3, insertion_order=0):
+ self.tempo = int(60000000 / tempo)
+ super(Tempo, self).__init__('tempo', time, ordinal, insertion_order)
+
+Any class that you define should include a type, time, ordinal (see below),
+and an insertion order.
+
+``self.ord`` and ``self.insertion_order`` are used to order the events
+in the MIDI stream. Events are first ordered in time. Events at the
+same time are then ordered by ``self.ord``, with lower numbers appearing
+in the stream first. The extant classes in the code all allow the user
+to specify an ordinal for the object, but they include default values
+that are meant to be reasonable.
+
+Lastly events are sorted on the ``self.insertion_order`` member. This
+makes it possible to, say, create a Registered Parameter Number call
+from a collection of Control Change events. Since all the CC events will
+have the same time and class (and therefore default ordinal), you can control
+the order of the events by the order in which you add them to the MIDIFile.
+
+Next, if you want the code to be able to de-duplicate events which may
+lay over top of one another, the parent class, ``GenericEvent``, has a
+member function called ``__eq__()``. If two events do not coincide in
+time or type they are not equal, but it they do the ``__eq__`` function
+must be modified to show equality. In the case of the ``Tempo`` class,
+two tempo events are considered equivalent if they are the same tempo.
+In other words, if there are two tempo events at the same time and
+the same tempo, one will be removed in the de-duplication process
+(which is the default behavious for ``MIDIFile``, but it can be
+turned off). From ``GenericEvent.__eq__()``:
+
+.. code:: python
+
+ if self.type == 'tempo':
+ if self.tempo != other.tempo:
+ return False
+
+If events are not equivalent, the code should return ``False``. If they are, the
+code can be allowed to fall through to its default return of ``True``.
+
+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:
+
+.. code:: python
+
+ def addTempo(self,time,tempo, insertion_order=0):
+ '''
+ Add a tempo change (or set) event.
+ '''
+ self.eventList.append(Tempo(time,tempo, insertion_order = insertion_order))
+
+(Most/many MIDI events require a channel specification, but the tempo event
+does not.)
+
+The public accessor function is via the MIDIFile object, and must include
+the track number to which the event is written. So in ``MIDIFile``:
+
+.. code:: python
+
+ def addTempo(self,track, time,tempo):
+ if self.header.numeric_format == 1:
+ track = 0
+ self.tracks[track].addTempo(time,tempo, insertion_order = self.event_counter)
+ self.event_counter = self.event_counter + 1
+
+Note that a track has been added (which is zero-origined and needs to be
+constrained by the number of tracks that the ``MIDIFile`` was created with),
+and ``insertion_order`` is taken from the class ``event_counter``
+data member. This should be followed in each function you add. Also note that
+the tempo event is handled differently in format 1 files and format 2 files.
+This function ensures that the tempo event is written to the first track
+(track 0) for a format 1 file, otherwise it writes it to the track specified.
+In most of the public functions a check it done on format, and the track is
+incremented by one for format 1 files so that the event is not written to the
+tempo track (but preserving the zero-origined convention for all tracks in
+both formats.)
+
+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 relevant section for the tempo event is:
+
+.. code:: python
+
+ elif thing.type == 'tempo':
+ event = MIDIEvent("Tempo", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order)
+ event.tempo = thing.tempo
+ self.MIDIEventList.append(event)
+
+THe ``MIDIEvent`` class is expected to have a ``type``, ``time``
+(which should be converted from beats to ticks as above), ordinal, and an
+insertion order, which are similar to the values in the ``GenericEvent`` class.
+You are free, of course, to add any other data items that need to be specified.
+In the case of ``Tempo`` this is the tempo to be written.
+
+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:
+
+.. code:: python
+
+ 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 sub-code 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.
+
+All MIDI events begin with a time, which is stored in a slightly bizarre
+variable-length format. This time should be converted to MIDI variable-length
+data with the ``writeVarLength()`` function before writing to the stream.
+In the MIDI standard's variable length data 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
+converssion 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
+sub-code. 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``.
+
+The reason that there are separate classes for ``GenericEvent`` and ``MIDIEvent``
+is that there need not be a one-to-one correspondance. For example, the
+code defines a ``Note`` object, but when this is processed in
+``processEventList()`` two ``MIDIEvent`` objects are created, one for
+the ``note on`` event, one for the ``note off`` event.
+
+.. code:: python
+
+ if thing.type == 'note':
+ event = MIDIEvent("NoteOn", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
+ event.pitch = thing.pitch
+ event.volume = thing.volume
+ event.channel = thing.channel
+ self.MIDIEventList.append(event)
+
+ event = MIDIEvent("NoteOff", (thing.time+ thing.duration) * TICKSPERBEAT,
+ thing.ord -0.1,
+ thing.insertion_order)
+ event.pitch = thing.pitch
+ event.volume = thing.volume
+ event.channel = thing.channel
+ self.MIDIEventList.append(event)
+
+Note that the ``NoteOff`` event is created with a slightly lower ordinality
+than the ``NoteOn`` event. This is so that at any given time the note off
+events will be processed before the note on events.
+
+Write Some Tests
+----------------
+
+Yea, it's a hassle, but you know it's the right thing to do!
diff --git a/documentation/index.rst b/documentation/index.rst
new file mode 100644
index 0000000..87cf0b5
--- /dev/null
+++ b/documentation/index.rst
@@ -0,0 +1,164 @@
+.. MIDIUtil documentation master file, created by
+ sphinx-quickstart on Thu Sep 22 19:23:00 2016.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+MIDIUtil
+========
+
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+
+ creating
+ common
+ tuning
+ extending
+ class
+
+Introduction
+------------
+
+MIDIUtil is a pure Python library that allows one to write multi-track
+Musical Instrument Digital Interface (MIDI) files from within Python
+programs (both format 1 and format 2 files are now supported).
+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 made use of
+some features that were introduced in 2.5. More recently Python 2 and 3
+support has been unified, so the code should work in both environments.
+However, support for versions of Python previous to 2.7 has been dropped.
+Any mission-critical music generation systems should probably be updated
+to a version of Python supported and maintained by the Python foundation,
+lest society devolve into lawlessness.
+
+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
+------------
+
+he latest, stable version of MIDIUtil is hosted at the `Python Package
+Index <https://pypi.python.org/pypi/MIDIUtil/>`__ and can be installed
+via the normal channels:
+
+.. code:: bash
+
+ pip install MIDIUtil
+
+Source code is available on `Github <https://github.com/MarkCWirt/MIDIUtil>`__ ,
+and be cloned with one of the following URLS:
+
+.. code:: bash
+
+ git clone git at github.com:MarkCWirt/MIDIUtil.git
+ # or
+ git clone https://github.com/MarkCWirt/MIDIUtil.git
+
+depending on if you want to use SSH or HTTPS. (The source code
+for stable releases can also be downloaded from the
+`Releases <https://github.com/MarkCWirt/MIDIUtil/releases>`__
+page.)
+
+To use the library one can either install it on one's system:
+
+.. code:: bash
+
+ python setup.py install
+
+or point your ``$PYTHONPATH`` environment variable to the directory
+containing ``midiutil`` (i.e., ``src``).
+
+MIDIUtil is pure Python and should work on any platform to which
+Python has been ported.
+
+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:
+
+* The package must be imported into your namespace
+* A MIDIFile object is created
+* Events (notes, tempo-changes, etc.) are added to the object
+* 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 tempo to the track, and write a C-Major scale. Then we
+write it to disk.
+
+.. code:: python
+
+ #!/usr/bin/env python
+
+ from midiutil import MIDIFile
+
+ degrees = [60, 62, 64, 65, 67, 69, 71, 72] # MIDI note number
+ track = 0
+ channel = 0
+ time = 0 # In beats
+ duration = 1 # In beats
+ tempo = 60 # In BPM
+ volume = 100 # 0-127, as per the MIDI standard
+
+ MyMIDI = MIDIFile(1) # One track, defaults to format 1 (tempo track
+ # automatically created)
+ MyMIDI.addTempo(track,time, tempo)
+
+ for pitch in degrees:
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ time = time + 1
+
+ with open("major-scale.mid", "wb") as output_file:
+ MyMIDI.writeFile(output_file)
+
+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!
+
+Thank You
+---------
+
+I'd like to mention the following people who have given feedback, bug
+fixes, and suggestions on the library:
+
+* Bram de Jong
+* Mike Reeves-McMillan
+* Egg Syntax
+* Nils Gey
+* Francis G.
+
+Indices and tables
+------------------
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/documentation/tuning.rst b/documentation/tuning.rst
new file mode 100644
index 0000000..7c4bdd2
--- /dev/null
+++ b/documentation/tuning.rst
@@ -0,0 +1,64 @@
+Tuning and Microtonalities
+==========================
+
+.. currentmodule:: midiutil.MidiFile
+
+One of my interests is microtonalities/non-standard tunings, so support
+for such explorations has been included in the library.
+
+There are several ways that tuning data can be specified in the MIDI standard,
+two of the most common being note pitch-bend and bulk tuning dumps. In this
+library I have implemented the real-time change note tuning of the MIDI
+tuning standard. I chose that as a first implementation because most of the
+soft-synthesizers I use support this standard.
+
+Note, however, that implementation of the MIDI tuning standard is somewhat spotty,
+so you may want to verify that your hardware and/or software supports it before
+you spend too much time.
+
+The main function to support a tuning change is ``changeNoteTuning``.
+
+.. automethod:: MIDIFile.changeNoteTuning
+
+Tuning Program
+--------------
+
+With some instruments, such as `timidity <http://timidity.sourceforge.net/>`_, this
+is all you need to do: timidity will apply the tuning change to the notes.
+Other instruments, such as `fluidsynth <http://www.fluidsynth.org/>`_, require
+that the tuning program be explicitly assigned. This is done with the
+``changeTuningProgram`` function:
+
+.. automethod:: MIDIFile.changeTuningProgram
+
+Tuning Bank
+-----------
+
+The tuning bank can also be specified (fluidsynth assumes that any tuning
+you transmit via ``changeNoteTuning`` is assigned to bank zero):
+
+.. automethod:: MIDIFile.changeTuningBank
+
+An Example
+----------
+
+So, as a complete example, the following code fragment would get rid of that
+pesky 440 Hz A and tell the instrument to use the tuning that you just
+transmitted:
+
+.. code:: python
+
+ track = 0
+ channel = 0
+ tuning = [(69, 500)]
+ program = 0
+ bank = 0
+ time = 0
+ MyMIDI.changeNoteTuning(track, tuning, tuningProgam=program)
+ MyMIDI.changeTuningBank(track, channel, time, bank) # may or may not be needed
+ MyMIDI.changeTuningProgram(track, channel, time, program) # ditto
+
+To Do
+-----
+
+* Implement the tuning change with bank select event type.
diff --git a/examples/c-major-scale.py b/examples/c-major-scale.py
new file mode 100755
index 0000000..4d0b733
--- /dev/null
+++ b/examples/c-major-scale.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+from midiutil import MIDIFile
+
+degrees = [60, 62, 64, 65, 67, 69, 71, 72] # MIDI note number
+track = 0
+channel = 0
+time = 0 # In beats
+duration = 1 # In beats
+tempo = 60 # In BPM
+volume = 100 # 0-127, as per the MIDI standard
+
+MyMIDI = MIDIFile(1) # One track
+MyMIDI.addTempo(track, time, tempo)
+
+for i, pitch in enumerate(degrees):
+ MyMIDI.addNote(track, channel, pitch, time + i, duration, volume)
+
+with open("major-scale.mid", "wb") as output_file:
+ MyMIDI.writeFile(output_file)
diff --git a/examples/single-note-example.py b/examples/single-note-example.py
old mode 100644
new mode 100755
index 07b4445..34b2b27
--- a/examples/single-note-example.py
+++ b/examples/single-note-example.py
@@ -3,8 +3,8 @@
# and write to disk.
############################################################################
-#Import the library
-from midiutil.MidiFile import MIDIFile
+# Import the library
+from midiutil import MIDIFile
# Create the MIDIFile Object
MyMIDI = MIDIFile(1)
@@ -13,8 +13,8 @@ MyMIDI = MIDIFile(1)
# addTempo is the time to write the event.
track = 0
time = 0
-MyMIDI.addTrackName(track,time,"Sample Track")
-MyMIDI.addTempo(track,time, 120)
+MyMIDI.addTrackName(track, time, "Sample Track")
+MyMIDI.addTempo(track, time, 120)
# Add a note. addNote expects the following information:
channel = 0
@@ -23,10 +23,8 @@ duration = 1
volume = 100
# Now add the note.
-MyMIDI.addNote(track,channel,pitch,time,duration,volume)
+MyMIDI.addNote(track, channel, pitch, time, duration, volume)
# And write it to disk.
-binfile = open("output.mid", 'wb')
-MyMIDI.writeFile(binfile)
-binfile.close()
-
+with open("output.mid", 'wb') as binfile:
+ MyMIDI.writeFile(binfile)
diff --git a/setup.py b/setup.py
index ad3d380..b28481d 100644
--- a/setup.py
+++ b/setup.py
@@ -1,18 +1,38 @@
-from distutils.core import setup
+from setuptools import setup, find_packages
+
+
+with open('README.rst') as file:
+ long_description = file.read()
setup(name='MIDIUtil',
- version='0.89',
- description='MIDIUtil, a MIDI Interface for Python',
+ version='1.1.3',
+ description='A pure python library for creating multi-track MIDI files',
author='Mark Conway Wirt',
- author_email='emergentmusics) at (gmail . com',
- license='Copyright (C) 2009, Mark Conway Wirt. See License.txt for details.',
- url='www.emergentmusics.org',
- packages=["midiutil"],
- package_dir = {'midiutil': 'src/midiutil'},
- package_data={'midiutil' : ['../../documentation/*']},
- scripts=['examples/single-note-example.py'],
+ author_email='markcwirt) at (gmail . com',
+ license='MIT',
+ url='https://github.com/MarkCWirt/MIDIUtil',
+ packages=find_packages(where="src"),
+ package_dir = {'': 'src'},
+ package_data={
+ '' : ['License.txt', 'README.rst', 'documentation/*'],
+ 'examples' : ['single-note-example.py', 'c-major-scale.py']},
+ include_package_data = True,
platforms='Platform Independent',
- long_description='''
-This package provides a simple interface to allow Python programs to
-write multi-track MIDI files.'''
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Intended Audience :: Developers',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Topic :: Multimedia :: Sound/Audio :: MIDI',
+ ],
+ keywords = 'Music MIDI',
+ long_description=long_description
)
diff --git a/src/midiutil/MidiFile.py b/src/midiutil/MidiFile.py
index d349e9b..c28e249 100644
--- a/src/midiutil/MidiFile.py
+++ b/src/midiutil/MidiFile.py
@@ -1,80 +1,85 @@
-#-----------------------------------------------------------------------------
+# -----------------------------------------------------------------------------
# Name: MidiFile.py
# Purpose: MIDI file manipulation utilities
#
# Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
#
# Created: 2008/04/17
-# Copyright: (c) 2009 Mark Conway Wirt
+# Copyright: (c) 2009-2016 Mark Conway Wirt
# License: Please see License.txt for the terms under which this
# software is distributed.
-#-----------------------------------------------------------------------------
+# -----------------------------------------------------------------------------
-import struct, sys, math
+from __future__ import division, print_function
+import math
+import struct
+import warnings
-# 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.
+__version__ = '1.1.3'
+
+# 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 = 960
-controllerEventTypes = {
- 'pan' : 0x0a
- }
-class MIDIEvent:
+controllerEventTypes = {'pan': 0x0a}
+
+# Define some constants
+
+MAJOR = 0
+MINOR = 1
+SHARPS = 1
+FLATS = -1
+
+__all__ = ['MIDIFile', 'MAJOR', 'MINOR', 'SHARPS', 'FLATS']
+
+
+class MIDIEvent(object):
'''
- The class to contain the MIDI Event (placed on MIDIEventList.
+ 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
+ def __init__(self, type="unknown", time=0, ordinal=0, insertion_order=0):
+ self.type = type
+ self.time = time
+ self.ord = ordinal
+ self.insertion_order = insertion_order
-class GenericEvent():
- '''The event class from which specific events are derived
- '''
- def __init__(self,time):
- self.time = time
- self.type = 'Unknown'
+class GenericEvent(object):
+ '''
+ The event class from which specific events are derived
+ '''
+ def __init__(self, event_type, time, ordinal, insertion_order):
+ self.type = event_type
+ self.time = time
+ self.ord = ordinal
+ self.insertion_order = insertion_order
+ # 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.
+
+ 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.
-
+
+ # 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
@@ -82,201 +87,284 @@ class GenericEvent():
if self.tempo != other.tempo:
return False
if self.type == 'programChange':
- if self.programNumber != other.programNumber or self.channel != other.channel:
+ 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.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
-
+ if self.type in ('controllerEvent', 'SysEx', 'UniversalSysEx'):
+ 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.
+
+ 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)
+ 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
+
+class Note(GenericEvent):
'''
- # 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
+ A class that encapsulates a note
+ '''
+ def __init__(self, channel, pitch, time, duration, volume, ordinal=3,
+ annotation=None, insertion_order=0):
+ self.pitch = pitch
+ self.duration = duration
+ self.volume = volume
+ self.channel = channel
+ self.annotation = annotation
+ super(Note, self).__init__('note', time, ordinal, insertion_order)
- 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
-
+class Tempo(GenericEvent):
+ '''
+ A class that encapsulates a tempo meta-event
+ '''
+ def __init__(self, time, tempo, ordinal=3, insertion_order=0):
+ self.tempo = int(60000000 / tempo)
+ super(Tempo, self).__init__('tempo', time, ordinal, insertion_order)
+
+
+class Copyright(GenericEvent):
+ '''
+ A class that encapsulates a copyright event
+ '''
+ def __init__(self, time, notice, ordinal=1, insertion_order=0):
+ self.notice = notice.encode("ISO-8859-1")
+ super(Copyright, self).__init__('Copyright', time, ordinal,
+ insertion_order)
+
+
+class Text(GenericEvent):
+ '''
+ A class that encapsulates a text event
+ '''
+ def __init__(self, time, text, ordinal=1, insertion_order=0):
+ self.text = text.encode("ISO-8859-1")
+ super(Text, self).__init__('Text', time, ordinal, insertion_order)
+
+
+class KeySignature(GenericEvent):
+ '''
+ A class that encapsulates a text event
+ '''
+ def __init__(self, time, accidentals, accidental_type, mode, ordinal=1,
+ insertion_order=0):
+ self.accidentals = accidentals
+ self.accidental_type = accidental_type
+ self.mode = mode
+ super(KeySignature, self).__init__('KeySignature', time, ordinal,
+ insertion_order)
+
+
+class ProgramChange(GenericEvent):
+ '''
+ A class that encapsulates a program change event.
+ '''
+
+ def __init__(self, channel, time, programNumber, ordinal=1,
+ insertion_order=0):
+ self.programNumber = programNumber
+ self.channel = channel
+ super(ProgramChange, self).__init__('programChange', time, ordinal,
+ insertion_order)
+
+
+class SysExEvent(GenericEvent):
+ '''
+ A class that encapsulates a System Exclusive event.
+ '''
+
+ def __init__(self, time, manID, payload, ordinal=1, insertion_order=0):
+ self.manID = manID
+ self.payload = payload
+ super(SysExEvent, self).__init__('SysEx', time, ordinal,
+ insertion_order)
+
+
+class UniversalSysExEvent(GenericEvent):
+ '''
+ A class that encapsulates a Universal System Exclusive event.
+ '''
+
+ def __init__(self, time, realTime, sysExChannel, code, subcode,
+ payload, ordinal=1, insertion_order=0):
+ self.realTime = realTime
+ self.sysExChannel = sysExChannel
+ self.code = code
+ self.subcode = subcode
+ self.payload = payload
+ super(UniversalSysExEvent, self).__init__('UniversalSysEx', time,
+ ordinal, insertion_order)
+
+
+class ControllerEvent(GenericEvent):
+ '''
+ A class that encapsulates a program change event.
+ '''
+
+ def __init__(self, channel, time, controller_number, parameter,
+ ordinal=1, insertion_order=0):
+ self.parameter = parameter
+ self.channel = channel
+ self.controller_number = controller_number
+ super(ControllerEvent, self).__init__('controllerEvent', time, ordinal,
+ insertion_order)
+
+
+class TrackName(GenericEvent):
+ '''
+ A class that encapsulates a program change event.
+ '''
+
+ def __init__(self, time, trackName, ordinal=0, insertion_order=0):
+ # GenericEvent.__init__(self, time,)
+ self.trackName = trackName.encode("ISO-8859-1")
+ super(TrackName, self).__init__('trackName', time, ordinal,
+ insertion_order)
+
+
+class TimeSignature(GenericEvent):
+ '''
+ A class that encapsulates a time signature.
+ '''
+
+ def __init__(self, time, numerator, denominator, clocks_per_tick,
+ notes_per_quarter, ordinal=0, insertion_order=0):
+ self.numerator = numerator
+ self.denominator = denominator
+ self.clocks_per_tick = clocks_per_tick
+ self.notes_per_quarter = notes_per_quarter
+ super(TimeSignature, self).__init__('TimeSignature', time, ordinal,
+ insertion_order)
+
+
+class MIDITrack(object):
+ '''
+ A class that encapsulates a MIDI track
+ '''
+
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.headerString = struct.pack('cccc', b'M', b'T', b'r', b'k')
+ self.dataLength = 0 # Is calculated after the data is in place
+ self.MIDIdata = b""
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
+
+ def addNoteByNumber(self, channel, pitch, time, duration, volume,
+ annotation=None, insertion_order=0):
'''
- self.eventList.append(MIDITrack.note(channel, pitch,time,duration,volume))
-
- def addControllerEvent(self,channel,time,eventType, paramerter1):
+ Add a note by chromatic MIDI number
+ '''
+ self.eventList.append(Note(channel, pitch, time, duration, volume,
+ annotation=annotation,
+ insertion_order=insertion_order))
+
+ def addControllerEvent(self, channel, time, controller_number, parameter,
+ insertion_order=0):
'''
Add a controller event.
'''
-
- self.eventList.append(MIDITrack.ControllerEvent(channel,time,eventType, \
- paramerter1))
-
- def addTempo(self,time,tempo):
+
+ self.eventList.append(ControllerEvent(channel, time, controller_number,
+ parameter,
+ insertion_order=insertion_order))
+
+ def addTempo(self, time, tempo, insertion_order=0):
'''
Add a tempo change (or set) event.
'''
- self.eventList.append(MIDITrack.tempo(time,tempo))
-
- def addSysEx(self,time,manID, payload):
+ self.eventList.append(Tempo(time, tempo,
+ insertion_order=insertion_order))
+
+ def addSysEx(self, time, manID, payload, insertion_order=0):
'''
Add a SysEx event.
'''
- self.eventList.append(MIDITrack.SysExEvent(time, manID, payload))
-
- def addUniversalSysEx(self,time,code, subcode, payload, sysExChannel=0x7F, \
- realTime=False):
+ self.eventList.append(SysExEvent(time, manID, payload,
+ insertion_order=insertion_order))
+
+ def addUniversalSysEx(self, time, code, subcode, payload,
+ sysExChannel=0x7F, realTime=False,
+ insertion_order=0):
'''
Add a Universal SysEx event.
'''
- self.eventList.append(MIDITrack.UniversalSysExEvent(time, realTime, \
- sysExChannel, code, subcode, payload))
-
- def addProgramChange(self,channel, time, program):
+ self.eventList.append(UniversalSysExEvent(time, realTime, sysExChannel,
+ code, subcode, payload,
+ insertion_order=insertion_order))
+
+ def addProgramChange(self, channel, time, program, insertion_order=0):
'''
Add a program change event.
'''
- self.eventList.append(MIDITrack.programChange(channel, time, program))
-
- def addTrackName(self,time,trackName):
+ self.eventList.append(ProgramChange(channel, time, program,
+ insertion_order=insertion_order))
+
+ def addTrackName(self, time, trackName, insertion_order=0):
'''
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
+ self.eventList.append(TrackName(time, trackName,
+ insertion_order=insertion_order))
+
+ def addTimeSignature(self, time, numerator, denominator, clocks_per_tick,
+ notes_per_quarter, insertion_order=0):
+ '''
+ Add a time signature.
+ '''
+ self.eventList.append(TimeSignature(time, numerator, denominator,
+ clocks_per_tick, notes_per_quarter,
+ insertion_order=insertion_order))
+
+ def addCopyright(self, time, notice, insertion_order=0):
+ '''
+ Add a copyright notice
+ '''
+ self.eventList.append(Copyright(time, notice,
+ insertion_order=insertion_order))
+
+ def addKeySignature(self, time, accidentals, accidental_type, mode,
+ insertion_order=0):
+ '''
+ Add a copyright notice
+ '''
+ self.eventList.append(KeySignature(time, accidentals, accidental_type,
+ mode,
+ insertion_order=insertion_order))
+
+ def addText(self, time, text, insertion_order=0):
+ '''
+ Add a text event
+ '''
+ self.eventList.append(Text(time, text,
+ insertion_order=insertion_order))
+
+ def changeNoteTuning(self, tunings, sysExChannel=0x7F, realTime=True,
+ tuningProgam=0, insertion_order=0):
+ '''
+ Change the tuning of MIDI notes
'''
payload = struct.pack('>B', tuningProgam)
payload = payload + struct.pack('>B', len(tunings))
@@ -285,175 +373,180 @@ class MIDITrack:
MIDIFreqency = frequencyTransform(frequency)
for byte in MIDIFreqency:
payload = payload + struct.pack('>B', byte)
-
- self.eventList.append(MIDITrack.UniversalSysExEvent(0, realTime, sysExChannel,\
- 8, 2, payload))
-
+
+ self.eventList.append(UniversalSysExEvent(0, realTime, sysExChannel,
+ 8, 2, payload, insertion_order=insertion_order))
+
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 = MIDIEvent("NoteOn", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
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 = MIDIEvent("NoteOff",
+ (thing.time + thing.duration) * TICKSPERBEAT,
+ thing.ord - 0.1, thing.insertion_order)
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 = MIDIEvent("Tempo", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
event.tempo = thing.tempo
- event.ord = 3
+ self.MIDIEventList.append(event)
+
+ elif thing.type == 'Copyright':
+ event = MIDIEvent("Copyright", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
+ event.notice = thing.notice
+ self.MIDIEventList.append(event)
+
+ elif thing.type == 'Text':
+ event = MIDIEvent("Text", thing.time * TICKSPERBEAT, thing.ord,
+ thing.insertion_order)
+ event.text = thing.text
+ self.MIDIEventList.append(event)
+
+ elif thing.type == 'KeySignature':
+ event = MIDIEvent("KeySignature", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
+ event.accidentals = thing.accidentals
+ event.accidental_type = thing.accidental_type
+ event.mode = thing.mode
self.MIDIEventList.append(event)
elif thing.type == 'programChange':
- event = MIDIEvent()
- event.type = "ProgramChange"
- event.time = thing.time * TICKSPERBEAT
+ event = MIDIEvent("ProgramChange", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
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 = MIDIEvent("TrackName", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
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 = MIDIEvent("ControllerEvent", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
+ event.controller_number = thing.controller_number
event.channel = thing.channel
- event.paramerter1 = thing.parameter1
- event.ord = 1
+ event.parameter = thing.parameter
self.MIDIEventList.append(event)
elif thing.type == 'SysEx':
- event = MIDIEvent()
- event.type = "SysEx"
- event.time = thing.time * TICKSPERBEAT
+ event = MIDIEvent("SysEx", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
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 = MIDIEvent("UniversalSysEx", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
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)
+
+ elif thing.type == 'TimeSignature':
+ event = MIDIEvent("TimeSignature", thing.time * TICKSPERBEAT,
+ thing.ord, thing.insertion_order)
+ event.numerator = thing.numerator
+ event.denominator = thing.denominator
+ event.clocks_per_tick = thing.clocks_per_tick
+ event.notes_per_quarter = thing.notes_per_quarter
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)
+ raise ValueError("Error in MIDITrack: Unknown event type %s" %
+ thing.type)
- self.MIDIEventList.sort(lambda x, y: int( 1000 * (x.time - y.time)))
+ # Assumptions in the code expect the list to be time-sorted.
+ self.MIDIEventList.sort(key=sort_events)
- if self.deinterleave:
+ 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.
+
+ 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.
+
+ # 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 = {item: 1 for item in self.eventList}
+ self.eventList = list(tempDict.keys())
+ self.eventList.sort(key=sort_events)
def closeTrack(self):
- '''Called to close a track before writing
-
+ '''
+ 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:
+ if self.closed:
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
+ # Process the events in the eventList
self.writeEventsToStream()
# Write MIDI close event.
- self.MIDIdata = self.MIDIdata + struct.pack('BBBB',0x00,0xFF, \
- 0x2F,0x00)
-
+ 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))
+
+ self.dataLength = struct.pack('>L', len(self.MIDIdata))
def writeEventsToStream(self):
'''
Write the events in MIDIEvents to the MIDI stream.
'''
- preciseTime = 0.0 # Actual time of event, ignoring round-off
- actualTime = 0.0 # Time as written to midi stream, include round-off
+ preciseTime = 0.0 # Actual time of event, ignoring round-off
+ actualTime = 0.0 # Time as written to midi stream, include round-off
for event in self.MIDIEventList:
preciseTime = preciseTime + event.time
@@ -461,136 +554,190 @@ class MIDITrack:
# Convert the time to variable length and back, to see how much
# error is introduced
- testBuffer = ""
+ testBuffer = b""
varTime = writeVarLength(event.time)
for timeByte in varTime:
- testBuffer = testBuffer + struct.pack('>B',timeByte)
- (roundedVal,discard) = readVarLength(0,testBuffer)
+ testBuffer = testBuffer + struct.pack('>B', timeByte)
+ (roundedVal, discard) = readVarLength(0, testBuffer)
roundedTime = actualTime + roundedVal
- # print "Rounded, Precise: %15.10f %15.10f" % (roundedTime, preciseTime)
- # Calculate the delta between the two and apply it to the event time.
+ # Calculate the delta between the two and apply it to event time.
delta = preciseTime - roundedTime
event.time = event.time + delta
# Now update the actualTime value, using the updated event time.
- testBuffer = ""
+ testBuffer = b""
varTime = writeVarLength(event.time)
for timeByte in varTime:
- testBuffer = testBuffer + struct.pack('>B',timeByte)
- (roundedVal,discard) = readVarLength(0,testBuffer)
+ testBuffer = testBuffer + struct.pack('>B', timeByte)
+
+ (roundedVal, discard) = readVarLength(0, testBuffer)
actualTime = actualTime + roundedVal
-
+
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)
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('>B', code)
+ self.MIDIdata += struct.pack('>B', event.pitch)
+ 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)
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('>B', code)
+ self.MIDIdata += struct.pack('>B', event.pitch)
+ 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
+ threebite = fourbite[1:4] # Just discard the MSB
+ varTime = writeVarLength(event.time)
+ for timeByte in varTime:
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('>B', code)
+ self.MIDIdata += struct.pack('>B', subcode)
+ self.MIDIdata += struct.pack('>B', 0x03)
+ self.MIDIdata += threebite
+ elif event.type == "Text":
+ code = 0xFF
+ subcode = 0x01
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
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('>B', code)
+ self.MIDIdata += struct.pack('>B', subcode)
+ payloadLength = len(event.text)
+ payloadLengthVar = writeVarLength(payloadLength)
+ for i in range(len(payloadLengthVar)):
+ self.MIDIdata += struct.pack("b", payloadLengthVar[i])
+ self.MIDIdata += event.text
+ elif event.type == "Copyright":
+ code = 0xFF
+ subcode = 0x02
+ varTime = writeVarLength(event.time)
+ for timeByte in varTime:
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('>B', code)
+ self.MIDIdata += struct.pack('>B', subcode)
+ payloadLength = len(event.notice)
+ payloadLengthVar = writeVarLength(payloadLength)
+ for i in range(len(payloadLengthVar)):
+ self.MIDIdata += struct.pack("b", payloadLengthVar[i])
+ self.MIDIdata += event.notice
+ elif event.type == "TimeSignature":
+ code = 0xFF
+ subcode = 0x58
+ varTime = writeVarLength(event.time)
+ for timeByte in varTime:
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('>B', code)
+ self.MIDIdata += struct.pack('>B', subcode)
+ self.MIDIdata += struct.pack('>B', 0x04)
+ self.MIDIdata += struct.pack('>B', event.numerator)
+ self.MIDIdata += struct.pack('>B', event.denominator)
+ self.MIDIdata += struct.pack('>B', event.clocks_per_tick)
+ # 32nd notes per quarter note
+ self.MIDIdata += struct.pack('>B', event.notes_per_quarter)
+ elif event.type == "KeySignature":
+ code = 0xFF
+ subcode = 0x59
+ event_subtype = 0x02
+ varTime = writeVarLength(event.time)
+ for timeByte in varTime:
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('>B', code)
+ self.MIDIdata += struct.pack('>B', subcode)
+ self.MIDIdata += struct.pack('>B', event_subtype)
+ self.MIDIdata += struct.pack('>b', event.accidentals *
+ event.accidental_type)
+ self.MIDIdata += struct.pack('>B', event.mode)
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)
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('>B', code)
+ 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
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('B', 0xFF)
+ self.MIDIdata += struct.pack('B', 0X03)
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
+ for i in range(0, len(dataLenghtVar)):
+ self.MIDIdata += struct.pack("b", dataLenghtVar[i])
+ 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)
+ self.MIDIdata += struct.pack('>B', timeByte)
+ self.MIDIdata += struct.pack('>B', code)
+ self.MIDIdata += struct.pack('>B', event.controller_number)
+ self.MIDIdata += struct.pack('>B', event.parameter)
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)
-
+ self.MIDIdata += struct.pack('>B', timeByte)
+ 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)
+ self.MIDIdata += struct.pack('>B', lenByte)
+
+ self.MIDIdata += struct.pack('>B', event.manID)
+ self.MIDIdata += event.payload
+ 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)
-
+ self.MIDIdata += struct.pack('>B', timeByte)
+ 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)
+ self.MIDIdata += struct.pack('>B', lenByte)
+
+ if event.realTime:
+ 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)
-
+ self.MIDIdata += struct.pack('>B', 0x7E)
+
+ self.MIDIdata += struct.pack('>B', event.sysExChannel)
+ self.MIDIdata += struct.pack('>B', event.code)
+ self.MIDIdata += struct.pack('>B', event.subcode)
+ self.MIDIdata += event.payload
+ self.MIDIdata += struct.pack('>B', 0xF7)
+
def deInterleaveNotes(self):
- '''Correct Interleaved notes.
-
+ '''
+ 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)):
+ if str(event.pitch)+str(event.channel) in stack:
stack[str(event.pitch)+str(event.channel)].append(event.time)
else:
stack[str(event.pitch)+str(event.channel)] = [event.time]
@@ -607,382 +754,733 @@ class MIDITrack:
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.
+ # Note that ``processEventList`` makes the ordinality of a note off event
+ # a bit lower than the note on event, so this sort will make concomitant
+ # note off events processed first.
- self.MIDIEventList.sort(lambda x, y: cmp(x.type , y.type))
- self.MIDIEventList.sort(lambda x, y: int( 1000 * (x.time - y.time)))
+ self.MIDIEventList.sort(key=sort_events)
- def adjustTime(self,origin):
+ def adjustTimeAndOrigin(self, origin, adjust):
'''
- Adjust Times to be relative, and zero-origined
+ Adjust Times to be relative, and zero-origined.
+
+ If adjust is True, the track will be shifted. Regardelss times
+ are converted to relative values here.
'''
-
+
if len(self.MIDIEventList) == 0:
return
tempEventList = []
-
- runningTime = 0
-
+ internal_origin = origin if adjust else 0.0
+ runningTime = 0
+
for event in self.MIDIEventList:
- adjustedTime = event.time - origin
+ adjustedTime = event.time - internal_origin
event.time = adjustedTime - runningTime
runningTime = adjustedTime
tempEventList.append(event)
-
+
self.MIDIEventList = tempEventList
-
- def writeTrack(self,fileHandle):
+
+ 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 MIDIHeader(object):
'''
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):
+ def __init__(self, numTracks, file_format):
''' Initialize the data structures
'''
- self.headerString = struct.pack('cccc','M','T','h','d')
- self.headerSize = struct.pack('>L',6)
+ self.headerString = struct.pack('cccc', b'M', b'T', b'h', b'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)
-
+ self.format = struct.pack('>H', file_format)
+ self.numeric_format = file_format
+ delta = 1 if file_format == 1 else 0
+ self.numTracks = struct.pack('>H', numTracks + delta)
+ self.ticksPerBeat = struct.pack('>H', TICKSPERBEAT)
- def writeFile(self,fileHandle):
+ 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.
+
+class MIDIFile(object):
+ '''
+ A class that encapsulates a full, well-formed MIDI file object.
+
+ This is a container object that contains a header (:class:`MIDIHeader`),
+ one or more tracks (class:`MIDITrack`), and the data associated with a
+ proper and well-formed MIDI file.
'''
-
- def __init__(self, numTracks, removeDuplicates=True, deinterleave=True):
+
+ def __init__(self, numTracks=1, removeDuplicates=True, deinterleave=True,
+ adjust_origin=None, file_format=1):
'''
- Initialize the class
+
+ Initialize the MIDIFile class
+
+ :param numTracks: The number of tracks the file contains. Integer,
+ one or greater
+ :param removeDuplicates: If set to ``True`` remove duplicate events
+ before writing to disk
+ :param deinterleave: If set to ``True`` deinterleave the notes in
+ the stream
+ :param adjust_origin: If set to ``True`` (or left at the default of
+ ``None``) shift all the events in the tracks so that the first
+ event takes place at time t=0
+ :param file_format: The format of the multi-track file. This should
+ either be ``1`` (the default, and the most widely supported
+ format) or ``2``.
+
+ Note that the default for ``adjust_origin`` will change in a future
+ release, so one should probably explicitly set it.
+
+ Example:
+
+ .. code::
+
+ # Create a two-track MIDIFile
+
+ from midiutil.MidiFile import MIDIFile
+ midi_file = MIDIFile(2)
+
+ A Note on File Formats
+ ----------------------
+
+ In previous versions of this code the file written was format 2
+ (which can be thought of as a collection of independent tracks) but
+ was identified as format 1. In this version one can specify either
+ format 1 or 2.
+
+ In format 1 files there is a separate tempo track which contains
+ tempo and time signature data, but contains no note data. If one
+ creates a single track format 1 file the actual file has two tracks
+ -- one for tempo data and one for note data. In the track indexing
+ the tempo track can be ignored. In other words track 0 is the note
+ track (the second track in the file). However, tempo and time
+ signature data will be written to the first, tempo track. This is
+ done to try and preserve as much interoperability with previous
+ versions as possible.
+
+ In a format 2 file all tracks are indexed and the track parameter
+ is interpreted literally.
'''
- self.header = MIDIHeader(numTracks)
-
+ self.header = MIDIHeader(numTracks, file_format)
+
self.tracks = list()
- self.numTracks = numTracks
+ if file_format == 1:
+ delta = 1
+ else:
+ delta = 0
+ self.numTracks = numTracks + delta
self.closed = False
-
- for i in range(0,numTracks):
+ if adjust_origin is None:
+ self.adjust_origin = True
+ warnings.warn("Please explicitly set adjust_origin. Default "
+ "behaviour will change in a future version.",
+ FutureWarning)
+ else:
+ self.adjust_origin = adjust_origin
+
+ for i in range(0, self.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):
+ # to keep track of the order of insertion for new sorting
+ self.event_counter = 0
+
+ # 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,
+ annotation=None):
"""
+
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].
+
+ :param track: The track to which the note is added.
+ :param channel: the MIDI channel to assign to the note. [Integer, 0-15]
+ :param pitch: the MIDI pitch number [Integer, 0-127].
+ :param time: the time (in beats) at which the note sounds [Float].
+ :param duration: the duration of the note (in beats) [Float].
+ :param volume: the volume (velocity) of the note. [Integer, 0-127].
+ :param annotation: Arbitrary data to attach to the note.
+
+ The ``annotation`` parameter attaches arbitrary data to the note. This
+ is not used in the code, but can be useful anyway. As an example,
+ I have created a project that uses MIDIFile to write
+ `csound <http://csound.github.io/>`_ orchestra files directly from the
+ class ``EventList``.
"""
- self.tracks[track].addNoteByNumber(channel, pitch, time, duration, volume)
+ if self.header.numeric_format == 1:
+ track += 1
+ self.tracks[track].addNoteByNumber(channel, pitch, time, duration,
+ volume, annotation=annotation,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
- def addTrackName(self,track, time,trackName):
+ 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].
+ Name a track.
+
+ :param track: The track to which the name is assigned.
+ :param time: The time (in beats) at which the track name event is
+ placed. In general this should probably be time 0 (the beginning
+ of the track).
+ :param trackName: The name to assign to the track [String]
"""
- self.tracks[track].addTrackName(time,trackName)
-
- def addTempo(self,track, time,tempo):
+ if self.header.numeric_format == 1:
+ track += 1
+ self.tracks[track].addTrackName(time, trackName,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
+
+ def addTimeSignature(self, track, time, numerator, denominator,
+ clocks_per_tick, notes_per_quarter=8):
+ '''
+ Add a time signature event.
+
+ :param track: The track to which the signature is assigned. Note that
+ in a format 1 file this parameter is ignored and the event is
+ written to the tempo track
+ :param time: The time (in beats) at which the event is placed.
+ In general this should probably be time 0 (the beginning of the
+ track).
+ :param numerator: The numerator of the time signature. [Int]
+ :param denominator: The denominator of the time signature, expressed as
+ a power of two (see below). [Int]
+ :param clocks_per_tick: The number of MIDI clock ticks per metronome
+ click (see below).
+ :param notes_per_quarter: The number of annotated 32nd notes in a MIDI
+ quarter note. This is almost always 8 (the default), but some
+ sequencers allow this value to be changed. Unless you know that
+ your sequencing software supports it, this should be left at its
+ default value.
+
+ The data format for this event is a little obscure.
+
+ The ``denominator`` should be specified as a power of 2, with
+ a half note being one, a quarter note being two, and eight note
+ being three, etc. Thus, for example, a 4/4 time signature would
+ have a ``numerator`` of 4 and a ``denominator`` of 2. A 7/8 time
+ signature would be a ``numerator`` of 7 and a ``denominator``
+ of 3.
+
+ The ``clocks_per_tick`` argument specifies the number of clock
+ ticks per metronome click. By definition there are 24 ticks in
+ a quarter note, so a metronome click per quarter note would be
+ 24. A click every third eighth note would be 3 * 12 = 36.
+ '''
+ if self.header.numeric_format == 1:
+ track = 0
+
+ self.tracks[track].addTimeSignature(time, numerator, denominator,
+ clocks_per_tick, notes_per_quarter,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
+
+ 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]
+
+ Add notes to the MIDIFile object
+
+ :param track: The track to which the tempo event is added. Note that
+ in a format 1 file this parameter is ignored and the tempo is
+ written to the tempo track
+ :param time: The time (in beats) at which tempo event is placed
+ :param tempo: The tempo, in Beats per Minute. [Integer]
"""
- self.tracks[track].addTempo(time,tempo)
-
- def addProgramChange(self,track, channel, time, program):
+ if self.header.numeric_format == 1:
+ track = 0
+ self.tracks[track].addTempo(time, tempo,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
+
+ def addCopyright(self, track, time, notice):
"""
- 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].
+
+ Add a copyright notice to the MIDIFile object
+
+ :param track: The track to which the notice is added.
+ :param time: The time (in beats) at which notice event is placed. In
+ general this sould be time t=0
+ :param notice: The copyright notice [String]
"""
- self.tracks[track].addProgramChange(channel, time, program)
-
- def addControllerEvent(self,track, channel,time,eventType, paramerter1):
+ if self.header.numeric_format == 1:
+ track += 1
+ self.tracks[track].addCopyright(time, notice,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
+
+ def addKeySignature(self, track, time, accidentals, accidental_type, mode,
+ insertion_order=0):
+ '''
+ Add a Key Signature to a track
+
+ :param track: The track to which this should be added
+ :param time: The time at which the signature should be placed
+ :param accidentals: The number of accidentals in the key signature
+ :param accidental_type: The type of accidental
+ :param mode: The mode of the scale
+
+ The easiest way to use this function is to make sure that the symbolic
+ constants for accidental_type and mode are imported. By doing this:
+
+ .. code::
+
+ from midiutil.MidiFile import *
+
+ one gets the following constants defined:
+
+ * ``SHARPS``
+ * ``FLATS``
+ * ``MAJOR``
+ * ``MINOR``
+
+ So, for example, if one wanted to create a key signature for a minor
+ scale with three sharps:
+
+ .. code::
+
+ MyMIDI.addKeySignature(0, 0, 3, SHARPS, MINOR)
+ '''
+ if self.header.numeric_format == 1:
+ track += 1
+ self.tracks[track].addKeySignature(time, accidentals, accidental_type,
+ mode,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
+
+ def addText(self, track, time, text):
"""
- 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.
+
+ Add a text event
+
+ :param track: The track to which the notice is added.
+ :param time: The time (in beats) at which text event is placed.
+ :param text: The text to adde [ASCII String]
"""
- self.tracks[track].addControllerEvent(channel,time,eventType, paramerter1)
-
- def changeNoteTuning(self, track, tunings, sysExChannel=0x7F, \
- realTime=False, tuningProgam=0):
+ if self.header.numeric_format == 1:
+ track += 1
+ self.tracks[track].addText(time, text,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
+
+ def addProgramChange(self, track, channel, time, program):
"""
- 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!
+
+ Add a MIDI program change event.
+
+ :param track: The track to which program change event is added.
+ :param channel: the MIDI channel to assign to the event.
+ [Integer, 0-15]
+ :param time: The time (in beats) at which the program change event is
+ placed [Float].
+ :param program: the program number. [Integer, 0-127].
+ """
+ self.tracks[track].addProgramChange(channel, time, program,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
+
+ def addControllerEvent(self, track, channel, time, controller_number,
+ parameter):
+ """
+
+ Add a channel control event
+
+ :param track: The track to which the event is added.
+ :param channel: the MIDI channel to assign to the event.
+ [Integer, 0-15]
+ :param time: The time (in beats) at which the event is placed [Float].
+ :param controller_number: The controller ID of the event.
+ :param parameter: The event's parameter, the meaning of which varies by
+ event type.
"""
- self.tracks[track].changeNoteTuning(tunings, sysExChannel, realTime,\
- tuningProgam)
-
- def writeFile(self,fileHandle):
+ if self.header.numeric_format == 1:
+ track += 1
+ self.tracks[track].addControllerEvent(channel, time, controller_number,
+ parameter, insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
+
+ def makeRPNCall(self, track, channel, time, controller_msb, controller_lsb,
+ data_msb, data_lsb, time_order=False):
'''
- Write the MIDI File.
-
- Use:
- MyMIDI.writeFile(filehandle)
-
- Arguments:
- filehandle: a file handle that has been opened for binary writing.
+
+ Perform a Registered Parameter Number Call
+
+ :param track: The track to which this applies
+ :param channel: The channel to which this applies
+ :param time: The time of the event
+ :param controller_msb: The Most significant byte of the controller. In
+ common usage this will usually be 0
+ :param controller_lsb: The Least significant Byte for the controller
+ message. For example, for a fine-tuning change this would be 01.
+ :param data_msb: The Most Significant Byte of the controller's
+ parameter.
+ :param data_lsb: The Least Significant Byte of the controller's
+ parameter. If not needed this should be set to ``None``
+ :param time_order: Order the control events in time (see below)
+
+ As an example, if one were to change a channel's tuning program::
+
+ makeRPNCall(track, channel, time, 0, 3, 0, program)
+
+ (Note, however, that there is a convenience function,
+ ``changeTuningProgram``, that does this for you.)
+
+ The ``time_order`` parameter is something of a work-around for
+ sequencers that do not preserve the order of events from the MIDI files
+ they import. Within this code care is taken to preserve the order of
+ events as specified, but some sequencers seem to transmit events
+ occurring at the same time in an arbitrary order. By setting this
+ parameter to ``True`` something of a work-around is performed: each
+ successive event (of which there are three or four for this event type)
+ is placed in the time stream a small delta from the preceding one.
+ Thus, for example, the controllers are set before the data bytes in
+ this call.
'''
-
- 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)
+ if self.header.numeric_format == 1:
+ track += 1
+ delta = 1.0 / (TICKSPERBEAT - 10) if time_order else 0.0
+ self.tracks[track].addControllerEvent(channel, time, 101,
+ controller_msb, insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
+ self.tracks[track].addControllerEvent(channel, time + delta, 100,
+ controller_lsb, insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
+ self.tracks[track].addControllerEvent(channel, time + (2.0 * delta), 6,
+ data_msb, insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
+ if data_lsb is not None:
+ self.tracks[track].addControllerEvent(channel, time + (3.0*delta),
+ 38, data_lsb, insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
- def addSysEx(self,track, time, manID, payload):
+ def makeNRPNCall(self, track, channel, time, controller_msb,
+ controller_lsb, data_msb, data_lsb, time_order=False):
+ '''
+
+ Perform a Non-Registered Parameter Number Call
+
+ :param track: The track to which this applies
+ :param channel: The channel to which this applies
+ :param time: The time of the event
+ :param controller_msb: The Most significant byte of thecontroller. In
+ common usage this will usually be 0
+ :param controller_lsb: The least significant byte for the controller
+ message. For example, for a fine-tunning change this would be 01.
+ :param data_msb: The most significant byte of the controller's
+ parameter.
+ :param data_lsb: The least significant byte of the controller's
+ parameter. If none is needed this should be set to ``None``
+ :param time_order: Order the control events in time (see below)
+
+ The ``time_order`` parameter is something of a work-around for
+ sequencers that do not preserve the order of events from the MIDI files
+ they import. Within this code care is taken to preserve the order of
+ events as specified, but some sequencers seem to transmit events
+ occurring at the same time in an arbitrary order. By setting this
+ parameter to ``True`` something of a work-around is performed: each
+ successive event (of which there are three or four for this event type)
+ is placed in the time stream a small delta from the preceding one.
+ Thus, for example, the controllers are set before the data bytes in
+ this call.
+
+ '''
+ if self.header.numeric_format == 1:
+ track += 1
+ delta = 1.0 / (TICKSPERBEAT - 10) if time_order else 0.0
+ self.tracks[track].addControllerEvent(channel, time, 99,
+ controller_msb, insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
+ self.tracks[track].addControllerEvent(channel, time + delta, 98,
+ controller_lsb, insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
+ self.tracks[track].addControllerEvent(channel, time + (2 * delta), 6,
+ data_msb, insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
+ if data_lsb is not None:
+ self.tracks[track].addControllerEvent(channel, time + (3 * delta),
+ 38, data_lsb, insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
+
+ def changeTuningBank(self, track, channel, time, bank, time_order=False):
+ '''
+
+ Change the tuning bank for a selected track
+
+ :param track: The track to which the data should be written
+ :param channel: The channel for the event
+ :param time: The time of the event
+ :param bank: The tuning bank (0-127)
+ :param time_order: Preserve the ordering of the component events by
+ ordering in time. See ``makeRPNCall()`` for a discussion of when
+ this may be necessary
+
+ Note that this is a convenience function, as the same
+ functionality is available from directly sequencing controller
+ events.
+
+ The specified tuning should already have been written to the
+ stream with ``changeNoteTuning``. '''
+ self.makeRPNCall(track, channel, time, 0, 4, 0, bank,
+ time_order=time_order)
+
+ def changeTuningProgram(self, track, channel, time, program,
+ time_order=False):
+ '''
+
+ Change the tuning program for a selected track
+
+ :param track: The track to which the data should be written
+ :param channel: The channel for the event
+ :param time: The time of the event
+ :param program: The tuning program number (0-127)
+ :param time_order: Preserve the ordering of the component events by
+ ordering in time. See ``makeRPNCall()`` for a discussion of when
+ this may be necessary
+
+ Note that this is a convenience function, as the same
+ functionality is available from directly sequencing controller
+ events.
+
+ The specified tuning should already have been written to the
+ stream with ``changeNoteTuning``. '''
+ self.makeRPNCall(track, channel, time, 0, 3, 0, program,
+ time_order=time_order)
+
+ def changeNoteTuning(self, track, tunings, sysExChannel=0x7F,
+ realTime=True, tuningProgam=0):
"""
- 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
+ Add a real-time MIDI tuning standard update to a track.
+
+ :param track: The track to which the tuning is applied.
+ :param tunings: A list to tuples representing the tuning. See below for
+ an explanation.
+ :param sysExChannel: The SysEx channel of the event. This is mapped to
+ "manufacturer ID" in the event which is written. Unless there is a
+ specific reason for changing it, it should be left at its default
+ value.
+ :param realTime: Speicifes if the Universal SysEx event should be
+ flagged as real-time or non-real-time. As with the ``sysExChannel``
+ argument, this should in general be left at it's default value.
+ :param tuningProgram: The tuning program number.
+
+ This function specifically implements the "real time single note tuning
+ change" (although the name is misleading, as multiple notes can be
+ included in each event). It should be noted that not all hardware or
+ software implements the MIDI tuning standard, and that which does often
+ does not implement it in its entirety.
+
+ The ``tunings`` argument is a list of tuples, in (*note number*,
+ *frequency*) format. As an example, if one wanted to change the
+ frequency on MIDI note 69 to 500 (it is normally 440 Hz), one could do
+ it thus:
+
+ .. code:: python
+
+ from midiutil.MidiFile import MIDIFile
+ MyMIDI = MIDIFile(1)
+ tuning = [(69, 500)]
+ MyMIDI.changeNoteTuning(0, tuning, tuningProgam=0)
+ """
+ if self.header.numeric_format == 1:
+ track += 1
+ self.tracks[track].changeNoteTuning(tunings, sysExChannel, realTime,
+ tuningProgam,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
+
+ def addSysEx(self, track, time, manID, payload):
+ '''
+
+ Add a System Exclusive event.
+
+ :param track: The track to which the event should be written
+ :param time: The time of the event.
+ :param manID: The manufacturer ID for the event
+ :param payload: The payload for the event. This should be a
+ binary-packed value, and will vary for each type and function.
+
+ **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
+
+ '''
+ if self.header.numeric_format == 1:
+ track += 1
+ self.tracks[track].addSysEx(time, manID, payload,
+ insertion_order=self.event_counter)
+ self.event_counter += 1
+
+ def addUniversalSysEx(self, track, time, code, subcode, payload,
+ sysExChannel=0x7F, realTime=False):
+ '''
+
+ Add a Univeral System Exclusive event.
+
+ :param track: The track to which the event should be written
+ :param time: The time of the event, in beats.
+ :param code: The event code. [Integer]
+ :param subcode: The event sub-code [Integer]
+ :param payload: The payload for the event. This should be a
+ binary-packed value, and will vary for each type and function.
+ :param sysExChannel: The SysEx channel.
+ :param realTime: Sets the real-time flag. Defaults to non-real-time.
+ :param manID: The manufacturer ID for the event
+
+
+ **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)
-
+ a developer finds him or herself using the function heavily. As an
+ example of such a helper function, see the ``changeNoteTuning()``
+ function, which uses the event to create a real-time note tuning
+ update.
+
+ '''
+ if self.header.numeric_format == 1:
+ track += 1
+ self.tracks[track].addUniversalSysEx(time, code, subcode, payload,
+ sysExChannel, realTime,
+ insertion_order=self.event_counter) # noqa: E128
+ self.event_counter += 1
+
+ def writeFile(self, fileHandle):
+ '''
+ Write the MIDI File.
+
+ :param 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 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.
+
+ 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
+ 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
-
+ # runningTime = 0
+
for event in track.eventList:
adjustedTime = event.time - origin
- #event.time = adjustedTime - runningTime + offset
+ # event.time = adjustedTime - runningTime + offset
event.time = adjustedTime + offset
- #runningTime = adjustedTime
+ # runningTime = adjustedTime
tempEventList.append(event)
-
+
track.eventList = tempEventList
- #End Public Functions ########################
-
+ # 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:
+ 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:
return
-
- for i in range(0,self.numTracks):
+
+ 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()
-
+ # 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(key=sort_events)
+
origin = self.findOrigin()
- for i in range(0,self.numTracks):
- self.tracks[i].adjustTime(origin)
+ for i in range(0, self.numTracks):
+ self.tracks[i].adjustTimeAndOrigin(origin, self.adjust_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
+ 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.
- # 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
-
+ '''
+ 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+0.5)
- output = [0,0,0,0]
- reversed = [0,0,0,0]
+ 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 = input & 0x7F
result = result | 0x80
output[count] = result
count = count + 1
- input = input >> 7
+ input = input >> 7
reversed[0] = output[3]
reversed[1] = output[2]
@@ -990,10 +1488,12 @@ def writeVarLength(i):
reversed[3] = output[0]
return reversed[4-count:4]
+
# readVarLength is taken from the MidiFile class.
def readVarLength(offset, buffer):
- '''A function to read a MIDI variable length variable.
+ '''
+ A function to read a MIDI variable length variable.
It returns a tuple of the value read and the number of bytes processed. The
input is an offset into the buffer, and the buffer itself.
@@ -1003,7 +1503,7 @@ def readVarLength(offset, buffer):
bytesRead = 0
while True:
output = output << 7
- byte = struct.unpack_from('>B',buffer,toffset)[0]
+ byte = struct.unpack_from('>B', buffer, toffset)[0]
toffset = toffset + 1
bytesRead = bytesRead + 1
output = output + (byte & 127)
@@ -1011,33 +1511,63 @@ def readVarLength(offset, buffer):
break
return (output, bytesRead)
+
def frequencyTransform(freq):
- '''Returns a three-byte transform of a frequencyTransform
+ '''
+ Returns a three-byte transform of a frequency.
'''
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])
+ centDif = 1200 * math.log((freq/lowerFreq), 2) if freq != lowerFreq else 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.
+ '''
+ 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
+ frac = (float((int(freqBytes[1]) << 7) + int(freqBytes[2]))
+ * 100.0) / resolution
frequency = baseFrequency * pow(2.0, frac/1200.0)
return frequency
+
+
+def sort_events(event):
+ '''
+ .. py:function:: sort_events(event)
+
+ The key function used to sort events (both MIDI and Generic)
+
+ :param event: An object of type :class:`MIDIEvent` or (a derrivative)
+ :class:`GenericEvent`
+
+ This function should be provided as the ``key`` for both
+ ``list.sort()`` and ``sorted()``. By using it sorting will be as
+ follows:
+
+ * Events are ordered in time. An event that takes place earlier will
+ appear earlier
+ * If two events happen at the same time, the secondary sort key is
+ ``ord``. Thus a class of events can be processed earlier than
+ another. One place this is used in the code is to make sure that note
+ off events are processed before note on events.
+ * If time and ordinality are the same, they are sorted in the order in
+ which they were originally added to the list. Thus, for example, if
+ one is making an RPN call one can specify the controller change
+ events in the proper order and be sure that they will end up in the
+ file that way.
+ '''
+
+ return (event.time, event.ord, event.insertion_order)
diff --git a/src/midiutil/MidiFile3.py b/src/midiutil/MidiFile3.py
deleted file mode 100644
index 8645792..0000000
--- a/src/midiutil/MidiFile3.py
+++ /dev/null
@@ -1,1055 +0,0 @@
-#-----------------------------------------------------------------------------
-# Name: MidiFile.py
-# Purpose: MIDI file manipulation utilities
-#
-# Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
-#
-# Created: 2008/04/17
-# Copyright: (c) 2009 Mark Conway Wirt
-# License: Please see License.txt for the terms under which this
-# software is distributed.
-#-----------------------------------------------------------------------------
-
-import struct, sys, math
-
-# TICKSPERBEAT is the number of "ticks" (time measurement in the MIDI file) that
-# corresponds to one beat. This number is somewhat arbitrary, but should be chosen
-# to provide adequate temporal resolution.
-
-TICKSPERBEAT = 960
-
-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 __lt__(self, other):
- ''' Sorting function for events.'''
- if self.time < other.time:
- return True
- elif self.time > other.time:
- return False
- else:
- if self.ord < other.ord:
- return True
- elif self.ord > other.ord:
- return False
- else:
- return False
-
- 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.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',b'M',b'T',b'r',b'k')
- self.dataLength = 0 # Is calculated after the data is in place
- self.MIDIdata = b""
- 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(key=lambda x: (x.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 = list(tempDict.keys())
-
- # Sort on type, them on time. Necessary because keys() has no requirement to return
- # things in any order.
-
- self.eventList.sort(key=lambda x: (x.type))
- self.eventList.sort(key=lambda x: (x.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.
- '''
- preciseTime = 0.0 # Actual time of event, ignoring round-off
- actualTime = 0.0 # Time as written to midi stream, include round-off
- for event in self.MIDIEventList:
-
- preciseTime = preciseTime + event.time
-
- # Convert the time to variable length and back, to see how much
- # error is introduced
-
- testBuffer = bytes()
- varTime = writeVarLength(event.time)
- for timeByte in varTime:
- testBuffer = testBuffer + struct.pack('>B',timeByte)
- (roundedVal,discard) = readVarLength(0,testBuffer)
- roundedTime = actualTime + roundedVal
- # print "Rounded, Precise: %15.10f %15.10f" % (roundedTime, preciseTime)
-
- # Calculate the delta between the two and apply it to the event time.
-
- delta = preciseTime - roundedTime
- event.time = event.time + delta
-
- # Now update the actualTime value, using the updated event time.
-
- testBuffer = bytes()
- varTime = writeVarLength(event.time)
- for timeByte in varTime:
- testBuffer = testBuffer + struct.pack('>B',timeByte)
- (roundedVal,discard) = readVarLength(0,testBuffer)
- actualTime = actualTime + roundedVal
-
- 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.encode()
- 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 str(event.pitch)+str(event.channel) in stack:
- 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(key=lambda x: (x.type))
- self.MIDIEventList.sort(key=lambda x: (x.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',b'M',b'T',b'h',b'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+0.5)
- 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 readVarLength(offset, buffer):
- '''A function to read a MIDI variable length variable.
-
- It returns a tuple of the value read and the number of bytes processed. The
- input is an offset into the buffer, and the buffer itself.
- '''
- toffset = offset
- output = 0
- bytesRead = 0
- while True:
- output = output << 7
- byte = struct.unpack_from('>B',buffer,toffset)[0]
- toffset = toffset + 1
- bytesRead = bytesRead + 1
- output = output + (byte & 127)
- if (byte & 128) == 0:
- break
- return (output, bytesRead)
-
-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
index e69de29..2d4e6c5 100644
--- a/src/midiutil/__init__.py
+++ b/src/midiutil/__init__.py
@@ -0,0 +1,3 @@
+from midiutil.MidiFile import *
+
+__all__ = ['MIDIFile', 'MAJOR', 'MINOR', 'SHARPS', 'FLATS']
diff --git a/src/unittests/miditest.py b/src/unittests/miditest.py
deleted file mode 100644
index 3f11643..0000000
--- a/src/unittests/miditest.py
+++ /dev/null
@@ -1,235 +0,0 @@
-#-----------------------------------------------------------------------------
-# Name: miditest.py
-# Purpose: Unit testing harness for midiutil
-#
-# Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
-#
-# Created: 2008/04/17
-# Copyright: (c) 2009, Mark Conway Wirt
-# License: Please see License.txt for the terms under which this
-# software is distributed.
-#-----------------------------------------------------------------------------
-
-
-
-# Next few lines are necessary owing to limitations of the IDE and the
-# directory structure of the project.
-
-import sys, struct
-sys.path.append('..')
-
-import unittest
-from midiutil.MidiFile import MIDIFile, MIDIHeader, MIDITrack, writeVarLength, \
- frequencyTransform, returnFrequency
-import sys
-
-class TestMIDIUtils(unittest.TestCase):
-
- def testWriteVarLength(self):
- self.assertEquals(writeVarLength(0x70), [0x70])
- self.assertEquals(writeVarLength(0x80), [0x81, 0x00])
- self.assertEquals(writeVarLength(0x1FFFFF), [0xFF, 0xFF, 0x7F])
- self.assertEquals(writeVarLength(0x08000000), [0xC0, 0x80, 0x80, 0x00])
-
- def testAddNote(self):
- MyMIDI = MIDIFile(1)
- MyMIDI.addNote(0, 0, 100,0,1,100)
- self.assertEquals(MyMIDI.tracks[0].eventList[0].type, "note")
- self.assertEquals(MyMIDI.tracks[0].eventList[0].pitch, 100)
- self.assertEquals(MyMIDI.tracks[0].eventList[0].time, 0)
- self.assertEquals(MyMIDI.tracks[0].eventList[0].duration, 1)
- self.assertEquals(MyMIDI.tracks[0].eventList[0].volume, 100)
-
- def testDeinterleaveNotes(self):
- MyMIDI = MIDIFile(1)
- MyMIDI.addNote(0, 0, 100, 0, 2, 100)
- MyMIDI.addNote(0, 0, 100, 1, 2, 100)
- MyMIDI.close()
- self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn')
- self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0)
- self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff')
- self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960)
- 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, 1920)
-
- 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, 960)
-
- # 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, 960)
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 960)
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 960)
-
- # 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, 960)
-
- # 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, 960)
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 960)
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 960)
-
- 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)
-
diff --git a/src/unittests/miditest.py3 b/src/unittests/miditest.py3
deleted file mode 100644
index 639f81c..0000000
--- a/src/unittests/miditest.py3
+++ /dev/null
@@ -1,236 +0,0 @@
-#-----------------------------------------------------------------------------
-# Name: miditest.py
-# Purpose: Unit testing harness for midiutil
-#
-# Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
-#
-# Created: 2008/04/17
-# Copyright: (c) 2009, Mark Conway Wirt
-# License: Please see License.txt for the terms under which this
-# software is distributed.
-#-----------------------------------------------------------------------------
-
-
-
-# Next few lines are necessary owing to limitations of the IDE and the
-# directory structure of the project.
-
-import sys, struct
-sys.path.append('..')
-
-import unittest
-from midiutil.MidiFile3 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, 960)
- 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, 1920)
-
- 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, 960)
-
- # 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, 960)
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 960)
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 960)
-
- # 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, 960)
-
- # 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, 960)
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 960)
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
- self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 960)
-
- 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(MyMIDI.tracks[0].MIDIdata[0], 0x00)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[1], 0xf0)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[2], 3)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[3], 0x00)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[4], 0x01)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[5], 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(MyMIDI.tracks[0].MIDIdata[0], 0x00)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[1], 0xf0)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[2], 6)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[3], 0x7E)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[4], 0x7F)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[5], 0x01)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[6], 0x02)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[7], 0x01)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[8], 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(MyMIDI.tracks[0].MIDIdata[0], 0x00)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[1], 0xf0)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[2], 15)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[3], 0x7E)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[4], 0x7F)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[5], 0x08)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[6], 0x02)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[7], 0x00)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[8], 0x2)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[9], 0x1)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[10], 69)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[11], 0x00)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[12], 0x00)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[13], 0x02)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[14], 81)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[15], 0x00)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[16], 0x00)
- self.assertEquals(MyMIDI.tracks[0].MIDIdata[17], 0xf7)
-
-
-MIDISuite = unittest.TestLoader().loadTestsFromTestCase(TestMIDIUtils)
-
-if __name__ == '__main__':
- unittest.TextTestRunner(verbosity=1).run(MIDISuite)
-
diff --git a/src/unittests/test_midi.py b/src/unittests/test_midi.py
new file mode 100755
index 0000000..fa8e3bd
--- /dev/null
+++ b/src/unittests/test_midi.py
@@ -0,0 +1,866 @@
+#!/usr/bin/env python
+#-----------------------------------------------------------------------------
+# Name: miditest.py
+# Purpose: Unit testing harness for midiutil
+#
+# Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
+#
+# Created: 2008/04/17
+# Copyright: (c) 2009-2016, Mark Conway Wirt
+# License: Please see License.txt for the terms under which this
+# software is distributed.
+#-----------------------------------------------------------------------------
+
+
+from __future__ import division, print_function
+import sys, struct
+
+import unittest
+
+from midiutil.MidiFile import *
+
+from midiutil.MidiFile import writeVarLength, \
+ frequencyTransform, returnFrequency, TICKSPERBEAT, MAJOR, MINOR, SHARPS, FLATS, MIDIFile
+
+
+class Decoder(object):
+ '''
+ An immutable comtainer for MIDI data. This is needed bcause if one indexes
+ into a byte string in Python 3 one gets an ``int`` as a return.
+ '''
+ def __init__(self, data):
+ self.data = data.decode("ISO-8859-1")
+
+ def __len__(self):
+ return len(self.data)
+
+ def __getitem__(self, key):
+ return self.data[key].encode("ISO-8859-1")
+
+ def unpack_into_byte(self, key):
+ return struct.unpack('>B', self[key])[0]
+
+class TestMIDIUtils(unittest.TestCase):
+
+ def testWriteVarLength(self):
+ self.assertEqual(writeVarLength(0x70), [0x70])
+ self.assertEqual(writeVarLength(0x80), [0x81, 0x00])
+ self.assertEqual(writeVarLength(0x1FFFFF), [0xFF, 0xFF, 0x7F])
+ self.assertEqual(writeVarLength(0x08000000), [0xC0, 0x80, 0x80, 0x00])
+
+ def testAddNote(self):
+ MyMIDI = MIDIFile(1) # a format 1 file, so we increment the track number below
+ MyMIDI.addNote(0, 0, 100,0,1,100)
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].type, "note")
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].pitch, 100)
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].time, 0)
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].duration, 1)
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].volume, 100)
+
+ def testShiftTrack(self):
+ time = 1
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addNote(0, 0, 100,time,1,100)
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].type, "note")
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].pitch, 100)
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].time, time)
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].duration, 1)
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].volume, 100)
+ MyMIDI.shiftTracks()
+ self.assertEqual(MyMIDI.tracks[1].eventList[0].time, 0)
+
+ 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.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].time, 0)
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].time, TICKSPERBEAT)
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[2].type, 'NoteOn')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[2].time, 0)
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[3].type, 'NoteOff')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[3].time, TICKSPERBEAT * 2)
+
+ def testTimeShift(self):
+
+ # With one track
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addNote(0, 0, 100, 5, 1, 100)
+ MyMIDI.close()
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].time, 0)
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].time, TICKSPERBEAT)
+
+ # 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.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].time, 0)
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].time, TICKSPERBEAT)
+ self.assertEqual(MyMIDI.tracks[2].MIDIEventList[0].type, 'NoteOn')
+ self.assertEqual(MyMIDI.tracks[2].MIDIEventList[0].time, TICKSPERBEAT)
+ self.assertEqual(MyMIDI.tracks[2].MIDIEventList[1].type, 'NoteOff')
+ self.assertEqual(MyMIDI.tracks[2].MIDIEventList[1].time, TICKSPERBEAT)
+
+ # Negative Time
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addNote(0, 0, 100, -5, 1, 100)
+ MyMIDI.close()
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].time, 0)
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].time, TICKSPERBEAT)
+
+ # 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.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].time, 0)
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff')
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].time, TICKSPERBEAT)
+ self.assertEqual(MyMIDI.tracks[2].MIDIEventList[0].type, 'NoteOn')
+ self.assertEqual(MyMIDI.tracks[2].MIDIEventList[0].time, TICKSPERBEAT)
+ self.assertEqual(MyMIDI.tracks[2].MIDIEventList[1].type, 'NoteOff')
+ self.assertEqual(MyMIDI.tracks[2].MIDIEventList[1].time, TICKSPERBEAT)
+
+ def testFrequency(self):
+ freq = frequencyTransform(8.1758)
+ self.assertEqual(freq[0], 0x00)
+ self.assertEqual(freq[1], 0x00)
+ self.assertEqual(freq[2], 0x00)
+ freq = frequencyTransform(8.66196) # 8.6620 in MIDI documentation
+ self.assertEqual(freq[0], 0x01)
+ self.assertEqual(freq[1], 0x00)
+ self.assertEqual(freq[2], 0x00)
+ freq = frequencyTransform(440.00)
+ self.assertEqual(freq[0], 0x45)
+ self.assertEqual(freq[1], 0x00)
+ self.assertEqual(freq[2], 0x00)
+ freq = frequencyTransform(440.0016)
+ self.assertEqual(freq[0], 0x45)
+ self.assertEqual(freq[1], 0x00)
+ self.assertEqual(freq[2], 0x01)
+ freq = frequencyTransform(439.9984)
+ self.assertEqual(freq[0], 0x44)
+ self.assertEqual(freq[1], 0x7f)
+ self.assertEqual(freq[2], 0x7f)
+ freq = frequencyTransform(8372.0190)
+ self.assertEqual(freq[0], 0x78)
+ self.assertEqual(freq[1], 0x00)
+ self.assertEqual(freq[2], 0x00)
+ freq = frequencyTransform(8372.062) #8372.0630 in MIDI documentation
+ self.assertEqual(freq[0], 0x78)
+ self.assertEqual(freq[1], 0x00)
+ self.assertEqual(freq[2], 0x01)
+ freq = frequencyTransform(13289.7300)
+ self.assertEqual(freq[0], 0x7F)
+ self.assertEqual(freq[1], 0x7F)
+ self.assertEqual(freq[2], 0x7E)
+ freq = frequencyTransform(12543.8760)
+ self.assertEqual(freq[0], 0x7F)
+ self.assertEqual(freq[1], 0x00)
+ self.assertEqual(freq[2], 0x00)
+ freq = frequencyTransform(8.2104) # Just plain wrong in documentation, as far as I can tell.
+ #self.assertEqual(freq[0], 0x0)
+ #self.assertEqual(freq[1], 0x0)
+ #self.assertEqual(freq[2], 0x1)
+
+ # Test the inverse
+ testFreq = 15.0
+ accuracy = 0.00001
+ x = returnFrequency(frequencyTransform(testFreq))
+ delta = abs(testFreq - x)
+ self.assertEqual(delta < (accuracy*testFreq), True)
+ testFreq = 200.0
+ x = returnFrequency(frequencyTransform(testFreq))
+ delta = abs(testFreq - x)
+ self.assertEqual(delta < (accuracy*testFreq), True)
+ testFreq = 400.0
+ x = returnFrequency(frequencyTransform(testFreq))
+ delta = abs(testFreq - x)
+ self.assertEqual(delta < (accuracy*testFreq), True)
+ testFreq = 440.0
+ x = returnFrequency(frequencyTransform(testFreq))
+ delta = abs(testFreq - x)
+ self.assertEqual(delta < (accuracy*testFreq), True)
+ testFreq = 1200.0
+ x = returnFrequency(frequencyTransform(testFreq))
+ delta = abs(testFreq - x)
+ self.assertEqual(delta < (accuracy*testFreq), True)
+ testFreq = 5000.0
+ x = returnFrequency(frequencyTransform(testFreq))
+ delta = abs(testFreq - x)
+ self.assertEqual(delta < (accuracy*testFreq), True)
+ testFreq = 12000.0
+ x = returnFrequency(frequencyTransform(testFreq))
+ delta = abs(testFreq - x)
+ self.assertEqual(delta < (accuracy*testFreq), True)
+
+
+ def testSysEx(self):
+ #import pdb; pdb.set_trace()
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addSysEx(0,0, 0, struct.pack('>B', 0x01))
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'SysEx')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00)
+ self.assertEqual(data.unpack_into_byte(1), 0xf0)
+ self.assertEqual(data.unpack_into_byte(2), 3)
+ self.assertEqual(data.unpack_into_byte(3), 0x00)
+ self.assertEqual(data.unpack_into_byte(4), 0x01)
+ self.assertEqual(data.unpack_into_byte(5), 0xf7)
+
+ def testTempo(self):
+ #import pdb; pdb.set_trace()
+ tempo = 60
+ MyMIDI = MIDIFile(1, file_format=2)
+ MyMIDI.addTempo(0, 0, tempo)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[0].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[0].MIDIEventList[0].type, 'Tempo')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xff) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x51)
+ self.assertEqual(data.unpack_into_byte(3), 0x03)
+ self.assertEqual(data[4:7], struct.pack('>L', int(60000000/tempo))[1:4])
+
+ # Also check the format 1 file
+
+ tempo = 60
+ MyMIDI = MIDIFile(2, file_format=1)
+ MyMIDI.addTempo(1, 0, tempo)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[0].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[0].MIDIEventList[0].type, 'Tempo')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xff) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x51)
+ self.assertEqual(data.unpack_into_byte(3), 0x03)
+ self.assertEqual(data[4:7], struct.pack('>L', int(60000000/tempo))[1:4])
+
+ def testCopyright(self):
+ #import pdb; pdb.set_trace()
+ notice ="2016(C) MCW"
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addCopyright(0, 0, notice)
+ MyMIDI.close()
+
+ payload_encoded = notice.encode("ISO-8859-1")
+ payloadLength = len(payload_encoded)
+ payloadLengthVar = writeVarLength(payloadLength)
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'Copyright')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xff) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x02) # Subcode
+ index = 3
+ for i in range(len(payloadLengthVar)):
+ self.assertEqual(data.unpack_into_byte(index), payloadLengthVar[i])
+ index = index + 1
+ for i in range(len(payload_encoded)):
+ if sys.version_info < (3,):
+ test_char = ord(payload_encoded[i])
+ else:
+ test_char = payload_encoded[i]
+ self.assertEqual(data.unpack_into_byte(index), test_char)
+ index = index + 1
+
+ def testText(self):
+ #import pdb; pdb.set_trace()
+ text ="2016(C) MCW"
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addText(0, 0, text)
+ MyMIDI.close()
+
+ payload_encoded = text.encode("ISO-8859-1")
+ payloadLength = len(payload_encoded)
+ payloadLengthVar = writeVarLength(payloadLength)
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'Text')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xff) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x01) # Subcode
+ index = 3
+ for i in range(len(payloadLengthVar)):
+ self.assertEqual(data.unpack_into_byte(index), payloadLengthVar[i])
+ index = index + 1
+ for i in range(len(payload_encoded)):
+ if sys.version_info < (3,):
+ test_char = ord(payload_encoded[i])
+ else:
+ test_char = payload_encoded[i]
+ self.assertEqual(data.unpack_into_byte(index), test_char)
+ index = index + 1
+
+ def testTimeSignature(self):
+ time = 0
+ track = 0
+ numerator = 4
+ denominator = 2
+ clocks_per_tick = 24
+ MyMIDI = MIDIFile(1, file_format=2)
+ MyMIDI.addTimeSignature(track, time, numerator, denominator, clocks_per_tick)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[0].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[0].MIDIEventList[0].type, 'TimeSignature')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xFF) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x58) # subcode
+ self.assertEqual(data.unpack_into_byte(3), 0x04) # Data length
+ self.assertEqual(data.unpack_into_byte(4), numerator)
+ self.assertEqual(data.unpack_into_byte(5), denominator)
+ self.assertEqual(data.unpack_into_byte(6), clocks_per_tick) # Data length
+ self.assertEqual(data.unpack_into_byte(7), 0x08) # 32nd notes per quarter note
+
+ # We also want to check with a format 1 file, make sure it ends up in
+ # the tempo track
+
+ time = 0
+ track = 1
+ numerator = 4
+ denominator = 2
+ clocks_per_tick = 24
+ MyMIDI = MIDIFile(2, file_format=1)
+ MyMIDI.addTimeSignature(track, time, numerator, denominator, clocks_per_tick)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[0].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[0].MIDIEventList[0].type, 'TimeSignature')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xFF) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x58) # subcode
+ self.assertEqual(data.unpack_into_byte(3), 0x04) # Data length
+ self.assertEqual(data.unpack_into_byte(4), numerator)
+ self.assertEqual(data.unpack_into_byte(5), denominator)
+ self.assertEqual(data.unpack_into_byte(6), clocks_per_tick) # Data length
+ self.assertEqual(data.unpack_into_byte(7), 0x08) # 32nd notes per quarter note
+
+ def testKeySignature(self):
+ time = 0
+ track = 0
+ accidentals = 3
+ accidental_type = MINOR
+ mode = MAJOR
+
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addKeySignature(track, time, accidentals, accidental_type, mode)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'KeySignature')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xFF) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x59) # subcode
+ self.assertEqual(data.unpack_into_byte(3), 0x02) # Event subtype
+ self.assertEqual(data.unpack_into_byte(4), accidentals * accidental_type)
+ self.assertEqual(data.unpack_into_byte(5), mode)
+
+ def testProgramChange(self):
+ #import pdb; pdb.set_trace()
+ program = 10
+ channel = 0
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addProgramChange(0, channel, 0, program)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[0].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[0].MIDIEventList[0].type, 'ProgramChange')
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xC << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(2), program)
+
+ def testTrackName(self):
+ #import pdb; pdb.set_trace()
+ track_name = "track"
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addTrackName(0, 0, track_name)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'TrackName')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xFF) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x03) # subcodes
+
+ def testTuningBank(self):
+ #import pdb; pdb.set_trace()
+ bank = 1
+ channel = 0
+ MyMIDI = MIDIFile(1)
+ MyMIDI.changeTuningBank(0, 0, 0, bank)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'ControllerEvent')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x65) # Controller Number
+ self.assertEqual(data.unpack_into_byte(3), 0x0) # Controller Value
+ self.assertEqual(data.unpack_into_byte(4), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(5), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(6), 0x64) # Controller Number
+ self.assertEqual(data.unpack_into_byte(7), 0x4) # Controller Value
+ self.assertEqual(data.unpack_into_byte(8), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(9), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(10), 0x06) # Bank MSB
+ self.assertEqual(data.unpack_into_byte(11), 0x00) # Value
+ self.assertEqual(data.unpack_into_byte(12), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(13), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(14), 0x26) # Bank LSB
+ self.assertEqual(data.unpack_into_byte(15), bank) # Bank value (bank number)
+
+ def testTuningBankWithTimeOrder(self):
+ #import pdb; pdb.set_trace()
+ bank = 1
+ MyMIDI = MIDIFile(1)
+ MyMIDI.changeTuningBank(0, 0, 0, bank, time_order=True)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'ControllerEvent')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(4), 0x01) # time
+ self.assertEqual(data.unpack_into_byte(8), 0x01) # time
+ self.assertEqual(data.unpack_into_byte(12), 0x01) # time
+
+ def testTuningProgram(self):
+ #import pdb; pdb.set_trace()
+ program = 10
+ channel = 0
+ MyMIDI = MIDIFile(1)
+ MyMIDI.changeTuningProgram(0, 0, 0, program)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'ControllerEvent')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(2), 0x65) # Controller Number
+ self.assertEqual(data.unpack_into_byte(3), 0x0) # Controller Value
+ self.assertEqual(data.unpack_into_byte(4), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(5), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(6), 0x64) # Controller Number
+ self.assertEqual(data.unpack_into_byte(7), 0x03) # Controller Value
+ self.assertEqual(data.unpack_into_byte(8), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(9), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(10), 0x06) # Bank MSB
+ self.assertEqual(data.unpack_into_byte(11), 0x00) # Value
+ self.assertEqual(data.unpack_into_byte(12), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(13), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(14), 0x26) # Bank LSB
+ self.assertEqual(data.unpack_into_byte(15), program) # Bank value (bank number)
+
+ def testTuningProgramWithTimeOrder(self):
+ #import pdb; pdb.set_trace()
+ program = 10
+ MyMIDI = MIDIFile(1)
+ MyMIDI.changeTuningProgram(0, 0, 0, program, time_order=True)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'ControllerEvent')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(4), 0x01) # time
+ self.assertEqual(data.unpack_into_byte(8), 0x01) # time
+ self.assertEqual(data.unpack_into_byte(12), 0x01) # time
+
+
+
+ def testNRPNCall(self):
+ #import pdb; pdb.set_trace()
+ track = 0
+ time = 0
+ channel = 0
+ controller_msb = 1
+ controller_lsb = 2
+ data_msb = 3
+ data_lsb = 4
+ MyMIDI = MIDIFile(1)
+ MyMIDI.makeNRPNCall(track, channel, time, controller_msb, controller_lsb, data_msb, data_lsb)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'ControllerEvent')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(2), 99) # Controller Number
+ self.assertEqual(data.unpack_into_byte(3), controller_msb) # Controller Value
+ self.assertEqual(data.unpack_into_byte(4), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(5), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(6), 98) # Controller Number
+ self.assertEqual(data.unpack_into_byte(7), controller_lsb) # Controller Value
+ self.assertEqual(data.unpack_into_byte(8), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(9), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(10), 0x06) # Bank MSB
+ self.assertEqual(data.unpack_into_byte(11), data_msb) # Value
+ self.assertEqual(data.unpack_into_byte(12), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(13), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(14), 0x26) # Bank LSB
+ self.assertEqual(data.unpack_into_byte(15), data_lsb) # Bank value (bank number)
+
+ def testNRPNCallWithTimeOrder(self):
+ #import pdb; pdb.set_trace()
+ track = 0
+ time = 0
+ channel = 0
+ controller_msb = 1
+ controller_lsb = 2
+ data_msb = 3
+ data_lsb = 4
+ MyMIDI = MIDIFile(1)
+ MyMIDI.makeNRPNCall(track, channel, time, controller_msb, controller_lsb, data_msb, data_lsb, time_order=True)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'ControllerEvent')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(2), 99) # Controller Number
+ self.assertEqual(data.unpack_into_byte(3), controller_msb) # Controller Value
+ self.assertEqual(data.unpack_into_byte(4), 0x01) # time
+ self.assertEqual(data.unpack_into_byte(5), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(6), 98) # Controller Number
+ self.assertEqual(data.unpack_into_byte(7), controller_lsb) # Controller Value
+ self.assertEqual(data.unpack_into_byte(8), 0x01) # time
+ self.assertEqual(data.unpack_into_byte(9), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(10), 0x06) # Bank MSB
+ self.assertEqual(data.unpack_into_byte(11), data_msb) # Value
+ self.assertEqual(data.unpack_into_byte(12), 0x01) # time
+ self.assertEqual(data.unpack_into_byte(13), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(14), 0x26) # Bank LSB
+ self.assertEqual(data.unpack_into_byte(15), data_lsb) # Bank value (bank number)
+
+ def testAddControllerEvent(self):
+ #import pdb; pdb.set_trace()
+ track = 0
+ time = 0
+ channel = 3
+ controller_number = 1
+ parameter = 2
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addControllerEvent(track, channel, time, controller_number, parameter)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'ControllerEvent')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # time
+ self.assertEqual(data.unpack_into_byte(1), 0xB << 4 | channel) # Code
+ self.assertEqual(data.unpack_into_byte(2), controller_number) # Controller Number
+ self.assertEqual(data.unpack_into_byte(3), parameter) # Controller Value
+
+ def testNonRealTimeUniversalSysEx(self):
+ code = 1
+ subcode = 2
+ payload_number = 42
+
+ payload = struct.pack('>B', payload_number)
+
+ MyMIDI = MIDIFile(1, adjust_origin=False)
+
+ # Just for fun we'll use a multi-byte time
+ time = 1
+ time_bytes = writeVarLength(time*TICKSPERBEAT)
+ MyMIDI.addUniversalSysEx(0, time, code, subcode, payload, realTime=False)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'UniversalSysEx')
+
+ self.assertEqual(data.unpack_into_byte(0), time_bytes[0]) # Time
+ self.assertEqual(data.unpack_into_byte(1), time_bytes[1]) # Time
+ self.assertEqual(data.unpack_into_byte(2), 0xf0) # UniversalSysEx == 0xF0
+ self.assertEqual(data.unpack_into_byte(3), 5 + len(payload)) # Payload length = 5+actual pyayload
+ self.assertEqual(data.unpack_into_byte(4), 0x7E) # 0x7E == non-realtime
+ self.assertEqual(data.unpack_into_byte(5), 0x7F) # Sysex channel (always 0x7F)
+ self.assertEqual(data.unpack_into_byte(6), code)
+ self.assertEqual(data.unpack_into_byte(7), subcode)
+ self.assertEqual(data.unpack_into_byte(8), payload_number) # Data
+ self.assertEqual(data.unpack_into_byte(9), 0xf7) # End of message
+
+ def testRealTimeUniversalSysEx(self):
+ code = 1
+ subcode = 2
+ payload_number = 47
+
+ payload = struct.pack('>B', payload_number)
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addUniversalSysEx(0, 0, code, subcode, payload, realTime=True)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'UniversalSysEx')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00)
+ self.assertEqual(data.unpack_into_byte(1), 0xf0)
+ self.assertEqual(data.unpack_into_byte(2), 5 + len(payload))
+ self.assertEqual(data.unpack_into_byte(3), 0x7F) # 0x7F == real-time
+ self.assertEqual(data.unpack_into_byte(4), 0x7F)
+ self.assertEqual(data.unpack_into_byte(5), code)
+ self.assertEqual(data.unpack_into_byte(6), subcode)
+ self.assertEqual(data.unpack_into_byte(7), payload_number)
+ self.assertEqual(data.unpack_into_byte(8), 0xf7)
+
+ def testTuning(self):
+ MyMIDI = MIDIFile(1)
+ MyMIDI.changeNoteTuning(0, [(1, 440), (2, 880)])
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].type, 'UniversalSysEx')
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00)
+ self.assertEqual(data.unpack_into_byte(1), 0xf0)
+ self.assertEqual(data.unpack_into_byte(2), 15)
+ self.assertEqual(data.unpack_into_byte(3), 0x7F)
+ self.assertEqual(data.unpack_into_byte(4), 0x7F)
+ self.assertEqual(data.unpack_into_byte(5), 0x08)
+ self.assertEqual(data.unpack_into_byte(6), 0x02)
+ self.assertEqual(data.unpack_into_byte(7), 0x00)
+ self.assertEqual(data.unpack_into_byte(8), 0x2)
+ self.assertEqual(data.unpack_into_byte(9), 0x1)
+ self.assertEqual(data.unpack_into_byte(10), 69)
+ self.assertEqual(data.unpack_into_byte(11), 0)
+ self.assertEqual(data.unpack_into_byte(12), 0)
+ self.assertEqual(data.unpack_into_byte(13), 0x2)
+ self.assertEqual(data.unpack_into_byte(14), 81)
+ self.assertEqual(data.unpack_into_byte(15), 0)
+ self.assertEqual(data.unpack_into_byte(16), 0)
+ self.assertEqual(data.unpack_into_byte(17), 0xf7)
+
+ def testWriteFile(self):
+ # Just to make sure the stream can be written without throwing an error.
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addNote(0, 0, 100,0,1,100)
+ with open("/tmp/test.mid", "wb") as output_file:
+ MyMIDI.writeFile(output_file)
+
+ def testAdujustOrigin(self):
+ track = 0
+ channel = 0
+ pitch = 69
+ time = 1
+ duration = 0.1
+ volume = 64
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ time = 1.1
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(data.unpack_into_byte(0), 0x00) # first time
+ self.assertEqual(data.unpack_into_byte(8), 0x00) # seconds time
+
+ MyMIDI = MIDIFile(1, adjust_origin=False)
+ time = 0.1
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ time = 0.2
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ MyMIDI.close()
+
+ data = Decoder(MyMIDI.tracks[1].MIDIdata)
+
+ self.assertEqual(data.unpack_into_byte(0), TICKSPERBEAT/10) # first time, should be an integer < 127
+ self.assertEqual(data.unpack_into_byte(8), 0x00) # first time
+
+ def testMultiClose(self):
+ track = 0
+ channel = 0
+ pitch = 69
+ time = 0
+ duration = 1.0
+ volume = 64
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ MyMIDI.close()
+ data_length_1 = len(MyMIDI.tracks[0].MIDIdata)
+ MyMIDI.close()
+ data_length_2 = len(MyMIDI.tracks[0].MIDIdata)
+
+ self.assertEqual(data_length_1, data_length_2)
+ MyMIDI.tracks[0].closeTrack()
+ data_length_3 = len(MyMIDI.tracks[0].MIDIdata)
+ self.assertEqual(data_length_1, data_length_3)
+
+ def testEmptyEventList(self):
+ MyMIDI = MIDIFile(1)
+ MyMIDI.close()
+ data_length = len(MyMIDI.tracks[0].MIDIdata)
+ self.assertEqual(data_length, 4) # Header length 4
+
+ def testUnknownEventType(self):
+ track = 0
+ channel = 0
+ pitch = 69
+ time = 0
+ duration = 1.0
+ volume = 64
+ bad_type = "bad"
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ MyMIDI.tracks[1].eventList[0].type = bad_type
+ with self.assertRaises(Exception) as context:
+ MyMIDI.close()
+ self.assertTrue(('Error in MIDITrack: Unknown event type %s' % bad_type) in str(context.exception))
+
+ def testRemoveDuplicates(self):
+ # First notes
+ track = 0
+ channel = 0
+ pitch = 69
+ time = 0
+ duration = 1
+ volume = 64
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ MyMIDI.close()
+ self.assertEqual(1, len(MyMIDI.tracks[1].eventList)) # One event
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ pitch = 70
+ MyMIDI.addNote(track, channel, pitch, time, duration, volume)
+ MyMIDI.close()
+ self.assertEqual(2, len(MyMIDI.tracks[1].eventList)) # Two events
+
+ # Next tempo
+ tempo = 60
+ track = 0
+ time = 0
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addTempo(track, time, tempo)
+ MyMIDI.addTempo(track, time, tempo)
+ MyMIDI.close()
+ self.assertEqual(1, len(MyMIDI.tracks[0].eventList))
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addTempo(track, time, tempo)
+ tempo = 80
+ MyMIDI.addTempo(track, time, tempo)
+ MyMIDI.close()
+ self.assertEqual(2, len(MyMIDI.tracks[0].eventList))
+
+ # Program Number
+ time = 0
+ track = 0
+ program = 10
+ channel = 0
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addProgramChange(track, channel, time, program)
+ MyMIDI.addProgramChange(track, channel, time, program)
+ MyMIDI.close()
+ self.assertEqual(1, len(MyMIDI.tracks[0].eventList))
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addProgramChange(track, channel, time, program)
+ program = 11
+ MyMIDI.addProgramChange(track, channel, time, program)
+ MyMIDI.close()
+ self.assertEqual(2, len(MyMIDI.tracks[0].eventList))
+
+ # Track Name
+ track = 0
+ time = 0
+ track_name = "track"
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addTrackName(track, time, track_name)
+ MyMIDI.addTrackName(track, time, track_name)
+ MyMIDI.close()
+ self.assertEqual(1, len(MyMIDI.tracks[1].eventList))
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addTrackName(track, time, track_name)
+ track_name = "track 2"
+ MyMIDI.addTrackName(track, time, track_name)
+ MyMIDI.close()
+ self.assertEqual(2, len(MyMIDI.tracks[1].eventList))
+
+ # SysEx. These are never removed
+ track = 0
+ time = 0
+ manufacturer = 10
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addSysEx(track,time, manufacturer, struct.pack('>B', 0x01))
+ MyMIDI.addSysEx(track,time, manufacturer, struct.pack('>B', 0x01))
+ MyMIDI.close()
+ self.assertEqual(2, len(MyMIDI.tracks[1].eventList))
+
+ # UniversalSysEx. Same thing -- never remove
+
+ track = 0
+ time = 0
+ code = 1
+ subcode = 2
+ payload_number = 47
+
+ payload = struct.pack('>B', payload_number)
+ MyMIDI = MIDIFile(1)
+ MyMIDI.addUniversalSysEx(track, time, code, subcode, payload, realTime=True)
+ MyMIDI.addUniversalSysEx(track, time, code, subcode, payload, realTime=True)
+ MyMIDI.close()
+ self.assertEqual(2, len(MyMIDI.tracks[1].eventList))
+
+def suite():
+ MIDISuite = unittest.TestLoader().loadTestsFromTestCase(TestMIDIUtils)
+
+ return MIDISuite
+
+if __name__ == '__main__':
+ print("Begining MIDIUtil Test Suite")
+ MIDISuite = suite()
+ runner = unittest.TextTestRunner(verbosity=2, stream=sys.stdout)
+ return_value = not runner.run(MIDISuite).wasSuccessful()
+ sys.exit(return_value)
+
+
--
python-midiutil packaging
More information about the pkg-multimedia-commits
mailing list