1#!/usr/bin/python 2 3# Audio Tools, a module and set of tools for manipulating audio data 4# Copyright (C) 2007-2014 Brian Langenberger 5 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (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 Street, Fifth Floor, Boston, MA 02110-1301 USA 19 20 21(RG_NO_REPLAYGAIN, RG_TRACK_GAIN, RG_ALBUM_GAIN) = range(3) 22DEFAULT_FORMAT = (44100, 2, 0x3, 16) 23 24(PLAYER_STOPPED, PLAYER_PAUSED, PLAYER_PLAYING) = range(3) 25 26 27class Player(object): 28 """a class for operating an audio player 29 30 the player itself runs in a seperate thread, 31 which this sends commands to""" 32 33 def __init__(self, audio_output, 34 replay_gain=RG_NO_REPLAYGAIN, 35 next_track_callback=lambda: None): 36 """audio_output is an AudioOutput object 37 38 replay_gain is RG_NO_REPLAYGAIN, RG_TRACK_GAIN or RG_ALBUM_GAIN, 39 indicating how the player should apply ReplayGain 40 41 next_track_callback is a function with no arguments 42 which is called by the player when the current track is finished 43 44 Raises :exc:`ValueError` if unable to start player subprocess.""" 45 46 import threading 47 try: 48 from queue import Queue 49 except ImportError: 50 from Queue import Queue 51 52 if not isinstance(audio_output, AudioOutput): 53 raise TypeError("invalid output object") 54 55 self.__audio_output__ = audio_output 56 self.__player__ = AudioPlayer(audio_output, 57 next_track_callback, 58 replay_gain) 59 self.__commands__ = Queue() 60 self.__responses__ = Queue() 61 62 self.__thread__ = threading.Thread( 63 target=self.__player__.run, 64 kwargs={"commands": self.__commands__, 65 "responses": self.__responses__}) 66 self.__thread__.daemon = False 67 self.__thread__.start() 68 69 def open(self, track): 70 """opens the given AudioFile for playing 71 72 stops playing the current file, if any""" 73 74 self.__commands__.put(("open", (track,))) 75 76 def play(self): 77 """begins or resumes playing an opened AudioFile, if any""" 78 79 self.__commands__.put(("play", tuple())) 80 81 def set_replay_gain(self, replay_gain): 82 """sets the given ReplayGain level to apply during playback 83 84 choose from RG_NO_REPLAYGAIN, RG_TRACK_GAIN or RG_ALBUM_GAIN 85 replayGain cannot be applied mid-playback 86 one must stop() and play() a file for it to take effect""" 87 88 self.__commands__.put(("set_replay_gain", (replay_gain,))) 89 90 def set_output(self, output): 91 """given an AudioOutput object, 92 sets the player's output to that device 93 94 any currently playing audio is stopped""" 95 96 self.__audio_output__ = output 97 self.__commands__.put(("set_output", (output,))) 98 99 def pause(self): 100 """pauses playback of the current file 101 102 playback may be resumed with play() or toggle_play_pause()""" 103 104 self.__commands__.put(("pause", tuple())) 105 106 def toggle_play_pause(self): 107 """pauses the file if playing, play the file if paused""" 108 109 self.__commands__.put(("toggle_play_pause", tuple())) 110 111 def stop(self): 112 """stops playback of the current file 113 114 if play() is called, playback will start from the beginning""" 115 116 self.__commands__.put(("stop", tuple())) 117 118 def state(self): 119 """returns the current state of the Player 120 as either PLAYER_STOPPED, PLAYER_PAUSED, or PLAYER_PLAYING ints""" 121 122 return self.__player__.state() 123 124 def close(self): 125 """closes the player for playback 126 127 the player thread is halted and the AudioOutput is closed""" 128 129 self.__commands__.put(("close", tuple())) 130 131 def progress(self): 132 """returns a (pcm_frames_played, pcm_frames_total) tuple 133 134 this indicates the current playback status in PCM frames""" 135 136 return self.__player__.progress() 137 138 def current_output_description(self): 139 """returns the human-readable description of the current output device 140 as a Unicode string""" 141 142 return self.__audio_output__.description() 143 144 def current_output_name(self): 145 """returns the ``NAME`` attribute of the current output device 146 as a plain string""" 147 148 return self.__audio_output__.NAME 149 150 def get_volume(self): 151 """returns the current volume level as a floating point value 152 between 0.0 and 1.0, inclusive""" 153 154 return self.__audio_output__.get_volume() 155 156 def set_volume(self, volume): 157 """given a floating point value between 0.0 and 1.0, inclusive, 158 sets the current volume level to that value""" 159 160 self.__audio_output__.set_volume(volume) 161 162 def change_volume(self, delta): 163 """changes the volume by the given floating point amount 164 where delta may be positive or negative 165 and returns the new volume as a floating point value""" 166 167 self.__audio_output__.set_volume( 168 min(max(self.__audio_output__.get_volume() + delta, 0.0), 1.0)) 169 return self.__audio_output__.get_volume() 170 171 172class AudioPlayer(object): 173 def __init__(self, audio_output, 174 next_track_callback=lambda: None, 175 replay_gain=RG_NO_REPLAYGAIN): 176 """audio_output is an AudioOutput object to play audio to 177 178 next_track_callback is an optional function which 179 is called with no arguments when the current track is finished""" 180 181 self.__state__ = PLAYER_STOPPED 182 self.__audio_output__ = audio_output 183 self.__next_track_callback__ = next_track_callback 184 self.__audiofile__ = None 185 self.__pcmreader__ = None 186 self.__buffer_size__ = 1 187 self.__replay_gain__ = replay_gain 188 self.__current_frames__ = 0 189 self.__total_frames__ = 1 190 191 def set_audiofile(self, audiofile): 192 """sets audiofile to play""" 193 194 self.__audiofile__ = audiofile 195 196 def state(self): 197 """returns current state of player which is one of: 198 PLAYER_STOPPED, PLAYER_PAUSED, PLAYER_PLAYING""" 199 200 return self.__state__ 201 202 def progress(self): 203 """returns current progress 204 as a (current frames, total frames) tuple""" 205 206 return (self.__current_frames__, self.__total_frames__) 207 208 def stop(self): 209 """changes current state of player to PLAYER_STOPPED""" 210 211 if self.__state__ == PLAYER_STOPPED: 212 # already stopped, so nothing to do 213 return 214 else: 215 if self.__state__ == PLAYER_PAUSED: 216 self.__audio_output__.resume() 217 218 self.__state__ = PLAYER_STOPPED 219 self.__pcmreader__ = None 220 self.__current_frames__ = 0 221 self.__total_frames__ = 1 222 223 def pause(self): 224 """if playing, changes current state of player to PLAYER_PAUSED""" 225 226 # do nothing if player is stopped or already paused 227 if self.__state__ == PLAYER_PLAYING: 228 self.__audio_output__.pause() 229 self.__state__ = PLAYER_PAUSED 230 231 def play(self): 232 """if audiofile has been opened, 233 changes current state of player to PLAYER_PLAYING""" 234 235 from audiotools import BufferedPCMReader 236 237 if self.__state__ == PLAYER_PLAYING: 238 # already playing, so nothing to do 239 return 240 elif self.__state__ == PLAYER_PAUSED: 241 # go from unpaused to playing 242 self.__audio_output__.resume() 243 self.__state__ = PLAYER_PLAYING 244 elif ((self.__state__ == PLAYER_STOPPED) and 245 (self.__audiofile__ is not None)): 246 # go from stopped to playing 247 # if an audiofile has been opened 248 249 # get PCMReader from selected audiofile 250 pcmreader = self.__audiofile__.to_pcm() 251 252 # apply ReplayGain if requested 253 if self.__replay_gain__ in (RG_TRACK_GAIN, RG_ALBUM_GAIN): 254 gain = self.__audiofile__.get_replay_gain() 255 if gain is not None: 256 from audiotools.replaygain import ReplayGainReader 257 258 if self.__replay_gain__ == RG_TRACK_GAIN: 259 pcmreader = ReplayGainReader(pcmreader, 260 gain.track_gain, 261 gain.track_peak) 262 else: 263 pcmreader = ReplayGainReader(pcmreader, 264 gain.album_gain, 265 gain.album_peak) 266 267 # buffer PCMReader so that one can process small chunks of data 268 self.__pcmreader__ = BufferedPCMReader(pcmreader) 269 270 # calculate quarter second buffer size 271 # (or at least 256 samples) 272 self.__buffer_size__ = max(int(round(0.25 * 273 pcmreader.sample_rate)), 274 256) 275 276 # set output to be compatible with PCMReader 277 self.__audio_output__.set_format( 278 sample_rate=self.__pcmreader__.sample_rate, 279 channels=self.__pcmreader__.channels, 280 channel_mask=self.__pcmreader__.channel_mask, 281 bits_per_sample=self.__pcmreader__.bits_per_sample) 282 283 # reset progress 284 self.__current_frames__ = 0 285 self.__total_frames__ = self.__audiofile__.total_frames() 286 287 # update state so audio begins playing 288 self.__state__ = PLAYER_PLAYING 289 290 def output_audio(self): 291 """if player is playing, output the next chunk of audio if possible 292 293 if audio is exhausted, stop playing and call the next_track callback""" 294 295 if self.__state__ == PLAYER_PLAYING: 296 try: 297 frame = self.__pcmreader__.read(self.__buffer_size__) 298 except (IOError, ValueError) as err: 299 # some sort of read error occurred 300 # so cease playing file and move on to next 301 self.stop() 302 if callable(self.__next_track_callback__): 303 self.__next_track_callback__() 304 return 305 306 if len(frame) > 0: 307 self.__current_frames__ += frame.frames 308 self.__audio_output__.play(frame) 309 else: 310 # audio has been exhausted 311 self.stop() 312 if callable(self.__next_track_callback__): 313 self.__next_track_callback__() 314 315 def run(self, commands, responses): 316 """runs the audio playing thread while accepting commands 317 from the given Queue""" 318 319 try: 320 from queue import Empty 321 except ImportError: 322 from Queue import Empty 323 324 while True: 325 try: 326 (command, 327 args) = commands.get(self.__state__ != PLAYER_PLAYING) 328 # got a command to process 329 if command == "open": 330 # stop whatever's playing and prepare new track for playing 331 self.stop() 332 self.set_audiofile(args[0]) 333 elif command == "play": 334 self.play() 335 elif command == "set_replay_gain": 336 self.__replay_gain__ = args[0] 337 elif command == "set_output": 338 # resume (if necessary) and close existing output 339 if self.__state__ == PLAYER_PAUSED: 340 self.__audio_output__.resume() 341 self.__audio_output__.close() 342 343 # set new output and set format (if necessary) 344 self.__audio_output__ = args[0] 345 if self.__pcmreader__ is not None: 346 self.__audio_output__.set_format( 347 sample_rate=self.__pcmreader__.sample_rate, 348 channels=self.__pcmreader__.channels, 349 channel_mask=self.__pcmreader__.channel_mask, 350 bits_per_sample=self.__pcmreader__.bits_per_sample) 351 352 # if paused, reset audio output to paused 353 if self.__state__ == PLAYER_PAUSED: 354 self.__audio_output__.pause() 355 elif command == "pause": 356 self.pause() 357 elif command == "toggle_play_pause": 358 # switch from paused to playing or playing to paused 359 if self.__state__ == PLAYER_PAUSED: 360 self.play() 361 elif self.__state__ == PLAYER_PLAYING: 362 self.pause() 363 elif command == "stop": 364 self.stop() 365 self.__audio_output__.close() 366 elif command == "close": 367 self.stop() 368 self.__audio_output__.close() 369 return 370 except Empty: 371 # no commands to process 372 # so output audio if playing 373 self.output_audio() 374 375 376class CDPlayer(Player): 377 def __init__(self, cddareader, audio_output, 378 next_track_callback=lambda: None): 379 """cdda is a CDDAReader object 380 381 audio_output is an AudioOutput object 382 383 next_track_callback is a function with no arguments 384 which is called by the player when the current track is finished""" 385 386 import threading 387 try: 388 from queue import Queue 389 except ImportError: 390 from Queue import Queue 391 392 if not isinstance(audio_output, AudioOutput): 393 raise TypeError("invalid output object") 394 395 self.__audio_output__ = audio_output 396 self.__player__ = CDAudioPlayer(cddareader, 397 audio_output, 398 next_track_callback) 399 self.__commands__ = Queue() 400 self.__responses__ = Queue() 401 402 self.__thread__ = threading.Thread( 403 target=self.__player__.run, 404 kwargs={"commands": self.__commands__, 405 "responses": self.__responses__}) 406 self.__thread__.daemon = False 407 self.__thread__.start() 408 409 def open(self, track_number): 410 """opens the given track_number for playing 411 412 stops playing the current track, if any""" 413 414 self.__commands__.put(("open", (track_number,))) 415 416 def set_replay_gain(self, replay_gain): 417 """ReplayGain not applicable to CDDA, so this does nothing""" 418 419 pass 420 421 422class CDAudioPlayer(AudioPlayer): 423 def __init__(self, cddareader, audio_output, 424 next_track_callback=lambda: None): 425 """cdda is a CDDAReader object to play tracks from 426 427 audio_output is an AudioOutput object to play audio to 428 429 next_track_callback is an optional function 430 which is called with no arguments when the current track is finished""" 431 432 self.__state__ = PLAYER_STOPPED 433 self.__audio_output__ = audio_output 434 self.__next_track_callback__ = next_track_callback 435 self.__cddareader__ = cddareader 436 self.__offsets__ = cddareader.track_offsets 437 self.__lengths__ = cddareader.track_lengths 438 self.__track_number__ = None 439 self.__pcmreader__ = None 440 self.__buffer_size__ = 1 441 self.__replay_gain__ = RG_NO_REPLAYGAIN 442 self.__current_frames__ = 0 443 self.__total_frames__ = 1 444 445 def set_audiofile(self, track_number): 446 """set tracks number to play""" 447 448 # ensure track number is in the proper range 449 if track_number in self.__offsets__.keys(): 450 self.__track_number__ = track_number 451 452 def play(self): 453 """if track has been selected, 454 changes current state of player to PLAYER_PLAYING""" 455 456 from audiotools import (BufferedPCMReader, 457 ThreadedPCMReader, 458 PCMReaderHead) 459 460 if self.__state__ == PLAYER_PLAYING: 461 # already playing, so nothing to do 462 return 463 elif self.__state__ == PLAYER_PAUSED: 464 # go from unpaused to playing 465 self.__audio_output__.resume() 466 self.__state__ = PLAYER_PLAYING 467 elif ((self.__state__ == PLAYER_STOPPED) and 468 (self.__track_number__ is not None)): 469 # go from stopped to playing 470 # if a track number has been selected 471 472 # seek to specified track number 473 self.__cddareader__.seek(self.__offsets__[self.__track_number__]) 474 track = PCMReaderHead(self.__cddareader__, 475 self.__lengths__[self.__track_number__], 476 False) 477 478 # decode PCMReader in thread 479 # and place in buffer so one can process small chunks of data 480 self.__pcmreader__ = BufferedPCMReader(ThreadedPCMReader(track)) 481 482 # calculate quarter second buffer size 483 self.__buffer_size__ = int(round(0.25 * 44100)) 484 485 # set output to be compatible with PCMReader 486 self.__audio_output__.set_format( 487 sample_rate=44100, 488 channels=2, 489 channel_mask=0x3, 490 bits_per_sample=16) 491 492 # reset progress 493 self.__current_frames__ = 0 494 self.__total_frames__ = self.__lengths__[self.__track_number__] 495 496 # update state so audio begins playing 497 self.__state__ = PLAYER_PLAYING 498 499 500class AudioOutput(object): 501 """an abstract parent class for playing audio""" 502 503 def __init__(self): 504 self.sample_rate = None 505 self.channels = None 506 self.channel_mask = None 507 self.bits_per_sample = None 508 509 def __getstate__(self): 510 """gets internal state for use by Pickle module""" 511 512 return "" 513 514 def __setstate__(self, name): 515 """sets internal state for use by Pickle module""" 516 517 # audio outputs are initialized closed for obvious reasons 518 self.sample_rate = None 519 self.channels = None 520 self.channel_mask = None 521 self.bits_per_sample = None 522 523 def description(self): 524 """returns user-facing name of output device as unicode""" 525 526 raise NotImplementedError() 527 528 def compatible(self, sample_rate, channels, channel_mask, bits_per_sample): 529 """returns True if the given pcmreader is compatible 530 with the given format""" 531 532 return ((self.sample_rate == sample_rate) and 533 (self.channels == channels) and 534 (self.channel_mask == channel_mask) and 535 (self.bits_per_sample == bits_per_sample)) 536 537 def set_format(self, sample_rate, channels, channel_mask, bits_per_sample): 538 """sets the output stream to the given format 539 540 if the stream hasn't been initialized, this method initializes it 541 542 if the stream has been initialized to a different format, 543 this method closes and reopens the stream to the new format 544 545 if the stream has been initialized to the same format, 546 this method does nothing""" 547 548 self.sample_rate = sample_rate 549 self.channels = channels 550 self.channel_mask = channel_mask 551 self.bits_per_sample = bits_per_sample 552 553 def play(self, framelist): 554 """plays a FrameList""" 555 556 raise NotImplementedError() 557 558 def pause(self): 559 """pauses audio output, with the expectation it will be resumed""" 560 561 raise NotImplementedError() 562 563 def resume(self): 564 """resumes playing paused audio output""" 565 566 raise NotImplementedError() 567 568 def get_volume(self): 569 """returns a floating-point volume value between 0.0 and 1.0""" 570 571 return 0.0 572 573 def set_volume(self, volume): 574 """sets the output volume to a floating point value 575 between 0.0 and 1.0""" 576 577 pass 578 579 def close(self): 580 """closes the output stream""" 581 582 self.sample_rate = None 583 self.channels = None 584 self.channel_mask = None 585 self.bits_per_sample = None 586 587 @classmethod 588 def available(cls): 589 """returns True if the AudioOutput is available on the system""" 590 591 return False 592 593 594class NULLAudioOutput(AudioOutput): 595 """an AudioOutput subclass which does not actually play anything 596 597 although this consumes audio output at the rate it would normally 598 play, it generates no output""" 599 600 NAME = "NULL" 601 602 def __init__(self): 603 self.__volume__ = 0.30 604 AudioOutput.__init__(self) 605 606 def __getstate__(self): 607 return "NULL" 608 609 def __setstate__(self, name): 610 AudioOutput.__setstate__(self, name) 611 self.__volume__ = 0.30 612 613 def description(self): 614 """returns user-facing name of output device as unicode""" 615 616 return u"Dummy Output" 617 618 def play(self, framelist): 619 """plays a chunk of converted data""" 620 621 import time 622 623 time.sleep(float(framelist.frames) / self.sample_rate) 624 625 def pause(self): 626 """pauses audio output, with the expectation it will be resumed""" 627 628 pass 629 630 def resume(self): 631 """resumes playing paused audio output""" 632 633 pass 634 635 def get_volume(self): 636 """returns a floating-point volume value between 0.0 and 1.0""" 637 638 return self.__volume__ 639 640 def set_volume(self, volume): 641 """sets the output volume to a floating point value 642 between 0.0 and 1.0""" 643 644 if (volume >= 0) and (volume <= 1.0): 645 self.__volume__ = volume 646 else: 647 raise ValueError("volume must be between 0.0 and 1.0") 648 649 def close(self): 650 """closes the output stream""" 651 652 AudioOutput.close(self) 653 654 @classmethod 655 def available(cls): 656 """returns True""" 657 658 return True 659 660 661class OSSAudioOutput(AudioOutput): 662 """an AudioOutput subclass for OSS output""" 663 664 NAME = "OSS" 665 666 def __init__(self): 667 """automatically initializes output format for playing 668 CD quality audio""" 669 670 self.__ossaudio__ = None 671 self.__ossmixer__ = None 672 AudioOutput.__init__(self) 673 674 def __getstate__(self): 675 """gets internal state for use by Pickle module""" 676 677 return "OSS" 678 679 def __setstate__(self, name): 680 """sets internal state for use by Pickle module""" 681 682 AudioOutput.__setstate__(self, name) 683 self.__ossaudio__ = None 684 self.__ossmixer__ = None 685 686 def description(self): 687 """returns user-facing name of output device as unicode""" 688 689 return u"Open Sound System" 690 691 def set_format(self, sample_rate, channels, channel_mask, bits_per_sample): 692 """sets the output stream to the given format 693 694 if the stream hasn't been initialized, this method initializes it 695 696 if the stream has been initialized to a different format, 697 this method closes and reopens the stream to the new format 698 699 if the stream has been initialized to the same format, 700 this method does nothing""" 701 702 if self.__ossaudio__ is None: 703 # output hasn't been initialized 704 705 import ossaudiodev 706 707 AudioOutput.set_format(self, sample_rate, channels, 708 channel_mask, bits_per_sample) 709 710 # initialize audio output device and setup framelist converter 711 self.__ossaudio__ = ossaudiodev.open('w') 712 self.__ossmixer__ = ossaudiodev.openmixer() 713 if self.bits_per_sample == 8: 714 self.__ossaudio__.setfmt(ossaudiodev.AFMT_S8_LE) 715 self.__converter__ = lambda f: f.to_bytes(False, True) 716 elif self.bits_per_sample == 16: 717 self.__ossaudio__.setfmt(ossaudiodev.AFMT_S16_LE) 718 self.__converter__ = lambda f: f.to_bytes(False, True) 719 elif self.bits_per_sample == 24: 720 from audiotools.pcm import from_list 721 722 self.__ossaudio__.setfmt(ossaudiodev.AFMT_S16_LE) 723 self.__converter__ = lambda f: from_list( 724 [i >> 8 for i in list(f)], 725 self.channels, 16, True).to_bytes(False, True) 726 else: 727 raise ValueError("Unsupported bits-per-sample") 728 729 self.__ossaudio__.channels(channels) 730 self.__ossaudio__.speed(sample_rate) 731 elif (not self.compatible(sample_rate=sample_rate, 732 channels=channels, 733 channel_mask=channel_mask, 734 bits_per_sample=bits_per_sample)): 735 # output has been initialized to a different format 736 737 self.close() 738 self.set_format(sample_rate=sample_rate, 739 channels=channels, 740 channel_mask=channel_mask, 741 bits_per_sample=bits_per_sample) 742 743 def play(self, framelist): 744 """plays a FrameList""" 745 746 self.__ossaudio__.writeall(self.__converter__(framelist)) 747 748 def pause(self): 749 """pauses audio output, with the expectation it will be resumed""" 750 751 pass 752 753 def resume(self): 754 """resumes playing paused audio output""" 755 756 pass 757 758 def get_volume(self): 759 """returns a floating-point volume value between 0.0 and 1.0""" 760 761 import ossaudiodev 762 763 if self.__ossmixer__ is None: 764 self.set_format(*DEFAULT_FORMAT) 765 766 controls = self.__ossmixer__.controls() 767 for control in (ossaudiodev.SOUND_MIXER_VOLUME, 768 ossaudiodev.SOUND_MIXER_PCM): 769 if controls & (1 << control): 770 try: 771 volumes = self.__ossmixer__.get(control) 772 return (sum(volumes) / float(len(volumes))) / 100.0 773 except ossaudiodev.OSSAudioError: 774 continue 775 else: 776 return 0.0 777 778 def set_volume(self, volume): 779 """sets the output volume to a floating point value 780 between 0.0 and 1.0""" 781 782 if (volume >= 0) and (volume <= 1.0): 783 if self.__ossmixer__ is None: 784 self.set_format(*DEFAULT_FORMAT) 785 786 controls = self.__ossmixer__.controls() 787 ossvolume = max(min(int(round(volume * 100)), 100), 0) 788 for control in (ossaudiodev.SOUND_MIXER_VOLUME, 789 ossaudiodev.SOUND_MIXER_PCM): 790 if controls & (1 << control): 791 try: 792 self.__ossmixer__.set(control, (ossvolume, ossvolume)) 793 except ossaudiodev.OSSAudioError: 794 continue 795 else: 796 raise ValueError("volume must be between 0.0 and 1.0") 797 798 def close(self): 799 """closes the output stream""" 800 801 AudioOutput.close(self) 802 803 if self.__ossaudio__ is not None: 804 self.__ossaudio__.close() 805 self.__ossaudio__ = None 806 if self.__ossmixer__ is not None: 807 self.__ossmixer__.close() 808 self.__ossmixer__ = None 809 810 @classmethod 811 def available(cls): 812 """returns True if OSS output is available on the system""" 813 814 try: 815 import ossaudiodev 816 ossaudiodev.open("w").close() 817 return True 818 except (ImportError, IOError): 819 return False 820 821 822class PulseAudioOutput(AudioOutput): 823 """an AudioOutput subclass for PulseAudio output""" 824 825 NAME = "PulseAudio" 826 827 def __init__(self): 828 self.__pulseaudio__ = None 829 AudioOutput.__init__(self) 830 831 def __getstate__(self): 832 """gets internal state for use by Pickle module""" 833 834 return "PulseAudio" 835 836 def __setstate__(self, name): 837 """sets internal state for use by Pickle module""" 838 839 AudioOutput.__setstate__(self, name) 840 self.__pulseaudio__ = None 841 842 def description(self): 843 """returns user-facing name of output device as unicode""" 844 845 # FIXME - pull this from device description 846 return u"Pulse Audio" 847 848 def set_format(self, sample_rate, channels, channel_mask, bits_per_sample): 849 """sets the output stream to the given format 850 851 if the stream hasn't been initialized, this method initializes it 852 853 if the stream has been initialized to a different format, 854 this method closes and reopens the stream to the new format 855 856 if the stream has been initialized to the same format, 857 this method does nothing""" 858 859 if self.__pulseaudio__ is None: 860 # output hasn't been initialized 861 862 from audiotools.output import PulseAudio 863 864 AudioOutput.set_format(self, sample_rate, channels, 865 channel_mask, bits_per_sample) 866 867 self.__pulseaudio__ = PulseAudio(sample_rate, 868 channels, 869 bits_per_sample, 870 "Python Audio Tools") 871 self.__converter__ = { 872 8: lambda f: f.to_bytes(True, False), 873 16: lambda f: f.to_bytes(False, True), 874 24: lambda f: f.to_bytes(False, True)}[self.bits_per_sample] 875 elif (not self.compatible(sample_rate=sample_rate, 876 channels=channels, 877 channel_mask=channel_mask, 878 bits_per_sample=bits_per_sample)): 879 # output has been initialized to a different format 880 881 self.close() 882 self.set_format(sample_rate=sample_rate, 883 channels=channels, 884 channel_mask=channel_mask, 885 bits_per_sample=bits_per_sample) 886 887 def play(self, framelist): 888 """plays a FrameList""" 889 890 self.__pulseaudio__.play(self.__converter__(framelist)) 891 892 def pause(self): 893 """pauses audio output, with the expectation it will be resumed""" 894 895 if self.__pulseaudio__ is not None: 896 self.__pulseaudio__.pause() 897 898 def resume(self): 899 """resumes playing paused audio output""" 900 901 if self.__pulseaudio__ is not None: 902 self.__pulseaudio__.resume() 903 904 def get_volume(self): 905 """returns a floating-point volume value between 0.0 and 1.0""" 906 907 if self.__pulseaudio__ is None: 908 self.set_format(*DEFAULT_FORMAT) 909 910 return self.__pulseaudio__.get_volume() 911 912 def set_volume(self, volume): 913 """sets the output volume to a floating point value 914 between 0.0 and 1.0""" 915 916 if (volume >= 0) and (volume <= 1.0): 917 if self.__pulseaudio__ is None: 918 self.set_format(*DEFAULT_FORMAT) 919 920 self.__pulseaudio__.set_volume(volume) 921 else: 922 raise ValueError("volume must be between 0.0 and 1.0") 923 924 def close(self): 925 """closes the output stream""" 926 927 AudioOutput.close(self) 928 929 if self.__pulseaudio__ is not None: 930 self.__pulseaudio__.flush() 931 self.__pulseaudio__.close() 932 self.__pulseaudio__ = None 933 934 @classmethod 935 def available(cls): 936 """returns True if PulseAudio is available and running on the system""" 937 938 try: 939 from audiotools.output import PulseAudio 940 941 return True 942 except ImportError: 943 return False 944 945 946class ALSAAudioOutput(AudioOutput): 947 """an AudioOutput subclass for ALSA output""" 948 949 NAME = "ALSA" 950 951 def __init__(self): 952 self.__alsaaudio__ = None 953 AudioOutput.__init__(self) 954 955 def __getstate__(self): 956 """gets internal state for use by Pickle module""" 957 958 return "ALSA" 959 960 def __setstate__(self, name): 961 """sets internal state for use by Pickle module""" 962 963 AudioOutput.__setstate__(self, name) 964 self.__alsaaudio__ = None 965 966 def description(self): 967 """returns user-facing name of output device as unicode""" 968 969 # FIXME - pull this from device description 970 return u"Advanced Linux Sound Architecture" 971 972 def set_format(self, sample_rate, channels, channel_mask, bits_per_sample): 973 """sets the output stream to the given format 974 975 if the stream hasn't been initialized, this method initializes it 976 977 if the stream has been initialized to a different format, 978 this method closes and reopens the stream to the new format 979 980 if the stream has been initialized to the same format, 981 this method does nothing""" 982 983 if self.__alsaaudio__ is None: 984 # output hasn't been initialized 985 986 from audiotools.output import ALSAAudio 987 988 AudioOutput.set_format(self, sample_rate, channels, 989 channel_mask, bits_per_sample) 990 991 self.__alsaaudio__ = ALSAAudio("default", 992 sample_rate, 993 channels, 994 bits_per_sample) 995 elif (not self.compatible(sample_rate=sample_rate, 996 channels=channels, 997 channel_mask=channel_mask, 998 bits_per_sample=bits_per_sample)): 999 # output has been initialized to different format 1000 1001 self.close() 1002 self.set_format(sample_rate=sample_rate, 1003 channels=channels, 1004 channel_mask=channel_mask, 1005 bits_per_sample=bits_per_sample) 1006 1007 def play(self, framelist): 1008 """plays a FrameList""" 1009 1010 self.__alsaaudio__.play(framelist) 1011 1012 def pause(self): 1013 """pauses audio output, with the expectation it will be resumed""" 1014 1015 if self.__alsaaudio__ is not None: 1016 self.__alsaaudio__.pause() 1017 1018 def resume(self): 1019 """resumes playing paused audio output""" 1020 1021 if self.__alsaaudio__ is not None: 1022 self.__alsaaudio__.resume() 1023 1024 def get_volume(self): 1025 """returns a floating-point volume value between 0.0 and 1.0""" 1026 1027 if self.__alsaaudio__ is None: 1028 self.set_format(*DEFAULT_FORMAT) 1029 return self.__alsaaudio__.get_volume() 1030 1031 def set_volume(self, volume): 1032 """sets the output volume to a floating point value 1033 between 0.0 and 1.0""" 1034 1035 if (volume >= 0) and (volume <= 1.0): 1036 if self.__alsaaudio__ is None: 1037 self.set_format(*DEFAULT_FORMAT) 1038 self.__alsaaudio__.set_volume(volume) 1039 else: 1040 raise ValueError("volume must be between 0.0 and 1.0") 1041 1042 def close(self): 1043 """closes the output stream""" 1044 1045 AudioOutput.close(self) 1046 1047 if self.__alsaaudio__ is not None: 1048 self.__alsaaudio__.flush() 1049 self.__alsaaudio__.close() 1050 self.__alsaaudio__ = None 1051 1052 @classmethod 1053 def available(cls): 1054 """returns True if ALSA is available and running on the system""" 1055 1056 try: 1057 from audiotools.output import ALSAAudio 1058 1059 return True 1060 except ImportError: 1061 return False 1062 1063 1064class CoreAudioOutput(AudioOutput): 1065 """an AudioOutput subclass for CoreAudio output""" 1066 1067 NAME = "CoreAudio" 1068 1069 def __init__(self): 1070 self.__coreaudio__ = None 1071 AudioOutput.__init__(self) 1072 1073 def __getstate__(self): 1074 """gets internal state for use by Pickle module""" 1075 1076 return "CoreAudio" 1077 1078 def __setstate__(self, name): 1079 """sets internal state for use by Pickle module""" 1080 1081 AudioOutput.__setstate__(self, name) 1082 self.__coreaudio__ = None 1083 1084 def description(self): 1085 """returns user-facing name of output device as unicode""" 1086 1087 # FIXME - pull this from device description 1088 return u"Core Audio" 1089 1090 def set_format(self, sample_rate, channels, channel_mask, bits_per_sample): 1091 """sets the output stream to the given format 1092 1093 if the stream hasn't been initialized, this method initializes it 1094 1095 if the stream has been initialized to a different format, 1096 this method closes and reopens the stream to the new format 1097 1098 if the stream has been initialized to the same format, 1099 this method does nothing""" 1100 1101 if self.__coreaudio__ is None: 1102 # output hasn't been initialized 1103 1104 from audiotools.output import CoreAudio 1105 1106 AudioOutput.set_format(self, sample_rate, channels, 1107 channel_mask, bits_per_sample) 1108 1109 self.__coreaudio__ = CoreAudio(sample_rate, 1110 channels, 1111 channel_mask, 1112 bits_per_sample) 1113 elif (not self.compatible(sample_rate=sample_rate, 1114 channels=channels, 1115 channel_mask=channel_mask, 1116 bits_per_sample=bits_per_sample)): 1117 # output has been initialized in a different format 1118 1119 self.close() 1120 self.set_format(sample_rate=sample_rate, 1121 channels=channels, 1122 channel_mask=channel_mask, 1123 bits_per_sample=bits_per_sample) 1124 1125 def play(self, framelist): 1126 """plays a FrameList""" 1127 1128 self.__coreaudio__.play(framelist.to_bytes(False, True)) 1129 1130 def pause(self): 1131 """pauses audio output, with the expectation it will be resumed""" 1132 1133 if self.__coreaudio__ is not None: 1134 self.__coreaudio__.pause() 1135 1136 def resume(self): 1137 """resumes playing paused audio output""" 1138 1139 if self.__coreaudio__ is not None: 1140 self.__coreaudio__.resume() 1141 1142 def get_volume(self): 1143 """returns a floating-point volume value between 0.0 and 1.0""" 1144 1145 if self.__coreaudio__ is None: 1146 self.set_format(*DEFAULT_FORMAT) 1147 try: 1148 return self.__coreaudio__.get_volume() 1149 except ValueError: 1150 # get_volume_scalar() call was unsuccessful 1151 return 1.0 1152 1153 def set_volume(self, volume): 1154 """sets the output volume to a floating point value 1155 between 0.0 and 1.0""" 1156 1157 if (volume >= 0) and (volume <= 1.0): 1158 if self.__coreaudio__ is None: 1159 self.set_format(*DEFAULT_FORMAT) 1160 try: 1161 self.__coreaudio__.set_volume(volume) 1162 except ValueError: 1163 # set_volume_scalar() call was unsuccessful 1164 pass 1165 else: 1166 raise ValueError("volume must be between 0.0 and 1.0") 1167 1168 def close(self): 1169 """closes the output stream""" 1170 1171 AudioOutput.close(self) 1172 1173 if self.__coreaudio__ is not None: 1174 self.__coreaudio__.flush() 1175 self.__coreaudio__.close() 1176 self.__coreaudio__ = None 1177 1178 @classmethod 1179 def available(cls): 1180 """returns True if the AudioOutput is available on the system""" 1181 1182 try: 1183 from audiotools.output import CoreAudio 1184 1185 return True 1186 except ImportError: 1187 return False 1188 1189 1190def available_outputs(): 1191 """iterates over all available AudioOutput objects 1192 this will always yield at least one output""" 1193 1194 if PulseAudioOutput.available(): 1195 yield PulseAudioOutput() 1196 1197 if ALSAAudioOutput.available(): 1198 yield ALSAAudioOutput() 1199 1200 if CoreAudioOutput.available(): 1201 yield CoreAudioOutput() 1202 1203 if OSSAudioOutput.available(): 1204 yield OSSAudioOutput() 1205 1206 yield NULLAudioOutput() 1207 1208 1209def open_output(output): 1210 """given an output type string (e.g. "PulseAudio") 1211 returns that AudioOutput instance 1212 or raises ValueError if it is unavailable""" 1213 1214 for audio_output in available_outputs(): 1215 if audio_output.NAME == output: 1216 return audio_output 1217 else: 1218 raise ValueError("no such outout %s" % (output)) 1219