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 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
|
# GNU Solfege - free ear training software
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006, 2007, 2008 Tom Cato Amundsen
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
from rat import Rat
import mfutils
class EventBase(object):
def __init__(self):
self.m_time = None
def __str__(self):
return "(%s, time:%s)" % ( self.__class__.__name__, self.m_time)
class NoteEventBase(EventBase):
def __init__(self, pitch, velocity):
EventBase.__init__(self)
assert 0 <= pitch
self.m_pitch = pitch
self.m_velocity = velocity
def __str__(self):
return "(%s, pitch:%s, vel:%s, time:%s)" % (self.__class__.__name__, self.m_pitch, self.m_velocity, self.m_time)
class NoteOnEvent(NoteEventBase):
def __init__(self, pitch, velocity):
NoteEventBase.__init__(self, pitch, velocity)
class NoteOffEvent(NoteEventBase):
def __init__(self, pitch, velocity):
NoteEventBase.__init__(self, pitch, velocity)
class Delay(EventBase):
def __init__(self, duration):
"""
duration is a Rat. Rat(1, 4) denotes a quarter-note.
"""
EventBase.__init__(self)
self.m_duration = duration
def __str__(self):
return "(%s, dur:%s, time:%s)" % (self.__class__.__name__, self.m_duration, self.m_time)
class SetPatchEvent(EventBase):
def __init__(self, patch):
EventBase.__init__(self)
assert 0 <= patch < 128
self.m_patch = patch
def __str__(self):
return "(%s, time:%s, patch:%i)" % ( self.__class__.__name__, self.m_time, self.m_patch)
class TempoEvent(EventBase):
def __init__(self, bpm, notelen):
EventBase.__init__(self)
self.m_bpm = bpm
self.m_notelen = notelen
class MidiEventStream(object):
TEMPO = 'tempo'
NOTE_ON = 'note-on'
NOTE_OFF = 'note-off'
NOTELEN_TIME = 'notelen-time'
BENDER = 'bender'
SET_PATCH = 'program-change'
class ChannelDevice(object):
"""
Bad name, but I don't have a better idea right now. This
class will handle all 16 midi channels.
"""
class MidiChannel(object):
def __init__(self, number):
self.m_number = number
self.m_tones = set()
def start_tone(self, i):
assert i not in self.m_tones
self.m_tones.add(i)
def stop_tone(self, i):
assert i in self.m_tones
self.m_tones.remove(i)
def is_silent(self):
"""
Return True if no tones are playing on this channel.
"""
return not bool(self.m_tones)
def __init__(self):
# We are zero-indexed, so this is MIDI channel 10
self.percussion_MIDI_channel = 9
self.free_MIDI_channels = []
for i in range(self.percussion_MIDI_channel) \
+ range(self.percussion_MIDI_channel, 16):
self.free_MIDI_channels.append(self.MidiChannel(i))
# The dict key will be the patch number.
# The value is a MidiChannel object.
self.allocated_MIDI_channels = {}
# This dict maps from MIDI channel number to the actual
# MidiChannel object
self.int_to_channel_object = {}
for channel in self.free_MIDI_channels:
self.int_to_channel_object[channel.m_number] = channel
def alloc_channel(self, patch):
"""
Allocate a midi channel for the patch and
return the MIDI channel number of the allocated channel.
"""
#FIXME need to handle running out of available midi channels.
assert patch not in self.allocated_MIDI_channels
for num, channel in self.allocated_MIDI_channels.items():
if channel.is_silent():
self.allocated_MIDI_channels[patch] = \
self.allocated_MIDI_channels[num]
del self.allocated_MIDI_channels[num]
return self.allocated_MIDI_channels[patch].m_number
self.allocated_MIDI_channels[patch] = self.free_MIDI_channels.pop(0)
return self.allocated_MIDI_channels[patch].m_number
def get_channel_for_patch(self, patch):
"""
Return the MIDI channel number we should use to play a tone
with this patch number. Raise KeyError if no channel is allocated
yet.
"""
return self.allocated_MIDI_channels[patch].m_number
def start_note(self, channel, pitch):
self.int_to_channel_object[channel].start_tone(pitch)
def stop_note(self, channel, pitch):
self.int_to_channel_object[channel].stop_tone(pitch)
def is_playing(self, channel, pitch):
return pitch in self.int_to_channel_object[channel].m_tones
def __init__(self, *tracks):
self.m_tracks = tracks
for track in self.m_tracks:
track.calculate_event_times()
def __iter__(self):
for e in self.generate_track_events():
yield e
def _create_dict_of_track(self, track):
# create a dict of the track, where the key is a list with
# all events with the same m_time variable.
# The dict values are a dict with three keys:
# NoteOffEvents, OtherEvents, NoteOnEvents
retval = {}
for event in track.m_v:
retval[event.m_time] = retval.get(event.m_time, [])
retval[event.m_time].append(event)
for key in retval:
retval[key] = {
'NoteOffEvents': [x for x in retval[key] if isinstance(x, NoteOffEvent)],
'OtherEvents': [x for x in retval[key] if not isinstance(x, (NoteOffEvent, NoteOnEvent))],
'NoteOnEvents': [x for x in retval[key] if isinstance(x, NoteOnEvent)]}
return retval
def generate_track_events(self):
#FIXME find better method name
# tpos_set will know all the positions in time where anything happens
# on any staff
tpos_set = set()
for track in self.m_tracks:
tpos_set.update([x.m_time for x in track.m_v if x.m_time])
tracks2 = [self._create_dict_of_track(track) for track in self.m_tracks]
tpos_list = list(tpos_set)
tpos_list.sort()
# We use this variable to remember which instrument
# we want the track to play.
track_instrument = [0] * len(self.m_tracks)
# We use this list of dicts to know which tones are playing
# which patch on the tracks. The key of the dicts are the integer
# value representing the pitch, and the value is the patch number.
track_notes = []
for x in range(len(self.m_tracks)):
track_notes.append({})
ch_dev = self.ChannelDevice()
last_pos = Rat(0, 1)
for tpos in tpos_list:
if tpos != last_pos: # Just to not insert before the first events
yield self.NOTELEN_TIME, tpos - last_pos
for idx, track in enumerate(tracks2):
if tpos in track:
for e in track[tpos]['NoteOffEvents']:
if e.m_pitch not in track_notes[idx]:
# This could happen if the user adds extra NoteOffEvents or adds one
# with the wrong pitch.
print "info: not stopping, not playing now:", e
continue
patch = track_notes[idx][e.m_pitch]
del track_notes[idx][e.m_pitch]
if isinstance(self.m_tracks[idx], PercussionTrack):
chn = ch_dev.percussion_MIDI_channel
else:
chn = ch_dev.get_channel_for_patch(patch)
if ch_dev.is_playing(chn, e.m_pitch):
ch_dev.stop_note(chn, e.m_pitch)
yield self.NOTE_OFF, chn, e.m_pitch, e.m_velocity
for e in track[tpos]['OtherEvents']:
if isinstance(e, SetPatchEvent):
track_instrument[idx] = e.m_patch
elif isinstance(e, TempoEvent):
yield self.TEMPO, e.m_bpm, e.m_notelen
else:
print "NOT HANDLING EVENT:", e
for e in track[tpos]['NoteOnEvents']:
assert e.m_pitch not in track_notes[idx]
if isinstance(self.m_tracks[idx], PercussionTrack):
chn = ch_dev.percussion_MIDI_channel
else:
try:
chn = ch_dev.get_channel_for_patch(track_instrument[idx])
except KeyError:
chn = ch_dev.alloc_channel(track_instrument[idx])
# We will only yield SET_PATCH if a channel
# was allocated.
yield self.SET_PATCH, chn, track_instrument[idx]
if ch_dev.is_playing(chn, e.m_pitch):
print "info: ignoring duplicate tone:", e
continue
track_notes[idx][e.m_pitch] = track_instrument[idx]
# ch_dev must know which tones are sounding on which
# MIDI channels, so it can handle the midi resources.
ch_dev.start_note(chn, e.m_pitch)
yield self.NOTE_ON, chn, e.m_pitch, e.m_velocity
last_pos = tpos
def create_midifile(self, filename, appendstreams=[]):
"""
filename -- a string naming the file to write the generated midi file to.
Will overwrite a existing file.
appendstrings -- a list of additional MidiEventStreams to append to
the midi file.
"""
v = []
notelen = 0
v += mfutils.mf_tempo(60 * 4 / 4)
for stream in [self] + appendstreams:
for e in stream:
if e[0] == self.TEMPO:
v = v + mfutils.mf_tempo(e[1] * 4 / e[2])
elif e[0] == self.NOTELEN_TIME:
notelen = e[1]
elif e[0] == self.NOTE_ON:
v = v + mfutils.mf_note_on(int(96 * 4 * notelen), e[1], e[2], e[3])
notelen = 0
elif e[0] == self.NOTE_OFF:
v = v + mfutils.mf_note_off(int(96 * 4 * notelen), e[1], e[2], e[3])
notelen = 0
elif e[0] == self.SET_PATCH:
v = v + mfutils.mf_program_change(e[1], e[2])
elif e[0] == self.BENDER:
print "FIXME todo: seq_bender for play_with_drvmidi"
#m.seq_bender(DEV, e[1], e[2])
else:
raise Exception("mpd.track: Corrupt track error")
f = open(filename, "w")
mfutils.MThd(f)
f.write("MTrk")
mfutils.write_int32(f, len(v)+4)
v = v + mfutils.mf_end_of_track()
mfutils.write_vect(f, v)
f.close()
def str_repr(self, details=0):
v = []
v.append([])
for e in self:
if e[0] == self.TEMPO:
v[-1].append("t%s/%s" % (e[1], e[2]))
elif e[0] == self.NOTE_ON:
if e[1] == 9:
v[-1].append("P%s" % e[2])
else:
if details == 0:
v[-1].append("n%s" % e[2])
elif details == 1:
v[-1].append("n%s:%s" % (e[1], e[2]))
elif e[0] == self.NOTE_OFF:
v[-1].append("o%s" % e[2])
elif e[0] == self.SET_PATCH:
v[-1].append("p%i:%i" % (e[1], e[2]))
elif e[0] == self.NOTELEN_TIME:
v[-1].append("d%i/%i" % (e[1].m_num, e[1].m_den))
v.append([])
retval = []
for x in v:
n = [e for e in x if e.startswith('t')]
if n:
retval.append(" ".join([e for e in x if e.startswith('t')]))
for typ in ('p', 'o', 'P', 'n', 'd'):
n = [e for e in x if e.startswith(typ)]
if n:
n.sort()
retval.append(" ".join(n))
return " ".join(retval)
class Track:
"""
A pitch is represented by an integer value 0-127.
* There can only be one instance of a pitch sounding at the same time.
* There can only be one instrument sounding at the same time.
Right now there are no code that checks that this is true while
adding notes.
"""
def txtdump(self):
for event in self.m_v:
print event
def str_repr(self):
retval = []
for e in self.m_v:
if isinstance(e, SetPatchEvent):
retval.append('p%i' % e.m_patch)
elif isinstance(e, NoteOnEvent):
retval.append('n%i' % e.m_pitch)
elif isinstance(e, NoteOffEvent):
retval.append('o%i' % e.m_pitch)
elif isinstance(e, Delay):
retval.append('d%i/%i' % (e.m_duration.m_num, e.m_duration.m_den))
return " ".join(retval)
def __init__(self):
self.m_v = []
def start_note(self, pitch, vel):
assert 0 <= int(pitch) < 128
assert 0 <= vel < 128
self.m_v.append(NoteOnEvent(int(pitch), int(vel)))
def stop_note(self, pitch, vel):
assert 0 <= int(pitch) < 128
assert 0 <= vel < 128
self.m_v.append(NoteOffEvent(int(pitch), int(vel)))
def notelen_time(self, notelen):
"""
To avoid having to alter all code calling this, we interpret
notelen in two different ways depending on its type:
int: replace to Rat(1, notelen)
Rat: the value tell the note length. For example Rat(1, 4) for a
quarter note.
"""
if isinstance(notelen, int):
self.m_v.append(Delay(Rat(1, notelen)))
else:
assert isinstance(notelen, Rat)
self.m_v.append(Delay(notelen))
def note(self, notelen, pitch, vel):
"""
See notelen_time docstring.
"""
self.start_note(pitch, vel)
self.notelen_time(notelen)
self.stop_note(pitch, vel)
def set_patch(self, patch):
"""
Add an event that will change the midi instrument for the
notes following this event.
"""
self.m_v.append(SetPatchEvent(patch))
def prepend_patch(self, patch):
"""
Insert an event that will change the midi instrument at the
beginning of the track. If you call this method several times,
only the first call will have any effect.
"""
self.m_v.insert(0, SetPatchEvent(patch))
def set_bpm(self, bpm, notelen=4):
self.m_v.append(TempoEvent(bpm, notelen))
def prepend_bpm(self, bpm, notelen=4):
self.m_v.insert(0, TempoEvent(bpm, notelen))
def bender(self, chn, value):
"value >= 0"
self.m_v.append([self.BENDER, chn, value])
def create_merge(self, track1, track2):
"""
Return a new track that merges track1 and track2.
"""
create_merge = staticmethod(create_merge)
def merge_with(self, B):
D = {}
for track in [self, B]:
pos = Rat(0, 1)
for event in track.m_v:
if isinstance(event, Delay):
pos = pos + event.m_duration
else:
if pos not in D:
D[pos] = []
D[pos].append(event)
kv = D.keys()
kv.sort()
self.m_v = []
for x in range(len(kv)-1):
for event in D[kv[x]]:
self.m_v.append(event)
self.m_v.append(Delay(kv[x+1]-kv[x]))
for event in D[kv[-1]]:
self.m_v.append(event)
def replace_note(self, old, new):
assert isinstance(old, int)
assert 0 <= old < 128
assert isinstance(new, int)
assert 0 <= new < 128
for event in self.m_v:
if isinstance(event, (NoteOnEvent, NoteOffEvent)) \
and event.m_pitch == old:
event.m_pitch = new
def calculate_event_times(self):
"""
Set the variable m_time on each Event. Well actually we don't set
it on the Delay events because events of that type does not generate
any events when generating music.
"""
pos = Rat(0, 1)
for e in self.m_v:
if isinstance(e, Delay):
pos += e.m_duration
else:
e.m_time = pos
class PercussionTrack(Track):
def __init__(self):
Track.__init__(self)
|