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

Source Code for Module speakeasy.sound_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   
  10  # Modify to allow more than 8 simultaneous sounds: 
  11  NUM_SIMULTANEOUS_SOUNDS = 8; 
  12  NUM_OF_CACHED_SOUNDS    = 500; 
  13   
14 -class SoundPlayer(object):
15 ''' 16 Plays up to NUM_SIMULTANEOUS_SOUNDS .wav/.ogg files simultaneously. Allows pause/unpause on 17 any of these sounds. (.ogg untested) 18 19 Theory of operation: The class uses the pygame module. This module involves Sound instances 20 and Channel instances. A Sound is created from a .wav or .ogg file, and can play on one or 21 more Channel instances. 22 23 All channels can simultaneously play, one Sound per channel. The 24 number of channels can be set via the module-global constant 25 NUM_SIMULTANEOUS_SOUNDS. Default is 8. Channels and Sounds are managed 26 by pygame.mixer. 27 28 This class uses parameter polymorphism to unify all these concepts as 29 much as possible. The only public methods are 30 <code>play(), stop(), pause(), unpause(),set_volume(), 31 get_volume()</code>. Each of these methods takes either the path to 32 a sound file, a Sound instances, or a Channel instance. The 33 SoundPlayer takes care of creating Sound instances by loading them 34 from files, caching sounds, and tracking which Sounds are playing on 35 which Channel at any time. Callers of these methods need to deal only 36 with the sound file paths. These paths, when passed to, say, the 37 pause() method, will do the right thing; Sound instances are cached, 38 so they will not be loaded repeatedly. 39 40 Note that this wonderful collapsing of complex underlying pygame 41 concepts comes at a price in code ugliness. Since Python does not 42 have parameter based polymorphism, methods must figure out incoming 43 parameter types via duck typing methods: Treat the parameters as some 44 type and see whether an error occurs. Terrible, but the 'pythonic' way. 45 46 47 As background: The pygame API provides methods three entities: 48 Mixer, Channel, and Sound. The main methods are: 49 50 1. Mixer: 51 - stop() 52 - pause() 53 - unpause() 54 - get_num_channels() 55 - set_num_channels() 56 2. Channel: 57 - play() 58 - stop() 59 - pause() 60 - unpause() 61 3. Sound: 62 - play() 63 - stop() 64 - pause() 65 - unpause() 66 ''' 67 68 # Used to enforce Singleton pattern: 69 singletonInstanceRunning = False; 70 71 supportedFormats = ['ogg', 'wav']; 72 73 #-------------------------------- 74 # Initializer 75 #--------------- 76
77 - def __init__(self):
78 if SoundPlayer.singletonInstanceRunning: 79 raise RuntimeError("Must only instantiate SoundPlayer once; an instance is already running.") 80 else: 81 SoundPlayer.singletonInstanceRunning = True; 82 pygame.mixer.init(); 83 self.lock = threading.Lock(); 84 self.loadedSounds = {}; # Map filenames to Sound instances 85 self.loadedFilenames = {} # Map Sound instances to filenames 86 self.soundChannelBindings = {}; 87 self.lastUsedTime = {}; 88 self.globalVolume = 0.99; 89 pygame.mixer.set_num_channels(NUM_SIMULTANEOUS_SOUNDS);
90 91 #-------------------------------- 92 # play 93 #--------------- 94
95 - def play(self, whatToPlay, blockTillDone=False, volume=None):
96 ''' 97 Play a file, or an already loaded Sound instance. 98 Offers choice of blocking return until the sound is finished, or 99 returning immediately. 100 101 @param whatToPlay: Full path to .wav or .ogg file, or Sound instance. 102 @type whatToPlay: {string | Sound} 103 @param blockTillDone: True to delay return until sound is done playing. 104 @type blockTillDone: boolean 105 @param volume: Volume to play at. Float between 0.0 and 1.0. None: use current volume. 106 @type volume: float 107 @return: The sound instance. 108 @raise IOError: if given sound file path does not exist, or some other playback error occurred. 109 @raise ValueError: if given volume is not between 0.0 and 1.0 110 @raise TypeError: if whatToPlay is not a filename (string), or Sound instance. 111 ''' 112 113 if volume is None: 114 volume = self.globalVolume; 115 elif (volume is not None) and ( (volume < 0.0) or (volume > 1.0) ): 116 raise ValueError("Volume must be between 0.0 and 1.0"); 117 118 with self.lock: 119 self.cleanupSoundChannelBindings(); 120 121 try: 122 # Play a file? 123 (sound, channel) = self.playFile(whatToPlay, volume); 124 # Remember that this sound is now playing on that channel: 125 if channel is not None: 126 self.addSoundToChannelBinding(sound, channel); 127 self.lastUsedTime[sound] = time.time(); 128 else: 129 raise IOError("Could not play sound '" + str(whatToPlay) + "'"); 130 except TypeError: 131 # Nope, not a file, must be a Sound instance or something illegal: 132 try: 133 # Hypothesis: whatToPlay is a Sound instance: 134 sound = whatToPlay; 135 channel = sound.play(); 136 self.addSoundToChannelBinding(sound, channel); 137 self.lastUsedTime[sound] = time.time(); 138 except AttributeError: 139 # whatToPlay is not a file path (i.e. string), nor a Sound instance: 140 raise TypeError("Play method takes the path to a sound file, or a Sound instance. Instead received:" + 141 str(whatToPlay)); 142 143 # At this point, sound and channel vars are correctly set. 144 # Caller wants to block till sound done? If so, we release 145 # the lock (exit the 'with' block), and hang out: 146 if blockTillDone: 147 self.waitForSoundDone(sound) 148 with self.lock: 149 # Protect the sound-channel binding data structure: 150 self.cleanupSoundChannelBindings(); 151 return sound;
152 153 #-------------------------------- 154 # stop 155 #--------------- 156
157 - def stop(self, whatToStop=None):
158 ''' 159 Stop either all currently playing sounds, or a particular sound. 160 The sound to stop (parameter whatToStop) may be specified as a 161 the full-path filename of the respective .wav/.ogg file, as a 162 Sound instance, or as a Channel instance. 163 @param whatToStop: If None, top all currently playing sounds. If any a 164 Sound instance, stop this sound on all channels on which 165 it might currently be playing. If a Channel instance, 166 stop only whatever is currently playing on this channel. 167 @type whatToStop: {NoneType | string | Sound | Channel} 168 ''' 169 with self.lock: 170 try: 171 if whatToStop is None: 172 pygame.mixer.stop(); 173 return; 174 try: 175 self.stopFile(whatToStop); 176 return; 177 except TypeError: 178 pass 179 180 # Must be a Sound or Channel instance, which 181 # support stop() (or something illegal): 182 try: 183 whatToStop.stop(); 184 except: 185 raise TypeError("Parameter whatToStop must be a filename, Sound instance, or Channel instance.") 186 finally: 187 self.cleanupSoundChannelBindings();
188 189 #-------------------------------- 190 # pause 191 #--------------- 192
193 - def pause(self, whatToPause=None):
194 ''' 195 Pause either all currently playing sounds, or a particular sound. 196 The sound to pause (parameter whatToStop) may be specified as a 197 the full-path filename of the respective .wav/.ogg file, as a 198 Sound instance, or as a Channel instance. 199 @param whatToPause: If None, top all currently playing sounds. If any a 200 Sound instance, pause this sound on all channels on which 201 it might currently be playing. If a Channel or array of 202 channel instances, pause whatever is currently playing 203 on the given channel(s). 204 @type whatToPause: {NoneType | string | Sound | Channel | [Channel]} 205 @raise TypeError: if whatToPause is of illegal type. 206 ''' 207 208 with self.lock: 209 self.cleanupSoundChannelBindings(); 210 211 if whatToPause is None: 212 # Pause everything: 213 pygame.mixer.pause(); 214 return 215 216 try: 217 # Pause by filename? 218 if self.pauseFile(whatToPause) is not None: 219 # whatToPause was a filename, but it wasn't 220 # playing, or Pause succeeded. Either way 221 # we're done. 222 return; 223 except TypeError: 224 # whatToPause is not a filename: 225 pass; 226 227 # whatToPause is a Channel or Sound instance: 228 # Is it a Sound instance? 229 channels = self.getChannelsFromSound(whatToPause); 230 if channels is None: 231 # No sound bound to whatToPause is playing on any channel. 232 # So maybe whatToUnPause is a channel or array of channels. 233 # Ensure that we have an array: 234 channels = whatToPause; 235 try: 236 len(channels) 237 except TypeError: 238 channels = [whatToPause]; 239 240 # Must be an array of channels at this point, or something illegal: 241 try: 242 for channel in channels: 243 if channel.get_busy(): 244 channel.pause(); 245 except: 246 # The passed-in whatToPause is neither, string (i.e. filename), 247 # nor Sound instance, nor Channel instance. Chastise caller: 248 raise TypeError("Parameter whatToPause must be a filename, Sound instance, Channel instance, or iterable of channel instances.") 249 return;
250 251 #-------------------------------- 252 # unpause 253 #--------------- 254
255 - def unpause(self, whatToUnPause=None):
256 ''' 257 Unpause either all currently playing sounds, or a particular sound. 258 The sound to unpause (parameter whatToStop) may be specified as a 259 the full-path filename of the respective .wav/.ogg file, as a 260 Sound instance, or as a Channel instance. 261 @param whatToUnPause: If None, unpause all currently playing sounds. If whatToUnPause 262 is a Sound instance, pause this sound on all channels on which 263 it might currently be playing. If whatToUnPause is a Channel or array of 264 channel instances, pause whatever is currently playing 265 on the given channel(s). 266 @type whatToUnPause: {NoneType | string | Sound | Channel} 267 ''' 268 269 with self.lock: 270 self.cleanupSoundChannelBindings(); 271 272 if whatToUnPause is None: 273 # Unpause everything: 274 pygame.mixer.unpause(); 275 return 276 277 try: 278 # Unpause by filename? 279 if self.unpauseFile(whatToUnPause) is not None: 280 # whatToUnPause was a filename, but it wasn't 281 # playing, or Pause succeeded. Either way 282 # we're done. 283 return; 284 except TypeError: 285 # whatToUnPause is not a filename: 286 pass; 287 288 # whatToUnPause is a Channel or Sound instance: 289 # Is it a Sound? 290 channels = self.getChannelsFromSound(whatToUnPause); 291 if channels is None: 292 # No sound that is whatToUnPause is playing on any channel. 293 # So maybe whatToUnPause is a Channel instance or array: 294 # Ensure that we have an array: 295 channels = whatToUnPause; 296 try: 297 len(channels) 298 except TypeError: 299 channels = [whatToUnPause]; 300 301 try: 302 for channel in channels: 303 if channel.get_busy(): 304 channel.unpause(); 305 except: 306 # The passed-in whatToUnPause is neither, string (i.e. filename), 307 # nor Sound instance, nor Channel instance. Chastise caller: 308 raise TypeError("Parameter whatToUnPause must be a filename, Sound instance, or Channel instance.") 309 return;
310 311 #-------------------------------- 312 # setSoundVolume 313 #--------------- 314
315 - def setSoundVolume(self, volume, whatToSetVolFor=None):
316 ''' 317 Set sound volume for a particular sound or channel. 318 The entity for which to set the volume (i.e. whatToSetVolFor) 319 may be the full-path filename of a .wav/.ogg file, a Sound instance, 320 or a Channel instance. Here is the interaction between settings 321 of Sound vs. Channel volume: 322 - sound.set_volume(0.9) # Now plays at 90% of full volume. 323 - sound.set_volume(0.6) # Now plays at 60% (previous value replaced). 324 - channel.set_volume(0.5) # Now plays at 30% (0.6 * 0.5). 325 Passing in a filename will load the file, and set the volume of the 326 corresponding Sound. 327 @param volume: Value between 0.0 and 1.0. 328 @type volume: float 329 @param whatToSetVolFor: The soundfile, Sound, or Channel instance whose volume to set. 330 If None, sets volume for all sounds. (This setting is overridden 331 by volume settings provided in calls to the play() method. 332 @type whatToSetVolFor: {NoneType | string | Sound | Channel} 333 @raise OSError: if given filename that does not exist. 334 ''' 335 336 if (volume is not None) and ( (volume < 0.0) or (volume > 1.0) ): 337 raise ValueError("Sound volume must be between 0.0 and 1.0. Was " + str(volume)); 338 339 try: 340 # Lock if not already locked. Lock is already acquired if 341 # this call to setSoundVolume() did not originate from a 342 # client, but from a method within SoundPlayer(), which 343 # acquired the lock. In that case, remember that the lock 344 # was already set, so that we don't release it on exit: 345 wasUnlocked = self.lock.acquire(False); 346 347 if whatToSetVolFor is None: 348 self.globalVolume = volume; 349 return; 350 351 # If whatToSetVolFor is a Sound or Channel instance, 352 # a set_volume() method call will work: 353 try: 354 whatToSetVolFor.set_volume(volume); 355 return; 356 except: 357 # Was not a sound or channel instance 358 pass 359 360 # whatToSetVolFor must be a filename (or something illegal). 361 # Is this sound cached? 362 sound = self.getSoundFromFileName(whatToSetVolFor); 363 if sound is not None: 364 # Yep, set the Sound instance's volume: 365 sound.set_volume(volume); 366 return; 367 else: 368 # Try loading the sound. Will barf with OSError if file not found: 369 sound = self.loadSound(whatToSetVolFor); 370 sound.set_volume(volume); 371 return; 372 except: 373 pass; 374 finally: 375 # Only release the lock if the call to this method came from 376 # a client of SoundPlayer, not from a method within SoundPlayer: 377 if wasUnlocked: 378 self.lock.release();
379 380 #-------------------------------- 381 # getSoundVolume 382 #--------------- 383
384 - def getSoundVolume(self, whatToGetVolFor=None):
385 ''' 386 Get sound volume for a particular sound or channel. 387 The entity for which to get the volume for (i.e. whatToSetVolFor) 388 may be the full-path filename of a .wav/.ogg file, a Sound instance, 389 or a Channel instance. Here is the interaction between settings 390 of Sound vs. Channel volume: 391 - sound.set_volume(0.9) # Now plays at 90% of full volume. 392 - sound.set_volume(0.6) # Now plays at 60% (previous value replaced). 393 - channel.set_volume(0.5) # Now plays at 30% (0.6 * 0.5). 394 Passing in a filename will load the file, and get the volume of the 395 corresponding Sound. 396 397 @param whatToGetVolFor: The soundfile, Sound, or Channel instance whose volume to get. 398 If None, return global value setting 399 @type whatToGetVolFor: {NoneType | string | Sound | Channel} 400 @raise OSError: if given filename that does not exist. 401 ''' 402 403 404 try: 405 # Lock if not already locked. Lock is already acquired if 406 # this call to getSoundVolume() did not originate from a 407 # client, but from a method within SoundPlayer(), which 408 # acquired the lock. In that case, remember that the lock 409 # was already set, so that we don't release it on exit: 410 wasUnlocked = self.lock.acquire(False); 411 412 # Asking for global volume? 413 if whatToGetVolFor is None: 414 return self.globalVolume; 415 416 # If whatToGetVolFor is a Sound or Channel instance, 417 # a get_volume() method call will work: 418 try: 419 return whatToGetVolFor.get_volume(); 420 except: 421 pass 422 423 # whatToGetVolFor must be a filename (or something illegal): 424 sound = self.getSoundFromFileName(whatToGetVolFor); 425 if sound is not None: 426 return sound.get_volume(); 427 else: 428 # Try loading the sound. Will barf with OSError if file not found: 429 sound = self.loadSound(whatToGetVolFor); 430 return sound.get_volume(); 431 except: 432 pass 433 finally: 434 # Only release the lock if the call to this method came from 435 # a client of SoundPlayer, not from a method within SoundPlayer: 436 if wasUnlocked: 437 self.lock.release();
438 439 #-------------------------------- 440 # formatSupported 441 #--------------- 442
443 - def formatSupported(self, fileExtension):
444 ''' 445 Checks whether the given file extension implies a supported 446 sound format. 447 @param fileExtension: file extension with or without leading period. Example: ".ogg" 448 @type fileExtension: string 449 @return: True if the format is supported, else False. 450 @rtype: boolean 451 @raise ValueError: if fileExtension is anything other than a string with length > 0. 452 ''' 453 if (fileExtension is None) or (not isinstance(fileExtension, basestring)) or (len(fileExtension) == 0): 454 raise ValueError("File format specification must be the format's file extension string."); 455 if fileExtension[0] == '.': 456 fileExtension = fileExtension[1:]; 457 return fileExtension in SoundPlayer.supportedFormats;
458 459 460 #-------------------------------- 461 # numChannels 462 #--------------- 463
464 - def numChannels(self):
465 ''' 466 Number of sounds to play simultaneously. Controlled by module variable NUM_SIMULTANEOUS_SOUNDS. 467 ''' 468 return pygame.mixer.get_num_channels();
469 470 # ----------------------------------- Private Methods ---------------------------- 471 472 #-------------------------------- 473 # loadSound 474 #--------------- 475
476 - def loadSound(self, filename):
477 ''' 478 Create a Sound instance from the given file. Cache the sound, 479 and initialize the LRU cache last-used time. 480 @param filename: Full path to sound file (.wav/.ogg) 481 @type filename: string 482 @raise IOError: if file does not exist. 483 ''' 484 try: 485 # Already loaded? 486 return self.loadedSounds[filename]; 487 except KeyError: 488 if not os.path.exists(filename): 489 raise IOError("Sound file " + str(filename) + " does not exist."); 490 sound = pygame.mixer.Sound(filename); 491 # See whether cache is full, (and make room if not): 492 self.checkCacheStatus(); 493 self.loadedSounds[filename] = sound; 494 self.loadedFilenames[sound] = filename; 495 # Initialize last-used time for this sound to 496 # be load time. Whenever the sound is played, 497 # this time will be updated: 498 self.lastUsedTime[sound] = time.time(); 499 return sound;
500 501 #-------------------------------- 502 # checkCacheStatus 503 #--------------- 504
505 - def checkCacheStatus(self):
506 ''' 507 Ensures that number of cached sounds does not exceed 508 NUM_OF_CACHED_SOUNDS. If it does, half the cache is 509 emptied in order of least-recently-used first. 510 ''' 511 512 if len(self.loadedSounds.keys()) < NUM_OF_CACHED_SOUNDS: 513 return; 514 515 # Must unload some sounds by removing references to half of 516 # the Sound instances we loaded. We go by LRU discipline. 517 # From the dict {sound:time-last-used} get a list of 518 # (sound,time-last-used) sorted by descending time. 519 # The 'itemgetter(1)' is a special operator imported above 520 # from the operator module. It expects one element of the sort 521 # to be passed in, and returns the key to sort on. In this 522 # case the actuals will be sound/time pairs, and the operator 523 # returns the time part: 524 525 soundTimePairList = sorted(self.lastUsedTime.items(), key=itemgetter(1)); 526 for soundTimePair in soundTimePairList[:NUM_OF_CACHED_SOUNDS / 2]: 527 self.unloadSound(soundTimePair[0]);
528 529 #-------------------------------- 530 # unloadSound 531 #--------------- 532
533 - def unloadSound(self, sound):
534 ''' 535 Remove Sound instance from cache and all other references. 536 @param sound: Sound instance to remove 537 @type sound: Sound 538 ''' 539 540 soundFilename = None 541 # If this sound playing on one or more channels, stop them all 542 # before unloading: 543 544 channels = self.getChannelsFromSound(sound); 545 if channels is not None: 546 self.stop(sound); 547 548 try: 549 soundFilename = self.loadedFilenames[sound]; 550 del self.loadedSounds[soundFilename]; 551 except KeyError: 552 # Already unloaded, but clear out other 553 # possible references anyway: 554 pass; 555 556 if soundFilename is not None: 557 try: 558 del self.loadedFilenames[sound] 559 except: 560 pass; 561 562 try: 563 del self.soundChannelBindings[sound]; 564 except KeyError: 565 # Already unloaded, but clear out other 566 # possible references anyway: 567 pass; 568 try: 569 del self.lastUsedTime[sound]; 570 except KeyError: 571 pass; 572 573 return;
574 575 #-------------------------------- 576 # playFile 577 #--------------- 578
579 - def playFile(self, filename, volume=None):
580 ''' 581 Private Method! Called by play(); 582 Given a filename, load it into a Sound instance, if it is not 583 already cached. Set the volume, if given, and play. It is assumed 584 that the volume value, if given, has been verified by the caller 585 to be between 0.0 and 1.0. 586 @param filename: Name of sound file to play 587 @type filename: string 588 @param volume: Volume to play at: 0.0 to 1.0 589 @type volume: float 590 @return: Sound and channel instances 591 @rtype: (Sound, Channel) 592 @raise OSError: if file does not exist. 593 ''' 594 if not isinstance(filename, basestring): 595 raise TypeError("Filename must be a string.") 596 597 sound = self.getSoundFromFileName(filename); 598 if sound is None: 599 sound = self.loadSound(filename); 600 if volume is not None: 601 self.setSoundVolume(volume, sound); 602 channel = sound.play(); 603 return (sound, channel);
604 605 #-------------------------------- 606 # stopFile 607 #--------------- 608
609 - def stopFile(self, filename):
610 ''' 611 Private Method! Called by stop(); 612 Stops currently playing sound from the given filename. 613 Does nothing if this file is not currently playing. 614 @param filename: Filename from which the Sound instance to be stopped was created. 615 @type filename: string 616 @raise TypeError: filename is not a string. 617 ''' 618 619 if not isinstance(filename, basestring): 620 raise TypeError("Filename must be a string.") 621 sound = self.getSoundFromFileName(filename); 622 if sound is None: 623 # This file can't be playing, b/c it's not loaded: 624 return None; 625 sound.stop(); 626 return sound;
627 628 #-------------------------------- 629 # pauseFile 630 #--------------- 631
632 - def pauseFile(self, filename):
633 ''' 634 Private Method! Called by pause(); 635 Pauses currently playing sound from the given filename. 636 Does nothing if this file is not currently playing. 637 @param filename: Filename from which the Sound instance to be paused was created. 638 @type filename: string 639 @return: Sound instance 640 @raise TypeError: filename is not a string. 641 ''' 642 if not isinstance(filename, basestring): 643 raise TypeError("Filename must be a string.") 644 645 sound = self.getSoundFromFileName(filename); 646 if sound is None: 647 # This file can't be playing, b/c it's not loaded. 648 # Not an error condition, but all done with the pause operation: 649 return True; 650 # Sound exists in cache; get the all the Channel instances that are currently 651 # playing that Sound instance: 652 channels = self.getChannelsFromSound(sound); 653 if channels is None: 654 # Sound is loaded, but not currently playing. 655 # Not an error, but all done with the pause operation: 656 return True; 657 for channel in channels: 658 channel.pause(); 659 return sound;
660 661 #-------------------------------- 662 # unpauseFile 663 #--------------- 664
665 - def unpauseFile(self, filename):
666 ''' 667 Unpauses currently paused sound from the given filename. 668 Does nothing if this file is not currently playing or paused. 669 @param filename: Filename from which the Sound instance to be unpaused was created. 670 @type filename: string 671 @return: Sound instance 672 @rtype: Sound 673 @raise TypeError: filename is not a string. 674 ''' 675 676 if not isinstance(filename, basestring): 677 raise TypeError("Filename must be a string.") 678 679 sound = self.getSoundFromFileName(filename); 680 if sound is None: 681 # This file can't be playing, b/c it's not loaded: 682 # Not an error, but all done with the unpause operation: 683 return True; 684 channels = self.getChannelsFromSound(sound); 685 if channels is None: 686 # Not an error, but all done with the pause operation: 687 return True; 688 for channel in channels: 689 channel.unpause(); 690 return sound;
691 692 #-------------------------------- 693 # getSoundFromFileName 694 #--------------- 695
696 - def getSoundFromFileName(self, filename):
697 ''' 698 Given a sound file path name, return the corresponding 699 Sound instance, if the file was loaded. Else return None. 700 @param filename: Filename from which the Sound instance was created. 701 @type filename: string 702 @return: Sound instance created from the given file. None if file not yet loaded. 703 @rtype: {Sound | NoneType} 704 ''' 705 try: 706 return self.loadedSounds[filename] 707 except KeyError: 708 # This file hasn't been loaded: 709 return None;
710 711 #-------------------------------- 712 # getSoundFromChannel 713 #--------------- 714
715 - def getSoundFromChannel(self, channel):
716 ''' 717 Return Sound instance that is currently playing on the given Channel instance. 718 None if channel is inactive. 719 @param channel: Channel to be investigated. 720 @type channel: Channel 721 ''' 722 if not channel.get_busy(): 723 return None; 724 return channel.get_sound();
725 726 #-------------------------------- 727 # getChannelsFromSound 728 #--------------- 729
730 - def getChannelsFromSound(self, sound):
731 ''' 732 Return all channels on which the given Sound instance 733 is currently playing. 734 @param sound: Sound instance to be hunted down. 735 @type sound: Sound 736 @return: Array of Channel instances, or None, if Sound is not currently playing on any channel. 737 @rtype: {[Channel] | NoneType} 738 ''' 739 self.cleanupSoundChannelBindings(); 740 try: 741 return self.soundChannelBindings[sound]; 742 except (KeyError, TypeError): 743 return None;
744 745 #-------------------------------- 746 # addSoundToChannelBinding 747 #--------------- 748
749 - def addSoundToChannelBinding(self, sound, channel):
750 ''' 751 Register that a Sound is beginning to play on a given Channel. 752 @param sound: Sound to bind 753 @type sound: Sound 754 @param channel: Channel to bind to 755 @type channel: Channel 756 ''' 757 758 self.cleanupSoundChannelBindings(); 759 760 # Is this sound already playing on some channel? 761 channels = self.getChannelsFromSound(sound); 762 if channels is not None: 763 # Add this channel to the ones that are already playing this sound: 764 channels.append(channel); 765 self.soundChannelBindings[sound] = channels; 766 else: 767 self.soundChannelBindings[sound] = [channel];
768 769 #-------------------------------- 770 # cleanupSoundChannelBindings 771 #--------------- 772
774 ''' 775 Runs through the sound-to-channel bindings and removes 776 the entries of channels that are done playing. 777 ''' 778 maybePlayingSounds = self.soundChannelBindings.keys(); 779 for sound in maybePlayingSounds: 780 # Get all the channels that are currently playing this sound: 781 channels = self.soundChannelBindings[sound]; 782 # Find all channels that are no longer busy: 783 channelsCopy = list(channels); 784 for channel in channelsCopy: 785 if not channel.get_busy(): 786 channels.remove(channel); 787 if len(channels) != 0: 788 self.soundChannelBindings[sound] = channels; 789 else: 790 del self.soundChannelBindings[sound];
791 792 #-------------------------------- 793 # waitForSoundDone 794 #--------------- 795
796 - def waitForSoundDone(self, sound):
797 ''' 798 Block until sound is done playing on all channels 799 on which it is currently playing. 800 @param sound: Sound to monitor 801 @type sound: Sound 802 ''' 803 # Find all channels the Sound instance is currently playing on: 804 channels = self.getChannelsFromSound(sound); 805 if channels is None: 806 return; 807 for channel in channels: 808 while channel.get_busy(): 809 time.sleep(0.3);
810 811 812 # --------------------------------------- Testing ----------------------- 813 if __name__ == '__main__': 814 815 import os 816
817 - def playPauseUnpause(channel, whatToPause):
818 while channel.get_busy(): 819 time.sleep(1); 820 if not channel.get_busy(): 821 break; 822 player.pause(whatToPause); 823 time.sleep(3); 824 player.unpause(whatToPause);
825 826 testFileRooster = os.path.join(os.path.dirname(__file__), "../../sounds/rooster.wav"); 827 testFileSeagulls = os.path.join(os.path.dirname(__file__), "../../sounds/seagulls_shore.wav"); 828 testFileDrill = os.path.join(os.path.dirname(__file__), "../../sounds/drill.wav"); 829 testFileMoo2 = os.path.join(os.path.dirname(__file__), "../../sounds/moo.wav"); 830 testFileSteam = os.path.join(os.path.dirname(__file__), "../../sounds/steam.wav"); 831 832 player = SoundPlayer(); 833 print "Test one sound..." 834 channel = player.play(testFileRooster, blockTillDone=True); 835 print "Done test one sound..." 836 print "---------------" 837 838 # Test all-mixer pause: Play sound for 1 second, pause for 3 sec, play to completion: 839 print "Test pause/unpause all channels. Expect 1sec sound, 3sec pause, 1 second sound, 3sec pause, rest of sound..."; 840 # channel = player.play(testFileRooster, blockTillDone=False); 841 # playPauseUnpause(channel, None) 842 print "Done pause/unpause all channels."; 843 print "---------------" 844 845 print "Test pause/unpause by filename. Expect repeated cycles of 1sec sound, 3sec pause..."; 846 # channel = player.play(testFileSeagulls, blockTillDone=False); 847 # playPauseUnpause(channel, testFileSeagulls); 848 print "Done test pause/unpause by filename."; 849 print "---------------" 850 851 print "Test two sounds after another: seagulls, then rooster..." 852 # channelSeagulls = player.play(testFileSeagulls, blockTillDone=True); 853 # channelRooster = player.play(testFileRooster, blockTillDone=False); 854 # while channelRooster.get_busy() or channelSeagulls.get_busy(): 855 # time.sleep(0.5) 856 print "Done testing two sounds after another: seagulls, then rooster." 857 print "---------------" 858 859 860 print "Test pause/unpause while another sound is running. Expect repeated cycles of 1sec rooster and seagull sounds, 3sec, both sounds again for 1 sec,..."; 861 # channelSeagulls = player.play(testFileSeagulls, blockTillDone=False); 862 # channelRooster = player.play(testFileRooster, blockTillDone=False); 863 # playPauseUnpause(channelSeagulls, None) # None--> Both channels paused/unpaused 864 # 865 # while channelRooster.get_busy() or channelSeagulls.get_busy(): 866 # time.sleep(0.5) 867 print "Done test pause/unpause while another sound is running. Expect repeated cycles of 1sec rooster and seagull sounds, 3sec, both sounds again for 1 sec,..."; 868 print "---------------" 869 870 print "Test attempt to play on more than 8 channels at once..." 871 # channel1 = player.play(testFileSeagulls, blockTillDone=False); 872 # channel2 = player.play(testFileSeagulls, blockTillDone=False); 873 # channel3 = player.play(testFileSeagulls, blockTillDone=False); 874 # 875 # channel4 = player.play(testFileRooster, blockTillDone=False); 876 # channel5 = player.play(testFileRooster, blockTillDone=False); 877 # channel6 = player.play(testFileDrill, blockTillDone=False); 878 # channel7 = player.play(testFileRooster, blockTillDone=False); 879 # channel8 = player.play(testFileRooster, blockTillDone=False); 880 # 881 # channel9 = player.play(testFileDrill, blockTillDone=False); 882 # 883 # try: 884 # while channel3.get_busy() or channel7.get_busy() or channel9.get_busy(): 885 # time.sleep(0.5) 886 # except AttributeError: 887 # print "Loaded sounds dict is: " + str(player.loadedSounds); 888 # print "SoundChannelBindings dict is: " + str(player.soundChannelBindings); 889 print "Done test attempt to play on more than 8 channels at once." 890 print "---------------" 891 892 print "Test pausing by filename when respective sound is playing on multiple channels..." 893 # channel1 = player.play(testFileRooster, blockTillDone=False); 894 # channel2 = player.play(testFileRooster, blockTillDone=False); 895 # channel3 = player.play(testFileRooster, blockTillDone=False); 896 # 897 # # Test pause/unpause by filename: 898 # time.sleep(1); 899 # player.pause(testFileRooster); 900 # time.sleep(3); 901 # player.unpause(testFileRooster); 902 # print "SoundChannelBindings dict while playing three copies of rooster is: " + str(player.soundChannelBindings); 903 # time.sleep(1); 904 # player.pause(testFileRooster); 905 # time.sleep(3); 906 # player.unpause(testFileRooster); 907 # while channel1.get_busy() or channel2.get_busy() or channel3.get_busy(): 908 # time.sleep(0.5) 909 # 910 # # Test pausing/unpausing arrays of channels: 911 # channel1 = player.play(testFileRooster, blockTillDone=False); 912 # channel2 = player.play(testFileRooster, blockTillDone=False); 913 # channel3 = player.play(testFileRooster, blockTillDone=False); 914 # time.sleep(1); 915 # player.pause([channel1, channel2, channel3]); 916 # time.sleep(3); 917 # player.unpause([channel1, channel2, channel3]); 918 # while channel1.get_busy() or channel2.get_busy() or channel3.get_busy(): 919 # time.sleep(0.5) 920 # 921 # # Test pausing/unpausing individual channels: 922 # channel1 = player.play(testFileRooster, blockTillDone=False); 923 # channel2 = player.play(testFileRooster, blockTillDone=False); 924 # channel3 = player.play(testFileRooster, blockTillDone=False); 925 # time.sleep(1); 926 # player.pause(channel1) 927 # player.pause(channel2) 928 # player.pause(channel3) 929 # time.sleep(3); 930 # player.unpause(channel1); 931 # while channel1.get_busy(): 932 # time.sleep(0.5) 933 # player.unpause(channel2); 934 # while channel2.get_busy(): 935 # time.sleep(0.5) 936 # player.unpause(channel3); 937 # while channel3.get_busy(): 938 # time.sleep(0.5) 939 # while channel1.get_busy() or channel2.get_busy() or channel3.get_busy(): 940 # time.sleep(0.5) 941 942 # # Check the dictionaries: 943 # player.cleanupSoundChannelBindings() 944 # print "Loaded sounds dict is: " + str(player.loadedSounds); 945 # print "SoundChannelBindings dict before clean is: " + str(player.soundChannelBindings); 946 # player.cleanupSoundChannelBindings() 947 # print "SoundChannelBindings dict after clean is: " + str(player.soundChannelBindings); 948 print "Done test pausing by filename when respective sound is playing on multiple channels..." 949 print "---------------" 950 951 print "Test volume control. Hear rooster three times, once at full volume, then twice at the same lower volume" 952 # player.play(testFileRooster, blockTillDone=True, volume=1.0); 953 # player.play(testFileRooster, blockTillDone=True, volume=0.3); 954 # # Sound should play at same low volume even without vol spec: 955 # player.play(testFileRooster, blockTillDone=True); 956 print "Done test volume control. Hear rooster three times, once at full volume, then twice at the same lower volume" 957 print "---------------" 958 959 print "Test sound cache management; three second delay is normal." 960 # player.loadSound(testFileRooster); 961 # # Sleep a sec between each load to get a spread of last-used times: 962 # time.sleep(1); 963 # player.loadSound(testFileDrill); 964 # time.sleep(1); 965 # player.loadSound(testFileSeagulls); 966 # time.sleep(1); 967 # player.loadSound(testFileMoo2); 968 # assert(len(player.loadedSounds) == 4) 969 # assert(len(player.lastUsedTime) == 4) 970 # assert(len(player.soundChannelBindings) == 0); 971 # #print "Keys loadedSounds before cache reduction: " + str(player.loadedSounds); 972 # #print "Keys soundChannelBindings before cache reduction: " + str(player.soundChannelBindings); 973 # #print "Keys lastUsedTime before cache reduction: " + str(player.lastUsedTime); 974 # 975 # NUM_OF_CACHED_SOUNDS_SAVED = NUM_OF_CACHED_SOUNDS 976 # NUM_OF_CACHED_SOUNDS = 4; 977 # player.loadSound(testFileSteam); 978 # 979 # assert(len(player.loadedSounds) == 3) 980 # assert(len(player.lastUsedTime) == 3) 981 # assert(len(player.soundChannelBindings) == 0); 982 # 983 # #print "Keys loadedSounds after cache reduction: " + str(player.loadedSounds); 984 # #print "Keys soundChannelBindings after cache reduction: " + str(player.soundChannelBindings); 985 # #print "Keys lastUsedTime after cache reduction: " + str(player.lastUsedTime); 986 # 987 # NUM_OF_CACHED_SOUNDS = NUM_OF_CACHED_SOUNDS_SAVED 988 print "Done test sound cache management; three second delay is normal." 989 print "---------------" 990 991 print "Test singleton enforcement" 992 try: 993 player1 = SoundPlayer(); 994 raise ValueError("Should have raised a RuntimeError.") 995 except RuntimeError: 996 pass; 997 print "Done testing singleton enforcement" 998 print "---------------" 999 1000 print "All done" 1001