pax_global_header00006660000000000000000000000064132470402640014514gustar00rootroot0000000000000052 comment=302c61094460aee87b6b678e81514c17d014e996 MIDIUtil-1.2.1/000077500000000000000000000000001324704026400130755ustar00rootroot00000000000000MIDIUtil-1.2.1/.gitignore000066400000000000000000000000471324704026400150660ustar00rootroot00000000000000*.pyc *.swp _build dist venv *egg-info MIDIUtil-1.2.1/.travis.yml000066400000000000000000000002141324704026400152030ustar00rootroot00000000000000language: python python: - "2.7" - "3.5" - "3.6" install: - python setup.py -q install script: python src/unittests/test_midi.py MIDIUtil-1.2.1/CHANGELOG000066400000000000000000000122031324704026400143050ustar00rootroot00000000000000Date: 4 March 2018 Version: 1.2.1 * Fairly major restructuring of the code contributed by meteorsw, including: * Removing the MIDIEvent class and subsuming functionality in GenericEvent and derivative classes. * Allowing for the specification of times in MIDI ticks, as well as quarter notes. * General clean-up and restructuring. Logic on how an event serializes itself to the MIDI stream is now associated directly with the object. * Renaming of some variable to be less confusing to users. * Placing comparison operators in the derived classes of GenericEvent, where they really belong. * Addition of channel pressure event. * Clarifications on documentation. * Added pitch bend support with `addPitchWheelEvent`. Date: 6 March 2017 Version: 1.1.3 A code clean-up release. Not much in the way of new features. * Documentation changes, typo corrections, etc. * Simplify the importation of the library. It's now possible to just: from midiutil import MIDIFile which seems somewhat easier. * Some formatting and cleanup work (PEP-ish stuff). * Added Python 3.6 testing/support. * Removed support for Python 2.6. All good things eventually come to an end Date: 28 September 2016 Version: 1.1.1 * Just a couple of typo's corrected (what's a "but fix" anyway?), and the display of README.rst on Pypi. Version: 1.1.0 * Allow for the specification of either MIDI format 1 or 2 files. Note that in previous versions of the code the file being written was format 2, but it was identified as format 1. This error has been corrected. The default format for the file is 1, which is the most widely supported format. * Increased test coverage. * Fixed bug in MIDIFile for `adjust_origin` = `False` * Added ability to order RPC and NRPC sub-events in time, as a work-around for sequencers that aren't good about preserving event ordering for events at the time time. * Updates to documentation. * Added Travis CI to the build process. Verifying operation on python 2.6 - 3 development version. * Functions added: * `addTimeSignature()` * `addCopyright()` * `addText()` * `addKeySignature()` Date: 23 September 2016 Version: 1.0.1 * Minor updates to build system so that code can be hosted at Pypi (https://pypi.python.org/pypi/MIDIUtil/). Date: 23 September 2016 Version: 1.0.0 * Code ported to GitHub * Extensive updates to documentation, ported to Sphinx. * Added ability to *not* adjust the MIDIFile's time origin. Default behaviour is maintained, but will change in a future version (by default the origin is currently adjusted). * Changed the controller event parameter names to make them clearer. * Added support for Registered and Non-Registered parameter calls (``makeRPNCall`` and ``makeNRPNCall``). * General refactoring and clean-up. * Added function to select tuning program and bank. Some synthesizers, such as fluidsynth, require that uploaded tunings be explicitly assigned to a channel. This can be used after ``setNoteTuning`` in such a case. * Completed port to Python 3 / Unification of code base. Support for python < 2.6 has been dropped so that the Python 2 and 3 codebases could be unified. * Changes the way that sorting works, simplifying it and making it more expressive. The primary sort is on time; secondary on ordinality for the event (which is user-definable, but defaults to an ordinality for the class); and the third is the order in which the events were added. Thus is becomes easier to, say, make an RPN call, which entails and ordered series of control change events all occurring at the same time and of the same type. * Added 'annotation' as a parameter to note addition function. This can be used to attach an arbitrary python object to the note event. This is useful for extension development. Date: 1 December 2013 Version: 0.89 * Updated MIDIFile to support non-integral note values better. * 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.2.1/License.txt000066400000000000000000000025351324704026400152250ustar00rootroot00000000000000-------------------------------------------------------------------------- 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.2.1/MANIFEST.in000066400000000000000000000003431324704026400146330ustar00rootroot00000000000000include *.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.2.1/README.rst000066400000000000000000000135411324704026400145700ustar00rootroot00000000000000MIDIUtil ======== |build| This is just a brief adumbration. Full documentation for the release version can be found at `Read the Docs `_. The documentation for the development version is `here `_. |docs| 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). * Philippe-Adrien Nousse (Adphi) for the pitch bend implementation. * meteorsw (https://github.com/meteorsw) for major restructuring and clean-up of code. 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=1.1.3 :target: http://midiutil.readthedocs.io/ :alt: Documentation Status .. |build| image:: https://travis-ci.org/MarkCWirt/MIDIUtil.svg?branch=master :target: https://travis-ci.org/MarkCWirt/MIDIUtil MIDIUtil-1.2.1/VERSION000066400000000000000000000000271324704026400141440ustar00rootroot00000000000000This is version 1.2.1 MIDIUtil-1.2.1/documentation/000077500000000000000000000000001324704026400157465ustar00rootroot00000000000000MIDIUtil-1.2.1/documentation/Makefile000066400000000000000000000166761324704026400174260ustar00rootroot00000000000000# 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.2.1/documentation/class.rst000066400000000000000000000006311324704026400176050ustar00rootroot00000000000000.. _ClassRef: Class Reference =============== .. currentmodule:: midiutil.MidiFile .. autoclass:: MIDIFile :members: addNote, addTrackName, addTempo, addProgramChange, addControllerEvent, makeRPNCall, makeNRPNCall, changeTuningBank, changeTuningProgram, addPitchWheelEvent, changeNoteTuning, addSysEx, addUniversalSysEx, writeFile, __init__ , addTimeSignature, addCopyright, addText, addKeySignature MIDIUtil-1.2.1/documentation/common.rst000066400000000000000000000061451324704026400177760ustar00rootroot00000000000000Common 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.2.1/documentation/conf.py000066400000000000000000000232361324704026400172530ustar00rootroot00000000000000#!/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.2.1/documentation/creating.rst000066400000000000000000000117441324704026400203030ustar00rootroot00000000000000Creating 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=False, file_format=1, ticks_per_quarternote=TICKSPERQUARTERNOTE, eventtime_is_ticks=False): 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. The defaul value is ``False``. 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. ticks_per_quarternote --------------------- The MIDI ticks per quarter note. Defaults to 960. This defines the finest level of time resolution available in the file. 120, 240, 384, 480, and 960 are common values. eventtime_is_ticks ------------------ If set to ``True``, all times passed into the event creation functions should be specified in ticks. Otherwise they should be specified in quarter-notes (the default). MIDIUtil-1.2.1/documentation/extending.rst000066400000000000000000000206311324704026400204670ustar00rootroot00000000000000Extending 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 writting. 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. The MID standard defines the Tempo event as the following byte-stream:: FF 51 03 tttttt where ``FF 51`` is the code and sub-code of the event, ``03`` is the data length of the event, and ``tttttt`` are the three bytes of data, encoded as microseconds per quarter note. Create a New Event Type ----------------------- The majority of work involved with creating a new event type is the creation of a new subclass of the ``GenericEvent`` object of the MIDIFile module. This subclass: - Initializes any specific instance data that is needed for the MIDI event, and - Defines how the data are serialized to the byte stream - Defines the order in which an event is written to the byte stream (see below) In the case of the tempo, the actual data conversion is easy: people tend to specify tempos in beats per minute, so the input will need to be divided into 60000000. The serialization strategy is defined in the subclass' ``serialize`` member function, which is presented below. ``sec_sort_order`` and ``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 ``sec_sort_order``, with lower numbers appearing in the stream first. Lastly events are sorted on the ``self.insertion_order`` member. This strategy 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 secondary sort order), you can control the order of the events by the order in which you add them to the MIDIFile. Al of this will perhaps be more clear if we examine the code: .. code:: python class Tempo(GenericEvent): ''' A class that encapsulates a tempo meta-event ''' evtname = 'Tempo' sec_sort_order = 3 def __init__(self, tick, tempo, insertion_order=0): self.tempo = int(60000000 / tempo) super(Tempo, self).__init__(tick, insertion_order) def __eq__(self, other): return (self.evtname == other.evtname and self.tick == other.tick and self.tempo == other.tempo) __hash__ = GenericEvent.__hash__ def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = 0xFF subcode = 0x51 fourbite = struct.pack('>L', self.tempo) # big-endian uint32 threebite = fourbite[1:4] # Just discard the MSB varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', subcode) midibytes += struct.pack('>B', 0x03) # length in bytes of 24-bit tempo midibytes += threebite return midibytes The event name (``evtname``) and secondary sort order are defined in class data; any class that you create will do the same. ``tick`` is the time in MIDI ticks of the event and insertion order will be set in the code. All events should accept these parameters. ``tempo`` is the specific instance data needed for this event type. The ``__init__()`` member converts the tempo to number needed by the standard and then calls the super-class' initialization function with tick and insertion order. All event subclasses should do this. Next, the ``__eq__()`` function is defined that specifies when two events are the same. In this case they are the same if the tick, event name, and tempo are the same. This code is used to identify and remove duplicate events. ``__hash__()`` should explicitly be brought down from the parent class, in in Python 3 it is not implicitly inherited. Lastly, the ``serialize`` member function should be created. This will return a byte stream representing the MIDI data. A few things to note about this: - All MIDI events begin with a time, which is written in an idiosyncratic variable-length format. Use the ``writeVarLength`` utility function to calculate this. - Note that in the case of the tempo event, the standard only uses three bytes, whereas in python a long will be packed into four bytes. Hence we just discard the MSB. - In the temo the actual data is packed: - The time - The code (0xFF) - The subcode (0x51) - The length of that (defined in the event as 3 bytes) - The data proper 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, tick, tempo, insertion_order=0): ''' Add a tempo change (or set) event. ''' self.eventList.append(Tempo(tick, tempo, insertion_order=insertion_order)) (Most/many MIDI events require a channel specification, but the tempo event does not.) This is more-or-less boilerplate code, and just needs to appropriately create the object you defined above. Note that this function can in some cases create multiple events. For example, when one adds a note, both a ``NoneOn`` and a ``NoteOff`` event will be created. Lastly, 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): """ 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(self.time_to_ticks(time), tempo, insertion_order=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.) The only other complexity is that the public functions accept by default a time in quarter-notes, not MIDI ticks. So the public accessor function should pass the time through the ``time_to_ticks()`` member. If the MIDIFile was instantiated with ``eventtime_is_ticks = True``, this is just an identity fucntion and the public accessor will expect time in ticks. Otherwise it will convert from quarter-notes to ticks (suing the ``TICKSPERQUARTERNOTE`` instance data) This is the function you will use in your code to create an event of the desired type. Write Some Tests ---------------- Yea, it's a hassle, but you know it's the right thing to do! MIDIUtil-1.2.1/documentation/index.rst000066400000000000000000000130611324704026400176100ustar00rootroot00000000000000.. 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. * cclauss (Code formating cleanup and PEP-8 stuff, which I'm not good at following). * Philippe-Adrien Nousse (Adphi) for the pitch bend implementation. * meteorsw (https://github.com/meteorsw) for major restructuring and clean-up of code. Indices and tables ------------------ * :ref:`genindex` * :ref:`search` MIDIUtil-1.2.1/documentation/tuning.rst000066400000000000000000000047501324704026400200120ustar00rootroot00000000000000Tuning 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. Recently the pitch bend message had been implemented. The advantage of the pitch bend is that almost all software and hardware understand it. The disadvantage is that different software and hardware can interpret the values differently. 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 Using Pitch Bend ---------------- Pitch bend is a *channel level event*, meaning that if you pass a pitch wheel event, all notes on that channel will be affected. .. automethod:: MIDIFile.addPitchWheelEvent To Do ----- * Implement the tuning change with bank select event type. MIDIUtil-1.2.1/examples/000077500000000000000000000000001324704026400147135ustar00rootroot00000000000000MIDIUtil-1.2.1/examples/c-major-scale.py000077500000000000000000000010311324704026400177000ustar00rootroot00000000000000#!/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.2.1/examples/single-note-example.py000077500000000000000000000015151324704026400211470ustar00rootroot00000000000000############################################################################ # 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.2.1/setup.py000066400000000000000000000026251324704026400146140ustar00rootroot00000000000000from setuptools import setup, find_packages with open('README.rst') as file: long_description = file.read() setup(name='MIDIUtil', version='1.2.1', description='A pure python library for creating multi-track MIDI files', author='Mark Conway Wirt', author_email='MarkCWirt@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.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.2.1/src/000077500000000000000000000000001324704026400136645ustar00rootroot00000000000000MIDIUtil-1.2.1/src/midiutil/000077500000000000000000000000001324704026400155045ustar00rootroot00000000000000MIDIUtil-1.2.1/src/midiutil/MidiFile.py000066400000000000000000002171301324704026400175440ustar00rootroot00000000000000# ----------------------------------------------------------------------------- # 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__ = 'HEAD' # TICKSPERQUARTERNOTE is the number of "ticks" (time measurement in the MIDI file) that # corresponds to one quarter note. This number is somewhat arbitrary, but should # be chosen to provide adequate temporal resolution. TICKSPERQUARTERNOTE = 960 controllerEventTypes = {'pan': 0x0a} # Define some constants MAJOR = 0 MINOR = 1 SHARPS = 1 FLATS = -1 __all__ = ['MIDIFile', 'MAJOR', 'MINOR', 'SHARPS', 'FLATS'] class GenericEvent(object): ''' The event class from which specific events are derived ''' evtname = None sec_sort_order = 0 def __init__(self, tick, insertion_order): self.tick = tick self.insertion_order = insertion_order def __eq__(self, other): ''' Equality operator. 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). Some derived classes will need to override and consider their specific attributes in the comparison. ''' return (self.evtname == other.evtname and self.tick == other.tick) def __hash__(self): ''' Return a hash code for the object. This is needed in order to allow GenericObject classes to be used as the key in a dict or set. duplicate objects are removed from the event list by storing all the objects in a set, and then reconstructing the list from the set. 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.tick) 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 NoteOn(GenericEvent): ''' A class that encapsulates a note ''' evtname = 'NoteOn' midi_status = 0x90 # 0x9x is Note On sec_sort_order = 3 def __init__(self, channel, pitch, tick, duration, volume, annotation=None, insertion_order=0): self.pitch = pitch self.duration = duration self.volume = volume self.channel = channel self.annotation = annotation super(NoteOn, self).__init__(tick, insertion_order) def __eq__(self, other): return (self.evtname == other.evtname and self.tick == other.tick and self.pitch == other.pitch and self.channel == other.channel) # In Python 3, a class which overrides __eq__ also needs to provide __hash__, # because in Python 3 parent __hash__ is not inherited. __hash__ = GenericEvent.__hash__ def __str__(self): return 'NoteOn %d at tick %d duration %d ch %d vel %d' % ( self.pitch, self.tick, self.duration, self.channel, self.volume) def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = self.midi_status | self.channel varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', self.pitch) midibytes += struct.pack('>B', self.volume) return midibytes class NoteOff (GenericEvent): ''' A class that encapsulates a Note Off event ''' evtname = 'NoteOff' midi_status = 0x80 # 0x8x is Note Off sec_sort_order = 2 # must be less than that of NoteOn # If two events happen at the same time, the secondary sort key is # ``sec_sort_order``. 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. def __init__(self, channel, pitch, tick, volume, annotation=None, insertion_order=0): self.pitch = pitch self.volume = volume self.channel = channel self.annotation = annotation super(NoteOff, self).__init__(tick, insertion_order) def __eq__(self, other): return (self.evtname == other.evtname and self.tick == other.tick and self.pitch == other.pitch and self.channel == other.channel) __hash__ = GenericEvent.__hash__ def __str__(self): return 'NoteOff %d at tick %d ch %d vel %d' % ( self.pitch, self.tick, self.channel, self.volume) def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = self.midi_status | self.channel varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', self.pitch) midibytes += struct.pack('>B', self.volume) return midibytes class Tempo(GenericEvent): ''' A class that encapsulates a tempo meta-event ''' evtname = 'Tempo' sec_sort_order = 3 def __init__(self, tick, tempo, insertion_order=0): self.tempo = int(60000000 / tempo) super(Tempo, self).__init__(tick, insertion_order) def __eq__(self, other): return (self.evtname == other.evtname and self.tick == other.tick and self.tempo == other.tempo) __hash__ = GenericEvent.__hash__ def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ # Standard MIDI File Format says: # # FF 51 03 tttttt Set Tempo (in microseconds per MIDI quarter-note) # This event indicates a tempo change. Another way of putting # "microseconds per quarter-note" is "24ths of a microsecond per MIDI # clock". Representing tempos as time per beat instead of beat per time # allows absolutely exact long-term synchronisation with a time-based # sync protocol such as SMPTE time code or MIDI time code. The amount # of accuracy provided by this tempo resolution allows a four-minute # piece at 120 beats per minute to be accurate within 500 usec at the # end of the piece. Ideally, these events should only occur where MIDI # clocks would be located -- this convention is intended to guarantee, # or at least increase the likelihood, of compatibility with other # synchronisation devices so that a time signature/tempo map stored in # this format may easily be transferred to another device. # # Six identical lower-case letters such as tttttt refer to a 24-bit value, stored # most-significant-byte first. The notation len refers to the midibytes = b"" code = 0xFF subcode = 0x51 fourbite = struct.pack('>L', self.tempo) # big-endian uint32 threebite = fourbite[1:4] # Just discard the MSB varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', subcode) midibytes += struct.pack('>B', 0x03) # length in bytes of 24-bit tempo midibytes += threebite return midibytes class Copyright(GenericEvent): ''' A class that encapsulates a copyright event ''' evtname = 'Copyright' sec_sort_order = 1 def __init__(self, tick, notice, insertion_order=0): self.notice = notice.encode("ISO-8859-1") super(Copyright, self).__init__(tick, insertion_order) def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ # Standard MIDI File Format says: # # FF 02 len text Copyright Notice # Contains a copyright notice as printable ASCII text. The notice should # contain the characters (C), the year of the copyright, and the owner # of the copyright. If several pieces of music are in the same MIDI # File, all of the copyright notices should be placed together in this # event so that it will be at the beginning of the file. This event # should be the first event in the track chunk, at tick 0. midibytes = b"" code = 0xFF subcode = 0x02 varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', subcode) payloadLength = len(self.notice) payloadLengthVar = writeVarLength(payloadLength) for i in payloadLengthVar: midibytes += struct.pack("b", i) midibytes += self.notice return midibytes class Text(GenericEvent): ''' A class that encapsulates a text event ''' evtname = 'Text' sec_sort_order = 1 def __init__(self, tick, text, insertion_order=0): self.text = text.encode("ISO-8859-1") super(Text, self).__init__(tick, insertion_order) def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = 0xFF subcode = 0x01 varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', subcode) payloadLength = len(self.text) payloadLengthVar = writeVarLength(payloadLength) for i in payloadLengthVar: midibytes += struct.pack("B", i) midibytes += self.text return midibytes class KeySignature(GenericEvent): ''' A class that encapsulates a text event ''' evtname = 'KeySignature' sec_sort_order = 1 def __init__(self, tick, accidentals, accidental_type, mode, insertion_order=0): self.accidentals = accidentals self.accidental_type = accidental_type self.mode = mode super(KeySignature, self).__init__(tick, insertion_order) def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = 0xFF subcode = 0x59 event_subtype = 0x02 varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', subcode) midibytes += struct.pack('>B', event_subtype) midibytes += struct.pack('>b', self.accidentals * self.accidental_type) midibytes += struct.pack('>B', self.mode) return midibytes class ProgramChange(GenericEvent): ''' A class that encapsulates a program change event. ''' evtname = 'ProgramChange' midi_status = 0xc0 # 0xcx is Program Change sec_sort_order = 1 def __init__(self, channel, tick, programNumber, insertion_order=0): self.programNumber = programNumber self.channel = channel super(ProgramChange, self).__init__(tick, insertion_order) def __eq__(self, other): return (self.evtname == other.evtname and self.tick == other.tick and self.programNumber == other.programNumber and self.channel == other.channel) __hash__ = GenericEvent.__hash__ def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = self.midi_status | self.channel varTime = writeVarLength(self.tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', self.programNumber) return midibytes class SysExEvent(GenericEvent): ''' A class that encapsulates a System Exclusive event. ''' evtname = 'SysEx' # doesn't match class name like most others sec_sort_order = 1 def __init__(self, tick, manID, payload, insertion_order=0): self.manID = manID self.payload = payload super(SysExEvent, self).__init__(tick, insertion_order) def __eq__(self, other): return False __hash__ = GenericEvent.__hash__ def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = 0xF0 varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) payloadLength = writeVarLength(len(self.payload) + 2) for lenByte in payloadLength: midibytes += struct.pack('>B', lenByte) midibytes += struct.pack('>B', self.manID) midibytes += self.payload midibytes += struct.pack('>B', 0xF7) return midibytes class UniversalSysExEvent(GenericEvent): ''' A class that encapsulates a Universal System Exclusive event. ''' evtname = 'UniversalSysEx' # doesn't match class name like most others sec_sort_order = 1 def __init__(self, tick, realTime, sysExChannel, code, subcode, payload, insertion_order=0): self.realTime = realTime self.sysExChannel = sysExChannel self.code = code self.subcode = subcode self.payload = payload super(UniversalSysExEvent, self).__init__(tick, insertion_order) def __eq__(self, other): return False __hash__ = GenericEvent.__hash__ def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = 0xF0 varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) # Do we need to add a length? payloadLength = writeVarLength(len(self.payload) + 5) for lenByte in payloadLength: midibytes += struct.pack('>B', lenByte) if self.realTime: midibytes += struct.pack('>B', 0x7F) else: midibytes += struct.pack('>B', 0x7E) midibytes += struct.pack('>B', self.sysExChannel) midibytes += struct.pack('>B', self.code) midibytes += struct.pack('>B', self.subcode) midibytes += self.payload midibytes += struct.pack('>B', 0xF7) return midibytes class ControllerEvent(GenericEvent): ''' A class that encapsulates a program change event. ''' evtname = 'ControllerEvent' midi_status = 0xB0 # 0xBx is Control Change sec_sort_order = 1 def __init__(self, channel, tick, controller_number, parameter, insertion_order=0): self.parameter = parameter self.channel = channel self.controller_number = controller_number super(ControllerEvent, self).__init__(tick, insertion_order) def __eq__(self, other): return False __hash__ = GenericEvent.__hash__ def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = self.midi_status | self.channel varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', self.controller_number) midibytes += struct.pack('>B', self.parameter) return midibytes class ChannelPressureEvent(GenericEvent): ''' A class that encapsulates a Channel Pressure (Aftertouch) event. ''' evtname = 'ChannelPressure' midi_status = 0xD0 # 0xDx is Channel Pressure (Aftertouch) sec_sort_order = 1 def __init__(self, channel, tick, pressure_value, insertion_order=0): self.channel = channel self.pressure_value = pressure_value super(ChannelPressureEvent, self).__init__(tick, insertion_order) def __eq__(self, other): return (self.__class__.__name__ == other.__class__.__name__ and self.tick == other.tick and self.pressure_value == other.pressure_value and self.channel == other.channel) __hash__ = GenericEvent.__hash__ def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = self.midi_status | self.channel vartick = writeVarLength(self.tick - previous_event_tick) for x in vartick: midibytes += struct.pack('>B', x) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', self.pressure_value) return midibytes class PitchWheelEvent(GenericEvent): ''' A class that encapsulates a pitch wheel change event. ''' evtname = 'PitchWheelEvent' midi_status = 0xE0 # 0xEx is Pitch Wheel Change sec_sort_order = 1 def __init__(self, channel, tick, pitch_wheel_value, insertion_order=0): self.channel = channel self.pitch_wheel_value = pitch_wheel_value super(PitchWheelEvent, self).__init__(tick, insertion_order) def __eq__(self, other): return False __hash__ = GenericEvent.__hash__ def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = self.midi_status | self.channel varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes = midibytes + struct.pack('>B', timeByte) MSB = (self.pitch_wheel_value + 8192) >> 7 LSB = (self.pitch_wheel_value + 8192) & 0x7F midibytes = midibytes + struct.pack('>B', code) midibytes = midibytes + struct.pack('>B', LSB) midibytes = midibytes + struct.pack('>B', MSB) return midibytes class TrackName(GenericEvent): ''' A class that encapsulates a program change event. ''' evtname = 'TrackName' sec_sort_order = 0 def __init__(self, tick, trackName, insertion_order=0): # GenericEvent.__init__(self, tick) self.trackName = trackName.encode("ISO-8859-1") super(TrackName, self).__init__(tick, insertion_order) def __eq__(self, other): return (self.evtname == other.evtname and self.tick == other.tick and self.trackName == other.trackName) __hash__ = GenericEvent.__hash__ def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('B', 0xFF) midibytes += struct.pack('B', 0X03) dataLength = len(self.trackName) dataLengthVar = writeVarLength(dataLength) for i in dataLengthVar: midibytes += struct.pack("B", i) midibytes += self.trackName return midibytes class TimeSignature(GenericEvent): ''' A class that encapsulates a time signature. ''' evtname = 'TimeSignature' sec_sort_order = 0 def __init__(self, tick, numerator, denominator, clocks_per_tick, notes_per_quarter, 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__(tick, insertion_order) def serialize(self, previous_event_tick): """Return a bytestring representation of the event, in the format required for writing into a standard midi file. """ midibytes = b"" code = 0xFF subcode = 0x58 varTime = writeVarLength(self.tick - previous_event_tick) for timeByte in varTime: midibytes += struct.pack('>B', timeByte) midibytes += struct.pack('>B', code) midibytes += struct.pack('>B', subcode) midibytes += struct.pack('>B', 0x04) midibytes += struct.pack('>B', self.numerator) midibytes += struct.pack('>B', self.denominator) midibytes += struct.pack('>B', self.clocks_per_tick) # 32nd notes per quarter note midibytes += struct.pack('>B', self.notes_per_quarter) return midibytes 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, tick, duration, volume, annotation=None, insertion_order=0): ''' Add a note by chromatic MIDI number ''' self.eventList.append(NoteOn(channel, pitch, tick, duration, volume, annotation=annotation, insertion_order=insertion_order)) # This event is not in chronological order. But before writing all the # events to the file, I sort self.eventlist on (tick, sec_sort_order, insertion_order) # which puts the events in chronological order. self.eventList.append(NoteOff(channel, pitch, tick + duration, volume, annotation=annotation, insertion_order=insertion_order)) def addControllerEvent(self, channel, tick, controller_number, parameter, insertion_order=0): ''' Add a controller event. ''' self.eventList.append(ControllerEvent(channel, tick, controller_number, parameter, insertion_order=insertion_order)) def addPitchWheelEvent(self, channel, tick, pitch_wheel_value, insertion_order=0): ''' Add a pitch wheel event. ''' self.eventList.append(PitchWheelEvent(channel, tick, pitch_wheel_value, insertion_order=insertion_order)) def addTempo(self, tick, tempo, insertion_order=0): ''' Add a tempo change (or set) event. ''' self.eventList.append(Tempo(tick, tempo, insertion_order=insertion_order)) def addSysEx(self, tick, manID, payload, insertion_order=0): ''' Add a SysEx event. ''' self.eventList.append(SysExEvent(tick, manID, payload, insertion_order=insertion_order)) def addUniversalSysEx(self, tick, code, subcode, payload, sysExChannel=0x7F, realTime=False, insertion_order=0): ''' Add a Universal SysEx event. ''' self.eventList.append(UniversalSysExEvent(tick, realTime, sysExChannel, code, subcode, payload, insertion_order=insertion_order)) def addProgramChange(self, channel, tick, program, insertion_order=0): ''' Add a program change event. ''' self.eventList.append(ProgramChange(channel, tick, program, insertion_order=insertion_order)) def addChannelPressure(self, channel, tick, pressure_value, insertion_order=0): ''' Add a channel pressure event. ''' self.eventList.append(ChannelPressureEvent(channel, tick, pressure_value, insertion_order=insertion_order)) def addTrackName(self, tick, trackName, insertion_order=0): ''' Add a track name event. ''' self.eventList.append(TrackName(tick, trackName, insertion_order=insertion_order)) def addTimeSignature(self, tick, numerator, denominator, clocks_per_tick, notes_per_quarter, insertion_order=0): ''' Add a time signature. ''' self.eventList.append(TimeSignature(tick, numerator, denominator, clocks_per_tick, notes_per_quarter, insertion_order=insertion_order)) def addCopyright(self, tick, notice, insertion_order=0): ''' Add a copyright notice ''' self.eventList.append(Copyright(tick, notice, insertion_order=insertion_order)) def addKeySignature(self, tick, accidentals, accidental_type, mode, insertion_order=0): ''' Add a copyright notice ''' self.eventList.append(KeySignature(tick, accidentals, accidental_type, mode, insertion_order=insertion_order)) def addText(self, tick, text, insertion_order=0): ''' Add a text event ''' self.eventList.append(Text(tick, 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, which is then sorted to be in chronological order by start tick. ''' self.MIDIEventList = [evt for evt in self.eventList] # 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). s = set(self.eventList) self.eventList = list(s) 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. MIDIEventList is presumed to be already sorted in chronological order. ''' previous_event_tick = 0 for event in self.MIDIEventList: self.MIDIdata += event.serialize(previous_event_tick) # previous_event_tick = event.tick # I do not like that adjustTimeAndOrigin() changes GenericEvent.tick # from absolute to relative. I intend to change that, and just # calculate the relative tick here, without changing GenericEvent.tick 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.evtname in ['NoteOn', 'NoteOff']: # !!! Pitch 101 channel 5 produces the same key as pitch 10 channel 15. # !!! This is not the only pair of pitch,channel tuples which # !!! collide to the same key, just one example. Should fix by # !!! putting a separator char between pitch and channel. noteeventkey = str(event.pitch) + str(event.channel) if event.evtname == 'NoteOn': if noteeventkey in stack: stack[noteeventkey].append(event.tick) else: stack[noteeventkey] = [event.tick] tempEventList.append(event) elif event.evtname == 'NoteOff': if len(stack[noteeventkey]) > 1: event.tick = stack[noteeventkey].pop() tempEventList.append(event) else: stack[noteeventkey].pop() tempEventList.append(event) else: tempEventList.append(event) self.MIDIEventList = tempEventList # Note NoteOff events have a lower secondary sort key than NoteOn # events, so this sort will make concomitant NoteOff 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 runningTick = 0 for event in self.MIDIEventList: adjustedTick = event.tick - internal_origin event.tick = adjustedTick - runningTick runningTick = adjustedTick 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, ticks_per_quarternote): ''' Initialize the data structures :param numTracks: The number of tracks the file contains. Integer, one or greater :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``. :param ticks_per_quarternote: The number of ticks per quarter note is what the Standard MIDI File Format Specification calls "division". Ticks are the integer unit of time in the SMF, and in every MIDI sequencer I am aware of. Common values are 120, 240, 384, 480, 960. Note that all these numbers are evenly divisible by 2,3,4,6,8,12,16, and 24, except 120 does not have 16 as a divisor. ''' 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.formatnum = struct.pack('>H', file_format) self.numeric_format = file_format self.numTracks = struct.pack('>H', numTracks) self.ticks_per_quarternote = struct.pack('>H', ticks_per_quarternote) def writeFile(self, fileHandle): fileHandle.write(self.headerString) fileHandle.write(self.headerSize) fileHandle.write(self.formatnum) fileHandle.write(self.numTracks) fileHandle.write(self.ticks_per_quarternote) 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=False, file_format=1, ticks_per_quarternote=TICKSPERQUARTERNOTE, eventtime_is_ticks=False): '''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`` shift all the events in the tracks so that the first event takes place at time t=0. Default is ``False`` :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``. :param ticks_per_quarternote: The number of ticks per quarter note is what the Standard MIDI File Format Specification calls "division". Ticks are the integer unit of time in the SMF, and in most if not all MIDI sequencers. Common values are 120, 240, 384, 480, 960. Note that all these numbers are evenly divisible by 2,3,4,6,8,12,16, and 24, except 120 does not have 16 as a divisor. :param eventtime_is_ticks: If set True means event time and duration argument values are integer ticks instead of fractional quarter notes. Note that the default for ``adjust_origin`` will change in a future release, so one should probably explicitly set it. In a format 1 file, it would be a rare cirumstance where adjusting the origin of each track to the track's first note makes any sense. Example: .. code:: # Create a two-track MIDIFile from midiutil.MidiFile import MIDIFile midi_file = MIDIFile(1, adjust_origin=False) 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.tracks = list() if file_format == 1: self.numTracks = numTracks + 1 # self.tracks[0] is the baked-in tempo track else: self.numTracks = numTracks self.header = MIDIHeader(self.numTracks, file_format, ticks_per_quarternote) self.adjust_origin = adjust_origin self.closed = False self.ticks_per_quarternote = ticks_per_quarternote self.eventtime_is_ticks = eventtime_is_ticks if self.eventtime_is_ticks: self.time_to_ticks = lambda x: x else: self.time_to_ticks = self.quarter_to_tick 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 quarter_to_tick(self, quarternote_time): return int(quarternote_time * self.ticks_per_quarternote) def tick_to_quarter(self, ticknum): return float(ticknum) / self.ticks_per_quarternote 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 at which the note sounds. The value can be either quarter notes [Float], or ticks [Integer]. Ticks may be specified by passing eventtime_is_ticks=True to the MIDIFile constructor. The default is quarter notes. :param duration: the duration of the note. Like the time argument, the value can be either quarter notes [Float], or ticks [Integer]. :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, self.time_to_ticks(time), self.time_to_ticks(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(self.time_to_ticks(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. The ``notes_per_quarter`` value is also a little confusing. It specifies the number of 32nd notes in a MIDI quarter note. Usually there are 8 32nd notes in a quarter note (8/32 = 1/4), so the default value is 8. However, one can change this value if needed. Setting it to 16, for example, would cause the music to play at double speed, as there would be 16/32 (or what could be considered *two* quarter notes for every one MIDI quarter note. Note that both the ``clocks_per_tick`` and the ``notes_per_quarter`` are specified in terms of quarter notes, even is the score is not a quarter-note based score (i.e., even if the denominator is not ``4``). So if you're working with a time signature of, say, 6/8, one still needs to specify the clocks per quarter note. ''' if self.header.numeric_format == 1: track = 0 self.tracks[track].addTimeSignature(self.time_to_ticks(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(self.time_to_ticks(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(self.time_to_ticks(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 = 0 # User reported that this is needed. self.tracks[track].addKeySignature(self.time_to_ticks(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(self.time_to_ticks(time), text, insertion_order=self.event_counter) self.event_counter += 1 def addProgramChange(self, tracknum, channel, time, program): """ Add a MIDI program change event. :param tracknum: The zero-based track number 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]. """ if self.header.numeric_format == 1: tracknum += 1 self.tracks[tracknum].addProgramChange(channel, self.time_to_ticks(time), program, insertion_order=self.event_counter) self.event_counter += 1 def addChannelPressure(self, tracknum, channel, time, pressure_value): """ Add a Channel Pressure event. :param tracknum: The zero-based track number to which channel pressure 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 channel pressure event is placed [Float]. :param pressure_value: the pressure value. [Integer, 0-127]. """ if self.header.numeric_format == 1: tracknum += 1 track = self.tracks[tracknum] track.addChannelPressure(channel, self.time_to_ticks(time), pressure_value, 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, self.time_to_ticks(time), controller_number, parameter, insertion_order=self.event_counter) # noqa: E128 self.event_counter += 1 def addPitchWheelEvent(self, track, channel, time, pitchWheelValue): """ Add a channel pitch wheel 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 pitchWheelValue: 0 for no pitch change. [Integer, -8192-8192] """ if self.header.numeric_format == 1: track += 1 self.tracks[track].addPitchWheelEvent(channel, self.time_to_ticks(time), pitchWheelValue, insertion_order=self.event_counter) 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.) Registered/Non-Registered Parameter Number (RPN / NRPN) ------------------------------------------------------- Controller number 6 (Data Entry), in conjunction with Controller numbers 96 (Data Increment), 97 (Data Decrement), 98 (Registered Parameter Number LSB), 99 (Registered Parameter Number MSB), 100 (Non-Registered Parameter Number LSB), and 101 (Non-Registered Parameter Number MSB), extend the number of controllers available via MIDI. Parameter data is transferred by first selecting the parameter number to be edited using controllers 98 and 99 or 100 and 101, and then adjusting the data value for that parameter using controller number 6, 96, or 97. RPN and NRPN are typically used to send parameter data to a synthesizer in order to edit sound patches or other data. Registered parameters are those which have been assigned some particular function by the MIDI Manufacturers Association (MMA) and the Japan MIDI Standards Committee (JMSC). For example, there are Registered Parameter numbers assigned to control pitch bend sensitivity and master tuning for a synthesizer. Non-Registered parameters have not been assigned specific functions, and may be used for different functions by different manufacturers. 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. ''' tick = self.time_to_ticks(time) if self.header.numeric_format == 1: track += 1 track = self.tracks[track] tick_incr = 1 if time_order else 0 track.addControllerEvent(channel, tick, 101, # parameter number MSB controller_msb, insertion_order=self.event_counter) # noqa: E128 self.event_counter += 1 tick += tick_incr track.addControllerEvent(channel, tick, 100, controller_lsb, insertion_order=self.event_counter) # noqa: E128 self.event_counter += 1 tick += tick_incr track.addControllerEvent(channel, tick, 6, data_msb, insertion_order=self.event_counter) # noqa: E128 self.event_counter += 1 tick += tick_incr if data_lsb is not None: track.addControllerEvent(channel, tick, 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. ''' tick = self.time_to_ticks(time) if self.header.numeric_format == 1: track += 1 track = self.tracks[track] tick_incr = 1 if time_order else 0 track.addControllerEvent(channel, tick, 99, controller_msb, insertion_order=self.event_counter) # noqa: E128 self.event_counter += 1 tick += tick_incr track.addControllerEvent(channel, tick, 98, controller_lsb, insertion_order=self.event_counter) # noqa: E128 self.event_counter += 1 tick += tick_incr track.addControllerEvent(channel, tick, 6, data_msb, insertion_order=self.event_counter) # noqa: E128 self.event_counter += 1 tick += tick_incr if data_lsb is not None: track.addControllerEvent(channel, tick, 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. ''' tick = self.time_to_ticks(time) if self.header.numeric_format == 1: track += 1 self.tracks[track].addSysEx(tick, 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. ''' tick = self.time_to_ticks(time) if self.header.numeric_format == 1: track += 1 self.tracks[track].addUniversalSysEx(tick, 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 = 100000000 # A little silly, but we'll assume big enough tick_offset = self.time_to_ticks(offset) for track in self.tracks: if len(track.eventList) > 0: for event in track.eventList: if event.tick < origin: origin = event.tick for track in self.tracks: tempEventList = [] # runningTick = 0 for event in track.eventList: adjustedTick = event.tick - origin # event.time = adjustedTime - runningTick + tick_offset event.tick = adjustedTick + tick_offset # runningTick = adjustedTick 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 both # their start time and a secondary ordinality defined for each kind # of event. 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 = 100000000 # 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].tick < origin: origin = track.MIDIEventList[0].tick return origin def writeVarLength(i): ''' Accept an integer, and serialize it as a MIDI file variable length quantity Some numbers in MTrk chunks are represented in a form called a variable- length quantity. These numbers are represented in a sequence of bytes, each byte holding seven bits of the number, and ordered most significant bits first. All bytes in the sequence except the last have bit 7 set, and the last byte has bit 7 clear. This form allows smaller numbers to be stored in fewer bytes. For example, if the number is between 0 and 127, it is thus represented exactly as one byte. A number between 128 and 16383 uses two bytes, and so on. Examples: Number VLQ 128 81 00 8192 C0 00 16383 FF 7F 16384 81 80 00 ''' if i == 0: return [0] vlbytes = [] hibit = 0x00 # low-order byte has high bit cleared. while i > 0: vlbytes.append(((i & 0x7f) | hibit) & 0xff) i >>= 7 hibit = 0x80 vlbytes.reverse() # put most-significant byte first, least significant last return vlbytes # 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 ``sec_sort_order``. 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 event time and event 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.tick, event.sec_sort_order, event.insertion_order) MIDIUtil-1.2.1/src/midiutil/__init__.py000066400000000000000000000001351324704026400176140ustar00rootroot00000000000000from midiutil.MidiFile import * __all__ = ['MIDIFile', 'MAJOR', 'MINOR', 'SHARPS', 'FLATS'] MIDIUtil-1.2.1/src/unittests/000077500000000000000000000000001324704026400157265ustar00rootroot00000000000000MIDIUtil-1.2.1/src/unittests/test_midi.py000077500000000000000000001160421324704026400202700ustar00rootroot00000000000000#!/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 import struct import unittest from midiutil.MidiFile import * from midiutil.MidiFile import writeVarLength, \ frequencyTransform, returnFrequency, 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 track = 0 channel = 0 pitch = 100 time = 0 duration = 1 volume = 100 MyMIDI.addNote(track, channel, pitch, time, duration, volume) self.assertEqual(MyMIDI.tracks[1].eventList[0].evtname, "NoteOn") self.assertEqual(MyMIDI.tracks[1].eventList[0].pitch, pitch) self.assertEqual(MyMIDI.tracks[1].eventList[0].tick, MyMIDI.time_to_ticks(time)) self.assertEqual(MyMIDI.tracks[1].eventList[0].duration, MyMIDI.time_to_ticks(duration)) self.assertEqual(MyMIDI.tracks[1].eventList[0].volume, volume) def testShiftTrack(self): track = 0 channel = 0 pitch = 100 time = 1 duration = 1 volume = 100 MyMIDI = MIDIFile(1) MyMIDI.addNote(track, channel, pitch, time, duration, volume) self.assertEqual(MyMIDI.tracks[1].eventList[0].evtname, "NoteOn") self.assertEqual(MyMIDI.tracks[1].eventList[0].pitch, pitch) self.assertEqual(MyMIDI.tracks[1].eventList[0].tick, MyMIDI.time_to_ticks(time)) self.assertEqual(MyMIDI.tracks[1].eventList[0].duration, MyMIDI.time_to_ticks(duration)) self.assertEqual(MyMIDI.tracks[1].eventList[0].volume, volume) MyMIDI.shiftTracks() self.assertEqual(MyMIDI.tracks[1].eventList[0].tick, 0) def testDeinterleaveNotes(self): MyMIDI = MIDIFile(1, adjust_origin=False) track = 0 channel = 0 pitch = 100 time1 = 0 time2 = 1 duration = 2 volume = 100 MyMIDI.addNote(track, channel, pitch, time1, duration, volume) # on at 0 off at 2 MyMIDI.addNote(track, channel, pitch, time2, duration, volume + 1) # on at 1 off at 3 MyMIDI.close() # ticks have already been converted to delta ticks self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].evtname, 'NoteOn') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].tick, MyMIDI.time_to_ticks(time1)) self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].evtname, 'NoteOff') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].tick, MyMIDI.time_to_ticks(time2)) self.assertEqual(MyMIDI.tracks[1].MIDIEventList[2].evtname, 'NoteOn') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[2].tick, MyMIDI.time_to_ticks(time2 - time2)) self.assertEqual(MyMIDI.tracks[1].MIDIEventList[3].evtname, 'NoteOff') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[3].tick, MyMIDI.time_to_ticks(time2 - time2 + duration)) def testTimeShift(self): # With one track MyMIDI = MIDIFile(1, adjust_origin=True) track = 0 channel = 0 pitch = 100 time1 = 5 duration = 1 volume = 100 MyMIDI.addNote(track, channel, pitch, time1, duration, volume) MyMIDI.close() self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].evtname, 'NoteOn') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].tick, MyMIDI.time_to_ticks(0)) self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].evtname, 'NoteOff') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].tick, MyMIDI.time_to_ticks(duration)) # With two tracks track2 = 1 MyMIDI = MIDIFile(2, adjust_origin=True) MyMIDI.addNote(track, channel, pitch, time1, duration, volume) time2 = 6 MyMIDI.addNote(track2, channel, pitch, time2, duration, volume) MyMIDI.close() # ticks have already been converted to delta ticks self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].evtname, 'NoteOn') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].tick, MyMIDI.time_to_ticks(0)) self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].evtname, 'NoteOff') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].tick, MyMIDI.time_to_ticks(duration)) self.assertEqual(MyMIDI.tracks[2].MIDIEventList[0].evtname, 'NoteOn') self.assertEqual(MyMIDI.tracks[2].MIDIEventList[0].tick, MyMIDI.time_to_ticks(0 + duration)) self.assertEqual(MyMIDI.tracks[2].MIDIEventList[1].evtname, 'NoteOff') self.assertEqual(MyMIDI.tracks[2].MIDIEventList[1].tick, MyMIDI.time_to_ticks(0 + duration)) # Negative Time MyMIDI = MIDIFile(1, adjust_origin=True) track = 0 channel = 0 pitch = 100 time = -5 duration = 1 volume = 100 MyMIDI.addNote(track, channel, pitch, time, duration, volume) MyMIDI.close() self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].evtname, 'NoteOn') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].tick, MyMIDI.time_to_ticks(0)) self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].evtname, 'NoteOff') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].tick, MyMIDI.time_to_ticks(0 + duration)) # Negative time, two tracks MyMIDI = MIDIFile(2, adjust_origin=True) track = 0 channel = 0 pitch = 100 time = -1 duration = 1 volume = 100 MyMIDI.addNote(track, channel, pitch, time, duration, volume) track2 = 1 time2 = 0 MyMIDI.addNote(track2, channel, pitch, time2, duration, volume) MyMIDI.close() self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].evtname, 'NoteOn') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].tick, MyMIDI.time_to_ticks(0)) self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].evtname, 'NoteOff') self.assertEqual(MyMIDI.tracks[1].MIDIEventList[1].tick, MyMIDI.time_to_ticks(1)) self.assertEqual(MyMIDI.tracks[2].MIDIEventList[0].evtname, 'NoteOn') self.assertEqual(MyMIDI.tracks[2].MIDIEventList[0].tick, MyMIDI.time_to_ticks(1)) self.assertEqual(MyMIDI.tracks[2].MIDIEventList[1].evtname, 'NoteOff') self.assertEqual(MyMIDI.tracks[2].MIDIEventList[1].tick, MyMIDI.time_to_ticks(1)) 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): 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].evtname, '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 testPitchWheel(self): val = 1000 MyMIDI = MIDIFile(1) MyMIDI.addPitchWheelEvent(0, 0, 0, val) MyMIDI.close() MSB = (val + 8192) >> 7 LSB = (val + 8192) & 0x7F data = Decoder(MyMIDI.tracks[1].MIDIdata) self.assertEqual(data.unpack_into_byte(0), 0x00) # time self.assertEqual(data.unpack_into_byte(1), 224) # Code self.assertEqual(data.unpack_into_byte(2), LSB) self.assertEqual(data.unpack_into_byte(3), MSB) val = -1000 MyMIDI = MIDIFile(1) MyMIDI.addPitchWheelEvent(0, 0, 0, val) MyMIDI.close() MSB = (val + 8192) >> 7 LSB = (val + 8192) & 0x7F data = Decoder(MyMIDI.tracks[1].MIDIdata) self.assertEqual(data.unpack_into_byte(0), 0x00) # time self.assertEqual(data.unpack_into_byte(1), 224) # Code self.assertEqual(data.unpack_into_byte(2), LSB) self.assertEqual(data.unpack_into_byte(3), MSB) def testTempo(self): 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].evtname, '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].evtname, '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): 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].evtname, '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): 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].evtname, '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].evtname, '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].evtname, '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[0].MIDIdata) self.assertEqual(MyMIDI.tracks[0].MIDIEventList[0].evtname, '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): program = 10 channel = 0 tracknum = 0 realtracknum = tracknum time = 0.0 MyMIDI = MIDIFile(1) if MyMIDI.header.numeric_format == 1: realtracknum = tracknum + 1 MyMIDI.addProgramChange(tracknum, channel, time, program) MyMIDI.close() data = Decoder(MyMIDI.tracks[realtracknum].MIDIdata) self.assertEqual(MyMIDI.tracks[realtracknum].MIDIEventList[0].evtname, '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 testChannelPressure(self): pressure = 10 channel = 0 time = 0.0 tracknum = 0 realtracknum = tracknum MyMIDI = MIDIFile(1) if MyMIDI.header.numeric_format == 1: realtracknum = tracknum + 1 MyMIDI.addChannelPressure(tracknum, channel, time, pressure) MyMIDI.close() data = Decoder(MyMIDI.tracks[realtracknum].MIDIdata) self.assertEqual(MyMIDI.tracks[realtracknum].MIDIEventList[0].evtname, 'ChannelPressure') self.assertEqual(data.unpack_into_byte(0), 0x00) # time self.assertEqual(data.unpack_into_byte(1), 0xD0 | channel) # Code self.assertEqual(data.unpack_into_byte(2), pressure) def testTrackName(self): 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].evtname, '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 testLongTrackName(self): track_name = 'long track name ' * 8 MyMIDI = MIDIFile(1) MyMIDI.addTrackName(0, 0, track_name) MyMIDI.close() data = Decoder(MyMIDI.tracks[1].MIDIdata) self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].evtname, '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): 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].evtname, '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): 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].evtname, '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): 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].evtname, '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): 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].evtname, '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): 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].evtname, '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): 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].evtname, '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): 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].evtname, '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 * MyMIDI.ticks_per_quarternote) MyMIDI.addUniversalSysEx(0, time, code, subcode, payload, realTime=False) MyMIDI.close() data = Decoder(MyMIDI.tracks[1].MIDIdata) self.assertEqual(MyMIDI.tracks[1].MIDIEventList[0].evtname, '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].evtname, '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].evtname, '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 testAdjustOrigin(self): track = 0 channel = 0 pitch = 69 time = 1 duration = 0.1 volume = 64 MyMIDI = MIDIFile(1, adjust_origin=True) 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), MyMIDI.ticks_per_quarternote / 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 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) # also adds a corresponding NoteOff MyMIDI.addNote(track, channel, pitch, time, duration, volume) # also adds a corresponding NoteOff MyMIDI.close() self.assertEqual(2, len(MyMIDI.tracks[1].eventList)) # One NoteOn event, one NoteOff 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(4, len(MyMIDI.tracks[1].eventList)) # Two NoteOn events, two NoteOff 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[track + 1].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[track + 1].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)