diff --git a/.args b/.args new file mode 100644 index 0000000..1cd3b62 --- /dev/null +++ b/.args @@ -0,0 +1 @@ +/zip/gabctk.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..3cff3f8 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,19 @@ +name: Main + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create zip + run: make release + - name: Release + uses: softprops/action-gh-release@v2 + with: + files: '*.zip' diff --git a/.gitignore b/.gitignore index ea5bb31..2e75ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +gabctk.com + *.py[cod] *~ diff --git a/Makefile b/Makefile index 4882232..95e7d4a 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,9 @@ +com: + cp python.com gabctk.com + zip -r gabctk.com .args ./*.py midiutil/*.py abc2xml/*.py + +release: com + zip gabctk.zip gabctk.com + run: - python gabctk + python gabctk.py diff --git a/README-en.md b/README-en.md index 834c2ac..7e27924 100644 --- a/README-en.md +++ b/README-en.md @@ -59,11 +59,19 @@ this parameter. If alerts are defined, gabctk will return a message each time it detects the it detects the string in the song text. -For example, `gabctk -i \ -a j -a eumdem` will return a message +For example, `gabctk.py -i \ -a j -a eumdem` will return a message if the text contains *j* or the word *eumdem*. It is also possible to convert several files at the same time. In this case, parameter to `-o`, `-l`, `-c`, `-x` or `-b` is a folder and not an individual file. For example, to convert to midi all gabc files in the current directory: - gabctk -i *.gabc -o . + gabctk.py -i *.gabc -o . + +Standalone executable +--------------------- + +Thanks to [cosmopolitan](https://github.com/jart/cosmopolitan/), a standalone +`gabctk.com` executable can be found in [Releases](https://github.com/jperon/gabctk/releases), +or generated from sources with `make com` command (providden `zip` command is available to shell). +Its use is identical to what’s described above, replacing `gabctk.py` by `gabctk.com`. diff --git a/README.md b/README.md index c08d63a..a0e1e3c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Le *gabc* est le langage utilisé par le logiciel [Gregorio](https://gregorio-project.github.io/). Vous en trouverez la description [ici](https://gregorio-project.github.io/gabc/). - Gabctk ------ @@ -62,7 +61,7 @@ ce paramètre. Si des alertes sont définies, gabctk renverra un message chaque fois qu'il détecte la chaîne de caractères dans le texte du chant. -Par exemple, `gabctk -i \ -a j -a eumdem` renverra un message +Par exemple, `gabctk.py -i \ -a j -a eumdem` renverra un message si le texte contient des *j* ou le mot *eumdem*. Il est encore possible de convertir plusieurs fichiers à la fois. En ce cas, @@ -70,4 +69,15 @@ il faut donner en paramètre à `-o`, `-l`, `-c`, `-x` ou `-b` un dossier et non un fichier individuel. Par exemple, pour convertir en midi tous les gabc du répertoire courant : - gabctk -i *.gabc -o . + gabctk.py -i *.gabc -o . + +Exécutable autonome +------------------- + +Il est possible de récupérer dans [Releases](https://github.com/jperon/gabctk/releases) +ou de créer soi-même un exécutable contenant tout ce qui est nécessaire pour utiliser +gabctk, aussi bien sous Linux que sous MacOS ou encore Windows (grâce à +[cosmopolitan](https://github.com/jart/cosmopolitan/)). Étant admis que le programme +`zip` est accessible à l’interpréteur de commandes, `make com` devrait générer +`gabctk.com`, utilisable comme décrit ci-dessus (en remplaçant `gabctk.py` +par `gabctk.com`). diff --git a/midiutil/License.txt b/midiutil/License.txt new file mode 100644 index 0000000..6714626 --- /dev/null +++ b/midiutil/License.txt @@ -0,0 +1,26 @@ +-------------------------------------------------------------------------- +MIDUTIL, Copyright (c) 2009-2016, Mark Conway Wirt + + +This software is distributed under an Open Source license, the +details of which follow. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------- + diff --git a/midiutil/MidiFile.py b/midiutil/MidiFile.py index ac13b2b..3c1e920 100644 --- a/midiutil/MidiFile.py +++ b/midiutil/MidiFile.py @@ -1,4 +1,4 @@ -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Name: MidiFile.py # Purpose: MIDI file manipulation utilities # @@ -8,238 +8,641 @@ # Copyright: (c) 2009-2016 Mark Conway Wirt # License: Please see License.txt for the terms under which this # software is distributed. -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- from __future__ import division, print_function -import struct, math, warnings +import math +import struct +import warnings -# TICKSPERBEAT is the number of "ticks" (time measurement in the MIDI file) that -# corresponds to one beat. This number is somewhat arbitrary, but should be chosen -# to provide adequate temporal resolution. +__version__ = 'HEAD' -TICKSPERBEAT = 960 +# TICKSPERQUARTERNOTE is the number of "ticks" (time measurement in the MIDI file) that +# corresponds to one quarter note. This number is somewhat arbitrary, but should +# be chosen to provide adequate temporal resolution. +TICKSPERQUARTERNOTE = 960 -controllerEventTypes = { - 'pan' : 0x0a -} +controllerEventTypes = {'pan': 0x0a} # Define some constants -MAJOR = 0 -MINOR = 1 +MAJOR = 0 +MINOR = 1 SHARPS = 1 -FLATS = -1 +FLATS = -1 __all__ = ['MIDIFile', 'MAJOR', 'MINOR', 'SHARPS', 'FLATS'] -class MIDIEvent(object): - ''' - The class to contain the MIDI Event (placed on MIDIEventList). - ''' - def __init__(self, type="unknown", time = 0, ordinal=0, insertion_order=0): - self.type=type - self.time=time - self.ord = ordinal - self.insertion_order=insertion_order class GenericEvent(object): ''' The event class from which specific events are derived ''' - def __init__(self, event_type, time, ordinal, insertion_order): - self.type = event_type - self.time = time - self.ord = ordinal - self.insertion_order = insertion_order - #self.type = 'Unknown' + evtname = None + sec_sort_order = 0 + def __init__(self, tick, insertion_order): + self.tick = tick + self.insertion_order = insertion_order def __eq__(self, other): ''' - Equality operator for Generic Events and derived classes. + Equality operator. - In the processing of the event list, we have need to remove duplicates. To do this - we rely on the fact that the classes are hashable, and must therefore have an - equality operator (__hash__() and __eq__() must both be defined). + In the processing of the event list, we have need to remove duplicates. + To do this we rely on the fact that the classes are hashable, and must + therefore have an equality operator (__hash__() and __eq__() must both + be defined). - This is the most embarrassing portion of the code, and anyone who knows about OO - programming would find this almost unbelievable. Here we have a base class that - knows specifics about derived classes, thus breaking the very spirit of - OO programming. - - I suppose I should go back and restructure the code, perhaps removing the derived - classes altogether. At some point perhaps I will. + Some derived classes will need to override and consider their specific + attributes in the comparison. ''' - if self.time != other.time or self.type != other.type: - return False - - # What follows is code that encodes the concept of equality for each derived - # class. Believe it f you dare. - - if self.type == 'note': - if self.pitch != other.pitch or self.channel != other.channel: - return False - if self.type == 'tempo': - if self.tempo != other.tempo: - return False - if self.type == 'programChange': - if self.programNumber != other.programNumber or self.channel != other.channel: - return False - if self.type == 'trackName': - if self.trackName != other.trackName: - return False - if self.type == 'controllerEvent': - return False - - if self.type == 'SysEx': - return False - - if self.type == 'UniversalSysEx': - return False - - return True + return (self.evtname == other.evtname and self.tick == other.tick) def __hash__(self): ''' Return a hash code for the object. - This is needed for the removal of duplicate objects from the event list. The only - real requirement for the algorithm is that the hash of equal objects must be equal. - There is probably great opportunity for improvements in the hashing function. + This is needed in order to allow GenericObject classes to be used + as the key in a dict or set. duplicate objects are removed from + the event list by storing all the objects in a set, and then + reconstructing the list from the set. The only real requirement + for the algorithm is that the hash of equal objects must be equal. + There is probably great opportunity for improvements in the hashing + function. ''' # Robert Jenkin's 32 bit hash. - a = int(self.time) - a = (a+0x7ed55d16) + (a<<12) - a = (a^0xc761c23c) ^ (a>>19) - a = (a+0x165667b1) + (a<<5) - a = (a+0xd3a2646c) ^ (a<<9) - a = (a+0xfd7046c5) + (a<<3) - a = (a^0xb55a4f09) ^ (a>>16) + a = int(self.tick) + a = (a + 0x7ed55d16) + (a << 12) + a = (a ^ 0xc761c23c) ^ (a >> 19) + a = (a + 0x165667b1) + (a << 5) + a = (a + 0xd3a2646c) ^ (a << 9) + a = (a + 0xfd7046c5) + (a << 3) + a = (a ^ 0xb55a4f09) ^ (a >> 16) return a -class Note(GenericEvent): + +class NoteOn(GenericEvent): ''' A class that encapsulates a note ''' - def __init__(self,channel, pitch,time,duration,volume,ordinal=3,annotation=None, insertion_order=0): + evtname = 'NoteOn' + midi_status = 0x90 # 0x9x is Note On + sec_sort_order = 3 + + def __init__(self, channel, pitch, tick, duration, volume, + annotation=None, insertion_order=0): self.pitch = pitch self.duration = duration self.volume = volume self.channel = channel self.annotation = annotation - super(Note, self).__init__('note', time, ordinal, insertion_order) + super(NoteOn, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return (self.evtname == other.evtname and self.tick == other.tick and + self.pitch == other.pitch and self.channel == other.channel) + + # In Python 3, a class which overrides __eq__ also needs to provide __hash__, + # because in Python 3 parent __hash__ is not inherited. + __hash__ = GenericEvent.__hash__ + + def __str__(self): + return 'NoteOn %d at tick %d duration %d ch %d vel %d' % ( + self.pitch, self.tick, self.duration, self.channel, self.volume) + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = self.midi_status | self.channel + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', self.pitch) + midibytes += struct.pack('>B', self.volume) + return midibytes + + +class NoteOff (GenericEvent): + ''' + A class that encapsulates a Note Off event + ''' + evtname = 'NoteOff' + midi_status = 0x80 # 0x8x is Note Off + sec_sort_order = 2 # must be less than that of NoteOn + # If two events happen at the same time, the secondary sort key is + # ``sec_sort_order``. Thus a class of events can be processed earlier than + # another. One place this is used in the code is to make sure that note + # off events are processed before note on events. + + def __init__(self, channel, pitch, tick, volume, + annotation=None, insertion_order=0): + self.pitch = pitch + self.volume = volume + self.channel = channel + self.annotation = annotation + super(NoteOff, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return (self.evtname == other.evtname and self.tick == other.tick and + self.pitch == other.pitch and self.channel == other.channel) + + __hash__ = GenericEvent.__hash__ + + def __str__(self): + return 'NoteOff %d at tick %d ch %d vel %d' % ( + self.pitch, self.tick, self.channel, self.volume) + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = self.midi_status | self.channel + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', self.pitch) + midibytes += struct.pack('>B', self.volume) + return midibytes + class Tempo(GenericEvent): ''' A class that encapsulates a tempo meta-event ''' - def __init__(self,time,tempo, ordinal=3, insertion_order=0): + evtname = 'Tempo' + sec_sort_order = 3 + + def __init__(self, tick, tempo, insertion_order=0): self.tempo = int(60000000 / tempo) - super(Tempo, self).__init__('tempo', time, ordinal, insertion_order) + super(Tempo, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return (self.evtname == other.evtname and + self.tick == other.tick and + self.tempo == other.tempo) + + __hash__ = GenericEvent.__hash__ + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + # Standard MIDI File Format says: + # + # FF 51 03 tttttt Set Tempo (in microseconds per MIDI quarter-note) + # This event indicates a tempo change. Another way of putting + # "microseconds per quarter-note" is "24ths of a microsecond per MIDI + # clock". Representing tempos as time per beat instead of beat per time + # allows absolutely exact long-term synchronisation with a time-based + # sync protocol such as SMPTE time code or MIDI time code. The amount + # of accuracy provided by this tempo resolution allows a four-minute + # piece at 120 beats per minute to be accurate within 500 usec at the + # end of the piece. Ideally, these events should only occur where MIDI + # clocks would be located -- this convention is intended to guarantee, + # or at least increase the likelihood, of compatibility with other + # synchronisation devices so that a time signature/tempo map stored in + # this format may easily be transferred to another device. + # + # Six identical lower-case letters such as tttttt refer to a 24-bit value, stored + # most-significant-byte first. The notation len refers to the + + midibytes = b"" + code = 0xFF + subcode = 0x51 + fourbite = struct.pack('>L', self.tempo) # big-endian uint32 + threebite = fourbite[1:4] # Just discard the MSB + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', subcode) + midibytes += struct.pack('>B', 0x03) # length in bytes of 24-bit tempo + midibytes += threebite + return midibytes + class Copyright(GenericEvent): ''' A class that encapsulates a copyright event ''' - def __init__(self,time,notice, ordinal=1, insertion_order=0): - self.notice = notice.encode("UTF8") - super(Copyright, self).__init__('Copyright', time, ordinal, insertion_order) + evtname = 'Copyright' + sec_sort_order = 1 + + def __init__(self, tick, notice, insertion_order=0): + self.notice = notice.encode("ISO-8859-1") + super(Copyright, self).__init__(tick, insertion_order) + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + # Standard MIDI File Format says: + # + # FF 02 len text Copyright Notice + # Contains a copyright notice as printable ASCII text. The notice should + # contain the characters (C), the year of the copyright, and the owner + # of the copyright. If several pieces of music are in the same MIDI + # File, all of the copyright notices should be placed together in this + # event so that it will be at the beginning of the file. This event + # should be the first event in the track chunk, at tick 0. + midibytes = b"" + code = 0xFF + subcode = 0x02 + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', subcode) + payloadLength = len(self.notice) + payloadLengthVar = writeVarLength(payloadLength) + for i in payloadLengthVar: + midibytes += struct.pack("b", i) + midibytes += self.notice + return midibytes + class Text(GenericEvent): ''' A class that encapsulates a text event ''' - def __init__(self, time, text, ordinal=1, insertion_order=0): - self.text = text.encode("UTF8") - super(Text, self).__init__('Text', time, ordinal, insertion_order) + evtname = 'Text' + sec_sort_order = 1 + + def __init__(self, tick, text, insertion_order=0): + self.text = text.encode("ISO-8859-1") + super(Text, self).__init__(tick, insertion_order) + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = 0xFF + subcode = 0x01 + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', subcode) + payloadLength = len(self.text) + payloadLengthVar = writeVarLength(payloadLength) + for i in payloadLengthVar: + midibytes += struct.pack("B", i) + midibytes += self.text + return midibytes + class KeySignature(GenericEvent): ''' A class that encapsulates a text event ''' - def __init__(self, time, accidentals, accidental_type, mode, ordinal=1, insertion_order=0): + evtname = 'KeySignature' + sec_sort_order = 1 + + def __init__(self, tick, accidentals, accidental_type, mode, + insertion_order=0): self.accidentals = accidentals self.accidental_type = accidental_type self.mode = mode - super(KeySignature, self).__init__('KeySignature', time, ordinal, insertion_order) + super(KeySignature, self).__init__(tick, insertion_order) + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = 0xFF + subcode = 0x59 + event_subtype = 0x02 + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', subcode) + midibytes += struct.pack('>B', event_subtype) + midibytes += struct.pack('>b', self.accidentals * self.accidental_type) + midibytes += struct.pack('>B', self.mode) + return midibytes + class ProgramChange(GenericEvent): ''' A class that encapsulates a program change event. ''' + evtname = 'ProgramChange' + midi_status = 0xc0 # 0xcx is Program Change + sec_sort_order = 1 - def __init__(self, channel, time, programNumber, ordinal=1, insertion_order=0): + def __init__(self, channel, tick, programNumber, + insertion_order=0): self.programNumber = programNumber self.channel = channel - super(ProgramChange, self).__init__('programChange', time, ordinal, insertion_order) + super(ProgramChange, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return (self.evtname == other.evtname and + self.tick == other.tick and + self.programNumber == other.programNumber and + self.channel == other.channel) + + __hash__ = GenericEvent.__hash__ + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = self.midi_status | self.channel + varTime = writeVarLength(self.tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', self.programNumber) + return midibytes + class SysExEvent(GenericEvent): ''' A class that encapsulates a System Exclusive event. ''' + evtname = 'SysEx' # doesn't match class name like most others + sec_sort_order = 1 - def __init__(self, time, manID, payload, ordinal=1, insertion_order=0): + def __init__(self, tick, manID, payload, insertion_order=0): self.manID = manID self.payload = payload - super(SysExEvent, self).__init__('SysEx', time, ordinal, insertion_order) + super(SysExEvent, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return False + + __hash__ = GenericEvent.__hash__ + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = 0xF0 + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + + payloadLength = writeVarLength(len(self.payload) + 2) + for lenByte in payloadLength: + midibytes += struct.pack('>B', lenByte) + + midibytes += struct.pack('>B', self.manID) + midibytes += self.payload + midibytes += struct.pack('>B', 0xF7) + return midibytes + class UniversalSysExEvent(GenericEvent): ''' A class that encapsulates a Universal System Exclusive event. ''' + evtname = 'UniversalSysEx' # doesn't match class name like most others + sec_sort_order = 1 - def __init__(self, time, realTime, sysExChannel, code, subcode, payload, - ordinal=1, insertion_order=0): + def __init__(self, tick, realTime, sysExChannel, code, subcode, + payload, insertion_order=0): self.realTime = realTime self.sysExChannel = sysExChannel self.code = code self.subcode = subcode self.payload = payload - super(UniversalSysExEvent, self).__init__('UniversalSysEx', time, ordinal, insertion_order) + super(UniversalSysExEvent, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return False + + __hash__ = GenericEvent.__hash__ + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = 0xF0 + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + + # Do we need to add a length? + payloadLength = writeVarLength(len(self.payload) + 5) + for lenByte in payloadLength: + midibytes += struct.pack('>B', lenByte) + + if self.realTime: + midibytes += struct.pack('>B', 0x7F) + else: + midibytes += struct.pack('>B', 0x7E) + + midibytes += struct.pack('>B', self.sysExChannel) + midibytes += struct.pack('>B', self.code) + midibytes += struct.pack('>B', self.subcode) + midibytes += self.payload + midibytes += struct.pack('>B', 0xF7) + return midibytes + class ControllerEvent(GenericEvent): ''' A class that encapsulates a program change event. ''' + evtname = 'ControllerEvent' + midi_status = 0xB0 # 0xBx is Control Change + sec_sort_order = 1 - def __init__(self, channel, time, controller_number, parameter, ordinal=1, insertion_order=0): + def __init__(self, channel, tick, controller_number, parameter, + insertion_order=0): self.parameter = parameter self.channel = channel self.controller_number = controller_number - super(ControllerEvent, self).__init__('controllerEvent', time, ordinal, insertion_order) + super(ControllerEvent, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return False + + __hash__ = GenericEvent.__hash__ + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = self.midi_status | self.channel + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', self.controller_number) + midibytes += struct.pack('>B', self.parameter) + return midibytes + + +class ChannelPressureEvent(GenericEvent): + ''' + A class that encapsulates a Channel Pressure (Aftertouch) event. + ''' + evtname = 'ChannelPressure' + midi_status = 0xD0 # 0xDx is Channel Pressure (Aftertouch) + sec_sort_order = 1 + + def __init__(self, channel, tick, pressure_value, insertion_order=0): + self.channel = channel + self.pressure_value = pressure_value + super(ChannelPressureEvent, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return (self.__class__.__name__ == other.__class__.__name__ and + self.tick == other.tick and + self.pressure_value == other.pressure_value and + self.channel == other.channel) + + __hash__ = GenericEvent.__hash__ + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = self.midi_status | self.channel + vartick = writeVarLength(self.tick - previous_event_tick) + for x in vartick: + midibytes += struct.pack('>B', x) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', self.pressure_value) + return midibytes + + +class PitchWheelEvent(GenericEvent): + ''' + A class that encapsulates a pitch wheel change event. + ''' + evtname = 'PitchWheelEvent' + midi_status = 0xE0 # 0xEx is Pitch Wheel Change + sec_sort_order = 1 + + def __init__(self, channel, tick, pitch_wheel_value, insertion_order=0): + self.channel = channel + self.pitch_wheel_value = pitch_wheel_value + super(PitchWheelEvent, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return False + + __hash__ = GenericEvent.__hash__ + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = self.midi_status | self.channel + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes = midibytes + struct.pack('>B', timeByte) + MSB = (self.pitch_wheel_value + 8192) >> 7 + LSB = (self.pitch_wheel_value + 8192) & 0x7F + midibytes = midibytes + struct.pack('>B', code) + midibytes = midibytes + struct.pack('>B', LSB) + midibytes = midibytes + struct.pack('>B', MSB) + return midibytes + class TrackName(GenericEvent): ''' A class that encapsulates a program change event. ''' + evtname = 'TrackName' + sec_sort_order = 0 + + def __init__(self, tick, trackName, insertion_order=0): + # GenericEvent.__init__(self, tick) + self.trackName = trackName.encode("ISO-8859-1") + super(TrackName, self).__init__(tick, insertion_order) + + def __eq__(self, other): + return (self.evtname == other.evtname and + self.tick == other.tick and + self.trackName == other.trackName) + + __hash__ = GenericEvent.__hash__ + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('B', 0xFF) + midibytes += struct.pack('B', 0X03) + dataLength = len(self.trackName) + dataLengthVar = writeVarLength(dataLength) + for i in dataLengthVar: + midibytes += struct.pack("B", i) + midibytes += self.trackName + return midibytes - def __init__(self, time, trackName, ordinal=0, insertion_order=0): - #GenericEvent.__init__(self, time,) - self.trackName = trackName.encode("UTF8") - super(TrackName, self).__init__('trackName', time, ordinal, insertion_order) class TimeSignature(GenericEvent): ''' A class that encapsulates a time signature. ''' + evtname = 'TimeSignature' + sec_sort_order = 0 - def __init__(self, time, numerator, denominator, clocks_per_tick, notes_per_quarter, ordinal=0, insertion_order=0): + def __init__(self, tick, numerator, denominator, clocks_per_tick, + notes_per_quarter, insertion_order=0): self.numerator = numerator self.denominator = denominator self.clocks_per_tick = clocks_per_tick self.notes_per_quarter = notes_per_quarter - super(TimeSignature, self).__init__('TimeSignature', time, ordinal, insertion_order) + super(TimeSignature, self).__init__(tick, insertion_order) + + def serialize(self, previous_event_tick): + """Return a bytestring representation of the event, in the format required for + writing into a standard midi file. + """ + midibytes = b"" + code = 0xFF + subcode = 0x58 + varTime = writeVarLength(self.tick - previous_event_tick) + for timeByte in varTime: + midibytes += struct.pack('>B', timeByte) + midibytes += struct.pack('>B', code) + midibytes += struct.pack('>B', subcode) + midibytes += struct.pack('>B', 0x04) + midibytes += struct.pack('>B', self.numerator) + midibytes += struct.pack('>B', self.denominator) + midibytes += struct.pack('>B', self.clocks_per_tick) + # 32nd notes per quarter note + midibytes += struct.pack('>B', self.notes_per_quarter) + return midibytes + class MIDITrack(object): ''' A class that encapsulates a MIDI track ''' - def __init__(self, removeDuplicates, deinterleave): + def __init__(self, removeDuplicates, deinterleave): '''Initialize the MIDITrack object. ''' - self.headerString = struct.pack('cccc',b'M',b'T',b'r',b'k') - self.dataLength = 0 # Is calculated after the data is in place + self.headerString = struct.pack('cccc', b'M', b'T', b'r', b'k') + self.dataLength = 0 # Is calculated after the data is in place self.MIDIdata = b"" self.closed = False self.eventList = [] @@ -247,189 +650,138 @@ def __init__(self, removeDuplicates, deinterleave): self.remdep = removeDuplicates self.deinterleave = deinterleave - def addNoteByNumber(self,channel, pitch,time,duration,volume,annotation=None, - insertion_order=0): + def addNoteByNumber(self, channel, pitch, tick, duration, volume, + annotation=None, insertion_order=0): ''' Add a note by chromatic MIDI number ''' - self.eventList.append(Note(channel, pitch,time,duration,volume,annotation=annotation, - insertion_order = insertion_order)) + self.eventList.append(NoteOn(channel, pitch, tick, duration, volume, + annotation=annotation, + insertion_order=insertion_order)) - def addControllerEvent(self,channel,time,controller_number, parameter, insertion_order=0): + # This event is not in chronological order. But before writing all the + # events to the file, I sort self.eventlist on (tick, sec_sort_order, insertion_order) + # which puts the events in chronological order. + self.eventList.append(NoteOff(channel, pitch, tick + duration, volume, + annotation=annotation, + insertion_order=insertion_order)) + + def addControllerEvent(self, channel, tick, controller_number, parameter, + insertion_order=0): ''' Add a controller event. ''' - self.eventList.append(ControllerEvent(channel,time,controller_number, \ - parameter, insertion_order=insertion_order)) + self.eventList.append(ControllerEvent(channel, tick, controller_number, + parameter, + insertion_order=insertion_order)) - def addTempo(self,time,tempo, insertion_order=0): + def addPitchWheelEvent(self, channel, tick, pitch_wheel_value, insertion_order=0): + ''' + Add a pitch wheel event. + ''' + self.eventList.append(PitchWheelEvent(channel, tick, pitch_wheel_value, insertion_order=insertion_order)) + + def addTempo(self, tick, tempo, insertion_order=0): ''' Add a tempo change (or set) event. ''' - self.eventList.append(Tempo(time,tempo, insertion_order = insertion_order)) + self.eventList.append(Tempo(tick, tempo, + insertion_order=insertion_order)) - def addSysEx(self,time,manID, payload, insertion_order=0): + def addSysEx(self, tick, manID, payload, insertion_order=0): ''' Add a SysEx event. ''' - self.eventList.append(SysExEvent(time, manID, payload, - insertion_order = insertion_order)) + self.eventList.append(SysExEvent(tick, manID, payload, + insertion_order=insertion_order)) - def addUniversalSysEx(self,time,code, subcode, payload, sysExChannel=0x7F, \ - realTime=False, insertion_order = 0): + def addUniversalSysEx(self, tick, code, subcode, payload, + sysExChannel=0x7F, realTime=False, + insertion_order=0): ''' Add a Universal SysEx event. ''' - self.eventList.append(UniversalSysExEvent(time, realTime, \ - sysExChannel, code, subcode, payload, insertion_order = insertion_order)) + self.eventList.append(UniversalSysExEvent(tick, realTime, sysExChannel, + code, subcode, payload, + insertion_order=insertion_order)) - def addProgramChange(self,channel, time, program, insertion_order=0): + def addProgramChange(self, channel, tick, program, insertion_order=0): ''' Add a program change event. ''' - self.eventList.append(ProgramChange(channel, time, program, insertion_order = insertion_order)) + self.eventList.append(ProgramChange(channel, tick, program, + insertion_order=insertion_order)) + + def addChannelPressure(self, channel, tick, pressure_value, insertion_order=0): + ''' + Add a channel pressure event. + ''' + self.eventList.append(ChannelPressureEvent(channel, tick, pressure_value, + insertion_order=insertion_order)) - def addTrackName(self,time,trackName, insertion_order = 0): + def addTrackName(self, tick, trackName, insertion_order=0): ''' Add a track name event. ''' - self.eventList.append(TrackName(time,trackName, insertion_order = insertion_order)) + self.eventList.append(TrackName(tick, trackName, + insertion_order=insertion_order)) - def addTimeSignature(self, time, numerator, denominator, clocks_per_tick, notes_per_quarter, insertion_order = 0): + def addTimeSignature(self, tick, numerator, denominator, clocks_per_tick, + notes_per_quarter, insertion_order=0): ''' Add a time signature. ''' - self.eventList.append(TimeSignature(time, numerator, denominator, - clocks_per_tick, notes_per_quarter, insertion_order = insertion_order)) + self.eventList.append(TimeSignature(tick, numerator, denominator, + clocks_per_tick, notes_per_quarter, + insertion_order=insertion_order)) - def addCopyright(self, time, notice, insertion_order = 0): + def addCopyright(self, tick, notice, insertion_order=0): ''' Add a copyright notice ''' - self.eventList.append(Copyright(time, notice, insertion_order = insertion_order)) + self.eventList.append(Copyright(tick, notice, + insertion_order=insertion_order)) - def addKeySignature(self, time, accidentals, accidental_type, mode, insertion_order = 0): + def addKeySignature(self, tick, accidentals, accidental_type, mode, + insertion_order=0): ''' Add a copyright notice ''' - self.eventList.append(KeySignature(time, accidentals, accidental_type, mode, - insertion_order = insertion_order)) + self.eventList.append(KeySignature(tick, accidentals, accidental_type, + mode, + insertion_order=insertion_order)) - def addText(self, time, text, insertion_order = 0): + def addText(self, tick, text, insertion_order=0): ''' Add a text event ''' - self.eventList.append(Text(time, text, insertion_order = insertion_order)) + self.eventList.append(Text(tick, text, + insertion_order=insertion_order)) - def changeNoteTuning(self, tunings, sysExChannel=0x7F, realTime=True, \ - tuningProgam=0, insertion_order=0): + def changeNoteTuning(self, tunings, sysExChannel=0x7F, realTime=True, + tuningProgam=0, insertion_order=0): ''' Change the tuning of MIDI notes ''' - payload = struct.pack('>B', tuningProgam) - payload = payload + struct.pack('>B', len(tunings)) - for (noteNumber, frequency) in tunings: - payload = payload + struct.pack('>B', noteNumber) + payload = struct.pack('>B', tuningProgam) + payload = payload + struct.pack('>B', len(tunings)) + for (noteNumber, frequency) in tunings: + payload = payload + struct.pack('>B', noteNumber) MIDIFreqency = frequencyTransform(frequency) for byte in MIDIFreqency: - payload = payload + struct.pack('>B', byte) + payload = payload + struct.pack('>B', byte) - self.eventList.append(UniversalSysExEvent(0, realTime, sysExChannel,\ - 8, 2, payload, insertion_order = insertion_order)) + self.eventList.append(UniversalSysExEvent(0, realTime, sysExChannel, + 8, 2, payload, insertion_order=insertion_order)) def processEventList(self): ''' - Process the event list, creating a MIDIEventList - - For each item in the event list, one or more events in the MIDIEvent - list are created. - ''' - - # Loop over all items in the eventList - - for thing in self.eventList: - if thing.type == 'note': - event = MIDIEvent("NoteOn", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.pitch = thing.pitch - event.volume = thing.volume - event.channel = thing.channel - self.MIDIEventList.append(event) - - event = MIDIEvent("NoteOff", (thing.time+ thing.duration) * TICKSPERBEAT, thing.ord -0.1, - thing.insertion_order) - event.pitch = thing.pitch - event.volume = thing.volume - event.channel = thing.channel - self.MIDIEventList.append(event) - - elif thing.type == 'tempo': - event = MIDIEvent("Tempo", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.tempo = thing.tempo - self.MIDIEventList.append(event) - - elif thing.type == 'Copyright': - event = MIDIEvent("Copyright", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.notice = thing.notice - self.MIDIEventList.append(event) - - elif thing.type == 'Text': - event = MIDIEvent("Text", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.text = thing.text - self.MIDIEventList.append(event) - - elif thing.type == 'KeySignature': - event = MIDIEvent("KeySignature", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.accidentals = thing.accidentals - event.accidental_type = thing.accidental_type - event.mode = thing.mode - self.MIDIEventList.append(event) - - elif thing.type == 'programChange': - event = MIDIEvent("ProgramChange", thing.time * TICKSPERBEAT, thing.ord, - thing.insertion_order) - event.programNumber = thing.programNumber - event.channel = thing.channel - self.MIDIEventList.append(event) - - elif thing.type == 'trackName': - event = MIDIEvent("TrackName", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.trackName = thing.trackName - self.MIDIEventList.append(event) - - elif thing.type == 'controllerEvent': - event = MIDIEvent("ControllerEvent", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.controller_number = thing.controller_number - event.channel = thing.channel - event.parameter = thing.parameter - self.MIDIEventList.append(event) - - elif thing.type == 'SysEx': - event = MIDIEvent("SysEx", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.manID = thing.manID - event.payload = thing.payload - self.MIDIEventList.append(event) - - elif thing.type == 'UniversalSysEx': - event = MIDIEvent("UniversalSysEx", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.realTime = thing.realTime - event.sysExChannel = thing.sysExChannel - event.code = thing.code - event.subcode = thing.subcode - event.payload = thing.payload - self.MIDIEventList.append(event) - - elif thing.type == 'TimeSignature': - event = MIDIEvent("TimeSignature", thing.time * TICKSPERBEAT, thing.ord, thing.insertion_order) - event.numerator = thing.numerator - event.denominator = thing.denominator - event.clocks_per_tick = thing.clocks_per_tick - event.notes_per_quarter = thing.notes_per_quarter - self.MIDIEventList.append(event) - - else: - raise ValueError("Error in MIDITrack: Unknown event type %s" % thing.type) + Process the event list, creating a MIDIEventList, + which is then sorted to be in chronological order by start tick. + ''' + self.MIDIEventList = [evt for evt in self.eventList] # Assumptions in the code expect the list to be time-sorted. self.MIDIEventList.sort(key=sort_events) @@ -440,22 +792,19 @@ def removeDuplicates(self): ''' Remove duplicates from the eventList. - This function will remove duplicates from the eventList. This is necessary - because we the MIDI event stream can become confused otherwise. + This function will remove duplicates from the eventList. This is + necessary because we the MIDI event stream can become confused + otherwise. ''' - # For this algorithm to work, the events in the eventList must be hashable - # (that is, they must have a __hash__() and __eq__() function defined). - - tempDict = {} - for item in self.eventList: - tempDict[item] = 1 - - self.eventList = list(tempDict.keys()) + # For this algorithm to work, the events in the eventList must be + # hashable (that is, they must have a __hash__() and __eq__() function + # defined). + s = set(self.eventList) + self.eventList = list(s) self.eventList.sort(key=sort_events) - def closeTrack(self): ''' Called to close a track before writing @@ -467,14 +816,13 @@ def closeTrack(self): Called by the parent MIDIFile object. ''' - if self.closed == True: + if self.closed: return self.closed = True if self.remdep: self.removeDuplicates() - self.processEventList() def writeMIDIStream(self): @@ -482,196 +830,30 @@ def writeMIDIStream(self): Write the meta data and note data to the packed MIDI stream. ''' - #Process the events in the eventList + # Process the events in the eventList self.writeEventsToStream() # Write MIDI close event. - self.MIDIdata = self.MIDIdata + struct.pack('BBBB',0x00,0xFF, \ - 0x2F,0x00) + self.MIDIdata += struct.pack('BBBB', 0x00, 0xFF, 0x2F, 0x00) # Calculate the entire length of the data and write to the header - self.dataLength = struct.pack('>L',len(self.MIDIdata)) + self.dataLength = struct.pack('>L', len(self.MIDIdata)) def writeEventsToStream(self): ''' Write the events in MIDIEvents to the MIDI stream. + MIDIEventList is presumed to be already sorted in chronological order. ''' - preciseTime = 0.0 # Actual time of event, ignoring round-off - actualTime = 0.0 # Time as written to midi stream, include round-off + previous_event_tick = 0 for event in self.MIDIEventList: - - preciseTime = preciseTime + event.time - - # Convert the time to variable length and back, to see how much - # error is introduced - - testBuffer = b"" - varTime = writeVarLength(event.time) - for timeByte in varTime: - testBuffer = testBuffer + struct.pack('>B',timeByte) - (roundedVal,discard) = readVarLength(0,testBuffer) - roundedTime = actualTime + roundedVal - - # Calculate the delta between the two and apply it to the event time. - - delta = preciseTime - roundedTime - event.time = event.time + delta - - # Now update the actualTime value, using the updated event time. - - testBuffer = b"" - varTime = writeVarLength(event.time) - for timeByte in varTime: - testBuffer = testBuffer + struct.pack('>B',timeByte) - - (roundedVal,discard) = readVarLength(0,testBuffer) - actualTime = actualTime + roundedVal - - for event in self.MIDIEventList: - if event.type == "NoteOn": - code = 0x9 << 4 | event.channel - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B',code) - self.MIDIdata = self.MIDIdata + struct.pack('>B',event.pitch) - self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume) - elif event.type == "NoteOff": - code = 0x8 << 4 | event.channel - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B',code) - self.MIDIdata = self.MIDIdata + struct.pack('>B',event.pitch) - self.MIDIdata = self.MIDIdata + struct.pack('>B',event.volume) - elif event.type == "Tempo": - code = 0xFF - subcode = 0x51 - fourbite = struct.pack('>L', event.tempo) - threebite = fourbite[1:4] # Just discard the MSB - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B',code) - self.MIDIdata = self.MIDIdata + struct.pack('>B',subcode) - self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x03) - self.MIDIdata = self.MIDIdata + threebite - elif event.type == "Text": - code = 0xFF - subcode = 0x01 - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B',code) - self.MIDIdata = self.MIDIdata + struct.pack('>B',subcode) - payloadLength = len(event.text) - payloadLengthVar = writeVarLength(payloadLength) - for i in range(len(payloadLengthVar)): - self.MIDIdata = self.MIDIdata + struct.pack("b",payloadLengthVar[i]) - self.MIDIdata = self.MIDIdata + event.text - elif event.type == "Copyright": - code = 0xFF - subcode = 0x02 - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B',code) - self.MIDIdata = self.MIDIdata + struct.pack('>B',subcode) - payloadLength = len(event.notice) - payloadLengthVar = writeVarLength(payloadLength) - for i in range(len(payloadLengthVar)): - self.MIDIdata = self.MIDIdata + struct.pack("b",payloadLengthVar[i]) - self.MIDIdata = self.MIDIdata + event.notice - elif event.type == "TimeSignature": - code = 0xFF - subcode = 0x58 - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B',code) - self.MIDIdata = self.MIDIdata + struct.pack('>B',subcode) - self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x04) - self.MIDIdata = self.MIDIdata + struct.pack('>B', event.numerator) - self.MIDIdata = self.MIDIdata + struct.pack('>B', event.denominator) - self.MIDIdata = self.MIDIdata + struct.pack('>B', event.clocks_per_tick) - self.MIDIdata = self.MIDIdata + struct.pack('>B', event.notes_per_quarter) # 32nd notes per quarter note - elif event.type == "KeySignature": - code = 0xFF - subcode = 0x59 - event_subtype = 0x02 - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B',code) - self.MIDIdata = self.MIDIdata + struct.pack('>B',subcode) - self.MIDIdata = self.MIDIdata + struct.pack('>B',event_subtype) - self.MIDIdata = self.MIDIdata + struct.pack('>b',event.accidentals * event.accidental_type) - self.MIDIdata = self.MIDIdata + struct.pack('>B',event.mode) - elif event.type == 'ProgramChange': - code = 0xC << 4 | event.channel - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B',code) - self.MIDIdata = self.MIDIdata + struct.pack('>B',event.programNumber) - elif event.type == 'TrackName': - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('B',0xFF) - self.MIDIdata = self.MIDIdata + struct.pack('B',0X03) - dataLength = len(event.trackName) - dataLenghtVar = writeVarLength(dataLength) - for i in range(0,len(dataLenghtVar)): - self.MIDIdata = self.MIDIdata + struct.pack("b",dataLenghtVar[i]) - self.MIDIdata = self.MIDIdata + event.trackName - elif event.type == "ControllerEvent": - code = 0xB << 4 | event.channel - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B',code) - self.MIDIdata = self.MIDIdata + struct.pack('>B',event.controller_number) - self.MIDIdata = self.MIDIdata + struct.pack('>B',event.parameter) - elif event.type == "SysEx": - code = 0xF0 - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B', code) - - payloadLength = writeVarLength(len(event.payload)+2) - for lenByte in payloadLength: - self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte) - - self.MIDIdata = self.MIDIdata + struct.pack('>B', event.manID) - self.MIDIdata = self.MIDIdata + event.payload - self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7) - elif event.type == "UniversalSysEx": - code = 0xF0 - varTime = writeVarLength(event.time) - for timeByte in varTime: - self.MIDIdata = self.MIDIdata + struct.pack('>B',timeByte) - self.MIDIdata = self.MIDIdata + struct.pack('>B', code) - - # Do we need to add a length? - payloadLength = writeVarLength(len(event.payload)+5) - for lenByte in payloadLength: - self.MIDIdata = self.MIDIdata + struct.pack('>B',lenByte) - - if event.realTime : - self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7F) - else: - self.MIDIdata = self.MIDIdata + struct.pack('>B', 0x7E) - - self.MIDIdata = self.MIDIdata + struct.pack('>B', event.sysExChannel) - self.MIDIdata = self.MIDIdata + struct.pack('>B', event.code) - self.MIDIdata = self.MIDIdata + struct.pack('>B', event.subcode) - self.MIDIdata = self.MIDIdata + event.payload - self.MIDIdata = self.MIDIdata + struct.pack('>B',0xF7) + self.MIDIdata += event.serialize(previous_event_tick) + # previous_event_tick = event.tick + # I do not like that adjustTimeAndOrigin() changes GenericEvent.tick + # from absolute to relative. I intend to change that, and just + # calculate the relative tick here, without changing GenericEvent.tick def deInterleaveNotes(self): ''' @@ -687,32 +869,37 @@ def deInterleaveNotes(self): stack = {} for event in self.MIDIEventList: - - if event.type == 'NoteOn': - if str(event.pitch)+str(event.channel) in stack: - stack[str(event.pitch)+str(event.channel)].append(event.time) - else: - stack[str(event.pitch)+str(event.channel)] = [event.time] - tempEventList.append(event) - elif event.type == 'NoteOff': - if len(stack[str(event.pitch)+str(event.channel)]) > 1: - event.time = stack[str(event.pitch)+str(event.channel)].pop() - tempEventList.append(event) - else: - stack[str(event.pitch)+str(event.channel)].pop() + if event.evtname in ['NoteOn', 'NoteOff']: + # !!! Pitch 101 channel 5 produces the same key as pitch 10 channel 15. + # !!! This is not the only pair of pitch,channel tuples which + # !!! collide to the same key, just one example. Should fix by + # !!! putting a separator char between pitch and channel. + noteeventkey = str(event.pitch) + str(event.channel) + if event.evtname == 'NoteOn': + if noteeventkey in stack: + stack[noteeventkey].append(event.tick) + else: + stack[noteeventkey] = [event.tick] tempEventList.append(event) + elif event.evtname == 'NoteOff': + if len(stack[noteeventkey]) > 1: + event.tick = stack[noteeventkey].pop() + tempEventList.append(event) + else: + stack[noteeventkey].pop() + tempEventList.append(event) else: tempEventList.append(event) self.MIDIEventList = tempEventList - # Note that ``processEventList`` makes the ordinality of a note off event - # a bit lower than the note on event, so this sort will make concomitant - # note off events processed first. + # Note NoteOff events have a lower secondary sort key than NoteOn + # events, so this sort will make concomitant NoteOff events + # processed first. self.MIDIEventList.sort(key=sort_events) - def adjustTimeAndOrigin(self,origin, adjust): + def adjustTimeAndOrigin(self, origin, adjust): ''' Adjust Times to be relative, and zero-origined. @@ -723,23 +910,18 @@ def adjustTimeAndOrigin(self,origin, adjust): if len(self.MIDIEventList) == 0: return tempEventList = [] - - if adjust: - internal_origin = origin - else: - internal_origin = 0.0 - - runningTime = 0 + internal_origin = origin if adjust else 0 + runningTick = 0 for event in self.MIDIEventList: - adjustedTime = event.time - internal_origin - event.time = adjustedTime - runningTime - runningTime = adjustedTime + adjustedTick = event.tick - internal_origin + event.tick = adjustedTick - runningTick + runningTick = adjustedTick tempEventList.append(event) self.MIDIEventList = tempEventList - def writeTrack(self,fileHandle): + def writeTrack(self, fileHandle): ''' Write track to disk. ''' @@ -759,106 +941,143 @@ class MIDIHeader(object): complete and well formed MIDI pattern. ''' - def __init__(self,numTracks, file_format): + def __init__(self, numTracks, file_format, ticks_per_quarternote): ''' Initialize the data structures - ''' - self.headerString = struct.pack('cccc',b'M',b'T',b'h',b'd') - self.headerSize = struct.pack('>L',6) + + :param numTracks: The number of tracks the file contains. Integer, + one or greater + :param file_format: The format of the multi-track file. This should + either be ``1`` (the default, and the most widely supported + format) or ``2``. + :param ticks_per_quarternote: The number of ticks per quarter + note is what the Standard MIDI File Format Specification calls + "division". Ticks are the integer unit of time in the SMF, and in + every MIDI sequencer I am aware of. Common values are 120, 240, + 384, 480, 960. Note that all these numbers are evenly divisible by + 2,3,4,6,8,12,16, and 24, except 120 does not have 16 as a divisor. + ''' + self.headerString = struct.pack('cccc', b'M', b'T', b'h', b'd') + self.headerSize = struct.pack('>L', 6) # Format 1 = multi-track file - self.format = struct.pack('>H',file_format) + self.formatnum = struct.pack('>H', file_format) self.numeric_format = file_format - if file_format == 1: - delta = 1 - else: - delta = 0 - self.numTracks = struct.pack('>H',numTracks + delta) - self.ticksPerBeat = struct.pack('>H',TICKSPERBEAT) - + self.numTracks = struct.pack('>H', numTracks) + self.ticks_per_quarternote = struct.pack('>H', ticks_per_quarternote) - def writeFile(self,fileHandle): + def writeFile(self, fileHandle): fileHandle.write(self.headerString) fileHandle.write(self.headerSize) - fileHandle.write(self.format) + fileHandle.write(self.formatnum) fileHandle.write(self.numTracks) - fileHandle.write(self.ticksPerBeat) + fileHandle.write(self.ticks_per_quarternote) + class MIDIFile(object): ''' A class that encapsulates a full, well-formed MIDI file object. This is a container object that contains a header (:class:`MIDIHeader`), - one or more tracks (class:`MIDITrack`), and the data associated with a proper - and well-formed MIDI file. + one or more tracks (class:`MIDITrack`), and the data associated with a + proper and well-formed MIDI file. ''' - def __init__(self, numTracks=1, removeDuplicates=True, deinterleave=True, adjust_origin=None, file_format=1): - ''' - - Initialize the MIDIFile class + def __init__(self, numTracks=1, removeDuplicates=True, deinterleave=True, + adjust_origin=False, file_format=1, + ticks_per_quarternote=TICKSPERQUARTERNOTE, eventtime_is_ticks=False): + '''Initialize the MIDIFile class + + :param numTracks: The number of tracks the file contains. Integer, + one or greater + :param removeDuplicates: If set to ``True`` remove duplicate events + before writing to disk + :param deinterleave: If set to ``True`` deinterleave the notes in + the stream + :param adjust_origin: If set to ``True`` shift all the events in the tracks + so that the first event takes place at time t=0. Default is ``False`` + :param file_format: The format of the multi-track file. This should + either be ``1`` (the default, and the most widely supported + format) or ``2``. + :param ticks_per_quarternote: The number of ticks per quarter note is + what the Standard MIDI File Format Specification calls "division". + Ticks are the integer unit of time in the SMF, and in most if + not all MIDI sequencers. Common values are 120, 240, 384, + 480, 960. Note that all these numbers are evenly divisible by + 2,3,4,6,8,12,16, and 24, except 120 does not have 16 as a divisor. + + :param eventtime_is_ticks: If set True means event time and duration + argument values are integer ticks instead of fractional quarter + notes. + + Note that the default for ``adjust_origin`` will change in a future + release, so one should probably explicitly set it. + + In a format 1 file, it would be a rare cirumstance where adjusting the + origin of each track to the track's first note makes any sense. + + Example: - :param numTracks: The number of tracks the file contains. Integer, one or greater - :param removeDuplicates: If set to ``True`` remove duplicate events before writing - to disk - :param deinterleave: If set to ``True`` deinterleave the notes in the stream - :param adjust_origin: If set to ``True`` (or left at the default of ``None``) shift all the - events in the tracks so that the first event takes place at time t=0 - :param file_format: The format of the multi-track file. This should either be ``1`` (the default, - and the most widely supported format) or ``2``. - - Note that the default for ``adjust_origin`` will change in a future release, so one should probably - explicitly set it. - - Example: - - .. code:: + .. code:: - # Create a two-track MIDIFile + # Create a two-track MIDIFile - from midiutil.MidiFile import MIDIFile - midi_file = MIDIFile(2) + from midiutil.MidiFile import MIDIFile + midi_file = MIDIFile(1, adjust_origin=False) - A Note on File Formats - ---------------------- + A Note on File Formats + ---------------------- - In previous versions of this code the file written was format 2 (which can be thought of as - a collection of independent tracks) but was identified as format 1. In this version - one can specify either format 1 or 2. + In previous versions of this code the file written was format 2 + (which can be thought of as a collection of independent tracks) but + was identified as format 1. In this version one can specify either + format 1 or 2. - In format 1 files there is a separate tempo track which contains tempo and time signature data, - but contains no note data. If one creates a single track format 1 file the actual file has - two tracks -- one for tempo data and one for note data. In the track indexing the tempo track - can be ignored. In other words track 0 is the note track (the second track in the file). However, - tempo and time signature data will be written to the first, tempo track. This is done to - try and preserve as much interoperability with previous versions as possible. + In format 1 files there is a separate tempo track which contains + tempo and time signature data, but contains no note data. If one + creates a single track format 1 file the actual file has two tracks + -- one for tempo data and one for note data. In the track indexing + the tempo track can be ignored. In other words track 0 is the note + track (the second track in the file). However, tempo and time + signature data will be written to the first, tempo track. This is + done to try and preserve as much interoperability with previous + versions as possible. - In a format 2 file all tracks are indexed and the track parameter is interpreted literally. + In a format 2 file all tracks are indexed and the track parameter + is interpreted literally. ''' - self.header = MIDIHeader(numTracks, file_format) self.tracks = list() if file_format == 1: - delta = 1 + self.numTracks = numTracks + 1 # self.tracks[0] is the baked-in tempo track else: - delta = 0 - self.numTracks = numTracks + delta + self.numTracks = numTracks + self.header = MIDIHeader(self.numTracks, file_format, ticks_per_quarternote) + + self.adjust_origin = adjust_origin self.closed = False - if adjust_origin is None: - self.adjust_origin = True - warnings.warn("Please explicitly set adjust_origin. Default behaviour will change in a future version.", - FutureWarning) + + self.ticks_per_quarternote = ticks_per_quarternote + self.eventtime_is_ticks = eventtime_is_ticks + if self.eventtime_is_ticks: + self.time_to_ticks = lambda x: x else: - self.adjust_origin = adjust_origin + self.time_to_ticks = self.quarter_to_tick - for i in range(0,self.numTracks): - self.tracks.append(MIDITrack(removeDuplicates, deinterleave)) + for i in range(0, self.numTracks): + self.tracks.append(MIDITrack(removeDuplicates, deinterleave)) + # to keep track of the order of insertion for new sorting + self.event_counter = 0 - self.event_counter = 0 # to keep track of the order of insertion for new sorting + # Public Functions. These (for the most part) wrap the MIDITrack functions, + # where most Processing takes place. + def quarter_to_tick(self, quarternote_time): + return int(quarternote_time * self.ticks_per_quarternote) - # Public Functions. These (for the most part) wrap the MIDITrack functions, where most - # Processing takes place. + def tick_to_quarter(self, ticknum): + return float(ticknum) / self.ticks_per_quarternote - def addNote(self,track, channel, pitch, time, duration, volume, annotation=None): + def addNote(self, track, channel, pitch, time, duration, volume, + annotation=None): """ Add notes to the MIDIFile object @@ -866,8 +1085,12 @@ def addNote(self,track, channel, pitch, time, duration, volume, annotation=None) :param track: The track to which the note is added. :param channel: the MIDI channel to assign to the note. [Integer, 0-15] :param pitch: the MIDI pitch number [Integer, 0-127]. - :param time: the time (in beats) at which the note sounds [Float]. - :param duration: the duration of the note (in beats) [Float]. + :param time: the time at which the note sounds. The value can be either + quarter notes [Float], or ticks [Integer]. Ticks may be specified by + passing eventtime_is_ticks=True to the MIDIFile constructor. + The default is quarter notes. + :param duration: the duration of the note. Like the time argument, the + value can be either quarter notes [Float], or ticks [Integer]. :param volume: the volume (velocity) of the note. [Integer, 0-127]. :param annotation: Arbitrary data to attach to the note. @@ -878,34 +1101,40 @@ def addNote(self,track, channel, pitch, time, duration, volume, annotation=None) class ``EventList``. """ if self.header.numeric_format == 1: - track = track + 1 - self.tracks[track].addNoteByNumber(channel, pitch, time, duration, volume, - annotation = annotation, insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 - - def addTrackName(self,track, time,trackName): + track += 1 + self.tracks[track].addNoteByNumber(channel, pitch, + self.time_to_ticks(time), self.time_to_ticks(duration), + volume, annotation=annotation, + insertion_order=self.event_counter) + self.event_counter += 1 + + def addTrackName(self, track, time, trackName): """ Name a track. :param track: The track to which the name is assigned. - :param time: The time (in beats) at which the track name event is placed. - In general this should probably be time 0 (the beginning of the track). + :param time: The time (in beats) at which the track name event is + placed. In general this should probably be time 0 (the beginning + of the track). :param trackName: The name to assign to the track [String] """ if self.header.numeric_format == 1: - track = track + 1 - self.tracks[track].addTrackName(time,trackName, insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track += 1 + self.tracks[track].addTrackName(self.time_to_ticks(time), trackName, + insertion_order=self.event_counter) + self.event_counter += 1 - def addTimeSignature(self, track, time, numerator, denominator, clocks_per_tick, notes_per_quarter = 8): + def addTimeSignature(self, track, time, numerator, denominator, + clocks_per_tick, notes_per_quarter=8): ''' Add a time signature event. - :param track: The track to which the signature is assigned. Note that in - a format 1 file this parameter is ignored and the event is written to the tempo - track + :param track: The track to which the signature is assigned. Note that + in a format 1 file this parameter is ignored and the event is + written to the tempo track :param time: The time (in beats) at which the event is placed. - In general this should probably be time 0 (the beginning of the track). + In general this should probably be time 0 (the beginning of the + track). :param numerator: The numerator of the time signature. [Int] :param denominator: The denominator of the time signature, expressed as a power of two (see below). [Int] @@ -913,9 +1142,9 @@ def addTimeSignature(self, track, time, numerator, denominator, clocks_per_tick, click (see below). :param notes_per_quarter: The number of annotated 32nd notes in a MIDI quarter note. This is almost always 8 (the default), but some - sequencers allow this value to be changed. Unless you know that your - sequencing software supports it, this should be left at its default - value. + sequencers allow this value to be changed. Unless you know that + your sequencing software supports it, this should be left at its + default value. The data format for this event is a little obscure. @@ -930,45 +1159,65 @@ def addTimeSignature(self, track, time, numerator, denominator, clocks_per_tick, ticks per metronome click. By definition there are 24 ticks in a quarter note, so a metronome click per quarter note would be 24. A click every third eighth note would be 3 * 12 = 36. + + The ``notes_per_quarter`` value is also a little confusing. It + specifies the number of 32nd notes in a MIDI quarter note. Usually + there are 8 32nd notes in a quarter note (8/32 = 1/4), so + the default value is 8. However, one can change this value if + needed. Setting it to 16, for example, would cause the music to + play at double speed, as there would be 16/32 (or what could be + considered *two* quarter notes for every one MIDI quarter note. + + Note that both the ``clocks_per_tick`` and the + ``notes_per_quarter`` are specified in terms of quarter notes, + even is the score is not a quarter-note based score (i.e., + even if the denominator is not ``4``). So if you're working with a + time signature of, say, 6/8, one still needs to specify the clocks + per quarter note. ''' if self.header.numeric_format == 1: track = 0 - self.tracks[track].addTimeSignature(time, numerator, denominator, - clocks_per_tick, notes_per_quarter, insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + self.tracks[track].addTimeSignature(self.time_to_ticks(time), numerator, denominator, + clocks_per_tick, notes_per_quarter, + insertion_order=self.event_counter) + self.event_counter += 1 - def addTempo(self,track, time,tempo): + def addTempo(self, track, time, tempo): """ Add notes to the MIDIFile object - :param track: The track to which the tempo event is added. Note that in a format - 1 file this parameter is ignored and the tempo is written to the tempo track + :param track: The track to which the tempo event is added. Note that + in a format 1 file this parameter is ignored and the tempo is + written to the tempo track :param time: The time (in beats) at which tempo event is placed :param tempo: The tempo, in Beats per Minute. [Integer] """ if self.header.numeric_format == 1: track = 0 - self.tracks[track].addTempo(time,tempo, insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + self.tracks[track].addTempo(self.time_to_ticks(time), tempo, + insertion_order=self.event_counter) + self.event_counter += 1 - def addCopyright(self,track, time, notice): + def addCopyright(self, track, time, notice): """ Add a copyright notice to the MIDIFile object :param track: The track to which the notice is added. - :param time: The time (in beats) at which notice event is placed. In general - this sould be time t=0 + :param time: The time (in beats) at which notice event is placed. In + general this sould be time t=0 :param notice: The copyright notice [String] """ if self.header.numeric_format == 1: - track = track + 1 - self.tracks[track].addCopyright(time,notice, insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track += 1 + self.tracks[track].addCopyright(self.time_to_ticks(time), notice, + insertion_order=self.event_counter) + self.event_counter += 1 - def addKeySignature(self, track, time, accidentals, accidental_type, mode, insertion_order = 0): + def addKeySignature(self, track, time, accidentals, accidental_type, mode, + insertion_order=0): ''' Add a Key Signature to a track @@ -1000,12 +1249,12 @@ def addKeySignature(self, track, time, accidentals, accidental_type, mode, inser MyMIDI.addKeySignature(0, 0, 3, SHARPS, MINOR) ''' if self.header.numeric_format == 1: - track = track + 1 - self.tracks[track].addKeySignature(time, accidentals, accidental_type, mode, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track = 0 # User reported that this is needed. + self.tracks[track].addKeySignature(self.time_to_ticks(time), accidentals, accidental_type, + mode, insertion_order=self.event_counter) + self.event_counter += 1 - def addText(self,track, time, text): + def addText(self, track, time, text): """ Add a text event @@ -1015,43 +1264,85 @@ def addText(self,track, time, text): :param text: The text to adde [ASCII String] """ if self.header.numeric_format == 1: - track = track + 1 - self.tracks[track].addText(time, text, insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track += 1 + self.tracks[track].addText(self.time_to_ticks(time), text, + insertion_order=self.event_counter) + self.event_counter += 1 - def addProgramChange(self,track, channel, time, program): + def addProgramChange(self, tracknum, channel, time, program): """ Add a MIDI program change event. - :param track: The track to which program change event is added. - :param channel: the MIDI channel to assign to the event. [Integer, 0-15] - :param time: The time (in beats) at which the program change event is placed [Float]. + :param tracknum: The zero-based track number to which program change event is added. + :param channel: the MIDI channel to assign to the event. + [Integer, 0-15] + :param time: The time (in beats) at which the program change event is + placed [Float]. :param program: the program number. [Integer, 0-127]. """ - self.tracks[track].addProgramChange(channel, time, program, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + if self.header.numeric_format == 1: + tracknum += 1 + self.tracks[tracknum].addProgramChange(channel, self.time_to_ticks(time), program, + insertion_order=self.event_counter) + self.event_counter += 1 - def addControllerEvent(self,track, channel, time, controller_number, parameter): + def addChannelPressure(self, tracknum, channel, time, pressure_value): + """ + Add a Channel Pressure event. + + :param tracknum: The zero-based track number to which channel pressure event is added. + :param channel: the MIDI channel to assign to the event. + [Integer, 0-15] + :param time: The time (in beats) at which the channel pressure event is + placed [Float]. + :param pressure_value: the pressure value. [Integer, 0-127]. + """ + if self.header.numeric_format == 1: + tracknum += 1 + track = self.tracks[tracknum] + track.addChannelPressure(channel, self.time_to_ticks(time), pressure_value, + insertion_order=self.event_counter) + self.event_counter += 1 + + def addControllerEvent(self, track, channel, time, controller_number, + parameter): """ Add a channel control event :param track: The track to which the event is added. - :param channel: the MIDI channel to assign to the event. [Integer, 0-15] + :param channel: the MIDI channel to assign to the event. + [Integer, 0-15] :param time: The time (in beats) at which the event is placed [Float]. :param controller_number: The controller ID of the event. - :param parameter: The event's parameter, the meaning of which varies by event type. + :param parameter: The event's parameter, the meaning of which varies by + event type. + """ + if self.header.numeric_format == 1: + track += 1 + self.tracks[track].addControllerEvent(channel, self.time_to_ticks(time), controller_number, + parameter, insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 + + def addPitchWheelEvent(self, track, channel, time, pitchWheelValue): + """ + + Add a channel pitch wheel event + + :param track: The track to which the event is added. + :param channel: the MIDI channel to assign to the event. [Integer, 0-15] + :param time: The time (in beats) at which the event is placed [Float]. + :param pitchWheelValue: 0 for no pitch change. [Integer, -8192-8192] """ if self.header.numeric_format == 1: - track = track + 1 - self.tracks[track].addControllerEvent(channel,time,controller_number, parameter, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track += 1 + self.tracks[track].addPitchWheelEvent(channel, self.time_to_ticks(time), pitchWheelValue, + insertion_order=self.event_counter) + self.event_counter += 1 - def makeRPNCall(self, track, channel, time, controller_msb, controller_lsb, data_msb, - data_lsb, time_order=False): + def makeRPNCall(self, track, channel, time, controller_msb, controller_lsb, + data_msb, data_lsb, time_order=False): ''' Perform a Registered Parameter Number Call @@ -1059,13 +1350,14 @@ def makeRPNCall(self, track, channel, time, controller_msb, controller_lsb, data :param track: The track to which this applies :param channel: The channel to which this applies :param time: The time of the event - :param controller_msb: The Most significant byte of the controller. In common usage - this will usually be 0 - :param controller_lsb: The Least significant Byte for the controller message. For example, for - a fine-tuning change this would be 01. - :param data_msb: The Most Significant Byte of the controller's parameter. - :param data_lsb: The Least Significant Byte of the controller's parameter. If non needed this - should be set to ``None`` + :param controller_msb: The Most significant byte of the controller. In + common usage this will usually be 0 + :param controller_lsb: The Least significant Byte for the controller + message. For example, for a fine-tuning change this would be 01. + :param data_msb: The Most Significant Byte of the controller's + parameter. + :param data_lsb: The Least Significant Byte of the controller's + parameter. If not needed this should be set to ``None`` :param time_order: Order the control events in time (see below) As an example, if one were to change a channel's tuning program:: @@ -1075,35 +1367,60 @@ def makeRPNCall(self, track, channel, time, controller_msb, controller_lsb, data (Note, however, that there is a convenience function, ``changeTuningProgram``, that does this for you.) - The ``time_order`` parameter is something of a work-around for sequencers that - do not preserve the order of events from the MIDI files they import. Within this code - care is taken to preserve the order of events as specified, but some sequencers - seem to transmit events occurring at the same time in an arbitrary order. - By setting this parameter to ``True`` something of a work-around is performed: each - successive event (of which there are three or four for this event type) is placed - in the time stream a small delta from the preceding one. Thus, for example, the - controllers are set before the data bytes in this call. - ''' - if self.header.numeric_format == 1: - track = track + 1 + Registered/Non-Registered Parameter Number (RPN / NRPN) + ------------------------------------------------------- + Controller number 6 (Data Entry), in conjunction with Controller numbers 96 + (Data Increment), 97 (Data Decrement), 98 (Registered Parameter Number LSB), + 99 (Registered Parameter Number MSB), 100 (Non-Registered Parameter Number + LSB), and 101 (Non-Registered Parameter Number MSB), extend the number of + controllers available via MIDI. Parameter data is transferred by first + selecting the parameter number to be edited using controllers 98 and 99 or + 100 and 101, and then adjusting the data value for that parameter using + controller number 6, 96, or 97. + + RPN and NRPN are typically used to send parameter data to a synthesizer in + order to edit sound patches or other data. Registered parameters are those + which have been assigned some particular function by the MIDI Manufacturers + Association (MMA) and the Japan MIDI Standards Committee (JMSC). For + example, there are Registered Parameter numbers assigned to control pitch + bend sensitivity and master tuning for a synthesizer. Non-Registered + parameters have not been assigned specific functions, and may be used for + different functions by different manufacturers. + + The ``time_order`` parameter is something of a work-around for + sequencers that do not preserve the order of events from the MIDI files + they import. Within this code care is taken to preserve the order of + events as specified, but some sequencers seem to transmit events + occurring at the same time in an arbitrary order. By setting this + parameter to ``True`` something of a work-around is performed: each + successive event (of which there are three or four for this event type) + is placed in the time stream a small delta from the preceding one. + Thus, for example, the controllers are set before the data bytes in + this call. + ''' + tick = self.time_to_ticks(time) - if time_order: - delta = 1.0 / (TICKSPERBEAT - 10) - else: - delta = 0.0 - self.tracks[track].addControllerEvent(channel,time, 101, controller_msb, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 - self.tracks[track].addControllerEvent(channel,time+delta, 100, controller_lsb, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 - self.tracks[track].addControllerEvent(channel,time+(2.0*delta), 6, data_msb, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + if self.header.numeric_format == 1: + track += 1 + track = self.tracks[track] + + tick_incr = 1 if time_order else 0 + track.addControllerEvent(channel, tick, 101, # parameter number MSB + controller_msb, insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 + tick += tick_incr + track.addControllerEvent(channel, tick, 100, + controller_lsb, insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 + tick += tick_incr + track.addControllerEvent(channel, tick, 6, + data_msb, insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 + tick += tick_incr if data_lsb is not None: - self.tracks[track].addControllerEvent(channel,time+(3.0*delta), 38, data_lsb, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track.addControllerEvent(channel, tick, 38, + data_lsb, insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 def makeNRPNCall(self, track, channel, time, controller_msb, controller_lsb, data_msb, data_lsb, time_order=False): @@ -1114,46 +1431,53 @@ def makeNRPNCall(self, track, channel, time, controller_msb, :param track: The track to which this applies :param channel: The channel to which this applies :param time: The time of the event - :param controller_msb: The Most significant byte of thecontroller. In common usage - this will usually be 0 - :param controller_lsb: The least significant byte for the controller message. For example, for - a fine-tunning change this would be 01. - :param data_msb: The most significant byte of the controller's parameter. - :param data_lsb: The least significant byte of the controller's parameter. If none is needed this - should be set to ``None`` + :param controller_msb: The Most significant byte of thecontroller. In + common usage this will usually be 0 + :param controller_lsb: The least significant byte for the controller + message. For example, for a fine-tunning change this would be 01. + :param data_msb: The most significant byte of the controller's + parameter. + :param data_lsb: The least significant byte of the controller's + parameter. If none is needed this should be set to ``None`` :param time_order: Order the control events in time (see below) - The ``time_order`` parameter is something of a work-around for sequencers that - do not preserve the order of events from the MIDI files they import. Within this code - care is taken to preserve the order of events as specified, but some sequencers - seem to transmit events occurring at the same time in an arbitrary order. - By setting this parameter to ``True`` something of a work-around is performed: each - successive event (of which there are three or four for this event type) is placed - in the time stream a small delta from the preceding one. Thus, for example, the - controllers are set before the data bytes in this call. + The ``time_order`` parameter is something of a work-around for + sequencers that do not preserve the order of events from the MIDI files + they import. Within this code care is taken to preserve the order of + events as specified, but some sequencers seem to transmit events + occurring at the same time in an arbitrary order. By setting this + parameter to ``True`` something of a work-around is performed: each + successive event (of which there are three or four for this event type) + is placed in the time stream a small delta from the preceding one. + Thus, for example, the controllers are set before the data bytes in + this call. ''' + tick = self.time_to_ticks(time) + if self.header.numeric_format == 1: - track = track + 1 - if time_order: - delta = 1.0 / (TICKSPERBEAT - 10) - else: - delta = 0.0 - self.tracks[track].addControllerEvent(channel,time, 99, controller_msb, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 - self.tracks[track].addControllerEvent(channel,time+delta, 98, controller_lsb, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 - self.tracks[track].addControllerEvent(channel,time+(2*delta), 6, data_msb, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track += 1 + track = self.tracks[track] + + tick_incr = 1 if time_order else 0 + track.addControllerEvent(channel, tick, 99, + controller_msb, insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 + tick += tick_incr + track.addControllerEvent(channel, tick, 98, + controller_lsb, insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 + tick += tick_incr + track.addControllerEvent(channel, tick, 6, + data_msb, insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 + tick += tick_incr if data_lsb is not None: - self.tracks[track].addControllerEvent(channel,time+(3*delta), 38, data_lsb, - insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track.addControllerEvent(channel, tick, 38, + data_lsb, insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 - def changeTuningBank(self,track, channel, time, bank, time_order=False): + def changeTuningBank(self, track, channel, time, bank, time_order=False): ''' Change the tuning bank for a selected track @@ -1162,8 +1486,9 @@ def changeTuningBank(self,track, channel, time, bank, time_order=False): :param channel: The channel for the event :param time: The time of the event :param bank: The tuning bank (0-127) - :param time_order: Preserve the ordering of the component events by ordering - in time. See ``makeRPNCall()`` for a discussion of when this may be necessary + :param time_order: Preserve the ordering of the component events by + ordering in time. See ``makeRPNCall()`` for a discussion of when + this may be necessary Note that this is a convenience function, as the same functionality is available from directly sequencing controller @@ -1171,9 +1496,11 @@ def changeTuningBank(self,track, channel, time, bank, time_order=False): The specified tuning should already have been written to the stream with ``changeNoteTuning``. ''' - self.makeRPNCall(track, channel, time, 0, 4, 0, bank, time_order=time_order) + self.makeRPNCall(track, channel, time, 0, 4, 0, bank, + time_order=time_order) - def changeTuningProgram(self,track, channel, time, program, time_order=False): + def changeTuningProgram(self, track, channel, time, program, + time_order=False): ''' Change the tuning program for a selected track @@ -1182,42 +1509,47 @@ def changeTuningProgram(self,track, channel, time, program, time_order=False): :param channel: The channel for the event :param time: The time of the event :param program: The tuning program number (0-127) - :param time_order: Preserve the ordering of the component events by ordering - in time. See ``makeRPNCall()`` for a discussion of when this may be necessary + :param time_order: Preserve the ordering of the component events by + ordering in time. See ``makeRPNCall()`` for a discussion of when + this may be necessary Note that this is a convenience function, as the same functionality is available from directly sequencing controller events. The specified tuning should already have been written to the - stream with ``changeNoteTuning``. ''' - self.makeRPNCall(track, channel, time, 0, 3, 0, program, time_order=time_order) + stream with ``changeNoteTuning``. + ''' + self.makeRPNCall(track, channel, time, 0, 3, 0, program, + time_order=time_order) - def changeNoteTuning(self, track, tunings, sysExChannel=0x7F, \ - realTime=True, tuningProgam=0): + def changeNoteTuning(self, track, tunings, sysExChannel=0x7F, + realTime=True, tuningProgam=0): """ Add a real-time MIDI tuning standard update to a track. :param track: The track to which the tuning is applied. - :param tunings: A list to tuples representing the tuning. See below for an - explanation. - :param sysExChannel: The SysEx channel of the event. This is mapped to "manufacturer ID" - in the event which is written. Unless there is a specific reason for changing it, it - should be left at its default value. - :param realTime: Speicifes if the Universal SysEx event should be flagged as real-time or - non-real-time. As with the ``sysExChannel`` argument, this should in general - be left at it's default value. + :param tunings: A list to tuples representing the tuning. See below for + an explanation. + :param sysExChannel: The SysEx channel of the event. This is mapped to + "manufacturer ID" in the event which is written. Unless there is a + specific reason for changing it, it should be left at its default + value. + :param realTime: Speicifes if the Universal SysEx event should be + flagged as real-time or non-real-time. As with the ``sysExChannel`` + argument, this should in general be left at it's default value. :param tuningProgram: The tuning program number. This function specifically implements the "real time single note tuning - change" (although the name is misleading, as multiple notes can be included in - each event). It should be noted that not all hardware or software implements the - MIDI tuning standard, and that which does often does not implement it in its - entirety. + change" (although the name is misleading, as multiple notes can be + included in each event). It should be noted that not all hardware or + software implements the MIDI tuning standard, and that which does often + does not implement it in its entirety. - The ``tunings`` argument is a list of tuples, in (*note number*, *frequency*) format. - As an example, if one wanted to change the frequency on MIDI note 69 to 500 (it is normally - 440 Hz), one could do it thus: + The ``tunings`` argument is a list of tuples, in (*note number*, + *frequency*) format. As an example, if one wanted to change the + frequency on MIDI note 69 to 500 (it is normally 440 Hz), one could do + it thus: .. code:: python @@ -1227,14 +1559,13 @@ def changeNoteTuning(self, track, tunings, sysExChannel=0x7F, \ MyMIDI.changeNoteTuning(0, tuning, tuningProgam=0) """ if self.header.numeric_format == 1: - track = track + 1 - self.tracks[track].changeNoteTuning(tunings, sysExChannel, realTime,\ - tuningProgam, insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 - + track += 1 + self.tracks[track].changeNoteTuning(tunings, sysExChannel, realTime, + tuningProgam, + insertion_order=self.event_counter) + self.event_counter += 1 - - def addSysEx(self,track, time, manID, payload): + def addSysEx(self, track, time, manID, payload): ''' Add a System Exclusive event. @@ -1242,8 +1573,8 @@ def addSysEx(self,track, time, manID, payload): :param track: The track to which the event should be written :param time: The time of the event. :param manID: The manufacturer ID for the event - :param payload: The payload for the event. This should be a binary-packed - value, and will vary for each type and function. + :param payload: The payload for the event. This should be a + binary-packed value, and will vary for each type and function. **Note**: This is a low-level MIDI function, so care must be used in constructing the payload. It is recommended that higher-level helper @@ -1251,13 +1582,15 @@ def addSysEx(self,track, time, manID, payload): a developer finds him or herself using the function heavily. ''' + tick = self.time_to_ticks(time) if self.header.numeric_format == 1: - track = track + 1 - self.tracks[track].addSysEx(time,manID, payload, insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track += 1 + self.tracks[track].addSysEx(tick, manID, payload, + insertion_order=self.event_counter) + self.event_counter += 1 - def addUniversalSysEx(self, track, time, code, subcode, payload, \ - sysExChannel=0x7F, realTime=False): + def addUniversalSysEx(self, track, time, code, subcode, payload, + sysExChannel=0x7F, realTime=False): ''' Add a Univeral System Exclusive event. @@ -1266,8 +1599,8 @@ def addUniversalSysEx(self, track, time, code, subcode, payload, \ :param time: The time of the event, in beats. :param code: The event code. [Integer] :param subcode: The event sub-code [Integer] - :param payload: The payload for the event. This should be a binary-packed - value, and will vary for each type and function. + :param payload: The payload for the event. This should be a + binary-packed value, and will vary for each type and function. :param sysExChannel: The SysEx channel. :param realTime: Sets the real-time flag. Defaults to non-real-time. :param manID: The manufacturer ID for the event @@ -1276,136 +1609,149 @@ def addUniversalSysEx(self, track, time, code, subcode, payload, \ **Note**: This is a low-level MIDI function, so care must be used in constructing the payload. It is recommended that higher-level helper functions be written to wrap this function and construct the payload if - a developer finds him or herself using the function heavily. As an example - of such a helper function, see the ``changeNoteTuning()`` function, - which uses the event to create a real-time note tuning update. + a developer finds him or herself using the function heavily. As an + example of such a helper function, see the ``changeNoteTuning()`` + function, which uses the event to create a real-time note tuning + update. ''' + tick = self.time_to_ticks(time) if self.header.numeric_format == 1: - track = track + 1 - self.tracks[track].addUniversalSysEx(time, code, subcode, payload, sysExChannel,\ - realTime, insertion_order = self.event_counter) - self.event_counter = self.event_counter + 1 + track += 1 + self.tracks[track].addUniversalSysEx(tick, code, subcode, payload, + sysExChannel, realTime, + insertion_order=self.event_counter) # noqa: E128 + self.event_counter += 1 - def writeFile(self,fileHandle): + def writeFile(self, fileHandle): ''' Write the MIDI File. - :param fileHandle: A file handle that has been opened for binary writing. + :param fileHandle: A file handle that has been opened for binary + writing. ''' self.header.writeFile(fileHandle) - #Close the tracks and have them create the MIDI event data structures. + # Close the tracks and have them create the MIDI event data structures. self.close() - #Write the MIDI Events to file. - for i in range(0,self.numTracks): + # Write the MIDI Events to file. + for i in range(0, self.numTracks): self.tracks[i].writeTrack(fileHandle) - def shiftTracks(self, offset=0): + def shiftTracks(self, offset=0): """Shift tracks to be zero-origined, or origined at offset. - Note that the shifting of the time in the tracks uses the MIDIEventList -- in other - words it is assumed to be called in the stage where the MIDIEventList has been - created. This function, however, it meant to operate on the eventList itself. + Note that the shifting of the time in the tracks uses the MIDIEventList + -- in other words it is assumed to be called in the stage where the + MIDIEventList has been created. This function, however, it meant to + operate on the eventList itself. """ - origin = 1000000 # A little silly, but we'll assume big enough + origin = 100000000 # A little silly, but we'll assume big enough + tick_offset = self.time_to_ticks(offset) for track in self.tracks: - if len(track.eventList) > 0: - for event in track.eventList: - if event.time < origin: - origin = event.time + if len(track.eventList) > 0: + for event in track.eventList: + if event.tick < origin: + origin = event.tick for track in self.tracks: tempEventList = [] - #runningTime = 0 + # runningTick = 0 for event in track.eventList: - adjustedTime = event.time - origin - #event.time = adjustedTime - runningTime + offset - event.time = adjustedTime + offset - #runningTime = adjustedTime + adjustedTick = event.tick - origin + # event.time = adjustedTime - runningTick + tick_offset + event.tick = adjustedTick + tick_offset + # runningTick = adjustedTick tempEventList.append(event) track.eventList = tempEventList - #End Public Functions ######################## + # End Public Functions ######################## def close(self): ''' Close the MIDIFile for further writing. - To close the File for events, we must close the tracks, adjust the time to be - zero-origined, and have the tracks write to their MIDI Stream data structure. + To close the File for events, we must close the tracks, adjust the time + to be zero-origined, and have the tracks write to their MIDI Stream + data structure. ''' - if self.closed == True: + if self.closed: return - for i in range(0,self.numTracks): + for i in range(0, self.numTracks): self.tracks[i].closeTrack() - # We want things like program changes to come before notes when they are at the - # same time, so we sort the MIDI events by their ordinality + # We want things like program changes to come before notes when + # they are at the same time, so we sort the MIDI events by both + # their start time and a secondary ordinality defined for each kind + # of event. self.tracks[i].MIDIEventList.sort(key=sort_events) origin = self.findOrigin() - for i in range(0,self.numTracks): + for i in range(0, self.numTracks): self.tracks[i].adjustTimeAndOrigin(origin, self.adjust_origin) self.tracks[i].writeMIDIStream() self.closed = True - def findOrigin(self): ''' Find the earliest time in the file's tracks.append. ''' - origin = 1000000 # A little silly, but we'll assume big enough + origin = 100000000 # A little silly, but we'll assume big enough - # Note: This code assumes that the MIDIEventList has been sorted, so this should be insured - # before it is called. It is probably a poor design to do this. - # TODO: -- Consider making this less efficient but more robust by not assuming the list to be sorted. + # Note: This code assumes that the MIDIEventList has been sorted, so this + # should be insured before it is called. It is probably a poor design to do + # this. + # TODO: -- Consider making this less efficient but more robust by not + # assuming the list to be sorted. for track in self.tracks: - if len(track.MIDIEventList) > 0: - if track.MIDIEventList[0].time < origin: - origin = track.MIDIEventList[0].time - + if len(track.MIDIEventList) > 0: + if track.MIDIEventList[0].tick < origin: + origin = track.MIDIEventList[0].tick return origin + def writeVarLength(i): ''' - Accept an input, and write a MIDI-compatible variable length stream - - The MIDI format is a little strange, and makes use of so-called variable - length quantities. These quantities are a stream of bytes. If the most - significant bit is 1, then more bytes follow. If it is zero, then the - byte in question is the last in the stream + Accept an integer, and serialize it as a MIDI file variable length quantity + + Some numbers in MTrk chunks are represented in a form called a variable- + length quantity. These numbers are represented in a sequence of bytes, + each byte holding seven bits of the number, and ordered most significant + bits first. All bytes in the sequence except the last have bit 7 set, + and the last byte has bit 7 clear. This form allows smaller numbers to + be stored in fewer bytes. For example, if the number is between 0 and + 127, it is thus represented exactly as one byte. A number between 128 + and 16383 uses two bytes, and so on. + + Examples: + Number VLQ + 128 81 00 + 8192 C0 00 + 16383 FF 7F + 16384 81 80 00 ''' - input = int(i+0.5) - output = [0,0,0,0] - reversed = [0,0,0,0] - count = 0 - result = input & 0x7F - output[count] = result - count = count + 1 - input = input >> 7 - while input > 0: - result = input & 0x7F - result = result | 0x80 - output[count] = result - count = count + 1 - input = input >> 7 - - reversed[0] = output[3] - reversed[1] = output[2] - reversed[2] = output[1] - reversed[3] = output[0] - return reversed[4-count:4] + if i == 0: + return [0] + + vlbytes = [] + hibit = 0x00 # low-order byte has high bit cleared. + while i > 0: + vlbytes.append(((i & 0x7f) | hibit) & 0xff) + i >>= 7 + hibit = 0x80 + vlbytes.reverse() # put most-significant byte first, least significant last + return vlbytes + # readVarLength is taken from the MidiFile class. @@ -1421,7 +1767,7 @@ def readVarLength(offset, buffer): bytesRead = 0 while True: output = output << 7 - byte = struct.unpack_from('>B',buffer,toffset)[0] + byte = struct.unpack_from('>B', buffer, toffset)[0] toffset = toffset + 1 bytesRead = bytesRead + 1 output = output + (byte & 127) @@ -1429,60 +1775,62 @@ def readVarLength(offset, buffer): break return (output, bytesRead) + def frequencyTransform(freq): ''' Returns a three-byte transform of a frequency. ''' resolution = 16384 freq = float(freq) - dollars = 69 + 12 * math.log(freq/(float(440)), 2) + dollars = 69 + 12 * math.log(freq / (float(440)), 2) firstByte = int(dollars) - lowerFreq = 440 * pow(2.0, ((float(firstByte) - 69.0)/12.0)) - if freq != lowerFreq: - centDif = 1200 * math.log( (freq/lowerFreq), 2) - else: - centDif = 0 - cents = round(centDif/100 * resolution) # round? - secondByte = min([int(cents)>>7, 0x7F]) + lowerFreq = 440 * pow(2.0, ((float(firstByte) - 69.0) / 12.0)) + centDif = 1200 * math.log((freq / lowerFreq), 2) if freq != lowerFreq else 0 + cents = round(centDif / 100 * resolution) # round? + secondByte = min([int(cents) >> 7, 0x7F]) thirdByte = cents - (secondByte << 7) thirdByte = min([thirdByte, 0x7f]) if thirdByte == 0x7f and secondByte == 0x7F and firstByte == 0x7F: thirdByte = 0x7e - thirdByte = int(thirdByte) - return [firstByte, secondByte, thirdByte] + return [firstByte, secondByte, thirdByte] + def returnFrequency(freqBytes): - '''The reverse of frequencyTransform. Given a byte stream, return a frequency. + ''' + The reverse of frequencyTransform. Given a byte stream, return a frequency. ''' resolution = 16384.0 - baseFrequency = 440 * pow(2.0, (float(freqBytes[0]-69.0)/12.0)) + baseFrequency = 440 * pow(2.0, (float(freqBytes[0] - 69.0) / 12.0)) frac = (float((int(freqBytes[1]) << 7) + int(freqBytes[2])) * 100.0) / resolution - frequency = baseFrequency * pow(2.0, frac/1200.0) + frequency = baseFrequency * pow(2.0, frac / 1200.0) return frequency + def sort_events(event): ''' .. py:function:: sort_events(event) The key function used to sort events (both MIDI and Generic) - :param event: An object of type :class:`MIDIEvent` or (a derrivative of) + :param event: An object of type :class:`MIDIEvent` or (a derrivative) :class:`GenericEvent` - This function should be provided as the ``key`` for both ``list.sort()`` - and ``sorted()``. By using it sorting will be as follows: + This function should be provided as the ``key`` for both + ``list.sort()`` and ``sorted()``. By using it sorting will be as + follows: * Events are ordered in time. An event that takes place earlier will appear earlier * If two events happen at the same time, the secondary sort key is - ``ord``. Thus a class of events can be processed earlier than another. - One place this is used in the code is to make sure that note off events - are processed before note on events. - * If time and ordinality are the same, they are sorted in the order in which - they were originally added to the list. Thus, for example, if one is making - an RPN call one can specify the controller change events in the proper order - and be sure that they will end up in the file that way. + ``sec_sort_order``. Thus a class of events can be processed earlier + than another. One place this is used in the code is to make sure that + note off events are processed before note on events. + * If event time and event ordinality are the same, they are sorted in + the order in which they were originally added to the list. Thus, for + example, if one is making an RPN call one can specify the controller + change events in the proper order and be sure that they will end up in + the file that way. ''' - return (event.time, event.ord, event.insertion_order) + return (event.tick, event.sec_sort_order, event.insertion_order) diff --git a/midiutil/__init__.py b/midiutil/__init__.py index e69de29..2d4e6c5 100644 --- a/midiutil/__init__.py +++ b/midiutil/__init__.py @@ -0,0 +1,3 @@ +from midiutil.MidiFile import * + +__all__ = ['MIDIFile', 'MAJOR', 'MINOR', 'SHARPS', 'FLATS'] diff --git a/python.com b/python.com new file mode 100755 index 0000000..6a15ae6 Binary files /dev/null and b/python.com differ