~ubuntu-branches/ubuntu/vivid/frescobaldi/vivid

« back to all changes in this revision

Viewing changes to frescobaldi_app/midifile/player.py

  • Committer: Package Import Robot
  • Author(s): Ryan Kavanagh
  • Date: 2012-01-03 16:20:11 UTC
  • mfrom: (1.4.1)
  • Revision ID: package-import@ubuntu.com-20120103162011-tsjkwl4sntwmprea
Tags: 2.0.0-1
* New upstream release 
* Drop the following uneeded patches:
  + 01_checkmodules_no_python-kde4_build-dep.diff
  + 02_no_pyc.diff
  + 04_no_binary_lilypond_upgrades.diff
* Needs new dependency python-poppler-qt4
* Update debian/watch for new download path
* Update copyright file with new holders and years

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! python
 
2
 
 
3
# Python midifile package -- parse, load and play MIDI files.
 
4
# Copyright (C) 2011 by Wilbert Berendsen
 
5
#
 
6
# This program is free software; you can redistribute it and/or
 
7
# modify it under the terms of the GNU General Public License
 
8
# as published by the Free Software Foundation; either version 2
 
9
# of the License, or (at your option) any later version.
 
10
#
 
11
# This program is distributed in the hope that it will be useful,
 
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
# GNU General Public License for more details.
 
15
#
 
16
# You should have received a copy of the GNU General Public License
 
17
# along with this program; if not, write to the Free Software
 
18
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
19
# See http://www.gnu.org/licenses/ for more information.
 
20
 
 
21
"""
 
22
A MIDI Player.
 
23
"""
 
24
 
 
25
from __future__ import unicode_literals
 
26
 
 
27
import collections
 
28
import time
 
29
import threading
 
30
 
 
31
from . import song
 
32
 
 
33
 
 
34
class Player(object):
 
35
    """The base class for a MIDI player.
 
36
    
 
37
    Use set_output() to set a MIDI output instance (see midioutput.py).
 
38
    You can override: timer_midi_time(), timer_start() and timer_stop()
 
39
    to use another timing source than the Python threading.Timer instances.
 
40
    
 
41
    """
 
42
    def __init__(self):
 
43
        self._song = None
 
44
        self._events = []
 
45
        self._position = 0
 
46
        self._offset = 0
 
47
        self._sync_time = 0
 
48
        self._playing = False
 
49
        self._tempo_factor = 1.0
 
50
        self._output = None
 
51
        self._last_exception = None
 
52
    
 
53
    def set_output(self, output):
 
54
        """Sets an Output instance that handles the MIDI events.
 
55
        
 
56
        Use None to disable all output.
 
57
        
 
58
        """
 
59
        self._output = output
 
60
    
 
61
    def output(self):
 
62
        """Returns the currently set Output instance."""
 
63
        return self._output
 
64
        
 
65
    def load(self, filename, time=1000, beat=True):
 
66
        """Convenience function, loads a MIDI file.
 
67
        
 
68
        See set_song() for the other arguments.
 
69
        
 
70
        """
 
71
        self.set_song(song.load(filename), time, beat)
 
72
    
 
73
    def set_song(self, song, time=1000, beat=True):
 
74
        """Loads the specified Song (see song.py).
 
75
        
 
76
        If time is not None, it specifies at which interval (in msec) the
 
77
        time() method will be called. Default: 1000.
 
78
        If beat is True (default), the beat() method will be called on every
 
79
        beat.
 
80
        
 
81
        """
 
82
        playing = self._playing
 
83
        if playing:
 
84
            self.timer_stop_playing()
 
85
        self._song = song
 
86
        self._events = make_event_list(song, time, beat)
 
87
        self._position = 0
 
88
        self._offset = 0
 
89
        if playing:
 
90
            self.timer_start_playing()
 
91
    
 
92
    def song(self):
 
93
        """Returns the current Song."""
 
94
        return self._song
 
95
    
 
96
    def clear(self):
 
97
        """Unloads a loaded Song."""
 
98
        if self._playing:
 
99
            self.stop()
 
100
        self._song = None
 
101
        self._events = []
 
102
        self._position = 0
 
103
        self._offset = 0
 
104
        
 
105
    def total_time(self):
 
106
        """Returns the length in msec of the current song."""
 
107
        if self._events:
 
108
            return self._events[-1][0]
 
109
        return 0
 
110
    
 
111
    def current_time(self):
 
112
        """Returns the current time position."""
 
113
        if self._position >= len(self._events):
 
114
            time = self.total_time()
 
115
        else:
 
116
            time = self._events[self._position][0]
 
117
        if self._playing:
 
118
            return time - self.timer_offset()
 
119
        return time - self._offset
 
120
    
 
121
    def start(self):
 
122
        """Starts playing."""
 
123
        if self.has_events():
 
124
            self.timer_start_playing()
 
125
        
 
126
    def stop(self):
 
127
        """Stops playing."""
 
128
        self.timer_stop_playing()
 
129
    
 
130
    def is_playing(self):
 
131
        """Returns True if the player is playing, else False."""
 
132
        return self._playing
 
133
    
 
134
    def set_tempo_factor(self, factor):
 
135
        """Sets the tempo factor as a floating point value (1.0 is normal)."""
 
136
        self._tempo_factor = float(factor)
 
137
    
 
138
    def tempo_factor(self):
 
139
        """Returns the tempo factor (by default: 1.0)."""
 
140
        return self._tempo_factor
 
141
    
 
142
    def seek(self, time):
 
143
        """Goes to the specified time (in msec)."""
 
144
        pos = 0
 
145
        offset = 0
 
146
        if time:
 
147
            # bisect our way in the events list.
 
148
            end = len(self._events)
 
149
            while pos < end:
 
150
                mid = (pos + end) // 2
 
151
                if time > self._events[mid][0]:
 
152
                    pos = mid + 1
 
153
                else:
 
154
                    end = mid
 
155
            if pos < len(self._events):
 
156
                offset = self._events[pos][0] - time
 
157
        self.set_position(pos, offset)
 
158
    
 
159
    def seek_measure(self, measnum, beat=1):
 
160
        """Goes to the specified measure and beat (beat defaults to 1).
 
161
        
 
162
        Returns whether the measure position could be found (True or False).        
 
163
        
 
164
        """
 
165
        result = False
 
166
        for i, (t, e) in enumerate(self._events):
 
167
            if e.beat:
 
168
                if e.beat[0] == measnum:
 
169
                    position = i
 
170
                    result = True
 
171
                    if e.beat[1] >= beat:
 
172
                        break
 
173
                if e.beat[0] > measnum:
 
174
                    break
 
175
        if result:
 
176
            self.set_position(position)
 
177
            return True
 
178
        return False
 
179
        
 
180
    def set_position(self, position, offset=0):
 
181
        """(Private) Goes to the specified position in the internal events list.
 
182
        
 
183
        The default implementation does nothing with the time offset,
 
184
        but inherited implementations may wait that many msec before
 
185
        triggering the event at that position.
 
186
        
 
187
        This method is called by seek() and seek_measure().
 
188
        
 
189
        """
 
190
        old, self._position = self._position, position
 
191
        if self._playing:
 
192
            self.timer_stop()
 
193
            if old != self._position:
 
194
                self.position_event(old, self._position)
 
195
            self.timer_schedule(offset, False)
 
196
        else:
 
197
            self._offset = offset
 
198
        
 
199
    def has_events(self):
 
200
        """Returns True if there are events left to play."""
 
201
        return bool(self._events) and self._position < len(self._events)
 
202
        
 
203
    def next_event(self):
 
204
        """(Private) Handles the current event and advances to the next.
 
205
        
 
206
        Returns the time in ms (not adjusted by tempo factor!) before
 
207
        next_event should be called again.
 
208
        
 
209
        If there is no event to handle anymore, returns 0.
 
210
        If this event was the last, calls finish() and returns 0.
 
211
        
 
212
        """
 
213
        if self.has_events():
 
214
            time, event = self._events[self._position]
 
215
            self.handle_event(time, event)
 
216
            self._position += 1
 
217
            if self._position < len(self._events):
 
218
                return self._events[self._position][0] - time
 
219
        return 0
 
220
    
 
221
    def handle_event(self, time, event):
 
222
        """(Private) Called for every event."""
 
223
        if event.midi:
 
224
            self.midi_event(event.midi)
 
225
        if event.time:
 
226
            self.time_event(time)
 
227
        if event.beat:
 
228
            self.beat_event(*event.beat)
 
229
    
 
230
    def midi_event(self, midi):
 
231
        """(Private) Plays the specified MIDI events.
 
232
        
 
233
        The format depends on the way MIDI events are stored in the Song.
 
234
        
 
235
        """
 
236
        if self._output:
 
237
            try:
 
238
                self._output.midi_event(midi)
 
239
            except BaseException as e:
 
240
                self.exception_event(e)
 
241
    
 
242
    def time_event(self, msec):
 
243
        """(Private) Called on every time update."""
 
244
    
 
245
    def beat_event(self, measnum, beat, num, den):
 
246
        """(Private) Called on every beat."""
 
247
    
 
248
    def start_event(self):
 
249
        """Called when playback is started."""
 
250
    
 
251
    def stop_event(self):
 
252
        """Called when playback is stopped by the user."""
 
253
        if self._output:
 
254
            self._output.all_sounds_off()
 
255
        
 
256
    def finish_event(self):
 
257
        """Called when a song reaches the end by itself."""
 
258
    
 
259
    def position_event(self, old, new):
 
260
        """Called when the user seeks while playing and the position changes.
 
261
        
 
262
        This means MIDI events are skipped and it might be necessary to 
 
263
        issue an all notes off command to the MIDI output.
 
264
        
 
265
        """
 
266
        if self._output:
 
267
            self._output.all_sounds_off()
 
268
    
 
269
    def exception_event(self, exception):
 
270
        """Called when an exception occurs while writing to a MIDI output.
 
271
        
 
272
        The default implementation stores the exception in self._last_exception
 
273
        and sets the output to None.
 
274
        
 
275
        """
 
276
        self._last_exception = exception
 
277
        self.set_output(None)
 
278
        
 
279
    def timer_midi_time(self):
 
280
        """Should return a continuing time value in msec, used while playing.
 
281
        
 
282
        The default implementation returns the time in msec from the Python
 
283
        time module.
 
284
        
 
285
        """
 
286
        return int(time.time() * 1000)
 
287
    
 
288
    def timer_schedule(self, delay, sync=True):
 
289
        """Schedules the upcoming event.
 
290
        
 
291
        If sync is False, don't look at the previous synchronisation time.
 
292
        
 
293
        """
 
294
        msec = delay / self._tempo_factor
 
295
        if sync:
 
296
            self._sync_time += msec
 
297
            msec = self._sync_time - self.timer_midi_time()
 
298
        else:
 
299
            self._sync_time = self.timer_midi_time() + msec
 
300
        self.timer_start(max(0, msec))
 
301
    
 
302
    def timer_start(self, msec):
 
303
        """Starts the timer to fire once, the specified msec from now."""
 
304
        self._timer = None
 
305
        self._timer = threading.Timer(msec / 1000.0, self.timer_timeout)
 
306
        self._timer.start()
 
307
 
 
308
    def timer_stop(self):
 
309
        """Stops the timer."""
 
310
        if self._timer:
 
311
            self._timer.cancel()
 
312
            self._timer = None
 
313
 
 
314
    def timer_offset(self):
 
315
        """Returns the time before the next event.
 
316
        
 
317
        This value is only useful while playing.
 
318
        
 
319
        """
 
320
        return int((self._sync_time - self.timer_midi_time()) * self._tempo_factor)
 
321
    
 
322
    def timer_start_playing(self):
 
323
        """Starts playing by starting the timer for the first upcoming event."""
 
324
        reset = self.current_time() == 0
 
325
        self._playing = True
 
326
        self.start_event()
 
327
        if reset and self._output:
 
328
            try:
 
329
                self._output.reset()
 
330
            except BaseException as e:
 
331
                self.exception_event(e)
 
332
        self.timer_schedule(self._offset, False)
 
333
    
 
334
    def timer_timeout(self):
 
335
        """Called when the timer times out.
 
336
        
 
337
        Handles an event and schedules the next.
 
338
        If the end of a song is reached, calls finish_event()
 
339
        
 
340
        """
 
341
        offset = self.next_event()
 
342
        if offset:
 
343
            self.timer_schedule(offset)
 
344
        else:
 
345
            self._offset = 0
 
346
            self._playing = False
 
347
            self.finish_event()
 
348
    
 
349
    def timer_stop_playing(self):
 
350
        self.timer_stop()
 
351
        self._offset = self.timer_offset()
 
352
        self._playing = False
 
353
        self.stop_event()
 
354
 
 
355
 
 
356
class Event(object):
 
357
    """Any event (MIDI, Time and/or Beat).
 
358
    
 
359
    Has three attributes that determine what the Player does:
 
360
    
 
361
    time: if True, time_event() is caled with the current music time.
 
362
    beat: None or (measnum, beat, num, den), then beat_event() is called.
 
363
    midi: If not None, midi_event() is called with the midi.
 
364
    
 
365
    """
 
366
    __slots__ = ['midi', 'time', 'beat']
 
367
    def __init__(self):
 
368
        self.midi = None
 
369
        self.time = None
 
370
        self.beat = None
 
371
 
 
372
    def __repr__(self):
 
373
        l = []
 
374
        if self.time:
 
375
            l.append('time')
 
376
        if self.beat:
 
377
            l.append('beat({0}:{1})'.format(self.beat[0], self.beat[1]))
 
378
        if self.midi:
 
379
            l.append('midi')
 
380
        return '<Event ' + ', '.join(l) + '>'
 
381
 
 
382
 
 
383
def make_event_list(song, time=None, beat=None):
 
384
    """Returns a list of all the events in Song.
 
385
    
 
386
    Each item is a two-tuple(time, Event).
 
387
    
 
388
    If time is given, a time event is generated every that many microseconds
 
389
    If beat is True, beat events are generated as well.
 
390
    MIDI events are always created.
 
391
    
 
392
    """
 
393
    d = collections.defaultdict(Event)
 
394
    
 
395
    for t, evs in song.music:
 
396
        d[t].midi = evs
 
397
    
 
398
    if time:
 
399
        for t in range(0, song.length+1, time):
 
400
            d[t].time = True
 
401
    
 
402
    if beat:
 
403
        for i in song.beats:
 
404
            d[i[0]].beat = i[1:]
 
405
    
 
406
    return [(t, d[t]) for t in sorted(d)]
 
407
 
 
408