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