MIDI
index
./free/MIDI.py

This module offers functions:  concatenate_scores(), grep(),
merge_scores(), mix_scores(), midi2opus(), midi2score(), opus2midi(),
opus2score(), play_score(), score2midi(), score2opus(), score2stats(),
score_type(), segment(), timeshift() and to_millisecs(),
where "midi" means the MIDI-file bytes (as can be put in a .mid file,
or piped into aplaymidi), and "opus" and "score" are list-structures
as inspired by Sean Burke's MIDI-Perl CPAN module.
 
Warning: Version 6.4 is not necessarily backward-compatible with
previous versions, in that text-data is now bytes, not strings.
This reflects the fact that many MIDI files have text data in
encodings other that ISO-8859-1, for example in Shift-JIS.
 
Download MIDI.py from   http://www.pjb.com.au/midi/free/MIDI.py
and put it in your PYTHONPATH.  MIDI.py depends on Python3.
 
There is also a call-compatible translation into Lua of this
module: see http://www.pjb.com.au/comp/lua/MIDI.html
 
The "opus" is a direct translation of the midi-file-events, where
the times are delta-times, in ticks, since the previous event.
 
The "score" is more human-centric; it uses absolute times, and
combines the separate note_on and note_off events into one "note"
event, with a duration:
 ['note', start_time, duration, channel, note, velocity] # in a "score"
 
  EVENTS (in an "opus" structure)
     ['note_off', dtime, channel, note, velocity]       # in an "opus"
     ['note_on', dtime, channel, note, velocity]        # in an "opus"
     ['key_after_touch', dtime, channel, note, velocity]
     ['control_change', dtime, channel, controller(0-127), value(0-127)]
     ['patch_change', dtime, channel, patch]
     ['channel_after_touch', dtime, channel, velocity]
     ['pitch_wheel_change', dtime, channel, pitch_wheel]
     ['text_event', dtime, text]
     ['copyright_text_event', dtime, text]
     ['track_name', dtime, text]
     ['instrument_name', dtime, text]
     ['lyric', dtime, text]
     ['marker', dtime, text]
     ['cue_point', dtime, text]
     ['text_event_08', dtime, text]
     ['text_event_09', dtime, text]
     ['text_event_0a', dtime, text]
     ['text_event_0b', dtime, text]
     ['text_event_0c', dtime, text]
     ['text_event_0d', dtime, text]
     ['text_event_0e', dtime, text]
     ['text_event_0f', dtime, text]
     ['end_track', dtime]
     ['set_tempo', dtime, tempo]
     ['smpte_offset', dtime, hr, mn, se, fr, ff]
     ['time_signature', dtime, nn, dd, cc, bb]
     ['key_signature', dtime, sf, mi]
     ['sequencer_specific', dtime, raw]
     ['raw_meta_event', dtime, command(0-255), raw]
     ['sysex_f0', dtime, raw]
     ['sysex_f7', dtime, raw]
     ['song_position', dtime, song_pos]
     ['song_select', dtime, song_number]
     ['tune_request', dtime]
 
  DATA TYPES
     channel = a value 0 to 15
     controller = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html#cc )
     dtime = time measured in "ticks", 0 to 268435455
     velocity = a value 0 (soft) to 127 (loud)
     note = a value 0 to 127  (middle-C is 60)
     patch = 0 to 127 (see http://www.pjb.com.au/muscript/gm.html )
     pitch_wheel = a value -8192 to 8191 (0x1FFF)
     raw = bytes, of length 0 or more  (for sysex events see below)
     sequence_number = a value 0 to 65,535 (0xFFFF)
     song_pos = a value 0 to 16,383 (0x3FFF)
     song_number = a value 0 to 127
     tempo = microseconds per crochet (quarter-note), 0 to 16777215
     text = bytes, of length 0 or more
     ticks = the number of ticks per crochet (quarter-note)
 
   In sysex_f0 events, the raw data must not start with a \xF0 byte,
   since this gets added automatically;
   but it must end with an explicit \xF7 byte!
   In the very unlikely case that you ever need to split sysex data
   into one sysex_f0 followed by one or more sysex_f7s, then only the
   last of those sysex_f7 events must end with the explicit \xF7 byte
   (again, the raw data of individual sysex_f7 events must not start
   with any \xF7 byte, since this gets added automatically).
 
   Since version 6.4, text data is in bytes, not in a ISO-8859-1 string.
 
 
  GOING THROUGH A SCORE WITHIN A PYTHON PROGRAM
    channels = {2,3,5,8,13}
    itrack = 1   # skip 1st element which is ticks
    while itrack < len(score):
        for event in score[itrack]:
            if event[0] == 'note':   # for example,
                pass  # do something to all notes
            # or, to work on events in only particular channels...
            channel_index = MIDI.Event2channelindex.get(event[0], False)
            if channel_index and (event[channel_index] in channels):
                pass  # do something to channels 2,3,5,8 and 13
        itrack += 1
 
Copyright 2009  Peter J. Billam
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

 
Modules
       
copy
struct
sys

 
Functions
       
concatenate_scores(scores)
Concatenates a list of scores into one score.
If the scores differ in their "ticks" parameter,
they will all get converted to millisecond-tick format.
event2alsaseq(event=None)
Converts an event into the format needed by the alsaseq module,
http://pp.com.mx/python/alsaseq
The type of track (opus or score) is autodetected.
grep(score=None, channels=None)
Returns a "score" containing only the channels specified
merge_scores(scores)
Merges a list of scores into one score.  A merged score comprises
all of the tracks from all of the input scores; un-merging is possible
by selecting just some of the tracks.  If the scores differ in their
"ticks" parameter, they will all get converted to millisecond-tick
format.  merge_scores attempts to resolve channel-conflicts,
but there are of course only 15 available channels...
midi2ms_score(midi=b'')
Translates MIDI into a "score" with one beat per second and one
tick per millisecond, using midi2opus() then to_millisecs()
then opus2score()
midi2opus(midi=b'')
Translates MIDI into a "opus".  For a description of the
"opus" format, see opus2midi()
midi2score(midi=b'')
Translates MIDI into a "score", using midi2opus() then opus2score()
mix_opus_tracks(input_tracks)
Mixes an array of tracks into one track.  A mixed track
cannot be un-mixed.  It is assumed that the tracks share the same
ticks parameter and the same tempo.
Mixing score-tracks is trivial (just insert all events into one array).
Mixing opus-tracks is only slightly harder, but it's common enough
that a dedicated function is useful.
mix_scores(scores)
Mixes a list of scores into one one-track score.
A mixed score cannot be un-mixed.  Hopefully the scores
have no undesirable channel-conflicts between them.
If the scores differ in their "ticks" parameter,
they will all get converted to millisecond-tick format.
opus2midi(opus=[])
The argument is a list: the first item in the list is the "ticks"
parameter, the others are the tracks. Each track is a list
of midi-events, and each event is itself a list; see above.
opus2midi() returns a bytestring of the MIDI, which can then be
written either to a file opened in binary mode (mode='wb'),
or to stdout by means of:   sys.stdout.buffer.write()
 
my_opus = [
    96, 
    [   # track 0:
        ['patch_change', 0, 1, 8],   # and these are the events...
        ['note_on',   5, 1, 25, 96],
        ['note_off', 96, 1, 25, 0],
        ['note_on',   0, 1, 29, 96],
        ['note_off', 96, 1, 29, 0],
    ],   # end of track 0
]
my_midi = opus2midi(my_opus)
sys.stdout.buffer.write(my_midi)
opus2score(opus=[])
For a description of the "opus" and "score" formats,
see opus2midi() and score2opus().
play_score(score=None)
Converts the "score" to midi, and feeds it into 'aplaymidi -'
score2midi(score=None)
Translates a "score" into MIDI, using score2opus() then opus2midi()
score2opus(score=None)
The argument is a list: the first item in the list is the "ticks"
parameter, the others are the tracks. Each track is a list
of score-events, and each event is itself a list.  A score-event
is similar to an opus-event (see above), except that in a score:
 1) the times are expressed as an absolute number of ticks
    from the track's start time
 2) the pairs of 'note_on' and 'note_off' events in an "opus"
    are abstracted into a single 'note' event in a "score":
    ['note', start_time, duration, channel, pitch, velocity]
score2opus() returns a list specifying the equivalent "opus".
 
my_score = [
    96,
    [   # track 0:
        ['patch_change', 0, 1, 8],
        ['note', 5, 96, 1, 25, 96],
        ['note', 101, 96, 1, 29, 96]
    ],   # end of track 0
]
my_opus = score2opus(my_score)
score2stats(opus_or_score=None)
Returns a dict of some basic stats about the score, like
bank_select (list of tuples (msb,lsb)),
channels_by_track (list of lists), channels_total (set),
general_midi_mode (list),
ntracks, nticks, patch_changes_by_track (list of dicts),
num_notes_by_channel (list of numbers),
patch_changes_total (set),
percussion (dict histogram of channel 9 events),
pitches (dict histogram of pitches on channels other than 9),
pitch_range_by_track (list, by track, of two-member-tuples),
pitch_range_sum (sum over tracks of the pitch_ranges),
score_type(opus_or_score=None)
Returns a string, either 'opus' or 'score' or ''
segment(score=None, start_time=None, end_time=None, start=0, end=100000000, tracks={0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15})
Returns a "score" which is a segment of the one supplied
as the argument, beginning at "start_time" ticks and ending
at "end_time" ticks (or at the end if "end_time" is not supplied).
If the set "tracks" is specified, only those tracks will
be returned.
timeshift(score=None, shift=None, start_time=None, from_time=0, tracks={0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 13, 14, 15})
Returns a "score" shifted in time by "shift" ticks, or shifted
so that the first event starts at "start_time" ticks.
 
If "from_time" is specified, only those events in the score
that begin after it are shifted. If "start_time" is less than
"from_time" (or "shift" is negative), then the intermediate
notes are deleted, though patch-change events are preserved.
 
If "tracks" are specified, then only those tracks get shifted.
"tracks" can be a list, tuple or set; it gets converted to set
internally.
 
It is deprecated to specify both "shift" and "start_time".
If this does happen, timeshift() will print a warning to
stderr and ignore the "shift" argument.
 
If "shift" is negative and sufficiently large that it would
leave some event with a negative tick-value, then the score
is shifted so that the first event occurs at time 0. This
also occurs if "start_time" is negative, and is also the
default if neither "shift" nor "start_time" are specified.
to_millisecs(old_opus=None)
Recallibrates all the times in an "opus" to use one beat
per second and one tick per millisecond.  This makes it
hard to retrieve any information about beats or barlines,
but it does make it easy to mix different scores together.

 
Data
        All_events = ('note_off', 'note_on', 'key_after_touch', 'control_change', 'patch_change', 'channel_after_touch', 'pitch_wheel_change', 'text_event', 'copyright_text_event', 'track_name', 'instrument_name', 'lyric', 'marker', 'cue_point', 'text_event_08', 'text_event_09', 'text_event_0a', 'text_event_0b', 'text_event_0c', 'text_event_0d', ...)
Event2channelindex = {'channel_after_touch': 2, 'control_change': 2, 'key_after_touch': 2, 'note': 3, 'note_off': 2, 'note_on': 2, 'patch_change': 2, 'pitch_wheel_change': 2}
MIDI_events = ('note_off', 'note_on', 'key_after_touch', 'control_change', 'patch_change', 'channel_after_touch', 'pitch_wheel_change')
Meta_events = ('text_event', 'copyright_text_event', 'track_name', 'instrument_name', 'lyric', 'marker', 'cue_point', 'text_event_08', 'text_event_09', 'text_event_0a', 'text_event_0b', 'text_event_0c', 'text_event_0d', 'text_event_0e', 'text_event_0f', 'end_track', 'set_tempo', 'smpte_offset', 'time_signature', 'key_signature', ...)
Nontext_meta_events = ('end_track', 'set_tempo', 'smpte_offset', 'time_signature', 'key_signature', 'sequencer_specific', 'raw_meta_event', 'sysex_f0', 'sysex_f7', 'song_position', 'song_select', 'tune_request')
Notenum2percussion = {35: 'Acoustic Bass Drum', 36: 'Bass Drum 1', 37: 'Side Stick', 38: 'Acoustic Snare', 39: 'Hand Clap', 40: 'Electric Snare', 41: 'Low Floor Tom', 42: 'Closed Hi-Hat', 43: 'High Floor Tom', 44: 'Pedal Hi-Hat', ...}
Number2patch = {0: 'Acoustic Grand', 1: 'Bright Acoustic', 2: 'Electric Grand', 3: 'Honky-Tonk', 4: 'Electric Piano 1', 5: 'Electric Piano 2', 6: 'Harpsichord', 7: 'Clav', 8: 'Celesta', 9: 'Glockenspiel', ...}
Text_events = ('text_event', 'copyright_text_event', 'track_name', 'instrument_name', 'lyric', 'marker', 'cue_point', 'text_event_08', 'text_event_09', 'text_event_0a', 'text_event_0b', 'text_event_0c', 'text_event_0d', 'text_event_0e', 'text_event_0f')
Version = '6.7'
VersionDate = '20201120'