[go: up one dir, main page]

File: track.py

package info (click to toggle)
solfege 3.10.3-1
  • links: PTS
  • area: main
  • in suites: lenny
  • size: 12,408 kB
  • ctags: 4,270
  • sloc: python: 22,161; xml: 7,536; ansic: 4,442; makefile: 685; sh: 308
file content (422 lines) | stat: -rw-r--r-- 17,748 bytes parent folder | download
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)