1
2
3 import pygame
4 import os
5 import time
6 from operator import itemgetter
7 import threading
8
9
10
11
15
20
21
23 '''
24 Plays music files, currently ogg and wav. In contrast to SoundPlayer,
25 which is optimized for dealing with lots of short sounds, this facility
26 is for longer files, which are streamed, rather than loaded. Also in
27 contrast to SoundPlayer, MusicPlayer can only play one song at a time.
28
29 For ogg files the method setPlayhead() allows clients to move foward
30 and back within a song as it plays.
31
32 Public methods:
33
34 1. play()
35 2. pause()
36 3. unpause()
37 4. setSoundVolume()
38 5. getSoundVolume()
39 6. setPlayhead()
40 7. getPlayheadPosition()
41 8. getPlayStatus()
42
43 Requires pygame.
44 '''
45
46
47 singletonInstanceRunning = False;
48
49 supportedFormats = ['ogg', 'wav'];
50
51
52
53
54
68
69
70
71
72
73 - def play(self, whatToPlay, repeats=0, startTime=0.0, blockTillDone=False, volume=None):
74 '''
75 Play an .mp3 or .ogg file, or File class instance. Note that pygame does
76 not support startTime for .wav files. They will play, but startTime is ignored.
77 Offers choice of blocking return until the music is finished, or
78 returning immediately.
79
80 @param whatToPlay: Full path to .wav or .ogg file, or File instance.
81 @type whatToPlay: {string | File}
82 @param repeats: Number of times to repeat song after the first time. If -1: repeat forever, or until another song is played.
83 @type repeats: int
84 @param startTime: Time in seconds into the song to start the playback.
85 @type startTime: float
86 @param blockTillDone: True to delay return until music is done playing.
87 @type blockTillDone: boolean
88 @param volume: How loudly to play (0.0 to 1.0). None: current volume.
89 @type volume: float
90 @raise IOError: if given music file path does not exist, or some other playback error occurred.
91 @raise ValueError: if given volume is not between 0.0 and 1.0
92 @raise TypeError: if whatToPlay is not a filename (string), or Sound instance.
93 @raise NotImplementedError: if startTime is other than 0.0, but the underlying music engine does not support start time control.
94 '''
95
96 if (volume is not None) and ( (volume < 0.0) or (volume > 1.0) ):
97 raise ValueError("Volume must be between 0.0 and 1.0");
98
99 if not (isinstance(repeats, int) and (repeats >= -1)):
100 raise TypeError("Number of repeats must be an integer greater or equal to -1");
101
102 if not (isinstance(startTime, float) and (startTime >= 0.0)):
103 raise ValueError("Start time must be a float of value zero or greater.");
104
105
106
107 if not (isinstance(whatToPlay, basestring) or isinstance(whatToPlay, file)):
108 raise TypeError("Song must be a string or a Python file object.")
109
110
111
112 try:
113
114 if not os.path.exists(whatToPlay):
115 raise IOError("Music filename %s does not exist." % whatToPlay);
116 filename = whatToPlay;
117 except TypeError:
118
119 if not os.path.exists(whatToPlay.name):
120 raise IOError("Music filename %s does not exist." % whatToPlay.name);
121 filename = whatToPlay.name;
122
123
124 (fileBaseName, extension) = os.path.splitext(filename);
125 if extension.startswith("."):
126 extension = extension[1:]
127 if not extension in MusicPlayer.supportedFormats:
128 raise ValueError("Unsupported file format '%s'. Legal formats in order of feature power: %s." % (os.path.basename(filename),
129 str(MusicPlayer.supportedFormats)));
130 with self.lock:
131
132 self.currentAudioFormat = extension;
133
134 self.initialStartPos = startTime * 1000.0
135
136 pygame.mixer.music.load(filename);
137 self.loadedFile = filename;
138 if volume is not None:
139 self.setSoundVolume(volume);
140
141
142
143 pygame.event.clear(self.PLAY_ENDED_EVENT);
144
145
146 pygame.mixer.music.set_endevent(self.PLAY_ENDED_EVENT);
147
148
149
150
151
152 if filename.endswith('.wav'):
153 pygame.mixer.music.play(repeats+1);
154 else:
155 try:
156 pygame.mixer.music.play(repeats+1,startTime);
157 except:
158 self.playStatus = PlayStatus.STOPPED;
159 return;
160 self.playStatus = PlayStatus.PLAYING;
161
162 if blockTillDone:
163 self.waitForSongDone();
164
165
166
167
168
170 '''
171 Stop music if any is playing.
172 '''
173 with self.lock:
174 pygame.mixer.music.stop();
175 self.playStatus = PlayStatus.STOPPED;
176
177
178
179
180
182 '''
183 Pause either currently playing song, if any.
184 '''
185
186 if self.playStatus != PlayStatus.PLAYING:
187 return;
188 with self.lock:
189 pygame.mixer.music.pause();
190 self.playStatus = PlayStatus.PAUSED;
191
192
193
194
195
206
207
208
209
210
212 '''
213 Set sound playback volume.
214 @param volume: Value between 0.0 and 1.0.
215 @type volume: float
216 @raise TypeError: if volume is not a float.
217 @raise ValueError: if volume is not between 0.0 and 1.0
218 '''
219 if (volume is not None) and ( (volume < 0.0) or (volume > 1.0) ):
220 raise ValueError("Sound volume must be between 0.0 and 1.0. Was " + str(volume));
221
222 try:
223
224
225
226
227
228 wasUnlocked = self.lock.acquire(False);
229
230 pygame.mixer.music.set_volume(volume);
231 except:
232 pass
233 finally:
234
235
236 if wasUnlocked:
237 self.lock.release();
238
239
240
241
242
244 '''
245 Get currently set sound volume.
246 @return: Volume number between 0.0 and 1.0
247 @rtype: float
248 '''
249
250 try:
251
252
253
254
255
256 wasUnlocked = self.lock.acquire(False);
257
258 return pygame.mixer.music.get_volume();
259 finally:
260
261
262 if wasUnlocked:
263 self.lock.release();
264
265
266
267
268
270 '''
271 Set playhead to 'secs' seconds into the currently playing song. If nothing is being
272 played, this method has no effect. If a song is currently paused, the song will
273 be unpaused, continuing to play at the new playhead position.
274 @param secs: number of (possibly fractional) seconds to start into the song.
275 @type secs: float
276 @param timeReference: whether to interpret the secs parameter as absolute from the song start, or relative from current position.
277 Options are TimeRerence.ABSOLUTE and TimeRerence.RELATIVE
278 @type timeReference: TimeReference
279 @raise NotImplementedError: if called while playing song whose format does not support playhead setting in pygame.
280 '''
281
282 if self.currentAudioFormat != 'ogg':
283 raise NotImplementedError("Playhead setting is currently implemented only for the ogg format. Format of currently playing file: " +
284 str(self.currentAudioFormat));
285
286 if self.playStatus == PlayStatus.STOPPED:
287 return;
288
289 if not isinstance(secs, float):
290 raise ValueError("The playhead position must be a positive float. Instead it was: " + str(secs));
291
292 if (timeReference == TimeReference.ABSOLUTE) and (secs < 0.0):
293 raise ValueError("For absolute playhead positioning, the playhead position must be a positive float. Instead it was: " + str(secs));
294
295 with self.lock:
296 currentlyAt = self.getPlayheadPosition();
297 if timeReference == TimeReference.RELATIVE:
298 newAbsolutePlayheadPos = currentlyAt + secs;
299 else:
300 newAbsolutePlayheadPos = secs;
301
302
303 self.initialStartPos = newAbsolutePlayheadPos * 1000.0;
304
305 pygame.mixer.music.stop();
306 self.play(self.loadedFile, startTime=newAbsolutePlayheadPos);
307
308
309
310
311
313 '''
314 Return number of (possibly fractional) seconds to where the current
315 song is currently playing. If currently playing nothing, return 0.0.
316 @return: number of fractional seconds where virtual playhead is positioned.
317 @rtype: float
318 '''
319 if pygame.mixer.music.get_busy() != 1:
320 return 0.0;
321
322
323
324 timePlayedSinceStart = pygame.mixer.music.get_pos()
325
326 trueTimePlayed = timePlayedSinceStart + self.initialStartPos
327
328
329 return trueTimePlayed / 1000.0;
330
331
332
333
334
336 '''
337 Return one of PlayStatus.STOPPED, PlayStatus.PLAYING, PlayStatus.PAUSED to
338 reflect what the player is currently doing.
339 '''
340 if pygame.mixer.music.get_busy() != 1:
341 self.playStatus = PlayStatus.STOPPED;
342 return self.playStatus;
343
344
345
346
347
348
349
350
352 '''
353 Block until song is done playing. Used in play() method.
354 @param timeout: Maximum time to wait in seconds.
355 @type timeout: {int | float}
356 @return: True if song ended, False if timeout occurred.
357 @rtype: boolean
358 '''
359 if self.getPlayStatus() == PlayStatus.STOPPED:
360 return;
361 if timeout is not None:
362 startTime = time.time();
363 while (pygame.mixer.music.get_busy() == 1) and ((time.time() - startTime) < timeout):
364 time.sleep(0.3);
365 if pygame.mixer.music.get_busy() == 0:
366 return True;
367 else:
368 return False;
369 else:
370 while (pygame.mixer.music.get_busy() == 1):
371 time.sleep(0.3);
372 return True;
373
374
375
376
377
378
397
398
399
400 if __name__ == '__main__':
401
402 import os
403
405 while player.getPlayStatus() != PlayStatus.STOPPED:
406 time.sleep(2);
407 print "First pause...";
408 player.pause();
409 time.sleep(3);
410 player.unpause();
411 time.sleep(2);
412 print "Second pause...";
413 player.pause();
414 time.sleep(3);
415 player.unpause();
416 print "Stopping playback."
417 player.stop();
418
419
421 while player.getPlayStatus() != PlayStatus.STOPPED:
422 time.sleep(3.0);
423 player.pause();
424 print "Playhead position after 3 seconds: " + str(player.getPlayheadPosition());
425 time.sleep(2.0);
426 print "Set Playhead position to 10 seconds and play...";
427 player.setPlayhead(10.0);
428 time.sleep(3.0);
429 print "Set Playhead position back to 10 seconds without pausing...";
430 player.setPlayhead(10.0);
431 time.sleep(3.0);
432 player.pause();
433 print "Playhead position: " + str(player.getPlayheadPosition());
434 time.sleep(2.0);
435 print "set playhead position to -3, which should be 10 again...";
436 player.setPlayhead(-3.0, TimeReference.RELATIVE);
437 print "Playhead position after relative setting back to 10: " + str(player.getPlayheadPosition());
438 time.sleep(3.0);
439 print "Stopping."
440 player.stop();
441
442
443
444 testFileCottonFields = os.path.join(os.path.dirname(__file__), "../../sounds/music/cottonFields.ogg");
445 testFileRooster = os.path.join(os.path.dirname(__file__), "../../sounds/rooster.wav");
446
447 player = MusicPlayer();
448 print "Test existing song."
449
450
451
452 print "Done test existing song..."
453 print "---------------"
454
455 print "Test pause/unpause";
456
457
458 print "Done testing pause/unpause";
459 print "---------------"
460
461 print "Test playhead settings. Expect: Play from start for 3sec, play from 10 for 3sec, play from 10 again for 3sec. Stop."
462
463
464 print "Done testing playhead settings."
465 print "---------------"
466
467 print "Test waitForSongDone()"
468
469
470
471
472
473
474
475 print "Wait for song end with 10 second timeout:"
476
477
478
479 print "Done testing waitForSongDone()"
480 print "---------------"
481
482 print "Request play of .wav file with startTime (which should be ignored), and with start time longer than song:"
483
484
485
486
487
488
489
490 print "Done requesting play with start time longer than song:"
491 print "---------------"
492
493 print "All done"
494