Package speakeasy :: Module music_player
[hide private]
[frames] | no frames]

Source Code for Module speakeasy.music_player

  1  #!/usr/bin/env python 
  2   
  3  import pygame 
  4  import os 
  5  import time 
  6  from operator import itemgetter 
  7  import threading 
  8   
  9  # TODO: 
 10  #    - play needs block option impl. 
 11   
12 -class TimeReference:
13 RELATIVE = 0; 14 ABSOLUTE = 1;
15
16 -class PlayStatus:
17 STOPPED = 0; 18 PAUSED = 1; 19 PLAYING = 2;
20 21
22 -class MusicPlayer(object):
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 # Used to enforce Singleton pattern: 47 singletonInstanceRunning = False; 48 49 supportedFormats = ['ogg', 'wav']; 50 51 #-------------------------------- 52 # Initializer 53 #--------------- 54
55 - def __init__(self):
56 if MusicPlayer.singletonInstanceRunning: 57 raise RuntimeError("Must only instantiate MusicPlayer once; an instance is already running.") 58 else: 59 MusicPlayer.singletonInstanceRunning = True; 60 pygame.init(); 61 self.lock = threading.Lock(); 62 self.playStatus = PlayStatus.STOPPED; 63 self.currentAudioFormat = 'ogg'; 64 65 # Use the first available pygame user event number as 66 # a 'play ended naturally or via stop()' event: 67 self.PLAY_ENDED_EVENT = pygame.USEREVENT;
68 69 #-------------------------------- 70 # play 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 # Check type explicitly, b/c pygame's exceptions are 106 # not useful for incorrect type errors: 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 # Guaranteed that whatToPlay is string or file obj. 111 # Ensure existence of file: 112 try: 113 # Assume whatToPlay is a string: 114 if not os.path.exists(whatToPlay): 115 raise IOError("Music filename %s does not exist." % whatToPlay); 116 filename = whatToPlay; 117 except TypeError: 118 # Was a file object; check *it* for existence: 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 # Now filename has a legal file. Which audio format? 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 # Convert start time to msecs: 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 # Clear the event queue of any old 'done playing' 142 # events: 143 pygame.event.clear(self.PLAY_ENDED_EVENT); 144 # Ensure that pygame will queue such an event 145 # when done: 146 pygame.mixer.music.set_endevent(self.PLAY_ENDED_EVENT); 147 148 # Pygame play method wants total number of times 149 # to play the song; therefore: add 1. Start time is 150 # in seconds. If file is a .wav file, leave out the 151 # start time: 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 # stop 167 #--------------- 168
169 - def stop(self):
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 # pause 179 #--------------- 180
181 - def pause(self):
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 # unpause 194 #--------------- 195
196 - def unpause(self):
197 ''' 198 Unpause a paused song. 199 ''' 200 201 if self.playStatus != PlayStatus.PAUSED: 202 return; 203 with self.lock: 204 pygame.mixer.music.unpause(); 205 self.playStatus = PlayStatus.PLAYING;
206 207 #-------------------------------- 208 # setSoundVolume 209 #--------------- 210
211 - def setSoundVolume(self, volume):
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 # Lock if not already locked. Lock is already acquired if 224 # this call to setSoundVolume() did not originate from a 225 # client, but from a method within MusicPlayer(), which 226 # acquired the lock. In that case, remember that the lock 227 # was already set, so that we don't release it on exit: 228 wasUnlocked = self.lock.acquire(False); 229 230 pygame.mixer.music.set_volume(volume); 231 except: 232 pass 233 finally: 234 # Only release the lock if the call to this method came from 235 # a client of SoundPlayer, not from a method within SoundPlayer: 236 if wasUnlocked: 237 self.lock.release();
238 239 #-------------------------------- 240 # getSoundVolume 241 #--------------- 242
243 - def getSoundVolume(self):
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 # Lock if not already locked. Lock is already acquired if 252 # this call to setSoundVolume() did not originate from a 253 # client, but from a method within MusicPlayer(), which 254 # acquired the lock. In that case, remember that the lock 255 # was already set, so that we don't release it on exit: 256 wasUnlocked = self.lock.acquire(False); 257 258 return pygame.mixer.music.get_volume(); 259 finally: 260 # Only release the lock if the call to this method came from 261 # a client of SoundPlayer, not from a method within SoundPlayer: 262 if wasUnlocked: 263 self.lock.release();
264 265 #-------------------------------- 266 # setPlayhead 267 #--------------- 268
269 - def setPlayhead(self, secs, timeReference=TimeReference.ABSOLUTE):
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(); # fractional seconds 297 if timeReference == TimeReference.RELATIVE: 298 newAbsolutePlayheadPos = currentlyAt + secs; 299 else: 300 newAbsolutePlayheadPos = secs; 301 302 # New 'initial play position' is the target playhead position: 303 self.initialStartPos = newAbsolutePlayheadPos * 1000.0; 304 305 pygame.mixer.music.stop(); 306 self.play(self.loadedFile, startTime=newAbsolutePlayheadPos);
307 308 #-------------------------------- 309 # getPlayheadPosition 310 #--------------- 311
312 - def getPlayheadPosition(self):
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 # Get time played so far in msecs. Returns only time played, 323 # not considering start offset: 324 timePlayedSinceStart = pygame.mixer.music.get_pos() 325 # Add the start position (also kept in msgecs): 326 trueTimePlayed = timePlayedSinceStart + self.initialStartPos 327 328 # return seconds: 329 return trueTimePlayed / 1000.0;
330 331 #-------------------------------- 332 # currentlyPlaying() 333 #--------------- 334
335 - def getPlayStatus(self):
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 # ----------------------------------- Private Methods ---------------------------- 345 346 347 #-------------------------------- 348 # waitForSoundDone 349 #--------------- 350
351 - def waitForSongDone(self, timeout=None):
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 # formatSupported 377 #--------------- 378
379 - def formatSupported(self, fileExtension):
380 ''' 381 Checks whether the given file extension implies a supported 382 sound format. 383 @param fileExtension: file extension with or without leading period. Example: ".ogg" 384 @type fileExtension: string 385 @return: True if the format is supported, else False. 386 @rtype: boolean 387 @raise ValueError: if fileExtension is anything other than a string with length > 0. 388 ''' 389 if (fileExtension is None) or (not isinstance(fileExtension, basestring)) or (len(fileExtension) == 0): 390 raise ValueError("File format specification must be the format's file extension string."); 391 if fileExtension[0] == '.': 392 fileExtension = fileExtension[1:]; 393 return fileExtension in MusicPlayer.supportedFormats; 394 395 396 return fileExtension in MusicPlayer.supportedFormats;
397 398 399 # --------------------------------------- Testing ----------------------- 400 if __name__ == '__main__': 401 402 import os 403
404 - def playPauseUnpause():
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
420 - def testPlayheadMove():
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); # Play from 10sec mark on for 3secs 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 #testFileCottonFields = os.path.join(os.path.dirname(__file__), "../../sounds/music/cottonFields.wav"); 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 # player.play(testFileCottonFields, blockTillDone=True); 450 # time.sleep(10); 451 # player.stop(); 452 print "Done test existing song..." 453 print "---------------" 454 455 print "Test pause/unpause"; 456 # player.play(testFileCottonFields, blockTillDone=False); 457 # playPauseUnpause(); 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 # player.play(testFileCottonFields); 463 # testPlayheadMove(); 464 print "Done testing playhead settings." 465 print "---------------" 466 467 print "Test waitForSongDone()" 468 # print "Immediate return when stopped:..." 469 # player.waitForSongDone(); 470 # print "Immediate return when stopped OK." 471 # player.play(testFileRooster); 472 # print "Return when rooster done..." 473 # player.waitForSongDone(); 474 # print "Return when rooster done OK." 475 print "Wait for song end with 10 second timeout:" 476 # player.play(testFileCottonFields); 477 # player.waitForSongDone(timeout=10); 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 # player.play(testFileRooster, startTime=0.3, blockTillDone=True); 484 # # Plays from the 10sec mark for 10 seconds: 485 # player.play(testFileCottonFields, startTime=10.0); 486 # time.sleep(10); 487 # # This one would go past: 488 # player.play(testFileCottonFields, startTime=3600.0); 489 490 print "Done requesting play with start time longer than song:" 491 print "---------------" 492 493 print "All done" 494