├── .gitignore ├── LICENSE ├── MIDIUtil-0.89 └── MIDIUtil-0.89 │ ├── CHANGELOG │ ├── License.txt │ ├── MANIFEST │ ├── PKG-INFO │ ├── README.txt │ ├── VERSION │ ├── documentation │ ├── ClassReference.txt │ └── Extending.txt │ ├── examples │ └── single-note-example.py │ ├── setup.py │ └── src │ ├── midiutil │ ├── MidiFile.py │ ├── MidiFile3.py │ └── __init__.py │ └── unittests │ ├── miditest.py │ └── miditest.py3 ├── README.md ├── best_fit.py ├── main.py ├── note.py ├── rectangle.py └── resources ├── samples ├── Moonlight Shadow Flauta-1.png ├── VarnattstankarvidFridasruta.jpg ├── fire.jpg ├── lost.jpg ├── races.png └── sheet.jpg └── template ├── bar-rest.png ├── f-sharp.png ├── flat-line.png ├── flat-space.png ├── half-line.png ├── half-note-line.png ├── half-note-space.png ├── half-space.png ├── quarter.png ├── sharp.png ├── solid-note.png ├── staff.png ├── staff2.png ├── staff3.png ├── whole-line.png ├── whole-note-line.png ├── whole-note-space.png └── whole-space.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Calvert Pratt, Calvin Gregory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/CHANGELOG: -------------------------------------------------------------------------------- 1 | Date: 1 December 2013 2 | Version: 0.89 3 | * Updated MIDIFile to support non-integral note values better. 4 | * Changed default temporal resolution to 960 ticks per beat. 5 | * Updated Python3 support. It is still somewhat experimental. 6 | * Misc. Bug Fixes. 7 | 8 | Date: 20 October 2009 9 | Version: 0.87 10 | 11 | First public release. 12 | 13 | * Tweaked email address in contact information. 14 | * Added/updated documentation. 15 | * Tweaked the setup.py file to produce better distributions. 16 | 17 | Date: 9 October 2009 18 | Version: 0.86 19 | 20 | * added addNote as main interface into package (not 21 | addNoteByNumber). It's been a while since I've cut a release, 22 | so there may be other things that have happened. 23 | 24 | * Created distutils package. 25 | 26 | * Minor code clean-up. 27 | 28 | * Added documentation in-line and in text (MIDIFile.txt). 29 | 30 | * All public functions should now be accessed thought 31 | MIDIFile directly, and not the component tracks. 32 | 33 | Date: 15 January 2009 34 | Version: 0.85 35 | 36 | * Split out from existing work as a separate project. 37 | 38 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/License.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------- 2 | MIDUTIL, Copyright (c) 2009, Mark Conway Wirt 3 | 4 | 5 | This software is distributed under an Open Source license, the 6 | details of which follow. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a 9 | copy of this software and associated documentation files (the "Software"), 10 | to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | and/or sell copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included 16 | in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 22 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------- 26 | 27 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/MANIFEST: -------------------------------------------------------------------------------- 1 | README.txt 2 | setup.py 3 | License.txt 4 | CHANGELOG 5 | VERSION 6 | MANIFEST 7 | src/midiutil/MidiFile.py 8 | src/midiutil/MidiFile3.py 9 | src/midiutil/__init__.py 10 | examples/single-note-example.py 11 | documentation/Extending.txt 12 | documentation/ClassReference.txt 13 | src/unittests/miditest.py 14 | src/unittests/miditest.py3 15 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: MIDIUtil 3 | Version: 0.89 4 | Summary: MIDIUtil, a MIDI Interface for Python 5 | Home-page: www.emergentmusics.org 6 | Author: Mark Conway Wirt 7 | Author-email: emergentmusics) at (gmail . com 8 | License: Copyright (C) 2009, Mark Conway Wirt. See License.txt for details. 9 | Description: 10 | This package provides a simple interface to allow Python programs to 11 | write multi-track MIDI files. 12 | Platform: Platform Independent 13 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/README.txt: -------------------------------------------------------------------------------- 1 | ======== 2 | MIDIUtil 3 | ======== 4 | 5 | ------------ 6 | Introduction 7 | ------------ 8 | 9 | MIDIUtil is a pure Python library that allows one to write muti-track 10 | Musical Instrument Digital Interface (MIDI) files from within Python 11 | programs. It is object-oriented and allows one to create and write these 12 | files with a minimum of fuss. 13 | 14 | MIDIUtil isn't a full implementation of the MIDI specification. The actual 15 | specification is a large, sprawling document which has organically grown 16 | over the course of decades. I have selectively implemented some of the 17 | more useful and common aspects of the specification. The choices have 18 | been somewhat idiosyncratic; I largely implemented what I needed. When 19 | I decided that it could be of use to other people I fleshed it out a bit, 20 | but there are still things missing. Regardless, the code is fairly easy to 21 | understand and well structured. Additions can be made to the library by 22 | anyone with a good working knowledge of the MIDI file format and a good, 23 | working knowledge of Python. Documentation for extending the library 24 | is provided. 25 | 26 | This software was originally developed with Python 2.5.2 and it makes use 27 | of some features that were introduced in 2.5. I have used it extensively 28 | in Python 2.6. 29 | 30 | Included in this version is an intitial port to Python 3 (but which should 31 | work in 2.6.X also). The file is called MidiFile3.py. To use it, use 32 | the following import line in your code: 33 | 34 | from midiutil.MidiFile3 import MIDIFile 35 | 36 | (This assumes that the code has been installed into your system path or that 37 | the midiutil directory is copied into your script's working directory.) 38 | 39 | This software is distributed under an Open Source license and you are 40 | free to use it as you see fit, provided that attribution is maintained. 41 | See License.txt in the source distribution for details. 42 | 43 | ------------ 44 | Installation 45 | ------------ 46 | 47 | To use the library one can either install it on one's system or 48 | copy the midiutil directory of the source distribution to your 49 | project's directory (or to any directory pointed to  by the PYTHONPATH 50 | environment variable). For the Windows platforms an executable installer 51 | is provided. Alternately the source distribution can be downloaded, 52 | un-zipped (or un-tarred), and installed in the standard way: 53 | 54 | python setup.py install 55 | 56 | On non-Windows platforms (Linux, MacOS, etc.) the software should be 57 | installed in this way. MIDIUtil is pure Python and should work on any 58 | platform to which Python has been ported. 59 | 60 | If you do not wish to install in on your system, just copy the 61 | src/midiutil directory to your project's directory or elsewhere on 62 | your PYTHONPATH. If you're using this software in your own projects 63 | you may want to consider distributing the library bundled with yours; 64 | the library is small and self-contained, and such bundling makes things 65 | more convenient for your users. The best way of doing this is probably 66 | to copy the midiutil directory directly to your package directory and 67 | then refer to it with a fully qualified name. This will prevent it from 68 | conflicting with any version of the software that may be installed on 69 | the target system. 70 | 71 | ----------- 72 | Quick Start 73 | ----------- 74 | 75 | Using the software is easy: 76 | 77 | o The package must be imported into your namespace 78 | o A MIDIFile object is created 79 | o Events (notes, tempo-changes, etc.) are added to the object 80 | o The MIDI file is written to disk. 81 | 82 | Detailed documentation is provided; what follows is a simple example 83 | to get you going quickly. In this example we'll create a one track MIDI 84 | File, assign a name and tempo to the track, add a one beat middle-C to 85 | the track, and write it to disk. 86 | 87 | #Import the library 88 | from midiutil.MidiFile import MIDIFile 89 | 90 | # Create the MIDIFile Object with 1 track 91 | MyMIDI = MIDIFile(1) 92 | 93 | # Tracks are numbered from zero. Times are measured in beats. 94 | track = 0 95 | time = 0 96 | 97 | # Add track name and tempo. 98 | MyMIDI.addTrackName(track,time,"Sample Track") 99 | MyMIDI.addTempo(track,time,120) 100 | 101 | # Add a note. addNote expects the following information: 102 | track = 0 103 | channel = 0 104 | pitch = 60 105 | time = 0 106 | duration = 1 107 | volume = 100 108 | 109 | # Now add the note. 110 | MyMIDI.addNote(track,channel,pitch,time,duration,volume) 111 | 112 | # And write it to disk. 113 | binfile = open("output.mid", 'wb') 114 | MyMIDI.writeFile(binfile) 115 | binfile.close() 116 | 117 | There are several additional event types that can be added and there are 118 | various options available for creating the MIDIFile object, but the above 119 | is sufficient to begin using the library and creating note sequences. 120 | 121 | The above code is found in machine-readable form in the examples directory. 122 | A detailed class reference and documentation describing how to extend 123 | the library is provided in the documentation directory. 124 | 125 | Have fun! 126 | 127 | --------- 128 | Thank You 129 | --------- 130 | 131 | I'd like to mention the following people who have given feedback, but 132 | fixes, and suggestions on the library: 133 | 134 | Bram de Jong 135 | Mike Reeves-McMillan 136 | Egg Syntax 137 | Nils Gey 138 | Francis G. 139 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/VERSION: -------------------------------------------------------------------------------- 1 | This is version 0.89. 2 | 3 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/documentation/ClassReference.txt: -------------------------------------------------------------------------------- 1 | ======================== 2 | MIDIUtil Class Reference 3 | ======================== 4 | 5 | -------------- 6 | class MIDIFile 7 | -------------- 8 | 9 | A class that represents a full, well-formed MIDI pattern. 10 | 11 | This is a container object that contains a header, one or more 12 | tracks, and the data associated with a proper and well-formed 13 | MIDI pattern. 14 | 15 | Calling 16 | 17 | MyMIDI = MidiFile(tracks, removeDuplicates=True,  deinterleave=True) 18 | 19 | normally 20 | 21 | MyMIDI = MidiFile(tracks) 22 | 23 | Arguments 24 | 25 | o tracks: The number of tracks this object contains 26 | 27 | o removeDuplicates: If true (the default), the software will 28 | remove duplicate events which have been added. For example, 29 | two notes at the same channel, time, pitch, and duration would 30 | be considered duplicate. 31 | 32 | o deinterleave: If True (the default), overlapping notes 33 | (same pitch, same channel) will be modified so that they do 34 | not overlap. Otherwise the sequencing software will need to 35 | figure out how to interpret NoteOff events upon playback. 36 | 37 | ================ 38 | Public Functions 39 | ================ 40 | 41 | --------------------------------------------------- 42 | addNote(track, channel, pitch,time,duration,volume) 43 | --------------------------------------------------- 44 | 45 | Add notes to the MIDIFile object 46 | 47 | Use 48 | 49 | MyMIDI.addNotes(track,channel,pitch,time, duration, volume) 50 | 51 | Arguments 52 | 53 | o track: The track to which the note is added. 54 | o channel: the MIDI channel to assign to the note. [Integer, 0-15] 55 | o pitch: the MIDI pitch number [Integer, 0-127]. 56 | o time: the time (in beats) at which the note sounds [Float]. 57 | o duration: the duration of the note (in beats) [Float]. 58 | o lume: the volume (velocity) of the note. [Integer, 0-127]. 59 | 60 | 61 | ---------------------------------- 62 | addTrackName(track, time,trackName) 63 | ---------------------------------- 64 | 65 | Add a track name to a MIDI track. 66 | 67 | Use 68 | 69 | MyMIDI.addTrackName(track,time,trackName) 70 | 71 | Arguments 72 | 73 | o track: The track to which the name is added. [Integer, 0-127]. 74 | o time: The time at which the track name is added, in beats 75 | [Float]. 76 | o trackName: The track name. [String]. 77 | 78 | --------------------------- 79 | addTempo(track, time,tempo) 80 | --------------------------- 81 | 82 | Add a tempo event. 83 | 84 | Use 85 | 86 | MyMIDI.addTempo(track, time, tempo) 87 | 88 | Arguments 89 | 90 | o track: The track to which the event is added. [Integer, 0-127] 91 | o time: The time at which the event is added, in beats. [Float] 92 | o tempo: The tempo, in Beats per Minute. [Integer] 93 | 94 | 95 | ----------------------------------------------- 96 | addProgramChange(track, channel, time, program) 97 | ----------------------------------------------- 98 | 99 | Add a MIDI program change event. 100 | 101 | Use 102 | 103 | MyMIDI.addProgramChange(track,channel, time, program) 104 | 105 | Arguments 106 | 107 | o track: The track to which the event is added. [Integer, 0-127] 108 | o channel: The channel the event is assigned to. [Integer, 0-15] 109 | o time: The time at which the event is added, in beats. [Float] 110 | o program: the program number. [Integer, 0-127] 111 | 112 | 113 | -------------------------------------------------------------- 114 | addControllerEvent(track, channel,time,eventType, paramerter1) 115 | -------------------------------------------------------------- 116 | 117 | Add a MIDI controller event. 118 | 119 | Use 120 | 121 | MyMIDI.addControllerEvent(track, channel, time, eventType, \ 122 | parameter1) 123 | 124 | Arguments 125 | 126 | o track: The track to which the event is added. [Integer, 0-127] 127 | o channel: The channel the event is assigned to. [Integer, 0-15] 128 | o time: The time at which the event is added, in beats. [Float] 129 | o eventType: the controller event type. 130 | o parameter1: The event's parameter. The meaning of which varies 131 | by event type. 132 | 133 | --------------------------------------------------------------------- 134 | changeNoteTuning(track, tunings, sysExChannel=0x7F, realTime=False, \ 135 | tuningProgam=0) 136 | --------------------------------------------------------------------- 137 | 138 | Change a note's tuning using sysEx change tuning program. 139 | 140 | Use 141 | 142 | MyMIDI.changeNoteTuning(track,[tunings],realTime=False, \ 143 | tuningProgram=0) 144 | 145 | Arguments 146 | 147 | o track: The track to which the event is added. [Integer, 0-127]. 148 | o tunings: A list of tuples in the form (pitchNumber, 149 | frequency).  [[(Integer,Float]] 150 | o realTime: Boolean which sets the real-time flag. Defaults to false. 151 | o sysExChannel: do note use (see below). 152 | o tuningProgram: Tuning program to assign. Defaults to 153 | zero. [Integer, 0-127] 154 | 155 | In general the sysExChannel should not be changed (parameter will 156 | be depreciated). 157 | 158 | Also note that many software packages and hardware packages do not 159 | implement this standard! 160 | 161 | 162 | --------------------- 163 | writeFile(fileHandle) 164 | --------------------- 165 | 166 | Write the MIDI File. 167 | 168 | Use 169 | 170 | MyMIDI.writeFile(filehandle) 171 | 172 | Arguments 173 | 174 | o filehandle: a file handle that has been opened for binary 175 | writing. 176 | 177 | 178 | ------------------------------------- 179 | addSysEx(track, time, manID, payload) 180 | ------------------------------------- 181 | 182 | Add a SysEx event 183 | 184 | Use 185 | 186 | MyMIDI.addSysEx(track,time,ID,payload) 187 | 188 | Arguments 189 | 190 | o track: The track to which the event is added. [Integer, 0-127]. 191 | o time: The time at which the event is added, in beats. [Float]. 192 | o ID: The SysEx ID number 193 | o payload: the event payload. 194 | 195 | Note: This is a low-level MIDI function, so care must be used in 196 | constructing the payload. It is recommended that higher-level helper 197 | functions be written to wrap this function and construct the payload 198 | if a developer finds him or herself using the function heavily. 199 | 200 | 201 | --------------------------------------------------------- 202 | addUniversalSysEx(track,  time,code, subcode, payload, \ 203 | sysExChannel=0x7F,  realTime=False)}f 204 | --------------------------------------------------------- 205 | 206 | Add a Universal SysEx event. 207 | 208 | Use 209 | 210 | MyMIDI.addUniversalSysEx(track, time, code, subcode, payload, \ 211 | sysExChannel=0x7f, realTime=False) 212 | 213 | Arguments 214 | 215 | o track: The track to which the event is added. [Integer, 0-127]. 216 | o time: The time at which the event is added, in beats. [Float]. 217 | o code: The event code. [Integer] 218 | o subcode The event sub-code [Integer] 219 | o payload: The event payload. [Binary string] 220 | o sysExChannel: The SysEx channel. 221 | o realTime: Sets the real-time flag. Defaults to zero. 222 | 223 | Note: This is a low-level MIDI function, so care must be used in 224 | constructing the payload. It is recommended that higher-level helper 225 | functions be written to wrap this function and construct the payload 226 | if a developer finds him or herself using the function heavily. As an 227 | example of such a helper function, see the changeNoteTuning function, 228 | both here and in MIDITrack. 229 | 230 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/documentation/Extending.txt: -------------------------------------------------------------------------------- 1 | ===================== 2 | Extending the Library 3 | ===================== 4 | 5 | The choice of MIDI event types included in the library is somewhat 6 | idiosyncratic; I included the events I needed for another software 7 | project I was wrote. You may find that you need additional events in 8 | your work. For this reason I am including some instructions on extending 9 | the library. The process isn't too hard (provided you have a working 10 | knowledge of Python and the MIDI standard), so the task shouldn't present 11 | a competent coder too much difficulty. Alternately (if, for example, 12 | you *don't* have a working knowledge of MIDI and don't desire to gain it), 13 | you can submit new feature requests to me, and I will include them into 14 | the development branch of the code, subject to the constraints of time. 15 | 16 | To illustrate the process I show below how the MIDI tempo event is 17 | incorporated into the code. This is a relatively simple event, so while 18 | it may not illustrate some of the subtleties of MIDI programing, it 19 | provides a good, illustrative case. 20 | 21 | 22 | ----------------------- 23 | Create a New Event Type 24 | ----------------------- 25 | 26 | The first order of business is to create a new subclass of the GnericEvent 27 | object of the MIDIFile module. This subclass initializes any specific 28 | instance data that is needed for the MIDI event to be written. In 29 | the case of the tempo event, it is the actual tempo (which is defined 30 | in the MIDI standard to be 60000000 divided by the tempo in beats per 31 | minute). This class should also call the superclass' initializer with 32 | the event time and set the event type (a unique string used internally by 33 | the software) in the __init__() function. In the case of the tempo event: 34 | 35 | class tempo(GenericEvent): 36 | def __init__(self,time,tempo): 37 | GenericEvent.__init__(self,time) 38 | self.type = 'tempo' 39 | self.tempo = int(60000000 / tempo) 40 | 41 | Next (and this is an embarrassing break of OO programming) the __eq__() 42 | function of the GenericEvent class should be modified so that equality 43 | of these types of events can be calculated. In calculating equivalence 44 | time is always checked, so two tempo events are considered the same if 45 | the have the same tempo value. Thus the following snippet of code from 46 | GenericEvent's _eq__() function accomplishes this goal: 47 | 48 | 49 | if self.type == 'tempo': 50 | if self.tempo != other.tempo: 51 | return False 52 | 53 | 54 | If events are equivalent, the code should return False. If they are not 55 | equivalent no return should be called. 56 | 57 | --------------------------- 58 | Create an Accessor Function 59 | --------------------------- 60 | 61 | 62 | Next, an accessor function should be added to MIDITrack to create an 63 | event of this type. Continuing the example of the tempo event: 64 | 65 | 66 | def addTempo(self,time,tempo): 67 | self.eventList.append(MIDITrack.tempo(time,tempo)) 68 | 69 | 70 | The public accessor function is via the MIDIFile object, and must include 71 | the track number to which the event is written: 72 | 73 | 74 | def addTempo(self,track,time,tempo): 75 | self.tracks[track].addTempo(time,tempo) 76 | 77 | 78 | This is the function you will use in your code to create an event of 79 | the desired type. 80 | 81 | 82 | ----------------------- 83 | Modify processEventList 84 | ----------------------- 85 | 86 | Next, the logic pertaining to the new event type should be added to 87 | processEventList function of the MIDITrack class. In general this code 88 | will create a MIDIEvent object and set its type, time, ordinality, and 89 | any specific information that is needed for the event type. This object 90 | is then added to the MIDIEventList. 91 | 92 | The ordinality (self.ord) is a number that tells the software how to 93 | sequence MIDI events that occur at the same time. The higher the number, 94 | the later in the sequence the event will be written in comparison to 95 | other, simultaneous events. 96 | 97 | The relevant section for the tempo event is: 98 | 99 | 100 | elif thing.type == 'tempo': 101 | event = MIDIEvent() 102 | event.type = "Tempo" 103 | event.time = thing.time * TICKSPERBEAT 104 | event.tempo = thing.tempo 105 | event.ord = 3 106 | self.MIDIEventList.append(event) 107 | 108 | 109 | Thus if other events occur at the same time, type which have an ordinality 110 | of 1 or 2 will be written to the stream first. 111 | 112 | Time needs to be converted from beats (which the accessor function uses) 113 | and MIDI time by multiplying by the constant TICKSPERBEAT. The value 114 | of thing.type is the unique string you defined above, and event.type 115 | is another unique things (they can--and probably should--be the same, 116 | although the coding here is a little sloppy and changes case of the 117 | string). 118 | 119 | 120 | ---------------------------------------- 121 | Write the Event Data to the MIDI Stream 122 | ---------------------------------------- 123 | 124 | 125 | The last step is to modify the MIDIFile writeEventsToStream function; 126 | here is where some understanding of the MIDI standard is necessary. The 127 | following code shows the creation of a MIDI tempo event: 128 | 129 | 130 | elif event.type == "Tempo": 131 | code = 0xFF 132 | subcode = 0x51 133 | fourbite = struct.pack('>L', event.tempo) 134 | threebite = fourbite[1:4] # Just discard the MSB 135 | varTime = writeVarLength(event.time) 136 | for timeByte in varTime: 137 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 138 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 139 | self.MIDIdata = self.MIDIdata + struct.pack('>B',subcode) 140 | self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x03) 141 | self.MIDIdata = self.MIDIdata + threebite 142 | 143 | 144 | The event.type string ("Tempo") was the one chosen in the processEventList 145 | logic. 146 | 147 | The code and subcode are binary values that come from the MIDI 148 | specification. 149 | 150 | Next the data is packed into a three byte structure (or a four byte 151 | structure, discarding the most significant byte). Again, the MIDI 152 | specification determines the number of bytes used in the data payload. 153 | 154 | The event time should be converted to MIDI variable-length data with the 155 | writeVarLength() function before writing to the stream (as shown above). 156 | The MIDI standard utilizes a slightly bizarre variable length data 157 | record. In it, only seven bits of a word are used to store data; the 158 | eighth bit signifies if more bytes encoding the value follow. The total 159 | length may be 1 to 3 bytes, depending upon the size of the value encoded. 160 | The writeVarLength() function takes care of this conversion for you. 161 | 162 | Now the data is written to the binary object self.MIDIdata, which is 163 | the actual MIDI-encoded data stream. As per the MIDI standard, first we 164 | write our variable-length time value. Next we add the event type code and 165 | subcode. Then we write the length of the data payload, which in the case 166 | of the tempo event is three bytes. Lastly, we write the actual payload, 167 | which has been packed into the variable threebite. 168 | 169 | Clear as mud! 170 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/examples/single-note-example.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # A sample program to create a single-track MIDI file, add a note, 3 | # and write to disk. 4 | ############################################################################ 5 | 6 | #Import the library 7 | from midiutil.MidiFile import MIDIFile 8 | 9 | # Create the MIDIFile Object 10 | MyMIDI = MIDIFile(1) 11 | 12 | # Add track name and tempo. The first argument to addTrackName and 13 | # addTempo is the time to write the event. 14 | track = 0 15 | time = 0 16 | MyMIDI.addTrackName(track,time,"Sample Track") 17 | MyMIDI.addTempo(track,time, 120) 18 | 19 | # Add a note. addNote expects the following information: 20 | channel = 0 21 | pitch = 60 22 | duration = 1 23 | volume = 100 24 | 25 | # Now add the note. 26 | MyMIDI.addNote(track,channel,pitch,time,duration,volume) 27 | 28 | # And write it to disk. 29 | binfile = open("output.mid", 'wb') 30 | MyMIDI.writeFile(binfile) 31 | binfile.close() 32 | 33 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup(name='MIDIUtil', 4 | version='0.89', 5 | description='MIDIUtil, a MIDI Interface for Python', 6 | author='Mark Conway Wirt', 7 | author_email='emergentmusics) at (gmail . com', 8 | license='Copyright (C) 2009, Mark Conway Wirt. See License.txt for details.', 9 | url='www.emergentmusics.org', 10 | packages=["midiutil"], 11 | package_dir = {'midiutil': 'src/midiutil'}, 12 | package_data={'midiutil' : ['../../documentation/*']}, 13 | scripts=['examples/single-note-example.py'], 14 | platforms='Platform Independent', 15 | long_description=''' 16 | This package provides a simple interface to allow Python programs to 17 | write multi-track MIDI files.''' 18 | ) 19 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/src/midiutil/MidiFile.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Name: MidiFile.py 3 | # Purpose: MIDI file manipulation utilities 4 | # 5 | # Author: Mark Conway Wirt 6 | # 7 | # Created: 2008/04/17 8 | # Copyright: (c) 2009 Mark Conway Wirt 9 | # License: Please see License.txt for the terms under which this 10 | # software is distributed. 11 | #----------------------------------------------------------------------------- 12 | 13 | import struct, sys, math 14 | 15 | # TICKSPERBEAT is the number of "ticks" (time measurement in the MIDI file) that 16 | # corresponds to one beat. This number is somewhat arbitrary, but should be chosen 17 | # to provide adequate temporal resolution. 18 | 19 | TICKSPERBEAT = 960 20 | 21 | controllerEventTypes = { 22 | 'pan' : 0x0a 23 | } 24 | class MIDIEvent: 25 | ''' 26 | The class to contain the MIDI Event (placed on MIDIEventList. 27 | ''' 28 | def __init__(self): 29 | self.type='unknown' 30 | self.time=0 31 | self.ord = 0 32 | 33 | def __cmp__(self, other): 34 | ''' Sorting function for events.''' 35 | if self.time < other.time: 36 | return -1 37 | elif self.time > other.time: 38 | return 1 39 | else: 40 | if self.ord < other.ord: 41 | return -1 42 | elif self.ord > other.ord: 43 | return 1 44 | else: 45 | return 0 46 | 47 | class GenericEvent(): 48 | '''The event class from which specific events are derived 49 | ''' 50 | def __init__(self,time): 51 | self.time = time 52 | self.type = 'Unknown' 53 | 54 | 55 | 56 | def __eq__(self, other): 57 | ''' 58 | Equality operator for Generic Events and derived classes. 59 | 60 | In the processing of the event list, we have need to remove duplicates. To do this 61 | we rely on the fact that the classes are hashable, and must therefore have an 62 | equality operator (__hash__() and __eq__() must both be defined). 63 | 64 | This is the most embarrassing portion of the code, and anyone who knows about OO 65 | programming would find this almost unbelievable. Here we have a base class that 66 | knows specifics about derived classes, thus breaking the very spirit of 67 | OO programming. 68 | 69 | I suppose I should go back and restructure the code, perhaps removing the derived 70 | classes altogether. At some point perhaps I will. 71 | ''' 72 | if self.time != other.time or self.type != other.type: 73 | return False 74 | 75 | # What follows is code that encodes the concept of equality for each derived 76 | # class. Believe it f you dare. 77 | 78 | if self.type == 'note': 79 | if self.pitch != other.pitch or self.channel != other.channel: 80 | return False 81 | if self.type == 'tempo': 82 | if self.tempo != other.tempo: 83 | return False 84 | if self.type == 'programChange': 85 | if self.programNumber != other.programNumber or self.channel != other.channel: 86 | return False 87 | if self.type == 'trackName': 88 | if self.trackName != other.trackName: 89 | return False 90 | if self.type == 'controllerEvent': 91 | if self.parameter1 != other.parameter1 or \ 92 | self.channel != other.channel or \ 93 | self.eventType != other.eventType: 94 | return False 95 | 96 | if self.type == 'SysEx': 97 | if self.manID != other.manID: 98 | return False 99 | 100 | if self.type == 'UniversalSysEx': 101 | if self.code != other.code or\ 102 | self.subcode != other.subcode or \ 103 | self.sysExChannel != other.sysExChannel: 104 | return False 105 | 106 | return True 107 | 108 | def __hash__(self): 109 | ''' 110 | Return a hash code for the object. 111 | 112 | This is needed for the removal of duplicate objects from the event list. The only 113 | real requirement for the algorithm is that the hash of equal objects must be equal. 114 | There is probably great opportunity for improvements in the hashing function. 115 | ''' 116 | # Robert Jenkin's 32 bit hash. 117 | a = int(self.time) 118 | a = (a+0x7ed55d16) + (a<<12) 119 | a = (a^0xc761c23c) ^ (a>>19) 120 | a = (a+0x165667b1) + (a<<5) 121 | a = (a+0xd3a2646c) ^ (a<<9) 122 | a = (a+0xfd7046c5) + (a<<3) 123 | a = (a^0xb55a4f09) ^ (a>>16) 124 | return a 125 | 126 | class MIDITrack: 127 | '''A class that encapsulates a MIDI track 128 | ''' 129 | # Nested class definitions. 130 | 131 | class note(GenericEvent): 132 | '''A class that encapsulates a note 133 | ''' 134 | def __init__(self,channel, pitch,time,duration,volume): 135 | 136 | GenericEvent.__init__(self,time) 137 | self.pitch = pitch 138 | self.duration = duration 139 | self.volume = volume 140 | self.type = 'note' 141 | self.channel = channel 142 | 143 | def compare(self, other): 144 | '''Compare two notes for equality. 145 | ''' 146 | if self.pitch == other.pitch and \ 147 | self.time == other.time and \ 148 | self.duration == other.duration and \ 149 | self.volume == other.volume and \ 150 | self.type == other.type and \ 151 | self.channel == other.channel: 152 | return True 153 | else: 154 | return False 155 | 156 | 157 | class tempo(GenericEvent): 158 | '''A class that encapsulates a tempo meta-event 159 | ''' 160 | def __init__(self,time,tempo): 161 | 162 | GenericEvent.__init__(self,time) 163 | self.type = 'tempo' 164 | self.tempo = int(60000000 / tempo) 165 | 166 | class programChange(GenericEvent): 167 | '''A class that encapsulates a program change event. 168 | ''' 169 | 170 | def __init__(self, channel, time, programNumber): 171 | GenericEvent.__init__(self, time,) 172 | self.type = 'programChange' 173 | self.programNumber = programNumber 174 | self.channel = channel 175 | 176 | class SysExEvent(GenericEvent): 177 | '''A class that encapsulates a System Exclusive event. 178 | ''' 179 | 180 | def __init__(self, time, manID, payload): 181 | GenericEvent.__init__(self, time,) 182 | self.type = 'SysEx' 183 | self.manID = manID 184 | self.payload = payload 185 | 186 | class UniversalSysExEvent(GenericEvent): 187 | '''A class that encapsulates a Universal System Exclusive event. 188 | ''' 189 | 190 | def __init__(self, time, realTime, sysExChannel, code, subcode, payload): 191 | GenericEvent.__init__(self, time,) 192 | self.type = 'UniversalSysEx' 193 | self.realTime = realTime 194 | self.sysExChannel = sysExChannel 195 | self.code = code 196 | self.subcode = subcode 197 | self.payload = payload 198 | 199 | class ControllerEvent(GenericEvent): 200 | '''A class that encapsulates a program change event. 201 | ''' 202 | 203 | def __init__(self, channel, time, eventType, parameter1,): 204 | GenericEvent.__init__(self, time,) 205 | self.type = 'controllerEvent' 206 | self.parameter1 = parameter1 207 | self.channel = channel 208 | self.eventType = eventType 209 | 210 | class trackName(GenericEvent): 211 | '''A class that encapsulates a program change event. 212 | ''' 213 | 214 | def __init__(self, time, trackName): 215 | GenericEvent.__init__(self, time,) 216 | self.type = 'trackName' 217 | self.trackName = trackName 218 | 219 | 220 | def __init__(self, removeDuplicates, deinterleave): 221 | '''Initialize the MIDITrack object. 222 | ''' 223 | self.headerString = struct.pack('cccc','M','T','r','k') 224 | self.dataLength = 0 # Is calculated after the data is in place 225 | self.MIDIdata = "" 226 | self.closed = False 227 | self.eventList = [] 228 | self.MIDIEventList = [] 229 | self.remdep = removeDuplicates 230 | self.deinterleave = deinterleave 231 | 232 | def addNoteByNumber(self,channel, pitch,time,duration,volume): 233 | '''Add a note by chromatic MIDI number 234 | ''' 235 | self.eventList.append(MIDITrack.note(channel, pitch,time,duration,volume)) 236 | 237 | def addControllerEvent(self,channel,time,eventType, paramerter1): 238 | ''' 239 | Add a controller event. 240 | ''' 241 | 242 | self.eventList.append(MIDITrack.ControllerEvent(channel,time,eventType, \ 243 | paramerter1)) 244 | 245 | def addTempo(self,time,tempo): 246 | ''' 247 | Add a tempo change (or set) event. 248 | ''' 249 | self.eventList.append(MIDITrack.tempo(time,tempo)) 250 | 251 | def addSysEx(self,time,manID, payload): 252 | ''' 253 | Add a SysEx event. 254 | ''' 255 | self.eventList.append(MIDITrack.SysExEvent(time, manID, payload)) 256 | 257 | def addUniversalSysEx(self,time,code, subcode, payload, sysExChannel=0x7F, \ 258 | realTime=False): 259 | ''' 260 | Add a Universal SysEx event. 261 | ''' 262 | self.eventList.append(MIDITrack.UniversalSysExEvent(time, realTime, \ 263 | sysExChannel, code, subcode, payload)) 264 | 265 | def addProgramChange(self,channel, time, program): 266 | ''' 267 | Add a program change event. 268 | ''' 269 | self.eventList.append(MIDITrack.programChange(channel, time, program)) 270 | 271 | def addTrackName(self,time,trackName): 272 | ''' 273 | Add a track name event. 274 | ''' 275 | self.eventList.append(MIDITrack.trackName(time,trackName)) 276 | 277 | def changeNoteTuning(self, tunings, sysExChannel=0x7F, realTime=False, \ 278 | tuningProgam=0): 279 | '''Change the tuning of MIDI notes 280 | ''' 281 | payload = struct.pack('>B', tuningProgam) 282 | payload = payload + struct.pack('>B', len(tunings)) 283 | for (noteNumber, frequency) in tunings: 284 | payload = payload + struct.pack('>B', noteNumber) 285 | MIDIFreqency = frequencyTransform(frequency) 286 | for byte in MIDIFreqency: 287 | payload = payload + struct.pack('>B', byte) 288 | 289 | self.eventList.append(MIDITrack.UniversalSysExEvent(0, realTime, sysExChannel,\ 290 | 8, 2, payload)) 291 | 292 | def processEventList(self): 293 | ''' 294 | Process the event list, creating a MIDIEventList 295 | 296 | For each item in the event list, one or more events in the MIDIEvent 297 | list are created. 298 | ''' 299 | 300 | # Loop over all items in the eventList 301 | 302 | for thing in self.eventList: 303 | if thing.type == 'note': 304 | event = MIDIEvent() 305 | event.type = "NoteOn" 306 | event.time = thing.time * TICKSPERBEAT 307 | event.pitch = thing.pitch 308 | event.volume = thing.volume 309 | event.channel = thing.channel 310 | event.ord = 3 311 | self.MIDIEventList.append(event) 312 | 313 | event = MIDIEvent() 314 | event.type = "NoteOff" 315 | event.time = (thing.time + thing.duration) * TICKSPERBEAT 316 | event.pitch = thing.pitch 317 | event.volume = thing.volume 318 | event.channel = thing.channel 319 | event.ord = 2 320 | self.MIDIEventList.append(event) 321 | 322 | elif thing.type == 'tempo': 323 | event = MIDIEvent() 324 | event.type = "Tempo" 325 | event.time = thing.time * TICKSPERBEAT 326 | event.tempo = thing.tempo 327 | event.ord = 3 328 | self.MIDIEventList.append(event) 329 | 330 | elif thing.type == 'programChange': 331 | event = MIDIEvent() 332 | event.type = "ProgramChange" 333 | event.time = thing.time * TICKSPERBEAT 334 | event.programNumber = thing.programNumber 335 | event.channel = thing.channel 336 | event.ord = 1 337 | self.MIDIEventList.append(event) 338 | 339 | elif thing.type == 'trackName': 340 | event = MIDIEvent() 341 | event.type = "TrackName" 342 | event.time = thing.time * TICKSPERBEAT 343 | event.trackName = thing.trackName 344 | event.ord = 0 345 | self.MIDIEventList.append(event) 346 | 347 | elif thing.type == 'controllerEvent': 348 | event = MIDIEvent() 349 | event.type = "ControllerEvent" 350 | event.time = thing.time * TICKSPERBEAT 351 | event.eventType = thing.eventType 352 | event.channel = thing.channel 353 | event.paramerter1 = thing.parameter1 354 | event.ord = 1 355 | self.MIDIEventList.append(event) 356 | 357 | elif thing.type == 'SysEx': 358 | event = MIDIEvent() 359 | event.type = "SysEx" 360 | event.time = thing.time * TICKSPERBEAT 361 | event.manID = thing.manID 362 | event.payload = thing.payload 363 | event.ord = 1 364 | self.MIDIEventList.append(event) 365 | 366 | elif thing.type == 'UniversalSysEx': 367 | event = MIDIEvent() 368 | event.type = "UniversalSysEx" 369 | event.realTime = thing.realTime 370 | event.sysExChannel = thing.sysExChannel 371 | event.time = thing.time * TICKSPERBEAT 372 | event.code = thing.code 373 | event.subcode = thing.subcode 374 | event.payload = thing.payload 375 | event.ord = 1 376 | self.MIDIEventList.append(event) 377 | 378 | else: 379 | print "Error in MIDITrack: Unknown event type" 380 | sys.exit(2) 381 | 382 | # Assumptions in the code expect the list to be time-sorted. 383 | # self.MIDIEventList.sort(lambda x, y: x.time - y.time) 384 | 385 | self.MIDIEventList.sort(lambda x, y: int( 1000 * (x.time - y.time))) 386 | 387 | if self.deinterleave: 388 | self.deInterleaveNotes() 389 | 390 | def removeDuplicates(self): 391 | ''' 392 | Remove duplicates from the eventList. 393 | 394 | This function will remove duplicates from the eventList. This is necessary 395 | because we the MIDI event stream can become confused otherwise. 396 | ''' 397 | 398 | # For this algorithm to work, the events in the eventList must be hashable 399 | # (that is, they must have a __hash__() and __eq__() function defined). 400 | 401 | tempDict = {} 402 | for item in self.eventList: 403 | tempDict[item] = 1 404 | 405 | self.eventList = tempDict.keys() 406 | 407 | # Sort on type, them on time. Necessary because keys() has no requirement to return 408 | # things in any order. 409 | 410 | self.eventList.sort(lambda x, y: cmp(x.type , y.type)) 411 | self.eventList.sort(lambda x, y: int( 1000 * (x.time - y.time))) #A bit of a hack. 412 | 413 | def closeTrack(self): 414 | '''Called to close a track before writing 415 | 416 | This function should be called to "close a track," that is to 417 | prepare the actual data stream for writing. Duplicate events are 418 | removed from the eventList, and the MIDIEventList is created. 419 | 420 | Called by the parent MIDIFile object. 421 | ''' 422 | 423 | if self.closed == True: 424 | return 425 | self.closed = True 426 | 427 | if self.remdep: 428 | self.removeDuplicates() 429 | 430 | 431 | self.processEventList() 432 | 433 | def writeMIDIStream(self): 434 | ''' 435 | Write the meta data and note data to the packed MIDI stream. 436 | ''' 437 | 438 | #Process the events in the eventList 439 | 440 | self.writeEventsToStream() 441 | 442 | # Write MIDI close event. 443 | 444 | self.MIDIdata = self.MIDIdata + struct.pack('BBBB',0x00,0xFF, \ 445 | 0x2F,0x00) 446 | 447 | # Calculate the entire length of the data and write to the header 448 | 449 | self.dataLength = struct.pack('>L',len(self.MIDIdata)) 450 | 451 | def writeEventsToStream(self): 452 | ''' 453 | Write the events in MIDIEvents to the MIDI stream. 454 | ''' 455 | preciseTime = 0.0 # Actual time of event, ignoring round-off 456 | actualTime = 0.0 # Time as written to midi stream, include round-off 457 | for event in self.MIDIEventList: 458 | 459 | preciseTime = preciseTime + event.time 460 | 461 | # Convert the time to variable length and back, to see how much 462 | # error is introduced 463 | 464 | testBuffer = "" 465 | varTime = writeVarLength(event.time) 466 | for timeByte in varTime: 467 | testBuffer = testBuffer + struct.pack('>B',timeByte) 468 | (roundedVal,discard) = readVarLength(0,testBuffer) 469 | roundedTime = actualTime + roundedVal 470 | # print "Rounded, Precise: %15.10f %15.10f" % (roundedTime, preciseTime) 471 | 472 | # Calculate the delta between the two and apply it to the event time. 473 | 474 | delta = preciseTime - roundedTime 475 | event.time = event.time + delta 476 | 477 | # Now update the actualTime value, using the updated event time. 478 | 479 | testBuffer = "" 480 | varTime = writeVarLength(event.time) 481 | for timeByte in varTime: 482 | testBuffer = testBuffer + struct.pack('>B',timeByte) 483 | (roundedVal,discard) = readVarLength(0,testBuffer) 484 | actualTime = actualTime + roundedVal 485 | 486 | for event in self.MIDIEventList: 487 | if event.type == "NoteOn": 488 | code = 0x9 << 4 | event.channel 489 | varTime = writeVarLength(event.time) 490 | for timeByte in varTime: 491 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 492 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 493 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.pitch) 494 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume) 495 | elif event.type == "NoteOff": 496 | code = 0x8 << 4 | event.channel 497 | varTime = writeVarLength(event.time) 498 | for timeByte in varTime: 499 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 500 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 501 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.pitch) 502 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume) 503 | elif event.type == "Tempo": 504 | code = 0xFF 505 | subcode = 0x51 506 | fourbite = struct.pack('>L', event.tempo) 507 | threebite = fourbite[1:4] # Just discard the MSB 508 | varTime = writeVarLength(event.time) 509 | for timeByte in varTime: 510 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 511 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 512 | self.MIDIdata = self.MIDIdata + struct.pack('>B',subcode) 513 | self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x03) # Data length: 3 514 | self.MIDIdata = self.MIDIdata + threebite 515 | elif event.type == 'ProgramChange': 516 | code = 0xC << 4 | event.channel 517 | varTime = writeVarLength(event.time) 518 | for timeByte in varTime: 519 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 520 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 521 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.programNumber) 522 | elif event.type == 'TrackName': 523 | varTime = writeVarLength(event.time) 524 | for timeByte in varTime: 525 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 526 | self.MIDIdata = self.MIDIdata + struct.pack('B',0xFF) # Meta-event 527 | self.MIDIdata = self.MIDIdata + struct.pack('B',0X03) # Event Type 528 | dataLength = len(event.trackName) 529 | dataLenghtVar = writeVarLength(dataLength) 530 | for i in range(0,len(dataLenghtVar)): 531 | self.MIDIdata = self.MIDIdata + struct.pack("b",dataLenghtVar[i]) 532 | self.MIDIdata = self.MIDIdata + event.trackName 533 | elif event.type == "ControllerEvent": 534 | code = 0xB << 4 | event.channel 535 | varTime = writeVarLength(event.time) 536 | for timeByte in varTime: 537 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 538 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 539 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.eventType) 540 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.paramerter1) 541 | elif event.type == "SysEx": 542 | code = 0xF0 543 | varTime = writeVarLength(event.time) 544 | for timeByte in varTime: 545 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 546 | self.MIDIdata = self.MIDIdata + struct.pack('>B', code) 547 | 548 | payloadLength = writeVarLength(len(event.payload)+2) 549 | for lenByte in payloadLength: 550 | self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte) 551 | 552 | self.MIDIdata = self.MIDIdata + struct.pack('>B', event.manID) 553 | self.MIDIdata = self.MIDIdata + event.payload 554 | self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7) 555 | elif event.type == "UniversalSysEx": 556 | code = 0xF0 557 | varTime = writeVarLength(event.time) 558 | for timeByte in varTime: 559 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 560 | self.MIDIdata = self.MIDIdata + struct.pack('>B', code) 561 | 562 | # Do we need to add a length? 563 | payloadLength = writeVarLength(len(event.payload)+5) 564 | for lenByte in payloadLength: 565 | self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte) 566 | 567 | if event.realTime : 568 | self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7F) 569 | else: 570 | self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7E) 571 | 572 | self.MIDIdata = self.MIDIdata + struct.pack('>B', event.sysExChannel) 573 | self.MIDIdata = self.MIDIdata + struct.pack('>B', event.code) 574 | self.MIDIdata = self.MIDIdata + struct.pack('>B', event.subcode) 575 | self.MIDIdata = self.MIDIdata + event.payload 576 | self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7) 577 | 578 | def deInterleaveNotes(self): 579 | '''Correct Interleaved notes. 580 | 581 | Because we are writing multiple notes in no particular order, we 582 | can have notes which are interleaved with respect to their start 583 | and stop times. This method will correct that. It expects that the 584 | MIDIEventList has been time-ordered. 585 | ''' 586 | 587 | tempEventList = [] 588 | stack = {} 589 | 590 | for event in self.MIDIEventList: 591 | 592 | if event.type == 'NoteOn': 593 | if stack.has_key(str(event.pitch)+str(event.channel)): 594 | stack[str(event.pitch)+str(event.channel)].append(event.time) 595 | else: 596 | stack[str(event.pitch)+str(event.channel)] = [event.time] 597 | tempEventList.append(event) 598 | elif event.type == 'NoteOff': 599 | if len(stack[str(event.pitch)+str(event.channel)]) > 1: 600 | event.time = stack[str(event.pitch)+str(event.channel)].pop() 601 | tempEventList.append(event) 602 | else: 603 | stack[str(event.pitch)+str(event.channel)].pop() 604 | tempEventList.append(event) 605 | else: 606 | tempEventList.append(event) 607 | 608 | self.MIDIEventList = tempEventList 609 | 610 | # A little trickery here. We want to make sure that NoteOff events appear 611 | # before NoteOn events, so we'll do two sorts -- on on type, one on time. 612 | # This may have to be revisited, as it makes assumptions about how 613 | # the internal sort works, and is in essence creating a sort on a primary 614 | # and secondary key. 615 | 616 | self.MIDIEventList.sort(lambda x, y: cmp(x.type , y.type)) 617 | self.MIDIEventList.sort(lambda x, y: int( 1000 * (x.time - y.time))) 618 | 619 | def adjustTime(self,origin): 620 | ''' 621 | Adjust Times to be relative, and zero-origined 622 | ''' 623 | 624 | if len(self.MIDIEventList) == 0: 625 | return 626 | tempEventList = [] 627 | 628 | runningTime = 0 629 | 630 | for event in self.MIDIEventList: 631 | adjustedTime = event.time - origin 632 | event.time = adjustedTime - runningTime 633 | runningTime = adjustedTime 634 | tempEventList.append(event) 635 | 636 | self.MIDIEventList = tempEventList 637 | 638 | def writeTrack(self,fileHandle): 639 | ''' 640 | Write track to disk. 641 | ''' 642 | 643 | if not self.closed: 644 | self.closeTrack() 645 | 646 | fileHandle.write(self.headerString) 647 | fileHandle.write(self.dataLength) 648 | fileHandle.write(self.MIDIdata) 649 | 650 | 651 | class MIDIHeader: 652 | ''' 653 | Class to encapsulate the MIDI header structure. 654 | 655 | This class encapsulates a MIDI header structure. It isn't used for much, 656 | but it will create the appropriately packed identifier string that all 657 | MIDI files should contain. It is used by the MIDIFile class to create a 658 | complete and well formed MIDI pattern. 659 | 660 | ''' 661 | def __init__(self,numTracks): 662 | ''' Initialize the data structures 663 | ''' 664 | self.headerString = struct.pack('cccc','M','T','h','d') 665 | self.headerSize = struct.pack('>L',6) 666 | # Format 1 = multi-track file 667 | self.format = struct.pack('>H',1) 668 | self.numTracks = struct.pack('>H',numTracks) 669 | self.ticksPerBeat = struct.pack('>H',TICKSPERBEAT) 670 | 671 | 672 | def writeFile(self,fileHandle): 673 | fileHandle.write(self.headerString) 674 | fileHandle.write(self.headerSize) 675 | fileHandle.write(self.format) 676 | fileHandle.write(self.numTracks) 677 | fileHandle.write(self.ticksPerBeat) 678 | 679 | class MIDIFile: 680 | '''Class that represents a full, well-formed MIDI pattern. 681 | 682 | This is a container object that contains a header, one or more tracks, 683 | and the data associated with a proper and well-formed MIDI pattern. 684 | 685 | Calling: 686 | 687 | MyMIDI = MidiFile(tracks, removeDuplicates=True, deinterleave=True) 688 | 689 | normally 690 | 691 | MyMIDI = MidiFile(tracks) 692 | 693 | Arguments: 694 | 695 | tracks: The number of tracks this object contains 696 | 697 | removeDuplicates: If true (the default), the software will remove duplicate 698 | events which have been added. For example, two notes at the same channel, 699 | time, pitch, and duration would be considered duplicate. 700 | 701 | deinterleave: If True (the default), overlapping notes (same pitch, same 702 | channel) will be modified so that they do not overlap. Otherwise the sequencing 703 | software will need to figure out how to interpret NoteOff events upon playback. 704 | ''' 705 | 706 | def __init__(self, numTracks, removeDuplicates=True, deinterleave=True): 707 | ''' 708 | Initialize the class 709 | ''' 710 | self.header = MIDIHeader(numTracks) 711 | 712 | self.tracks = list() 713 | self.numTracks = numTracks 714 | self.closed = False 715 | 716 | for i in range(0,numTracks): 717 | self.tracks.append(MIDITrack(removeDuplicates, deinterleave)) 718 | 719 | 720 | # Public Functions. These (for the most part) wrap the MIDITrack functions, where most 721 | # Processing takes place. 722 | 723 | def addNote(self,track, channel, pitch,time,duration,volume): 724 | """ 725 | Add notes to the MIDIFile object 726 | 727 | Use: 728 | MyMIDI.addNotes(track,channel,pitch,time, duration, volume) 729 | 730 | Arguments: 731 | track: The track to which the note is added. 732 | channel: the MIDI channel to assign to the note. [Integer, 0-15] 733 | pitch: the MIDI pitch number [Integer, 0-127]. 734 | time: the time (in beats) at which the note sounds [Float]. 735 | duration: the duration of the note (in beats) [Float]. 736 | volume: the volume (velocity) of the note. [Integer, 0-127]. 737 | """ 738 | self.tracks[track].addNoteByNumber(channel, pitch, time, duration, volume) 739 | 740 | def addTrackName(self,track, time,trackName): 741 | """ 742 | Add a track name to a MIDI track. 743 | 744 | Use: 745 | MyMIDI.addTrackName(track,time,trackName) 746 | 747 | Argument: 748 | track: The track to which the name is added. [Integer, 0-127]. 749 | time: The time at which the track name is added, in beats [Float]. 750 | trackName: The track name. [String]. 751 | """ 752 | self.tracks[track].addTrackName(time,trackName) 753 | 754 | def addTempo(self,track, time,tempo): 755 | """ 756 | Add a tempo event. 757 | 758 | Use: 759 | MyMIDI.addTempo(track, time, tempo) 760 | 761 | Arguments: 762 | track: The track to which the event is added. [Integer, 0-127]. 763 | time: The time at which the event is added, in beats. [Float]. 764 | tempo: The tempo, in Beats per Minute. [Integer] 765 | """ 766 | self.tracks[track].addTempo(time,tempo) 767 | 768 | def addProgramChange(self,track, channel, time, program): 769 | """ 770 | Add a MIDI program change event. 771 | 772 | Use: 773 | MyMIDI.addProgramChange(track,channel, time, program) 774 | 775 | Arguments: 776 | track: The track to which the event is added. [Integer, 0-127]. 777 | channel: The channel the event is assigned to. [Integer, 0-15]. 778 | time: The time at which the event is added, in beats. [Float]. 779 | program: the program number. [Integer, 0-127]. 780 | """ 781 | self.tracks[track].addProgramChange(channel, time, program) 782 | 783 | def addControllerEvent(self,track, channel,time,eventType, paramerter1): 784 | """ 785 | Add a MIDI controller event. 786 | 787 | Use: 788 | MyMIDI.addControllerEvent(track, channel, time, eventType, parameter1) 789 | 790 | Arguments: 791 | track: The track to which the event is added. [Integer, 0-127]. 792 | channel: The channel the event is assigned to. [Integer, 0-15]. 793 | time: The time at which the event is added, in beats. [Float]. 794 | eventType: the controller event type. 795 | parameter1: The event's parameter. The meaning of which varies by event type. 796 | """ 797 | self.tracks[track].addControllerEvent(channel,time,eventType, paramerter1) 798 | 799 | def changeNoteTuning(self, track, tunings, sysExChannel=0x7F, \ 800 | realTime=False, tuningProgam=0): 801 | """ 802 | Change a note's tuning using SysEx change tuning program. 803 | 804 | Use: 805 | MyMIDI.changeNoteTuning(track,[tunings],realTime=False, tuningProgram=0) 806 | 807 | Arguments: 808 | track: The track to which the event is added. [Integer, 0-127]. 809 | tunings: A list of tuples in the form (pitchNumber, frequency). 810 | [[(Integer,Float]] 811 | realTime: Boolean which sets the real-time flag. Defaults to false. 812 | sysExChannel: do note use (see below). 813 | tuningProgram: Tuning program to assign. Defaults to zero. [Integer, 0-127] 814 | 815 | In general the sysExChannel should not be changed (parameter will be depreciated). 816 | 817 | Also note that many software packages and hardware packages do not implement 818 | this standard! 819 | """ 820 | self.tracks[track].changeNoteTuning(tunings, sysExChannel, realTime,\ 821 | tuningProgam) 822 | 823 | def writeFile(self,fileHandle): 824 | ''' 825 | Write the MIDI File. 826 | 827 | Use: 828 | MyMIDI.writeFile(filehandle) 829 | 830 | Arguments: 831 | filehandle: a file handle that has been opened for binary writing. 832 | ''' 833 | 834 | self.header.writeFile(fileHandle) 835 | 836 | #Close the tracks and have them create the MIDI event data structures. 837 | self.close() 838 | 839 | #Write the MIDI Events to file. 840 | for i in range(0,self.numTracks): 841 | self.tracks[i].writeTrack(fileHandle) 842 | 843 | def addSysEx(self,track, time, manID, payload): 844 | """ 845 | Add a SysEx event 846 | 847 | Use: 848 | MyMIDI.addSysEx(track,time,ID,payload) 849 | 850 | Arguments: 851 | track: The track to which the event is added. [Integer, 0-127]. 852 | time: The time at which the event is added, in beats. [Float]. 853 | ID: The SysEx ID number 854 | payload: the event payload. 855 | 856 | Note: This is a low-level MIDI function, so care must be used in 857 | constructing the payload. It is recommended that higher-level helper 858 | functions be written to wrap this function and construct the payload if 859 | a developer finds him or herself using the function heavily. 860 | """ 861 | self.tracks[track].addSysEx(time,manID, payload) 862 | 863 | def addUniversalSysEx(self,track, time,code, subcode, payload, \ 864 | sysExChannel=0x7F, realTime=False): 865 | """ 866 | Add a Universal SysEx event. 867 | 868 | Use: 869 | MyMIDI.addUniversalSysEx(track, time, code, subcode, payload,\ 870 | sysExChannel=0x7f, realTime=False) 871 | 872 | Arguments: 873 | track: The track to which the event is added. [Integer, 0-127]. 874 | time: The time at which the event is added, in beats. [Float]. 875 | code: The even code. [Integer] 876 | subcode The event sub-code [Integer] 877 | payload: The event payload. [Binary string] 878 | sysExChannel: The SysEx channel. 879 | realTime: Sets the real-time flag. Defaults to zero. 880 | 881 | Note: This is a low-level MIDI function, so care must be used in 882 | constructing the payload. It is recommended that higher-level helper 883 | functions be written to wrap this function and construct the payload if 884 | a developer finds him or herself using the function heavily. As an example 885 | of such a helper function, see the changeNoteTuning function, both here and 886 | in MIDITrack. 887 | """ 888 | 889 | self.tracks[track].addUniversalSysEx(time,code, subcode, payload, sysExChannel,\ 890 | realTime) 891 | 892 | def shiftTracks(self, offset=0): 893 | """Shift tracks to be zero-origined, or origined at offset. 894 | 895 | Note that the shifting of the time in the tracks uses the MIDIEventList -- in other 896 | words it is assumed to be called in the stage where the MIDIEventList has been 897 | created. This function, however, it meant to operate on the eventList itself. 898 | """ 899 | origin = 1000000 # A little silly, but we'll assume big enough 900 | 901 | for track in self.tracks: 902 | if len(track.eventList) > 0: 903 | for event in track.eventList: 904 | if event.time < origin: 905 | origin = event.time 906 | 907 | for track in self.tracks: 908 | tempEventList = [] 909 | #runningTime = 0 910 | 911 | for event in track.eventList: 912 | adjustedTime = event.time - origin 913 | #event.time = adjustedTime - runningTime + offset 914 | event.time = adjustedTime + offset 915 | #runningTime = adjustedTime 916 | tempEventList.append(event) 917 | 918 | track.eventList = tempEventList 919 | 920 | #End Public Functions ######################## 921 | 922 | def close(self): 923 | '''Close the MIDIFile for further writing. 924 | 925 | To close the File for events, we must close the tracks, adjust the time to be 926 | zero-origined, and have the tracks write to their MIDI Stream data structure. 927 | ''' 928 | 929 | if self.closed == True: 930 | return 931 | 932 | for i in range(0,self.numTracks): 933 | self.tracks[i].closeTrack() 934 | # We want things like program changes to come before notes when they are at the 935 | # same time, so we sort the MIDI events by their ordinality 936 | self.tracks[i].MIDIEventList.sort() 937 | 938 | origin = self.findOrigin() 939 | 940 | for i in range(0,self.numTracks): 941 | self.tracks[i].adjustTime(origin) 942 | self.tracks[i].writeMIDIStream() 943 | 944 | self.closed = True 945 | 946 | 947 | def findOrigin(self): 948 | '''Find the earliest time in the file's tracks.append. 949 | ''' 950 | origin = 1000000 # A little silly, but we'll assume big enough 951 | 952 | # Note: This code assumes that the MIDIEventList has been sorted, so this should be insured 953 | # before it is called. It is probably a poor design to do this. 954 | # TODO: -- Consider making this less efficient but more robust by not assuming the list to be sorted. 955 | 956 | for track in self.tracks: 957 | if len(track.MIDIEventList) > 0: 958 | if track.MIDIEventList[0].time < origin: 959 | origin = track.MIDIEventList[0].time 960 | 961 | 962 | return origin 963 | 964 | def writeVarLength(i): 965 | '''Accept an input, and write a MIDI-compatible variable length stream 966 | 967 | The MIDI format is a little strange, and makes use of so-called variable 968 | length quantities. These quantities are a stream of bytes. If the most 969 | significant bit is 1, then more bytes follow. If it is zero, then the 970 | byte in question is the last in the stream 971 | ''' 972 | input = int(i+0.5) 973 | output = [0,0,0,0] 974 | reversed = [0,0,0,0] 975 | count = 0 976 | result = input & 0x7F 977 | output[count] = result 978 | count = count + 1 979 | input = input >> 7 980 | while input > 0: 981 | result = input & 0x7F 982 | result = result | 0x80 983 | output[count] = result 984 | count = count + 1 985 | input = input >> 7 986 | 987 | reversed[0] = output[3] 988 | reversed[1] = output[2] 989 | reversed[2] = output[1] 990 | reversed[3] = output[0] 991 | return reversed[4-count:4] 992 | 993 | # readVarLength is taken from the MidiFile class. 994 | 995 | def readVarLength(offset, buffer): 996 | '''A function to read a MIDI variable length variable. 997 | 998 | It returns a tuple of the value read and the number of bytes processed. The 999 | input is an offset into the buffer, and the buffer itself. 1000 | ''' 1001 | toffset = offset 1002 | output = 0 1003 | bytesRead = 0 1004 | while True: 1005 | output = output << 7 1006 | byte = struct.unpack_from('>B',buffer,toffset)[0] 1007 | toffset = toffset + 1 1008 | bytesRead = bytesRead + 1 1009 | output = output + (byte & 127) 1010 | if (byte & 128) == 0: 1011 | break 1012 | return (output, bytesRead) 1013 | 1014 | def frequencyTransform(freq): 1015 | '''Returns a three-byte transform of a frequencyTransform 1016 | ''' 1017 | resolution = 16384 1018 | freq = float(freq) 1019 | dollars = 69 + 12 * math.log(freq/(float(440)), 2) 1020 | firstByte = int(dollars) 1021 | lowerFreq = 440 * pow(2.0, ((float(firstByte) - 69.0)/12.0)) 1022 | if freq != lowerFreq: 1023 | centDif = 1200 * math.log( (freq/lowerFreq), 2) 1024 | else: 1025 | centDif = 0 1026 | cents = round(centDif/100 * resolution) # round? 1027 | secondByte = min([int(cents)>>7, 0x7F]) 1028 | thirdByte = cents - (secondByte << 7) 1029 | thirdByte = min([thirdByte, 0x7f]) 1030 | if thirdByte == 0x7f and secondByte == 0x7F and firstByte == 0x7F: 1031 | thirdByte = 0x7e 1032 | 1033 | thirdByte = int(thirdByte) 1034 | return [firstByte, secondByte, thirdByte] 1035 | 1036 | def returnFrequency(freqBytes): 1037 | '''The reverse of frequencyTransform. Given a byte stream, return a frequency. 1038 | ''' 1039 | resolution = 16384.0 1040 | baseFrequency = 440 * pow(2.0, (float(freqBytes[0]-69.0)/12.0)) 1041 | frac = (float((int(freqBytes[1]) << 7) + int(freqBytes[2])) * 100.0) / resolution 1042 | frequency = baseFrequency * pow(2.0, frac/1200.0) 1043 | return frequency 1044 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/src/midiutil/MidiFile3.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Name: MidiFile.py 3 | # Purpose: MIDI file manipulation utilities 4 | # 5 | # Author: Mark Conway Wirt 6 | # 7 | # Created: 2008/04/17 8 | # Copyright: (c) 2009 Mark Conway Wirt 9 | # License: Please see License.txt for the terms under which this 10 | # software is distributed. 11 | #----------------------------------------------------------------------------- 12 | 13 | import struct, sys, math 14 | 15 | # TICKSPERBEAT is the number of "ticks" (time measurement in the MIDI file) that 16 | # corresponds to one beat. This number is somewhat arbitrary, but should be chosen 17 | # to provide adequate temporal resolution. 18 | 19 | TICKSPERBEAT = 960 20 | 21 | controllerEventTypes = { 22 | 'pan' : 0x0a 23 | } 24 | class MIDIEvent: 25 | ''' 26 | The class to contain the MIDI Event (placed on MIDIEventList. 27 | ''' 28 | def __init__(self): 29 | self.type='unknown' 30 | self.time=0 31 | self.ord = 0 32 | 33 | def __lt__(self, other): 34 | ''' Sorting function for events.''' 35 | if self.time < other.time: 36 | return True 37 | elif self.time > other.time: 38 | return False 39 | else: 40 | if self.ord < other.ord: 41 | return True 42 | elif self.ord > other.ord: 43 | return False 44 | else: 45 | return False 46 | 47 | def __cmp__(self, other): 48 | ''' Sorting function for events.''' 49 | if self.time < other.time: 50 | return -1 51 | elif self.time > other.time: 52 | return 1 53 | else: 54 | if self.ord < other.ord: 55 | return -1 56 | elif self.ord > other.ord: 57 | return 1 58 | else: 59 | return 0 60 | 61 | class GenericEvent(): 62 | '''The event class from which specific events are derived 63 | ''' 64 | def __init__(self,time): 65 | self.time = time 66 | self.type = 'Unknown' 67 | 68 | 69 | 70 | def __eq__(self, other): 71 | ''' 72 | Equality operator for Generic Events and derived classes. 73 | 74 | In the processing of the event list, we have need to remove duplicates. To do this 75 | we rely on the fact that the classes are hashable, and must therefore have an 76 | equality operator (__hash__() and __eq__() must both be defined). 77 | 78 | This is the most embarrassing portion of the code, and anyone who knows about OO 79 | programming would find this almost unbelievable. Here we have a base class that 80 | knows specifics about derived classes, thus breaking the very spirit of 81 | OO programming. 82 | 83 | I suppose I should go back and restructure the code, perhaps removing the derived 84 | classes altogether. At some point perhaps I will. 85 | ''' 86 | if self.time != other.time or self.type != other.type: 87 | return False 88 | 89 | # What follows is code that encodes the concept of equality for each derived 90 | # class. Believe it f you dare. 91 | 92 | if self.type == 'note': 93 | if self.pitch != other.pitch or self.channel != other.channel: 94 | return False 95 | if self.type == 'tempo': 96 | if self.tempo != other.tempo: 97 | return False 98 | if self.type == 'programChange': 99 | if self.programNumber != other.programNumber or self.channel != other.channel: 100 | return False 101 | if self.type == 'trackName': 102 | if self.trackName != other.trackName: 103 | return False 104 | if self.type == 'controllerEvent': 105 | if self.parameter1 != other.parameter1 or \ 106 | self.channel != other.channel or \ 107 | self.eventType != other.eventType: 108 | return False 109 | 110 | if self.type == 'SysEx': 111 | if self.manID != other.manID: 112 | return False 113 | 114 | if self.type == 'UniversalSysEx': 115 | if self.code != other.code or\ 116 | self.subcode != other.subcode or \ 117 | self.sysExChannel != other.sysExChannel: 118 | return False 119 | 120 | return True 121 | 122 | def __hash__(self): 123 | ''' 124 | Return a hash code for the object. 125 | 126 | This is needed for the removal of duplicate objects from the event list. The only 127 | real requirement for the algorithm is that the hash of equal objects must be equal. 128 | There is probably great opportunity for improvements in the hashing function. 129 | ''' 130 | # Robert Jenkin's 32 bit hash. 131 | a = int(self.time) 132 | a = (a+0x7ed55d16) + (a<<12) 133 | a = (a^0xc761c23c) ^ (a>>19) 134 | a = (a+0x165667b1) + (a<<5) 135 | a = (a+0xd3a2646c) ^ (a<<9) 136 | a = (a+0xfd7046c5) + (a<<3) 137 | a = (a^0xb55a4f09) ^ (a>>16) 138 | return a 139 | 140 | class MIDITrack: 141 | '''A class that encapsulates a MIDI track 142 | ''' 143 | # Nested class definitions. 144 | 145 | class note(GenericEvent): 146 | '''A class that encapsulates a note 147 | ''' 148 | def __init__(self,channel, pitch,time,duration,volume): 149 | 150 | GenericEvent.__init__(self,time) 151 | self.pitch = pitch 152 | self.duration = duration 153 | self.volume = volume 154 | self.type = 'note' 155 | self.channel = channel 156 | 157 | def compare(self, other): 158 | '''Compare two notes for equality. 159 | ''' 160 | if self.pitch == other.pitch and \ 161 | self.time == other.time and \ 162 | self.duration == other.duration and \ 163 | self.volume == other.volume and \ 164 | self.type == other.type and \ 165 | self.channel == other.channel: 166 | return True 167 | else: 168 | return False 169 | 170 | 171 | class tempo(GenericEvent): 172 | '''A class that encapsulates a tempo meta-event 173 | ''' 174 | def __init__(self,time,tempo): 175 | 176 | GenericEvent.__init__(self,time) 177 | self.type = 'tempo' 178 | self.tempo = int(60000000 / tempo) 179 | 180 | class programChange(GenericEvent): 181 | '''A class that encapsulates a program change event. 182 | ''' 183 | 184 | def __init__(self, channel, time, programNumber): 185 | GenericEvent.__init__(self, time,) 186 | self.type = 'programChange' 187 | self.programNumber = programNumber 188 | self.channel = channel 189 | 190 | class SysExEvent(GenericEvent): 191 | '''A class that encapsulates a System Exclusive event. 192 | ''' 193 | 194 | def __init__(self, time, manID, payload): 195 | GenericEvent.__init__(self, time,) 196 | self.type = 'SysEx' 197 | self.manID = manID 198 | self.payload = payload 199 | 200 | class UniversalSysExEvent(GenericEvent): 201 | '''A class that encapsulates a Universal System Exclusive event. 202 | ''' 203 | 204 | def __init__(self, time, realTime, sysExChannel, code, subcode, payload): 205 | GenericEvent.__init__(self, time,) 206 | self.type = 'UniversalSysEx' 207 | self.realTime = realTime 208 | self.sysExChannel = sysExChannel 209 | self.code = code 210 | self.subcode = subcode 211 | self.payload = payload 212 | 213 | class ControllerEvent(GenericEvent): 214 | '''A class that encapsulates a program change event. 215 | ''' 216 | 217 | def __init__(self, channel, time, eventType, parameter1,): 218 | GenericEvent.__init__(self, time,) 219 | self.type = 'controllerEvent' 220 | self.parameter1 = parameter1 221 | self.channel = channel 222 | self.eventType = eventType 223 | 224 | class trackName(GenericEvent): 225 | '''A class that encapsulates a program change event. 226 | ''' 227 | 228 | def __init__(self, time, trackName): 229 | GenericEvent.__init__(self, time,) 230 | self.type = 'trackName' 231 | self.trackName = trackName 232 | 233 | 234 | def __init__(self, removeDuplicates, deinterleave): 235 | '''Initialize the MIDITrack object. 236 | ''' 237 | self.headerString = struct.pack('cccc',b'M',b'T',b'r',b'k') 238 | self.dataLength = 0 # Is calculated after the data is in place 239 | self.MIDIdata = b"" 240 | self.closed = False 241 | self.eventList = [] 242 | self.MIDIEventList = [] 243 | self.remdep = removeDuplicates 244 | self.deinterleave = deinterleave 245 | 246 | def addNoteByNumber(self,channel, pitch,time,duration,volume): 247 | '''Add a note by chromatic MIDI number 248 | ''' 249 | self.eventList.append(MIDITrack.note(channel, pitch,time,duration,volume)) 250 | 251 | def addControllerEvent(self,channel,time,eventType, paramerter1): 252 | ''' 253 | Add a controller event. 254 | ''' 255 | 256 | self.eventList.append(MIDITrack.ControllerEvent(channel,time,eventType, \ 257 | paramerter1)) 258 | 259 | def addTempo(self,time,tempo): 260 | ''' 261 | Add a tempo change (or set) event. 262 | ''' 263 | self.eventList.append(MIDITrack.tempo(time,tempo)) 264 | 265 | def addSysEx(self,time,manID, payload): 266 | ''' 267 | Add a SysEx event. 268 | ''' 269 | self.eventList.append(MIDITrack.SysExEvent(time, manID, payload)) 270 | 271 | def addUniversalSysEx(self,time,code, subcode, payload, sysExChannel=0x7F, \ 272 | realTime=False): 273 | ''' 274 | Add a Universal SysEx event. 275 | ''' 276 | self.eventList.append(MIDITrack.UniversalSysExEvent(time, realTime, \ 277 | sysExChannel, code, subcode, payload)) 278 | 279 | def addProgramChange(self,channel, time, program): 280 | ''' 281 | Add a program change event. 282 | ''' 283 | self.eventList.append(MIDITrack.programChange(channel, time, program)) 284 | 285 | def addTrackName(self,time,trackName): 286 | ''' 287 | Add a track name event. 288 | ''' 289 | self.eventList.append(MIDITrack.trackName(time,trackName)) 290 | 291 | def changeNoteTuning(self, tunings, sysExChannel=0x7F, realTime=False, \ 292 | tuningProgam=0): 293 | '''Change the tuning of MIDI notes 294 | ''' 295 | payload = struct.pack('>B', tuningProgam) 296 | payload = payload + struct.pack('>B', len(tunings)) 297 | for (noteNumber, frequency) in tunings: 298 | payload = payload + struct.pack('>B', noteNumber) 299 | MIDIFreqency = frequencyTransform(frequency) 300 | for byte in MIDIFreqency: 301 | payload = payload + struct.pack('>B', byte) 302 | 303 | self.eventList.append(MIDITrack.UniversalSysExEvent(0, realTime, sysExChannel,\ 304 | 8, 2, payload)) 305 | 306 | def processEventList(self): 307 | ''' 308 | Process the event list, creating a MIDIEventList 309 | 310 | For each item in the event list, one or more events in the MIDIEvent 311 | list are created. 312 | ''' 313 | 314 | # Loop over all items in the eventList 315 | 316 | for thing in self.eventList: 317 | if thing.type == 'note': 318 | event = MIDIEvent() 319 | event.type = "NoteOn" 320 | event.time = thing.time * TICKSPERBEAT 321 | event.pitch = thing.pitch 322 | event.volume = thing.volume 323 | event.channel = thing.channel 324 | event.ord = 3 325 | self.MIDIEventList.append(event) 326 | 327 | event = MIDIEvent() 328 | event.type = "NoteOff" 329 | event.time = (thing.time + thing.duration) * TICKSPERBEAT 330 | event.pitch = thing.pitch 331 | event.volume = thing.volume 332 | event.channel = thing.channel 333 | event.ord = 2 334 | self.MIDIEventList.append(event) 335 | 336 | elif thing.type == 'tempo': 337 | event = MIDIEvent() 338 | event.type = "Tempo" 339 | event.time = thing.time * TICKSPERBEAT 340 | event.tempo = thing.tempo 341 | event.ord = 3 342 | self.MIDIEventList.append(event) 343 | 344 | elif thing.type == 'programChange': 345 | event = MIDIEvent() 346 | event.type = "ProgramChange" 347 | event.time = thing.time * TICKSPERBEAT 348 | event.programNumber = thing.programNumber 349 | event.channel = thing.channel 350 | event.ord = 1 351 | self.MIDIEventList.append(event) 352 | 353 | elif thing.type == 'trackName': 354 | event = MIDIEvent() 355 | event.type = "TrackName" 356 | event.time = thing.time * TICKSPERBEAT 357 | event.trackName = thing.trackName 358 | event.ord = 0 359 | self.MIDIEventList.append(event) 360 | 361 | elif thing.type == 'controllerEvent': 362 | event = MIDIEvent() 363 | event.type = "ControllerEvent" 364 | event.time = thing.time * TICKSPERBEAT 365 | event.eventType = thing.eventType 366 | event.channel = thing.channel 367 | event.paramerter1 = thing.parameter1 368 | event.ord = 1 369 | self.MIDIEventList.append(event) 370 | 371 | elif thing.type == 'SysEx': 372 | event = MIDIEvent() 373 | event.type = "SysEx" 374 | event.time = thing.time * TICKSPERBEAT 375 | event.manID = thing.manID 376 | event.payload = thing.payload 377 | event.ord = 1 378 | self.MIDIEventList.append(event) 379 | 380 | elif thing.type == 'UniversalSysEx': 381 | event = MIDIEvent() 382 | event.type = "UniversalSysEx" 383 | event.realTime = thing.realTime 384 | event.sysExChannel = thing.sysExChannel 385 | event.time = thing.time * TICKSPERBEAT 386 | event.code = thing.code 387 | event.subcode = thing.subcode 388 | event.payload = thing.payload 389 | event.ord = 1 390 | self.MIDIEventList.append(event) 391 | 392 | else: 393 | print ("Error in MIDITrack: Unknown event type") 394 | sys.exit(2) 395 | 396 | # Assumptions in the code expect the list to be time-sorted. 397 | # self.MIDIEventList.sort(lambda x, y: x.time - y.time) 398 | 399 | self.MIDIEventList.sort(key=lambda x: (x.time)) 400 | 401 | if self.deinterleave: 402 | self.deInterleaveNotes() 403 | 404 | def removeDuplicates(self): 405 | ''' 406 | Remove duplicates from the eventList. 407 | 408 | This function will remove duplicates from the eventList. This is necessary 409 | because we the MIDI event stream can become confused otherwise. 410 | ''' 411 | 412 | # For this algorithm to work, the events in the eventList must be hashable 413 | # (that is, they must have a __hash__() and __eq__() function defined). 414 | 415 | tempDict = {} 416 | for item in self.eventList: 417 | tempDict[item] = 1 418 | 419 | self.eventList = list(tempDict.keys()) 420 | 421 | # Sort on type, them on time. Necessary because keys() has no requirement to return 422 | # things in any order. 423 | 424 | self.eventList.sort(key=lambda x: (x.type)) 425 | self.eventList.sort(key=lambda x: (x.time)) #A bit of a hack. 426 | 427 | def closeTrack(self): 428 | '''Called to close a track before writing 429 | 430 | This function should be called to "close a track," that is to 431 | prepare the actual data stream for writing. Duplicate events are 432 | removed from the eventList, and the MIDIEventList is created. 433 | 434 | Called by the parent MIDIFile object. 435 | ''' 436 | 437 | if self.closed == True: 438 | return 439 | self.closed = True 440 | 441 | if self.remdep: 442 | self.removeDuplicates() 443 | 444 | 445 | self.processEventList() 446 | 447 | def writeMIDIStream(self): 448 | ''' 449 | Write the meta data and note data to the packed MIDI stream. 450 | ''' 451 | 452 | #Process the events in the eventList 453 | 454 | self.writeEventsToStream() 455 | 456 | # Write MIDI close event. 457 | 458 | self.MIDIdata = self.MIDIdata + struct.pack('BBBB',0x00,0xFF, \ 459 | 0x2F,0x00) 460 | 461 | # Calculate the entire length of the data and write to the header 462 | 463 | self.dataLength = struct.pack('>L',len(self.MIDIdata)) 464 | 465 | def writeEventsToStream(self): 466 | ''' 467 | Write the events in MIDIEvents to the MIDI stream. 468 | ''' 469 | preciseTime = 0.0 # Actual time of event, ignoring round-off 470 | actualTime = 0.0 # Time as written to midi stream, include round-off 471 | for event in self.MIDIEventList: 472 | 473 | preciseTime = preciseTime + event.time 474 | 475 | # Convert the time to variable length and back, to see how much 476 | # error is introduced 477 | 478 | testBuffer = bytes() 479 | varTime = writeVarLength(event.time) 480 | for timeByte in varTime: 481 | testBuffer = testBuffer + struct.pack('>B',timeByte) 482 | (roundedVal,discard) = readVarLength(0,testBuffer) 483 | roundedTime = actualTime + roundedVal 484 | # print "Rounded, Precise: %15.10f %15.10f" % (roundedTime, preciseTime) 485 | 486 | # Calculate the delta between the two and apply it to the event time. 487 | 488 | delta = preciseTime - roundedTime 489 | event.time = event.time + delta 490 | 491 | # Now update the actualTime value, using the updated event time. 492 | 493 | testBuffer = bytes() 494 | varTime = writeVarLength(event.time) 495 | for timeByte in varTime: 496 | testBuffer = testBuffer + struct.pack('>B',timeByte) 497 | (roundedVal,discard) = readVarLength(0,testBuffer) 498 | actualTime = actualTime + roundedVal 499 | 500 | for event in self.MIDIEventList: 501 | if event.type == "NoteOn": 502 | code = 0x9 << 4 | event.channel 503 | varTime = writeVarLength(event.time) 504 | for timeByte in varTime: 505 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 506 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 507 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.pitch) 508 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume) 509 | elif event.type == "NoteOff": 510 | code = 0x8 << 4 | event.channel 511 | varTime = writeVarLength(event.time) 512 | for timeByte in varTime: 513 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 514 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 515 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.pitch) 516 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume) 517 | elif event.type == "Tempo": 518 | code = 0xFF 519 | subcode = 0x51 520 | fourbite = struct.pack('>L', event.tempo) 521 | threebite = fourbite[1:4] # Just discard the MSB 522 | varTime = writeVarLength(event.time) 523 | for timeByte in varTime: 524 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 525 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 526 | self.MIDIdata = self.MIDIdata + struct.pack('>B',subcode) 527 | self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x03) # Data length: 3 528 | self.MIDIdata = self.MIDIdata + threebite 529 | elif event.type == 'ProgramChange': 530 | code = 0xC << 4 | event.channel 531 | varTime = writeVarLength(event.time) 532 | for timeByte in varTime: 533 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 534 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 535 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.programNumber) 536 | elif event.type == 'TrackName': 537 | varTime = writeVarLength(event.time) 538 | for timeByte in varTime: 539 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 540 | self.MIDIdata = self.MIDIdata + struct.pack('B',0xFF) # Meta-event 541 | self.MIDIdata = self.MIDIdata + struct.pack('B',0X03) # Event Type 542 | dataLength = len(event.trackName) 543 | dataLenghtVar = writeVarLength(dataLength) 544 | for i in range(0,len(dataLenghtVar)): 545 | self.MIDIdata = self.MIDIdata + struct.pack("b",dataLenghtVar[i]) 546 | self.MIDIdata = self.MIDIdata + event.trackName.encode() 547 | elif event.type == "ControllerEvent": 548 | code = 0xB << 4 | event.channel 549 | varTime = writeVarLength(event.time) 550 | for timeByte in varTime: 551 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 552 | self.MIDIdata = self.MIDIdata + struct.pack('>B',code) 553 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.eventType) 554 | self.MIDIdata = self.MIDIdata + struct.pack('>B',event.paramerter1) 555 | elif event.type == "SysEx": 556 | code = 0xF0 557 | varTime = writeVarLength(event.time) 558 | for timeByte in varTime: 559 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 560 | self.MIDIdata = self.MIDIdata + struct.pack('>B', code) 561 | 562 | payloadLength = writeVarLength(len(event.payload)+2) 563 | for lenByte in payloadLength: 564 | self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte) 565 | 566 | self.MIDIdata = self.MIDIdata + struct.pack('>B', event.manID) 567 | self.MIDIdata = self.MIDIdata + event.payload 568 | self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7) 569 | elif event.type == "UniversalSysEx": 570 | code = 0xF0 571 | varTime = writeVarLength(event.time) 572 | for timeByte in varTime: 573 | self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) 574 | self.MIDIdata = self.MIDIdata + struct.pack('>B', code) 575 | 576 | # Do we need to add a length? 577 | payloadLength = writeVarLength(len(event.payload)+5) 578 | for lenByte in payloadLength: 579 | self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte) 580 | 581 | if event.realTime : 582 | self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7F) 583 | else: 584 | self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7E) 585 | 586 | self.MIDIdata = self.MIDIdata + struct.pack('>B', event.sysExChannel) 587 | self.MIDIdata = self.MIDIdata + struct.pack('>B', event.code) 588 | self.MIDIdata = self.MIDIdata + struct.pack('>B', event.subcode) 589 | self.MIDIdata = self.MIDIdata + event.payload 590 | self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7) 591 | 592 | def deInterleaveNotes(self): 593 | '''Correct Interleaved notes. 594 | 595 | Because we are writing multiple notes in no particular order, we 596 | can have notes which are interleaved with respect to their start 597 | and stop times. This method will correct that. It expects that the 598 | MIDIEventList has been time-ordered. 599 | ''' 600 | 601 | tempEventList = [] 602 | stack = {} 603 | 604 | for event in self.MIDIEventList: 605 | 606 | if event.type == 'NoteOn': 607 | if str(event.pitch)+str(event.channel) in stack: 608 | stack[str(event.pitch)+str(event.channel)].append(event.time) 609 | else: 610 | stack[str(event.pitch)+str(event.channel)] = [event.time] 611 | tempEventList.append(event) 612 | elif event.type == 'NoteOff': 613 | if len(stack[str(event.pitch)+str(event.channel)]) > 1: 614 | event.time = stack[str(event.pitch)+str(event.channel)].pop() 615 | tempEventList.append(event) 616 | else: 617 | stack[str(event.pitch)+str(event.channel)].pop() 618 | tempEventList.append(event) 619 | else: 620 | tempEventList.append(event) 621 | 622 | self.MIDIEventList = tempEventList 623 | 624 | # A little trickery here. We want to make sure that NoteOff events appear 625 | # before NoteOn events, so we'll do two sorts -- on on type, one on time. 626 | # This may have to be revisited, as it makes assumptions about how 627 | # the internal sort works, and is in essence creating a sort on a primary 628 | # and secondary key. 629 | 630 | self.MIDIEventList.sort(key=lambda x: (x.type)) 631 | self.MIDIEventList.sort(key=lambda x: (x.time)) 632 | 633 | def adjustTime(self,origin): 634 | ''' 635 | Adjust Times to be relative, and zero-origined 636 | ''' 637 | 638 | if len(self.MIDIEventList) == 0: 639 | return 640 | tempEventList = [] 641 | 642 | runningTime = 0 643 | 644 | for event in self.MIDIEventList: 645 | adjustedTime = event.time - origin 646 | event.time = adjustedTime - runningTime 647 | runningTime = adjustedTime 648 | tempEventList.append(event) 649 | 650 | self.MIDIEventList = tempEventList 651 | 652 | def writeTrack(self,fileHandle): 653 | ''' 654 | Write track to disk. 655 | ''' 656 | 657 | if not self.closed: 658 | self.closeTrack() 659 | 660 | fileHandle.write(self.headerString) 661 | fileHandle.write(self.dataLength) 662 | fileHandle.write(self.MIDIdata) 663 | 664 | 665 | class MIDIHeader: 666 | ''' 667 | Class to encapsulate the MIDI header structure. 668 | 669 | This class encapsulates a MIDI header structure. It isn't used for much, 670 | but it will create the appropriately packed identifier string that all 671 | MIDI files should contain. It is used by the MIDIFile class to create a 672 | complete and well formed MIDI pattern. 673 | 674 | ''' 675 | def __init__(self,numTracks): 676 | ''' Initialize the data structures 677 | ''' 678 | self.headerString = struct.pack('cccc',b'M',b'T',b'h',b'd') 679 | self.headerSize = struct.pack('>L',6) 680 | # Format 1 = multi-track file 681 | self.format = struct.pack('>H',1) 682 | self.numTracks = struct.pack('>H',numTracks) 683 | self.ticksPerBeat = struct.pack('>H',TICKSPERBEAT) 684 | 685 | 686 | def writeFile(self,fileHandle): 687 | fileHandle.write(self.headerString) 688 | fileHandle.write(self.headerSize) 689 | fileHandle.write(self.format) 690 | fileHandle.write(self.numTracks) 691 | fileHandle.write(self.ticksPerBeat) 692 | 693 | class MIDIFile: 694 | '''Class that represents a full, well-formed MIDI pattern. 695 | 696 | This is a container object that contains a header, one or more tracks, 697 | and the data associated with a proper and well-formed MIDI pattern. 698 | 699 | Calling: 700 | 701 | MyMIDI = MidiFile(tracks, removeDuplicates=True, deinterleave=True) 702 | 703 | normally 704 | 705 | MyMIDI = MidiFile(tracks) 706 | 707 | Arguments: 708 | 709 | tracks: The number of tracks this object contains 710 | 711 | removeDuplicates: If true (the default), the software will remove duplicate 712 | events which have been added. For example, two notes at the same channel, 713 | time, pitch, and duration would be considered duplicate. 714 | 715 | deinterleave: If True (the default), overlapping notes (same pitch, same 716 | channel) will be modified so that they do not overlap. Otherwise the sequencing 717 | software will need to figure out how to interpret NoteOff events upon playback. 718 | ''' 719 | 720 | def __init__(self, numTracks, removeDuplicates=True, deinterleave=True): 721 | ''' 722 | Initialize the class 723 | ''' 724 | self.header = MIDIHeader(numTracks) 725 | 726 | self.tracks = list() 727 | self.numTracks = numTracks 728 | self.closed = False 729 | 730 | for i in range(0,numTracks): 731 | self.tracks.append(MIDITrack(removeDuplicates, deinterleave)) 732 | 733 | 734 | # Public Functions. These (for the most part) wrap the MIDITrack functions, where most 735 | # Processing takes place. 736 | 737 | def addNote(self,track, channel, pitch,time,duration,volume): 738 | """ 739 | Add notes to the MIDIFile object 740 | 741 | Use: 742 | MyMIDI.addNotes(track,channel,pitch,time, duration, volume) 743 | 744 | Arguments: 745 | track: The track to which the note is added. 746 | channel: the MIDI channel to assign to the note. [Integer, 0-15] 747 | pitch: the MIDI pitch number [Integer, 0-127]. 748 | time: the time (in beats) at which the note sounds [Float]. 749 | duration: the duration of the note (in beats) [Float]. 750 | volume: the volume (velocity) of the note. [Integer, 0-127]. 751 | """ 752 | self.tracks[track].addNoteByNumber(channel, pitch, time, duration, volume) 753 | 754 | def addTrackName(self,track, time,trackName): 755 | """ 756 | Add a track name to a MIDI track. 757 | 758 | Use: 759 | MyMIDI.addTrackName(track,time,trackName) 760 | 761 | Argument: 762 | track: The track to which the name is added. [Integer, 0-127]. 763 | time: The time at which the track name is added, in beats [Float]. 764 | trackName: The track name. [String]. 765 | """ 766 | self.tracks[track].addTrackName(time,trackName) 767 | 768 | def addTempo(self,track, time,tempo): 769 | """ 770 | Add a tempo event. 771 | 772 | Use: 773 | MyMIDI.addTempo(track, time, tempo) 774 | 775 | Arguments: 776 | track: The track to which the event is added. [Integer, 0-127]. 777 | time: The time at which the event is added, in beats. [Float]. 778 | tempo: The tempo, in Beats per Minute. [Integer] 779 | """ 780 | self.tracks[track].addTempo(time,tempo) 781 | 782 | def addProgramChange(self,track, channel, time, program): 783 | """ 784 | Add a MIDI program change event. 785 | 786 | Use: 787 | MyMIDI.addProgramChange(track,channel, time, program) 788 | 789 | Arguments: 790 | track: The track to which the event is added. [Integer, 0-127]. 791 | channel: The channel the event is assigned to. [Integer, 0-15]. 792 | time: The time at which the event is added, in beats. [Float]. 793 | program: the program number. [Integer, 0-127]. 794 | """ 795 | self.tracks[track].addProgramChange(channel, time, program) 796 | 797 | def addControllerEvent(self,track, channel,time,eventType, paramerter1): 798 | """ 799 | Add a MIDI controller event. 800 | 801 | Use: 802 | MyMIDI.addControllerEvent(track, channel, time, eventType, parameter1) 803 | 804 | Arguments: 805 | track: The track to which the event is added. [Integer, 0-127]. 806 | channel: The channel the event is assigned to. [Integer, 0-15]. 807 | time: The time at which the event is added, in beats. [Float]. 808 | eventType: the controller event type. 809 | parameter1: The event's parameter. The meaning of which varies by event type. 810 | """ 811 | self.tracks[track].addControllerEvent(channel,time,eventType, paramerter1) 812 | 813 | def changeNoteTuning(self, track, tunings, sysExChannel=0x7F, \ 814 | realTime=False, tuningProgam=0): 815 | """ 816 | Change a note's tuning using SysEx change tuning program. 817 | 818 | Use: 819 | MyMIDI.changeNoteTuning(track,[tunings],realTime=False, tuningProgram=0) 820 | 821 | Arguments: 822 | track: The track to which the event is added. [Integer, 0-127]. 823 | tunings: A list of tuples in the form (pitchNumber, frequency). 824 | [[(Integer,Float]] 825 | realTime: Boolean which sets the real-time flag. Defaults to false. 826 | sysExChannel: do note use (see below). 827 | tuningProgram: Tuning program to assign. Defaults to zero. [Integer, 0-127] 828 | 829 | In general the sysExChannel should not be changed (parameter will be depreciated). 830 | 831 | Also note that many software packages and hardware packages do not implement 832 | this standard! 833 | """ 834 | self.tracks[track].changeNoteTuning(tunings, sysExChannel, realTime,\ 835 | tuningProgam) 836 | 837 | def writeFile(self,fileHandle): 838 | ''' 839 | Write the MIDI File. 840 | 841 | Use: 842 | MyMIDI.writeFile(filehandle) 843 | 844 | Arguments: 845 | filehandle: a file handle that has been opened for binary writing. 846 | ''' 847 | 848 | self.header.writeFile(fileHandle) 849 | 850 | #Close the tracks and have them create the MIDI event data structures. 851 | self.close() 852 | 853 | #Write the MIDI Events to file. 854 | for i in range(0,self.numTracks): 855 | self.tracks[i].writeTrack(fileHandle) 856 | 857 | def addSysEx(self,track, time, manID, payload): 858 | """ 859 | Add a SysEx event 860 | 861 | Use: 862 | MyMIDI.addSysEx(track,time,ID,payload) 863 | 864 | Arguments: 865 | track: The track to which the event is added. [Integer, 0-127]. 866 | time: The time at which the event is added, in beats. [Float]. 867 | ID: The SysEx ID number 868 | payload: the event payload. 869 | 870 | Note: This is a low-level MIDI function, so care must be used in 871 | constructing the payload. It is recommended that higher-level helper 872 | functions be written to wrap this function and construct the payload if 873 | a developer finds him or herself using the function heavily. 874 | """ 875 | self.tracks[track].addSysEx(time,manID, payload) 876 | 877 | def addUniversalSysEx(self,track, time,code, subcode, payload, \ 878 | sysExChannel=0x7F, realTime=False): 879 | """ 880 | Add a Universal SysEx event. 881 | 882 | Use: 883 | MyMIDI.addUniversalSysEx(track, time, code, subcode, payload,\ 884 | sysExChannel=0x7f, realTime=False) 885 | 886 | Arguments: 887 | track: The track to which the event is added. [Integer, 0-127]. 888 | time: The time at which the event is added, in beats. [Float]. 889 | code: The even code. [Integer] 890 | subcode The event sub-code [Integer] 891 | payload: The event payload. [Binary string] 892 | sysExChannel: The SysEx channel. 893 | realTime: Sets the real-time flag. Defaults to zero. 894 | 895 | Note: This is a low-level MIDI function, so care must be used in 896 | constructing the payload. It is recommended that higher-level helper 897 | functions be written to wrap this function and construct the payload if 898 | a developer finds him or herself using the function heavily. As an example 899 | of such a helper function, see the changeNoteTuning function, both here and 900 | in MIDITrack. 901 | """ 902 | 903 | self.tracks[track].addUniversalSysEx(time,code, subcode, payload, sysExChannel,\ 904 | realTime) 905 | 906 | def shiftTracks(self, offset=0): 907 | """Shift tracks to be zero-origined, or origined at offset. 908 | 909 | Note that the shifting of the time in the tracks uses the MIDIEventList -- in other 910 | words it is assumed to be called in the stage where the MIDIEventList has been 911 | created. This function, however, it meant to operate on the eventList itself. 912 | """ 913 | origin = 1000000 # A little silly, but we'll assume big enough 914 | 915 | for track in self.tracks: 916 | if len(track.eventList) > 0: 917 | for event in track.eventList: 918 | if event.time < origin: 919 | origin = event.time 920 | 921 | for track in self.tracks: 922 | tempEventList = [] 923 | #runningTime = 0 924 | 925 | for event in track.eventList: 926 | adjustedTime = event.time - origin 927 | #event.time = adjustedTime - runningTime + offset 928 | event.time = adjustedTime + offset 929 | #runningTime = adjustedTime 930 | tempEventList.append(event) 931 | 932 | track.eventList = tempEventList 933 | 934 | #End Public Functions ######################## 935 | 936 | def close(self): 937 | '''Close the MIDIFile for further writing. 938 | 939 | To close the File for events, we must close the tracks, adjust the time to be 940 | zero-origined, and have the tracks write to their MIDI Stream data structure. 941 | ''' 942 | 943 | if self.closed == True: 944 | return 945 | 946 | for i in range(0,self.numTracks): 947 | self.tracks[i].closeTrack() 948 | # We want things like program changes to come before notes when they are at the 949 | # same time, so we sort the MIDI events by their ordinality 950 | self.tracks[i].MIDIEventList.sort() 951 | 952 | origin = self.findOrigin() 953 | 954 | for i in range(0,self.numTracks): 955 | self.tracks[i].adjustTime(origin) 956 | self.tracks[i].writeMIDIStream() 957 | 958 | self.closed = True 959 | 960 | 961 | def findOrigin(self): 962 | '''Find the earliest time in the file's tracks.append. 963 | ''' 964 | origin = 1000000 # A little silly, but we'll assume big enough 965 | 966 | # Note: This code assumes that the MIDIEventList has been sorted, so this should be insured 967 | # before it is called. It is probably a poor design to do this. 968 | # TODO: -- Consider making this less efficient but more robust by not assuming the list to be sorted. 969 | 970 | for track in self.tracks: 971 | if len(track.MIDIEventList) > 0: 972 | if track.MIDIEventList[0].time < origin: 973 | origin = track.MIDIEventList[0].time 974 | 975 | 976 | return origin 977 | 978 | def writeVarLength(i): 979 | '''Accept an input, and write a MIDI-compatible variable length stream 980 | 981 | The MIDI format is a little strange, and makes use of so-called variable 982 | length quantities. These quantities are a stream of bytes. If the most 983 | significant bit is 1, then more bytes follow. If it is zero, then the 984 | byte in question is the last in the stream 985 | ''' 986 | input = int(i+0.5) 987 | output = [0,0,0,0] 988 | reversed = [0,0,0,0] 989 | count = 0 990 | result = input & 0x7F 991 | output[count] = result 992 | count = count + 1 993 | input = input >> 7 994 | while input > 0: 995 | result = input & 0x7F 996 | result = result | 0x80 997 | output[count] = result 998 | count = count + 1 999 | input = input >> 7 1000 | 1001 | reversed[0] = output[3] 1002 | reversed[1] = output[2] 1003 | reversed[2] = output[1] 1004 | reversed[3] = output[0] 1005 | return reversed[4-count:4] 1006 | 1007 | def readVarLength(offset, buffer): 1008 | '''A function to read a MIDI variable length variable. 1009 | 1010 | It returns a tuple of the value read and the number of bytes processed. The 1011 | input is an offset into the buffer, and the buffer itself. 1012 | ''' 1013 | toffset = offset 1014 | output = 0 1015 | bytesRead = 0 1016 | while True: 1017 | output = output << 7 1018 | byte = struct.unpack_from('>B',buffer,toffset)[0] 1019 | toffset = toffset + 1 1020 | bytesRead = bytesRead + 1 1021 | output = output + (byte & 127) 1022 | if (byte & 128) == 0: 1023 | break 1024 | return (output, bytesRead) 1025 | 1026 | def frequencyTransform(freq): 1027 | '''Returns a three-byte transform of a frequencyTransform 1028 | ''' 1029 | resolution = 16384 1030 | freq = float(freq) 1031 | dollars = 69 + 12 * math.log(freq/(float(440)), 2) 1032 | firstByte = int(dollars) 1033 | lowerFreq = 440 * pow(2.0, ((float(firstByte) - 69.0)/12.0)) 1034 | if freq != lowerFreq: 1035 | centDif = 1200 * math.log( (freq/lowerFreq), 2) 1036 | else: 1037 | centDif = 0 1038 | cents = round(centDif/100 * resolution) # round? 1039 | secondByte = min([int(cents)>>7, 0x7F]) 1040 | thirdByte = cents - (secondByte << 7) 1041 | thirdByte = min([thirdByte, 0x7f]) 1042 | if thirdByte == 0x7f and secondByte == 0x7F and firstByte == 0x7F: 1043 | thirdByte = 0x7e 1044 | 1045 | thirdByte = int(thirdByte) 1046 | return [firstByte, secondByte, thirdByte] 1047 | 1048 | def returnFrequency(freqBytes): 1049 | '''The reverse of frequencyTransform. Given a byte stream, return a frequency. 1050 | ''' 1051 | resolution = 16384.0 1052 | baseFrequency = 440 * pow(2.0, (float(freqBytes[0]-69.0)/12.0)) 1053 | frac = (float((int(freqBytes[1]) << 7) + int(freqBytes[2])) * 100.0) / resolution 1054 | frequency = baseFrequency * pow(2.0, frac/1200.0) 1055 | return frequency 1056 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/src/midiutil/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/MIDIUtil-0.89/MIDIUtil-0.89/src/midiutil/__init__.py -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/src/unittests/miditest.py: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Name: miditest.py 3 | # Purpose: Unit testing harness for midiutil 4 | # 5 | # Author: Mark Conway Wirt 6 | # 7 | # Created: 2008/04/17 8 | # Copyright: (c) 2009, Mark Conway Wirt 9 | # License: Please see License.txt for the terms under which this 10 | # software is distributed. 11 | #----------------------------------------------------------------------------- 12 | 13 | 14 | 15 | # Next few lines are necessary owing to limitations of the IDE and the 16 | # directory structure of the project. 17 | 18 | import sys, struct 19 | sys.path.append('..') 20 | 21 | import unittest 22 | from midiutil.MidiFile import MIDIFile, MIDIHeader, MIDITrack, writeVarLength, \ 23 | frequencyTransform, returnFrequency 24 | import sys 25 | 26 | class TestMIDIUtils(unittest.TestCase): 27 | 28 | def testWriteVarLength(self): 29 | self.assertEquals(writeVarLength(0x70), [0x70]) 30 | self.assertEquals(writeVarLength(0x80), [0x81, 0x00]) 31 | self.assertEquals(writeVarLength(0x1FFFFF), [0xFF, 0xFF, 0x7F]) 32 | self.assertEquals(writeVarLength(0x08000000), [0xC0, 0x80, 0x80, 0x00]) 33 | 34 | def testAddNote(self): 35 | MyMIDI = MIDIFile(1) 36 | MyMIDI.addNote(0, 0, 100,0,1,100) 37 | self.assertEquals(MyMIDI.tracks[0].eventList[0].type, "note") 38 | self.assertEquals(MyMIDI.tracks[0].eventList[0].pitch, 100) 39 | self.assertEquals(MyMIDI.tracks[0].eventList[0].time, 0) 40 | self.assertEquals(MyMIDI.tracks[0].eventList[0].duration, 1) 41 | self.assertEquals(MyMIDI.tracks[0].eventList[0].volume, 100) 42 | 43 | def testDeinterleaveNotes(self): 44 | MyMIDI = MIDIFile(1) 45 | MyMIDI.addNote(0, 0, 100, 0, 2, 100) 46 | MyMIDI.addNote(0, 0, 100, 1, 2, 100) 47 | MyMIDI.close() 48 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 49 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 50 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 51 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 52 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[2].type, 'NoteOn') 53 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[2].time, 0) 54 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[3].type, 'NoteOff') 55 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[3].time, 1920) 56 | 57 | def testTimeShift(self): 58 | 59 | # With one track 60 | MyMIDI = MIDIFile(1) 61 | MyMIDI.addNote(0, 0, 100, 5, 1, 100) 62 | MyMIDI.close() 63 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 64 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 65 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 66 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 67 | 68 | # With two tracks 69 | MyMIDI = MIDIFile(2) 70 | MyMIDI.addNote(0, 0, 100, 5, 1, 100) 71 | MyMIDI.addNote(1, 0, 100, 6, 1, 100) 72 | MyMIDI.close() 73 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 74 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 75 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 76 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 77 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn') 78 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 960) 79 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff') 80 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 960) 81 | 82 | # Negative Time 83 | MyMIDI = MIDIFile(1) 84 | MyMIDI.addNote(0, 0, 100, -5, 1, 100) 85 | MyMIDI.close() 86 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 87 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 88 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 89 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 90 | 91 | # Negative time, two tracks 92 | 93 | MyMIDI = MIDIFile(2) 94 | MyMIDI.addNote(0, 0, 100, -1, 1, 100) 95 | MyMIDI.addNote(1, 0, 100, 0, 1, 100) 96 | MyMIDI.close() 97 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 98 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 99 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 100 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 101 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn') 102 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 960) 103 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff') 104 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 960) 105 | 106 | def testFrequency(self): 107 | freq = frequencyTransform(8.1758) 108 | self.assertEquals(freq[0], 0x00) 109 | self.assertEquals(freq[1], 0x00) 110 | self.assertEquals(freq[2], 0x00) 111 | freq = frequencyTransform(8.66196) # 8.6620 in MIDI documentation 112 | self.assertEquals(freq[0], 0x01) 113 | self.assertEquals(freq[1], 0x00) 114 | self.assertEquals(freq[2], 0x00) 115 | freq = frequencyTransform(440.00) 116 | self.assertEquals(freq[0], 0x45) 117 | self.assertEquals(freq[1], 0x00) 118 | self.assertEquals(freq[2], 0x00) 119 | freq = frequencyTransform(440.0016) 120 | self.assertEquals(freq[0], 0x45) 121 | self.assertEquals(freq[1], 0x00) 122 | self.assertEquals(freq[2], 0x01) 123 | freq = frequencyTransform(439.9984) 124 | self.assertEquals(freq[0], 0x44) 125 | self.assertEquals(freq[1], 0x7f) 126 | self.assertEquals(freq[2], 0x7f) 127 | freq = frequencyTransform(8372.0190) 128 | self.assertEquals(freq[0], 0x78) 129 | self.assertEquals(freq[1], 0x00) 130 | self.assertEquals(freq[2], 0x00) 131 | freq = frequencyTransform(8372.062) #8372.0630 in MIDI documentation 132 | self.assertEquals(freq[0], 0x78) 133 | self.assertEquals(freq[1], 0x00) 134 | self.assertEquals(freq[2], 0x01) 135 | freq = frequencyTransform(13289.7300) 136 | self.assertEquals(freq[0], 0x7F) 137 | self.assertEquals(freq[1], 0x7F) 138 | self.assertEquals(freq[2], 0x7E) 139 | freq = frequencyTransform(12543.8760) 140 | self.assertEquals(freq[0], 0x7F) 141 | self.assertEquals(freq[1], 0x00) 142 | self.assertEquals(freq[2], 0x00) 143 | freq = frequencyTransform(8.2104) # Just plain wrong in documentation, as far as I can tell. 144 | #self.assertEquals(freq[0], 0x0) 145 | #self.assertEquals(freq[1], 0x0) 146 | #self.assertEquals(freq[2], 0x1) 147 | 148 | # Test the inverse 149 | testFreq = 15.0 150 | accuracy = 0.00001 151 | x = returnFrequency(frequencyTransform(testFreq)) 152 | delta = abs(testFreq - x) 153 | self.assertEquals(delta < (accuracy*testFreq), True) 154 | testFreq = 200.0 155 | x = returnFrequency(frequencyTransform(testFreq)) 156 | delta = abs(testFreq - x) 157 | self.assertEquals(delta < (accuracy*testFreq), True) 158 | testFreq = 400.0 159 | x = returnFrequency(frequencyTransform(testFreq)) 160 | delta = abs(testFreq - x) 161 | self.assertEquals(delta < (accuracy*testFreq), True) 162 | testFreq = 440.0 163 | x = returnFrequency(frequencyTransform(testFreq)) 164 | delta = abs(testFreq - x) 165 | self.assertEquals(delta < (accuracy*testFreq), True) 166 | testFreq = 1200.0 167 | x = returnFrequency(frequencyTransform(testFreq)) 168 | delta = abs(testFreq - x) 169 | self.assertEquals(delta < (accuracy*testFreq), True) 170 | testFreq = 5000.0 171 | x = returnFrequency(frequencyTransform(testFreq)) 172 | delta = abs(testFreq - x) 173 | self.assertEquals(delta < (accuracy*testFreq), True) 174 | testFreq = 12000.0 175 | x = returnFrequency(frequencyTransform(testFreq)) 176 | delta = abs(testFreq - x) 177 | self.assertEquals(delta < (accuracy*testFreq), True) 178 | 179 | 180 | def testSysEx(self): 181 | MyMIDI = MIDIFile(1) 182 | MyMIDI.addSysEx(0,0, 0, struct.pack('>B', 0x01)) 183 | MyMIDI.close() 184 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'SysEx') 185 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[0])[0], 0x00) 186 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[1])[0], 0xf0) 187 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[2])[0], 3) 188 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[3])[0], 0x00) 189 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[4])[0], 0x01) 190 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[5])[0], 0xf7) 191 | 192 | def testUniversalSysEx(self): 193 | MyMIDI = MIDIFile(1) 194 | MyMIDI.addUniversalSysEx(0,0, 1, 2, struct.pack('>B', 0x01)) 195 | MyMIDI.close() 196 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'UniversalSysEx') 197 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[0])[0], 0x00) 198 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[1])[0], 0xf0) 199 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[2])[0], 6) 200 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[3])[0], 0x7E) 201 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[4])[0], 0x7F) 202 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[5])[0], 0x01) 203 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[6])[0], 0x02) 204 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[7])[0], 0x01) 205 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[8])[0], 0xf7) 206 | 207 | def testTuning(self): 208 | MyMIDI = MIDIFile(1) 209 | MyMIDI.changeNoteTuning(0, [(1, 440), (2, 880)]) 210 | MyMIDI.close() 211 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'UniversalSysEx') 212 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[0])[0], 0x00) 213 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[1])[0], 0xf0) 214 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[2])[0], 15) 215 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[3])[0], 0x7E) 216 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[4])[0], 0x7F) 217 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[5])[0], 0x08) 218 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[6])[0], 0x02) 219 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[7])[0], 0x00) 220 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[8])[0], 0x2) 221 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[9])[0], 0x1) 222 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[10])[0], 69) 223 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[11])[0], 0) 224 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[12])[0], 0) 225 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[13])[0], 0x2) 226 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[14])[0], 81) 227 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[15])[0], 0) 228 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[16])[0], 0) 229 | self.assertEquals(struct.unpack('>B', MyMIDI.tracks[0].MIDIdata[17])[0], 0xf7) 230 | 231 | MIDISuite = unittest.TestLoader().loadTestsFromTestCase(TestMIDIUtils) 232 | 233 | if __name__ == '__main__': 234 | unittest.TextTestRunner(verbosity=1).run(MIDISuite) 235 | 236 | -------------------------------------------------------------------------------- /MIDIUtil-0.89/MIDIUtil-0.89/src/unittests/miditest.py3: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Name: miditest.py 3 | # Purpose: Unit testing harness for midiutil 4 | # 5 | # Author: Mark Conway Wirt 6 | # 7 | # Created: 2008/04/17 8 | # Copyright: (c) 2009, Mark Conway Wirt 9 | # License: Please see License.txt for the terms under which this 10 | # software is distributed. 11 | #----------------------------------------------------------------------------- 12 | 13 | 14 | 15 | # Next few lines are necessary owing to limitations of the IDE and the 16 | # directory structure of the project. 17 | 18 | import sys, struct 19 | sys.path.append('..') 20 | 21 | import unittest 22 | from midiutil.MidiFile3 import MIDIFile, MIDIHeader, MIDITrack, writeVarLength, \ 23 | frequencyTransform, returnFrequency 24 | import sys 25 | 26 | class TestMIDIUtils(unittest.TestCase): 27 | 28 | def testWriteVarLength(self): 29 | self.assertEquals(writeVarLength(0x70), [0x70]) 30 | self.assertEquals(writeVarLength(0x80), [0x81, 0x00]) 31 | self.assertEquals(writeVarLength(0x1FFFFF), [0xFF, 0xFF, 0x7F]) 32 | self.assertEquals(writeVarLength(0x08000000), [0xC0, 0x80, 0x80, 0x00]) 33 | 34 | def testAddNote(self): 35 | MyMIDI = MIDIFile(1) 36 | MyMIDI.addNote(0, 0, 100,0,1,100) 37 | self.assertEquals(MyMIDI.tracks[0].eventList[0].type, "note") 38 | self.assertEquals(MyMIDI.tracks[0].eventList[0].pitch, 100) 39 | self.assertEquals(MyMIDI.tracks[0].eventList[0].time, 0) 40 | self.assertEquals(MyMIDI.tracks[0].eventList[0].duration, 1) 41 | self.assertEquals(MyMIDI.tracks[0].eventList[0].volume, 100) 42 | 43 | def testDeinterleaveNotes(self): 44 | MyMIDI = MIDIFile(1) 45 | MyMIDI.addNote(0, 0, 100, 0, 2, 100) 46 | MyMIDI.addNote(0, 0, 100, 1, 2, 100) 47 | MyMIDI.close() 48 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 49 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 50 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 51 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 52 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[2].type, 'NoteOn') 53 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[2].time, 0) 54 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[3].type, 'NoteOff') 55 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[3].time, 1920) 56 | 57 | def testTimeShift(self): 58 | 59 | # With one track 60 | MyMIDI = MIDIFile(1) 61 | MyMIDI.addNote(0, 0, 100, 5, 1, 100) 62 | MyMIDI.close() 63 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 64 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 65 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 66 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 67 | 68 | # With two tracks 69 | MyMIDI = MIDIFile(2) 70 | MyMIDI.addNote(0, 0, 100, 5, 1, 100) 71 | MyMIDI.addNote(1, 0, 100, 6, 1, 100) 72 | MyMIDI.close() 73 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 74 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 75 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 76 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 77 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn') 78 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 960) 79 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff') 80 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 960) 81 | 82 | # Negative Time 83 | MyMIDI = MIDIFile(1) 84 | MyMIDI.addNote(0, 0, 100, -5, 1, 100) 85 | MyMIDI.close() 86 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 87 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 88 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 89 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 90 | 91 | # Negative time, two tracks 92 | 93 | MyMIDI = MIDIFile(2) 94 | MyMIDI.addNote(0, 0, 100, -1, 1, 100) 95 | MyMIDI.addNote(1, 0, 100, 0, 1, 100) 96 | MyMIDI.close() 97 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'NoteOn') 98 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].time, 0) 99 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].type, 'NoteOff') 100 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[1].time, 960) 101 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].type, 'NoteOn') 102 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[0].time, 960) 103 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].type, 'NoteOff') 104 | self.assertEquals(MyMIDI.tracks[1].MIDIEventList[1].time, 960) 105 | 106 | def testFrequency(self): 107 | freq = frequencyTransform(8.1758) 108 | self.assertEquals(freq[0], 0x00) 109 | self.assertEquals(freq[1], 0x00) 110 | self.assertEquals(freq[2], 0x00) 111 | freq = frequencyTransform(8.66196) # 8.6620 in MIDI documentation 112 | self.assertEquals(freq[0], 0x01) 113 | self.assertEquals(freq[1], 0x00) 114 | self.assertEquals(freq[2], 0x00) 115 | freq = frequencyTransform(440.00) 116 | self.assertEquals(freq[0], 0x45) 117 | self.assertEquals(freq[1], 0x00) 118 | self.assertEquals(freq[2], 0x00) 119 | freq = frequencyTransform(440.0016) 120 | self.assertEquals(freq[0], 0x45) 121 | self.assertEquals(freq[1], 0x00) 122 | self.assertEquals(freq[2], 0x01) 123 | freq = frequencyTransform(439.9984) 124 | self.assertEquals(freq[0], 0x44) 125 | self.assertEquals(freq[1], 0x7f) 126 | self.assertEquals(freq[2], 0x7f) 127 | freq = frequencyTransform(8372.0190) 128 | self.assertEquals(freq[0], 0x78) 129 | self.assertEquals(freq[1], 0x00) 130 | self.assertEquals(freq[2], 0x00) 131 | freq = frequencyTransform(8372.062) #8372.0630 in MIDI documentation 132 | self.assertEquals(freq[0], 0x78) 133 | self.assertEquals(freq[1], 0x00) 134 | self.assertEquals(freq[2], 0x01) 135 | freq = frequencyTransform(13289.7300) 136 | self.assertEquals(freq[0], 0x7F) 137 | self.assertEquals(freq[1], 0x7F) 138 | self.assertEquals(freq[2], 0x7E) 139 | freq = frequencyTransform(12543.8760) 140 | self.assertEquals(freq[0], 0x7F) 141 | self.assertEquals(freq[1], 0x00) 142 | self.assertEquals(freq[2], 0x00) 143 | freq = frequencyTransform(8.2104) # Just plain wrong in documentation, as far as I can tell. 144 | #self.assertEquals(freq[0], 0x0) 145 | #self.assertEquals(freq[1], 0x0) 146 | #self.assertEquals(freq[2], 0x1) 147 | 148 | # Test the inverse 149 | testFreq = 15.0 150 | accuracy = 0.00001 151 | x = returnFrequency(frequencyTransform(testFreq)) 152 | delta = abs(testFreq - x) 153 | self.assertEquals(delta < (accuracy*testFreq), True) 154 | testFreq = 200.0 155 | x = returnFrequency(frequencyTransform(testFreq)) 156 | delta = abs(testFreq - x) 157 | self.assertEquals(delta < (accuracy*testFreq), True) 158 | testFreq = 400.0 159 | x = returnFrequency(frequencyTransform(testFreq)) 160 | delta = abs(testFreq - x) 161 | self.assertEquals(delta < (accuracy*testFreq), True) 162 | testFreq = 440.0 163 | x = returnFrequency(frequencyTransform(testFreq)) 164 | delta = abs(testFreq - x) 165 | self.assertEquals(delta < (accuracy*testFreq), True) 166 | testFreq = 1200.0 167 | x = returnFrequency(frequencyTransform(testFreq)) 168 | delta = abs(testFreq - x) 169 | self.assertEquals(delta < (accuracy*testFreq), True) 170 | testFreq = 5000.0 171 | x = returnFrequency(frequencyTransform(testFreq)) 172 | delta = abs(testFreq - x) 173 | self.assertEquals(delta < (accuracy*testFreq), True) 174 | testFreq = 12000.0 175 | x = returnFrequency(frequencyTransform(testFreq)) 176 | delta = abs(testFreq - x) 177 | self.assertEquals(delta < (accuracy*testFreq), True) 178 | 179 | 180 | def testSysEx(self): 181 | MyMIDI = MIDIFile(1) 182 | MyMIDI.addSysEx(0,0, 0, struct.pack('>B', 0x01)) 183 | MyMIDI.close() 184 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'SysEx') 185 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[0], 0x00) 186 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[1], 0xf0) 187 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[2], 3) 188 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[3], 0x00) 189 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[4], 0x01) 190 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[5], 0xf7) 191 | 192 | def testUniversalSysEx(self): 193 | MyMIDI = MIDIFile(1) 194 | MyMIDI.addUniversalSysEx(0,0, 1, 2, struct.pack('>B', 0x01)) 195 | MyMIDI.close() 196 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'UniversalSysEx') 197 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[0], 0x00) 198 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[1], 0xf0) 199 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[2], 6) 200 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[3], 0x7E) 201 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[4], 0x7F) 202 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[5], 0x01) 203 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[6], 0x02) 204 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[7], 0x01) 205 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[8], 0xf7) 206 | 207 | def testTuning(self): 208 | MyMIDI = MIDIFile(1) 209 | MyMIDI.changeNoteTuning(0, [(1, 440), (2, 880)]) 210 | MyMIDI.close() 211 | self.assertEquals(MyMIDI.tracks[0].MIDIEventList[0].type, 'UniversalSysEx') 212 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[0], 0x00) 213 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[1], 0xf0) 214 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[2], 15) 215 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[3], 0x7E) 216 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[4], 0x7F) 217 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[5], 0x08) 218 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[6], 0x02) 219 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[7], 0x00) 220 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[8], 0x2) 221 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[9], 0x1) 222 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[10], 69) 223 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[11], 0x00) 224 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[12], 0x00) 225 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[13], 0x02) 226 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[14], 81) 227 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[15], 0x00) 228 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[16], 0x00) 229 | self.assertEquals(MyMIDI.tracks[0].MIDIdata[17], 0xf7) 230 | 231 | 232 | MIDISuite = unittest.TestLoader().loadTestsFromTestCase(TestMIDIUtils) 233 | 234 | if __name__ == '__main__': 235 | unittest.TextTestRunner(verbosity=1).run(MIDISuite) 236 | 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Sheet Vision 2 | 3 | Sheet Vision is a python program which reads sheet music and turns it into midi files. 4 | 5 | ![image](https://cloud.githubusercontent.com/assets/7611406/17604255/9819f878-5fef-11e6-8f49-865d07284803.png) 6 | 7 | ## Developing a Sheet Music Reader 8 | Calvin Gregory, and Calvert Pratt 9 | 10 | ### I. Introduction 11 | Applications in the field of Music Optical Character Recognition (OCR), also referred to as Optical Music Recognition, are an application of machine vision which serves to simplify the sight reading learning process and speed up music transcription. The sheet music reader application SheetVision was developed to convert single-tone lines of written music into a computer readable format for audio song playback. It does this through a template image matching algorithm implemented in Python using OpenCV which searches the target image for instances of each music character type such as notes, flats, and sharps. These characters are then identified, sequenced, and exported to a MIDI file for playback. 12 | 13 | ### II. Project Scope 14 | The sheet music reader application was designed to convert images of written sheet music into a computer-readable format. SheetVision takes in an image of written sheet music, classifies all relevant music characters in the image, then generates and exports a MIDI file with all of the classified characters properly sequenced and identified as music notations. The application can handle single tone sequences of notes consisting of whole, half, quarter, and paired eighth (Ti-Ti) notes. It also interprets key signatures (sharp and flat symbols included at the beginning of a line) and rest characters. SheetVision is capable of interpreting most simple to moderate complexity sheet music arrangements written for single-tone instruments such as woodwinds, strings, and vocals. 15 | 16 | ### III. Algorithm 17 | The algorithm used for music character identification uses the following series of steps to categorize each note or symbol in the target image: 18 | - A. Image Filtering / Binary Conversion 19 | - B. Template Scaling 20 | - C. Character Classification 21 | - D. Classifier Thresholding 22 | - E. Note Identification and Sequencing 23 | - F. Export results to MIDI 24 | 25 | ------------------ 26 | 27 | #### Libraries sourced from http://www.lfd.uci.edu/~gohlke/pythonlibs/ 28 | - (for x64) 29 | - numpy-1.11.1+mkl-cp35-cp35m-win_amd64.whl 30 | - matplotlib-1.5.2-cp35-cp35m-win_amd64.whl 31 | - opencv_python-3.1.0-cp35-cp35m-win_amd64.whl 32 | - (for x86) 33 | - numpy-1.11.1+mkl-cp35-cp35m-win32.whl 34 | - matplotlib-1.5.2-cp35-cp35m-win32.whl 35 | - opencv_python-3.1.0-cp35-cp35m-win32.whl 36 | 37 | Midiutil Python 3 version is included in this repo 38 | -------------------------------------------------------------------------------- /best_fit.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | def fit(img, templates, start_percent, stop_percent, threshold): 6 | img_width, img_height = img.shape[::-1] 7 | best_location_count = -1 8 | best_locations = [] 9 | best_scale = 1 10 | 11 | plt.axis([0, 2, 0, 1]) 12 | plt.show(block=False) 13 | 14 | x = [] 15 | y = [] 16 | for scale in [i/100.0 for i in range(start_percent, stop_percent + 1, 3)]: 17 | locations = [] 18 | location_count = 0 19 | for template in templates: 20 | template = cv2.resize(template, None, 21 | fx = scale, fy = scale, interpolation = cv2.INTER_CUBIC) 22 | result = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED) 23 | result = np.where(result >= threshold) 24 | location_count += len(result[0]) 25 | locations += [result] 26 | print("scale: {0}, hits: {1}".format(scale, location_count)) 27 | x.append(location_count) 28 | y.append(scale) 29 | plt.plot(y, x) 30 | plt.pause(0.00001) 31 | if (location_count > best_location_count): 32 | best_location_count = location_count 33 | best_locations = locations 34 | best_scale = scale 35 | plt.axis([0, 2, 0, best_location_count]) 36 | elif (location_count < best_location_count): 37 | pass 38 | plt.close() 39 | 40 | return best_locations, best_scale -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | import cv2 4 | import time 5 | import numpy as np 6 | from best_fit import fit 7 | from rectangle import Rectangle 8 | from note import Note 9 | from random import randint 10 | from midiutil.MidiFile3 import MIDIFile 11 | 12 | staff_files = [ 13 | "resources/template/staff2.png", 14 | "resources/template/staff.png"] 15 | quarter_files = [ 16 | "resources/template/quarter.png", 17 | "resources/template/solid-note.png"] 18 | sharp_files = [ 19 | "resources/template/sharp.png"] 20 | flat_files = [ 21 | "resources/template/flat-line.png", 22 | "resources/template/flat-space.png" ] 23 | half_files = [ 24 | "resources/template/half-space.png", 25 | "resources/template/half-note-line.png", 26 | "resources/template/half-line.png", 27 | "resources/template/half-note-space.png"] 28 | whole_files = [ 29 | "resources/template/whole-space.png", 30 | "resources/template/whole-note-line.png", 31 | "resources/template/whole-line.png", 32 | "resources/template/whole-note-space.png"] 33 | 34 | staff_imgs = [cv2.imread(staff_file, 0) for staff_file in staff_files] 35 | quarter_imgs = [cv2.imread(quarter_file, 0) for quarter_file in quarter_files] 36 | sharp_imgs = [cv2.imread(sharp_files, 0) for sharp_files in sharp_files] 37 | flat_imgs = [cv2.imread(flat_file, 0) for flat_file in flat_files] 38 | half_imgs = [cv2.imread(half_file, 0) for half_file in half_files] 39 | whole_imgs = [cv2.imread(whole_file, 0) for whole_file in whole_files] 40 | 41 | staff_lower, staff_upper, staff_thresh = 50, 150, 0.77 42 | sharp_lower, sharp_upper, sharp_thresh = 50, 150, 0.70 43 | flat_lower, flat_upper, flat_thresh = 50, 150, 0.77 44 | quarter_lower, quarter_upper, quarter_thresh = 50, 150, 0.70 45 | half_lower, half_upper, half_thresh = 50, 150, 0.70 46 | whole_lower, whole_upper, whole_thresh = 50, 150, 0.70 47 | 48 | 49 | def locate_images(img, templates, start, stop, threshold): 50 | locations, scale = fit(img, templates, start, stop, threshold) 51 | img_locations = [] 52 | for i in range(len(templates)): 53 | w, h = templates[i].shape[::-1] 54 | w *= scale 55 | h *= scale 56 | img_locations.append([Rectangle(pt[0], pt[1], w, h) for pt in zip(*locations[i][::-1])]) 57 | return img_locations 58 | 59 | def merge_recs(recs, threshold): 60 | filtered_recs = [] 61 | while len(recs) > 0: 62 | r = recs.pop(0) 63 | recs.sort(key=lambda rec: rec.distance(r)) 64 | merged = True 65 | while(merged): 66 | merged = False 67 | i = 0 68 | for _ in range(len(recs)): 69 | if r.overlap(recs[i]) > threshold or recs[i].overlap(r) > threshold: 70 | r = r.merge(recs.pop(i)) 71 | merged = True 72 | elif recs[i].distance(r) > r.w/2 + recs[i].w/2: 73 | break 74 | else: 75 | i += 1 76 | filtered_recs.append(r) 77 | return filtered_recs 78 | 79 | def open_file(path): 80 | cmd = {'linux':'eog', 'win32':'explorer', 'darwin':'open'}[sys.platform] 81 | subprocess.run([cmd, path]) 82 | 83 | if __name__ == "__main__": 84 | img_file = sys.argv[1:][0] 85 | img = cv2.imread(img_file, 0) 86 | img_gray = img#cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 87 | img = cv2.cvtColor(img_gray,cv2.COLOR_GRAY2RGB) 88 | ret,img_gray = cv2.threshold(img_gray,127,255,cv2.THRESH_BINARY) 89 | img_width, img_height = img_gray.shape[::-1] 90 | 91 | print("Matching staff image...") 92 | staff_recs = locate_images(img_gray, staff_imgs, staff_lower, staff_upper, staff_thresh) 93 | 94 | print("Filtering weak staff matches...") 95 | staff_recs = [j for i in staff_recs for j in i] 96 | heights = [r.y for r in staff_recs] + [0] 97 | histo = [heights.count(i) for i in range(0, max(heights) + 1)] 98 | avg = np.mean(list(set(histo))) 99 | staff_recs = [r for r in staff_recs if histo[r.y] > avg] 100 | 101 | print("Merging staff image results...") 102 | staff_recs = merge_recs(staff_recs, 0.01) 103 | staff_recs_img = img.copy() 104 | for r in staff_recs: 105 | r.draw(staff_recs_img, (0, 0, 255), 2) 106 | cv2.imwrite('staff_recs_img.png', staff_recs_img) 107 | open_file('staff_recs_img.png') 108 | 109 | print("Discovering staff locations...") 110 | staff_boxes = merge_recs([Rectangle(0, r.y, img_width, r.h) for r in staff_recs], 0.01) 111 | staff_boxes_img = img.copy() 112 | for r in staff_boxes: 113 | r.draw(staff_boxes_img, (0, 0, 255), 2) 114 | cv2.imwrite('staff_boxes_img.png', staff_boxes_img) 115 | open_file('staff_boxes_img.png') 116 | 117 | print("Matching sharp image...") 118 | sharp_recs = locate_images(img_gray, sharp_imgs, sharp_lower, sharp_upper, sharp_thresh) 119 | 120 | print("Merging sharp image results...") 121 | sharp_recs = merge_recs([j for i in sharp_recs for j in i], 0.5) 122 | sharp_recs_img = img.copy() 123 | for r in sharp_recs: 124 | r.draw(sharp_recs_img, (0, 0, 255), 2) 125 | cv2.imwrite('sharp_recs_img.png', sharp_recs_img) 126 | open_file('sharp_recs_img.png') 127 | 128 | print("Matching flat image...") 129 | flat_recs = locate_images(img_gray, flat_imgs, flat_lower, flat_upper, flat_thresh) 130 | 131 | print("Merging flat image results...") 132 | flat_recs = merge_recs([j for i in flat_recs for j in i], 0.5) 133 | flat_recs_img = img.copy() 134 | for r in flat_recs: 135 | r.draw(flat_recs_img, (0, 0, 255), 2) 136 | cv2.imwrite('flat_recs_img.png', flat_recs_img) 137 | open_file('flat_recs_img.png') 138 | 139 | print("Matching quarter image...") 140 | quarter_recs = locate_images(img_gray, quarter_imgs, quarter_lower, quarter_upper, quarter_thresh) 141 | 142 | print("Merging quarter image results...") 143 | quarter_recs = merge_recs([j for i in quarter_recs for j in i], 0.5) 144 | quarter_recs_img = img.copy() 145 | for r in quarter_recs: 146 | r.draw(quarter_recs_img, (0, 0, 255), 2) 147 | cv2.imwrite('quarter_recs_img.png', quarter_recs_img) 148 | open_file('quarter_recs_img.png') 149 | 150 | print("Matching half image...") 151 | half_recs = locate_images(img_gray, half_imgs, half_lower, half_upper, half_thresh) 152 | 153 | print("Merging half image results...") 154 | half_recs = merge_recs([j for i in half_recs for j in i], 0.5) 155 | half_recs_img = img.copy() 156 | for r in half_recs: 157 | r.draw(half_recs_img, (0, 0, 255), 2) 158 | cv2.imwrite('half_recs_img.png', half_recs_img) 159 | open_file('half_recs_img.png') 160 | 161 | print("Matching whole image...") 162 | whole_recs = locate_images(img_gray, whole_imgs, whole_lower, whole_upper, whole_thresh) 163 | 164 | print("Merging whole image results...") 165 | whole_recs = merge_recs([j for i in whole_recs for j in i], 0.5) 166 | whole_recs_img = img.copy() 167 | for r in whole_recs: 168 | r.draw(whole_recs_img, (0, 0, 255), 2) 169 | cv2.imwrite('whole_recs_img.png', whole_recs_img) 170 | open_file('whole_recs_img.png') 171 | 172 | note_groups = [] 173 | for box in staff_boxes: 174 | staff_sharps = [Note(r, "sharp", box) 175 | for r in sharp_recs if abs(r.middle[1] - box.middle[1]) < box.h*5.0/8.0] 176 | staff_flats = [Note(r, "flat", box) 177 | for r in flat_recs if abs(r.middle[1] - box.middle[1]) < box.h*5.0/8.0] 178 | quarter_notes = [Note(r, "4,8", box, staff_sharps, staff_flats) 179 | for r in quarter_recs if abs(r.middle[1] - box.middle[1]) < box.h*5.0/8.0] 180 | half_notes = [Note(r, "2", box, staff_sharps, staff_flats) 181 | for r in half_recs if abs(r.middle[1] - box.middle[1]) < box.h*5.0/8.0] 182 | whole_notes = [Note(r, "1", box, staff_sharps, staff_flats) 183 | for r in whole_recs if abs(r.middle[1] - box.middle[1]) < box.h*5.0/8.0] 184 | staff_notes = quarter_notes + half_notes + whole_notes 185 | staff_notes.sort(key=lambda n: n.rec.x) 186 | staffs = [r for r in staff_recs if r.overlap(box) > 0] 187 | staffs.sort(key=lambda r: r.x) 188 | note_color = (randint(0, 255), randint(0, 255), randint(0, 255)) 189 | note_group = [] 190 | i = 0; j = 0; 191 | while(i < len(staff_notes)): 192 | if (staff_notes[i].rec.x > staffs[j].x and j < len(staffs)): 193 | r = staffs[j] 194 | j += 1; 195 | if len(note_group) > 0: 196 | note_groups.append(note_group) 197 | note_group = [] 198 | note_color = (randint(0, 255), randint(0, 255), randint(0, 255)) 199 | else: 200 | note_group.append(staff_notes[i]) 201 | staff_notes[i].rec.draw(img, note_color, 2) 202 | i += 1 203 | note_groups.append(note_group) 204 | 205 | for r in staff_boxes: 206 | r.draw(img, (0, 0, 255), 2) 207 | for r in sharp_recs: 208 | r.draw(img, (0, 0, 255), 2) 209 | flat_recs_img = img.copy() 210 | for r in flat_recs: 211 | r.draw(img, (0, 0, 255), 2) 212 | 213 | cv2.imwrite('res.png', img) 214 | open_file('res.png') 215 | 216 | for note_group in note_groups: 217 | print([ note.note + " " + note.sym for note in note_group]) 218 | 219 | midi = MIDIFile(1) 220 | 221 | track = 0 222 | time = 0 223 | channel = 0 224 | volume = 100 225 | 226 | midi.addTrackName(track, time, "Track") 227 | midi.addTempo(track, time, 140) 228 | 229 | for note_group in note_groups: 230 | duration = None 231 | for note in note_group: 232 | note_type = note.sym 233 | if note_type == "1": 234 | duration = 4 235 | elif note_type == "2": 236 | duration = 2 237 | elif note_type == "4,8": 238 | duration = 1 if len(note_group) == 1 else 0.5 239 | pitch = note.pitch 240 | midi.addNote(track,channel,pitch,time,duration,volume) 241 | time += duration 242 | 243 | midi.addNote(track,channel,pitch,time,4,0) 244 | # And write it to disk. 245 | binfile = open("output.mid", 'wb') 246 | midi.writeFile(binfile) 247 | binfile.close() 248 | open_file('output.mid') 249 | -------------------------------------------------------------------------------- /note.py: -------------------------------------------------------------------------------- 1 | from rectangle import Rectangle 2 | 3 | note_step = 0.0625 4 | 5 | note_defs = { 6 | -4 : ("g5", 79), 7 | -3 : ("f5", 77), 8 | -2 : ("e5", 76), 9 | -1 : ("d5", 74), 10 | 0 : ("c5", 72), 11 | 1 : ("b4", 71), 12 | 2 : ("a4", 69), 13 | 3 : ("g4", 67), 14 | 4 : ("f4", 65), 15 | 5 : ("e4", 64), 16 | 6 : ("d4", 62), 17 | 7 : ("c4", 60), 18 | 8 : ("b3", 59), 19 | 9 : ("a3", 57), 20 | 10 : ("g3", 55), 21 | 11 : ("f3", 53), 22 | 12 : ("e3", 52), 23 | 13 : ("d3", 50), 24 | 14 : ("c3", 48), 25 | 15 : ("b2", 47), 26 | 16 : ("a2", 45), 27 | 17 : ("f2", 53), 28 | } 29 | 30 | class Note(object): 31 | def __init__(self, rec, sym, staff_rec, sharp_notes = [], flat_notes = []): 32 | self.rec = rec 33 | self.sym = sym 34 | 35 | middle = rec.y + (rec.h / 2.0) 36 | height = (middle - staff_rec.y) / staff_rec.h 37 | note_def = note_defs[int(height/note_step + 0.5)] 38 | self.note = note_def[0] 39 | self.pitch = note_def[1] 40 | if any(n for n in sharp_notes if n.note[0] == self.note[0]): 41 | self.note += "#" 42 | self.pitch += 1 43 | if any(n for n in flat_notes if n.note[0] == self.note[0]): 44 | self.note += "b" 45 | self.pitch -= 1 46 | 47 | 48 | -------------------------------------------------------------------------------- /rectangle.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import math 3 | 4 | class Rectangle(object): 5 | def __init__(self, x, y, w, h): 6 | self.x = x; 7 | self.y = y; 8 | self.w = w; 9 | self.h = h; 10 | self.middle = self.x + self.w/2, self.y + self.h/2 11 | self.area = self.w * self.h 12 | 13 | def overlap(self, other): 14 | overlap_x = max(0, min(self.x + self.w, other.x + other.w) - max(self.x, other.x)); 15 | overlap_y = max(0, min(self.y + self.h, other.y + other.h) - max(self.y, other.y)); 16 | overlap_area = overlap_x * overlap_y 17 | return overlap_area / self.area 18 | 19 | def distance(self, other): 20 | dx = self.middle[0] - other.middle[0] 21 | dy = self.middle[1] - other.middle[1] 22 | return math.sqrt(dx*dx + dy*dy) 23 | 24 | def merge(self, other): 25 | x = min(self.x, other.x) 26 | y = min(self.y, other.y) 27 | w = max(self.x + self.w, other.x + other.w) - x 28 | h = max(self.y + self.h, other.y + other.h) - y 29 | return Rectangle(x, y, w, h) 30 | 31 | def draw(self, img, color, thickness): 32 | pos = ((int)(self.x), (int)(self.y)) 33 | size = ((int)(self.x + self.w), (int)(self.y + self.h)) 34 | cv2.rectangle(img, pos, size, color, thickness) 35 | -------------------------------------------------------------------------------- /resources/samples/Moonlight Shadow Flauta-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/samples/Moonlight Shadow Flauta-1.png -------------------------------------------------------------------------------- /resources/samples/VarnattstankarvidFridasruta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/samples/VarnattstankarvidFridasruta.jpg -------------------------------------------------------------------------------- /resources/samples/fire.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/samples/fire.jpg -------------------------------------------------------------------------------- /resources/samples/lost.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/samples/lost.jpg -------------------------------------------------------------------------------- /resources/samples/races.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/samples/races.png -------------------------------------------------------------------------------- /resources/samples/sheet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/samples/sheet.jpg -------------------------------------------------------------------------------- /resources/template/bar-rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/bar-rest.png -------------------------------------------------------------------------------- /resources/template/f-sharp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/f-sharp.png -------------------------------------------------------------------------------- /resources/template/flat-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/flat-line.png -------------------------------------------------------------------------------- /resources/template/flat-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/flat-space.png -------------------------------------------------------------------------------- /resources/template/half-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/half-line.png -------------------------------------------------------------------------------- /resources/template/half-note-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/half-note-line.png -------------------------------------------------------------------------------- /resources/template/half-note-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/half-note-space.png -------------------------------------------------------------------------------- /resources/template/half-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/half-space.png -------------------------------------------------------------------------------- /resources/template/quarter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/quarter.png -------------------------------------------------------------------------------- /resources/template/sharp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/sharp.png -------------------------------------------------------------------------------- /resources/template/solid-note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/solid-note.png -------------------------------------------------------------------------------- /resources/template/staff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/staff.png -------------------------------------------------------------------------------- /resources/template/staff2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/staff2.png -------------------------------------------------------------------------------- /resources/template/staff3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/staff3.png -------------------------------------------------------------------------------- /resources/template/whole-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/whole-line.png -------------------------------------------------------------------------------- /resources/template/whole-note-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/whole-note-line.png -------------------------------------------------------------------------------- /resources/template/whole-note-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/whole-note-space.png -------------------------------------------------------------------------------- /resources/template/whole-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cal-pratt/SheetVision/f1ce1c9c97d5c922aa95b9120152f9c62fab829d/resources/template/whole-space.png --------------------------------------------------------------------------------