pax_global_header00006660000000000000000000000064130573136610014517gustar00rootroot0000000000000052 comment=f0b0e9cdba160b5618f5063c3c15bfc0b5ff8ee7 MIDIUtil-1.1.3/000077500000000000000000000000001305731366100131015ustar00rootroot00000000000000MIDIUtil-1.1.3/.gitignore000066400000000000000000000000311305731366100150630ustar00rootroot00000000000000*.pyc *.swp _build dist MIDIUtil-1.1.3/.travis.yml000066400000000000000000000002701305731366100152110ustar00rootroot00000000000000language: 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 MIDIUtil-1.1.3/CHANGELOG000066400000000000000000000105331305731366100143150ustar00rootroot00000000000000Date: 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. * Changed default temporal resolution to 960 ticks per beat. * Updated Python3 support. It is still somewhat experimental. * Misc. Bug Fixes. Date: 20 October 2009 Version: 0.87 First public release. * Tweaked email address in contact information. * Added/updated documentation. * Tweaked the setup.py file to produce better distributions. Date: 9 October 2009 Version: 0.86 * added addNote as main interface into package (not addNoteByNumber). It's been a while since I've cut a release, so there may be other things that have happened. * Created distutils package. * Minor code clean-up. * Added documentation in-line and in text (MIDIFile.txt). * All public functions should now be accessed thought MIDIFile directly, and not the component tracks. Date: 15 January 2009 Version: 0.85 * Split out from existing work as a separate project. MIDIUtil-1.1.3/License.txt000066400000000000000000000025351305731366100152310ustar00rootroot00000000000000-------------------------------------------------------------------------- MIDUTIL, Copyright (c) 2009-2016, Mark Conway Wirt This software is distributed under an Open Source license, the details of which follow. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------- MIDIUtil-1.1.3/MANIFEST.in000066400000000000000000000003431305731366100146370ustar00rootroot00000000000000include *.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 * MIDIUtil-1.1.3/README.rst000066400000000000000000000133561305731366100146000ustar00rootroot00000000000000MIDIUtil ======== |build| This is just a brief adumbration. Full documentation for the development version can be found at `Read the Docs `_. |docs| The documentation for the latest stable release is `here `_. 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 `__ and can be installed via the normal channels: .. code:: bash pip install MIDIUtil Source code is available on `Github `__ , and be cloned with one of the following URLS: .. code:: bash git clone git@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 `__ 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 MIDIUtil-1.1.3/VERSION000066400000000000000000000000261305731366100141470ustar00rootroot00000000000000This is version HEAD MIDIUtil-1.1.3/documentation/000077500000000000000000000000001305731366100157525ustar00rootroot00000000000000MIDIUtil-1.1.3/documentation/Makefile000066400000000000000000000166761305731366100174320ustar00rootroot00000000000000# 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 ' where 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." MIDIUtil-1.1.3/documentation/class.rst000066400000000000000000000006051305731366100176120ustar00rootroot00000000000000.. _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 MIDIUtil-1.1.3/documentation/common.rst000066400000000000000000000061451305731366100200020ustar00rootroot00000000000000Common 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 `_, 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. MIDIUtil-1.1.3/documentation/conf.py000066400000000000000000000232361305731366100172570ustar00rootroot00000000000000#!/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. # " v 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 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 MIDIUtil-1.1.3/documentation/creating.rst000066400000000000000000000112061305731366100203000ustar00rootroot00000000000000Creating 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 `_ 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. MIDIUtil-1.1.3/documentation/extending.rst000066400000000000000000000234531305731366100205000ustar00rootroot00000000000000Extending 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! MIDIUtil-1.1.3/documentation/index.rst000066400000000000000000000124771305731366100176260ustar00rootroot00000000000000.. 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 `__ and can be installed via the normal channels: .. code:: bash pip install MIDIUtil Source code is available on `Github `__ , and be cloned with one of the following URLS: .. code:: bash git clone git@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 `__ 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` MIDIUtil-1.1.3/documentation/tuning.rst000066400000000000000000000040461305731366100200140ustar00rootroot00000000000000Tuning 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 `_, this is all you need to do: timidity will apply the tuning change to the notes. Other instruments, such as `fluidsynth `_, 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. MIDIUtil-1.1.3/examples/000077500000000000000000000000001305731366100147175ustar00rootroot00000000000000MIDIUtil-1.1.3/examples/c-major-scale.py000077500000000000000000000010311305731366100177040ustar00rootroot00000000000000#!/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) MIDIUtil-1.1.3/examples/single-note-example.py000077500000000000000000000015151305731366100211530ustar00rootroot00000000000000############################################################################ # A sample program to create a single-track MIDI file, add a note, # and write to disk. ############################################################################ # Import the library from midiutil import MIDIFile # Create the MIDIFile Object MyMIDI = MIDIFile(1) # Add track name and tempo. The first argument to addTrackName and # addTempo is the time to write the event. track = 0 time = 0 MyMIDI.addTrackName(track, time, "Sample Track") MyMIDI.addTempo(track, time, 120) # Add a note. addNote expects the following information: channel = 0 pitch = 60 duration = 1 volume = 100 # Now add the note. MyMIDI.addNote(track, channel, pitch, time, duration, volume) # And write it to disk. with open("output.mid", 'wb') as binfile: MyMIDI.writeFile(binfile) MIDIUtil-1.1.3/setup.py000066400000000000000000000027211305731366100146150ustar00rootroot00000000000000from setuptools import setup, find_packages with open('README.rst') as file: long_description = file.read() setup(name='MIDIUtil', version='1.1.3', description='A pure python library for creating multi-track MIDI files', author='Mark Conway Wirt', 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', 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 ) MIDIUtil-1.1.3/src/000077500000000000000000000000001305731366100136705ustar00rootroot00000000000000MIDIUtil-1.1.3/src/midiutil/000077500000000000000000000000001305731366100155105ustar00rootroot00000000000000MIDIUtil-1.1.3/src/midiutil/MidiFile.py000066400000000000000000001737641305731366100175660ustar00rootroot00000000000000# ----------------------------------------------------------------------------- # Name: MidiFile.py # Purpose: MIDI file manipulation utilities # # Author: Mark Conway Wirt # # 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 math import struct import warnings __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} # 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). ''' 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(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. ''' 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 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. ''' # 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 Note(GenericEvent): ''' 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 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', 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, annotation=None, insertion_order=0): ''' 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(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(Tempo(time, tempo, insertion_order=insertion_order)) def addSysEx(self, time, manID, payload, insertion_order=0): ''' Add a SysEx event. ''' 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(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(ProgramChange(channel, time, program, insertion_order=insertion_order)) def addTrackName(self, time, trackName, insertion_order=0): ''' Add a track name event. ''' 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)) 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(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("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) elif thing.type == 'tempo': event = MIDIEvent("Tempo", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) event.tempo = thing.tempo 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("ProgramChange", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) event.programNumber = thing.programNumber event.channel = thing.channel self.MIDIEventList.append(event) elif thing.type == 'trackName': event = MIDIEvent("TrackName", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) event.trackName = thing.trackName self.MIDIEventList.append(event) elif thing.type == 'controllerEvent': event = MIDIEvent("ControllerEvent", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) event.controller_number = thing.controller_number event.channel = thing.channel event.parameter = thing.parameter self.MIDIEventList.append(event) elif thing.type == 'SysEx': event = MIDIEvent("SysEx", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) event.manID = thing.manID event.payload = thing.payload self.MIDIEventList.append(event) elif thing.type == 'UniversalSysEx': event = MIDIEvent("UniversalSysEx", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) event.realTime = thing.realTime event.sysExChannel = thing.sysExChannel event.code = thing.code event.subcode = thing.subcode event.payload = thing.payload 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: raise ValueError("Error in MIDITrack: Unknown event type %s" % thing.type) # Assumptions in the code expect the list to be time-sorted. self.MIDIEventList.sort(key=sort_events) 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 = {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 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: 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 += 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 = b"" varTime = writeVarLength(event.time) for timeByte in varTime: testBuffer = testBuffer + struct.pack('>B', timeByte) (roundedVal, discard) = readVarLength(0, testBuffer) roundedTime = actualTime + roundedVal # 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 = b"" 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 += 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 += 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 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 += 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 += 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 += 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 += 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 += 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 += struct.pack('>B', timeByte) self.MIDIdata += struct.pack('>B', code) payloadLength = writeVarLength(len(event.payload)+2) for lenByte in payloadLength: 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 += 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 += struct.pack('>B', lenByte) if event.realTime: self.MIDIdata += struct.pack('>B', 0x7F) else: 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. 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 # 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(key=sort_events) def adjustTimeAndOrigin(self, origin, adjust): ''' 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 = [] internal_origin = origin if adjust else 0.0 runningTime = 0 for event in self.MIDIEventList: adjustedTime = event.time - internal_origin event.time = adjustedTime - runningTime runningTime = adjustedTime tempEventList.append(event) self.MIDIEventList = tempEventList def writeTrack(self, fileHandle): ''' Write track to disk. ''' fileHandle.write(self.headerString) fileHandle.write(self.dataLength) fileHandle.write(self.MIDIdata) 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, file_format): ''' 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', 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): fileHandle.write(self.headerString) fileHandle.write(self.headerSize) fileHandle.write(self.format) fileHandle.write(self.numTracks) fileHandle.write(self.ticksPerBeat) 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=1, removeDuplicates=True, deinterleave=True, adjust_origin=None, file_format=1): ''' 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, file_format) self.tracks = list() if file_format == 1: delta = 1 else: delta = 0 self.numTracks = numTracks + delta self.closed = False 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)) # 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 :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 `_ orchestra files directly from the class ``EventList``. """ 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): """ 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] """ 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 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] """ 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 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] """ 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 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] """ 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): """ 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. """ 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): ''' 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. ''' 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 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 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. ''' 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, 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. """ 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: 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(key=sort_events) origin = self.findOrigin() 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 # 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] # readVarLength is taken from the MidiFile class. 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 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)) 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. ''' 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 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) MIDIUtil-1.1.3/src/midiutil/__init__.py000066400000000000000000000001351305731366100176200ustar00rootroot00000000000000from midiutil.MidiFile import * __all__ = ['MIDIFile', 'MAJOR', 'MINOR', 'SHARPS', 'FLATS'] MIDIUtil-1.1.3/src/unittests/000077500000000000000000000000001305731366100157325ustar00rootroot00000000000000MIDIUtil-1.1.3/src/unittests/test_midi.py000077500000000000000000001106151305731366100202740ustar00rootroot00000000000000#!/usr/bin/env python #----------------------------------------------------------------------------- # Name: miditest.py # Purpose: Unit testing harness for midiutil # # Author: Mark Conway Wirt # # 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)