1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
|
# Python midifile package -- parse, load and play MIDI files.
# Copyright (c) 2011 - 2014 by Wilbert Berendsen
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# See http://www.gnu.org/licenses/ for more information.
"""
midifile.song -- structures MIDI file data as a song.
"""
import collections
from . import event
from . import parser
def load(filename):
"""Convenience function to instantiate a Song from a filename.
If the filename is a type 2 MIDI file, just returns the first track.
"""
with open(filename, 'rb') as midifile:
fmt, div, tracks = parser.parse_midi_data(midifile.read())
if fmt == 2:
tracks = tracks[:1]
return Song(div, tracks)
def events_dict(tracks):
"""Returns all events from the track grouped per and mapped to time-step.
every time step has a dictionary with the events per track at that time.
"""
d = collections.defaultdict(dict)
for n, track in enumerate(tracks):
for time, evs in parser.time_events_grouped(
parser.parse_midi_events(track)):
d[time][n] = evs
return d
def events_dict_together(tracks):
"""Returns all events from the track grouped per and mapped to time-step.
every time step has a list with all the events at that time.
"""
d = collections.defaultdict(list)
for track in tracks:
for time, evs in parser.time_events_grouped(
parser.parse_midi_events(track)):
d[time].extend(evs)
return d
def is_tempo(e):
"""Returns True if the event is a Set Tempo Meta-event."""
return isinstance(e, event.MetaEvent) and e.type == 0x51
def get_tempo(e):
"""Returns the tempo from the Set Tempo Meta-event."""
return e.data[0]*65536 + e.data[1]*256 + e.data[2]
def is_time_signature(e):
"""Returns True if the event is a Set Time Signature Meta-event."""
return isinstance(e, event.MetaEvent) and e.type == 0x58
def get_time_signature(e):
"""Returns the num, den, clocks, num_32s from the Time Signature event."""
return tuple(e.data)
def smpte_division(div):
"""Converts a MIDI header division from a SMPTE type, if necessary."""
if div & 0x8000:
frames = 256 - (div >> 8)
resolution = div & 0xFF
div = frames * resolution
return div
def events_iter(d):
"""Return an iterator function over the events in one value of dict d.
The values in d can be dicts (per-track) or lists (single track).
Returns None if the events dictionary is empty.
"""
for k in d:
return iter_events_dict if isinstance(d[k], dict) else iter
def iter_events_dict(evs):
"""Iter over the (per-track) dictionary's events."""
for k in sorted(evs):
for e in evs[k]:
yield e
class TempoMap(object):
"""Converts midi time to real time in microseconds."""
def __init__(self, d, division):
"""Initialize our tempo map based on events d and division."""
# are the events one list (single-track) or a dict (per-track)?
self.division = smpte_division(division)
self.times = times = []
events = events_iter(d)
if events:
for midi_time, evs in sorted(d.items()):
for e in events(evs):
if is_tempo(e):
times.append((midi_time, get_tempo(e)))
break
if not times or times[0][0] != 0:
times.insert(0, (0, 500000))
def real_time(self, midi_time):
"""Returns the real time in microseconds for the given MIDI time."""
real_time = 0
times = self.times
for i in range(1, len(times)):
if times[i][0] >= midi_time:
real_time += (midi_time - times[i-1][0]) * times[i-1][1]
break
real_time += (times[i][0] - times[i-1][0]) * times[i-1][1]
else:
real_time += (midi_time - times[-1][0]) * times[-1][1]
return real_time // self.division
def msec(self, midi_time):
"""Returns the real time in milliseconds."""
return self.real_time(midi_time) // 1000
def beats(d, division):
"""Yields tuples for every beat in the events dictionary d.
Each tuple is:
(midi_time, beat_num, beat_total, denominator)
With this you can easily add measure numbers and find measure positions
in the MIDI.
"""
events = events_iter(d)
if not events:
return
time_sigs = []
times = sorted(d)
for midi_time in times:
for e in events(d[midi_time]):
if is_time_signature(e):
time_sigs.append((midi_time, get_time_signature(e)))
if not time_sigs or time_sigs[0][0] != 0:
# default time signature at start
time_sigs.insert(0, (0, (4, 4, 24, 8)))
# now yield a tuple for every beat
time = 0
sigs_index = 0
while time <= times[-1]:
if sigs_index < len(time_sigs) and time >= time_sigs[sigs_index][0]:
# new time signature
time, (num, den, clocks, n32s) = time_sigs[sigs_index]
step = (4 * division) // (2 ** den)
beat = 1
sigs_index += 1
yield time, beat, num, den
time += step
beat = beat % num + 1
class Song(object):
"""A loaded MIDI file.
The following instance attributes are set on init:
division: the division set in the MIDI header
ntracks: the number of tracks
events: a dict mapping MIDI times to a dict with per-track lists of events.
tempo_map: TempoMap instance that computes real time from MIDI time.
length: the length in milliseconds of the song (same as the time of the last
event).
beats: a list of tuples(msec, measnum, beat, num, den) for every beat
music: a list of tuples(msec, d) where d is a dict mapping tracknr to events
"""
def __init__(self, division, tracks):
"""Initialize the Song with the given division and track chunks."""
self.division = division
self.ntracks = len(tracks)
self.events = events_dict(tracks)
self.tempo_map = t = TempoMap(self.events, division)
self.length = t.msec(max(self.events))
self.beats = b = []
measnum = 0
for midi_time, beat, num, den in beats(self.events, division):
if beat == 1:
measnum += 1
b.append((t.msec(midi_time), measnum, beat, num, den))
self.music = [(t.msec(midi_time), evs)
for midi_time, evs in sorted(self.events.items())]
def beat(self, time):
"""Returns (time, measnum, beat, num, den) for the beat at time."""
if not self.beats:
return (0, 0, 0, 4, 2)
pos = 0
if time:
# bisect our way in the beats list.
end = len(self.beats)
while pos < end:
mid = (pos + end) // 2
if time > self.beats[mid][0]:
pos = mid + 1
else:
end = mid
return self.beats[min(pos, len(self.beats) - 1)]
|