805 lines
26 KiB
Python
805 lines
26 KiB
Python
#!/usr/bin/bin
|
|
|
|
#Audio Tools, a module and set of tools for manipulating audio data
|
|
#Copyright (C) 2007-2011 Brian Langenberger
|
|
|
|
#This program is free software; you can redistribute it and/or modify
|
|
#it under the terms of the GNU General Public License as published by
|
|
#the Free Software Foundation; either version 2 of the License, or
|
|
#(at your option) any later version.
|
|
|
|
#This program is distributed in the hope that it will be useful,
|
|
#but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
#GNU General Public License for more details.
|
|
|
|
#You should have received a copy of the GNU General Public License
|
|
#along with this program; if not, write to the Free Software
|
|
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
|
|
import os
|
|
import sys
|
|
import cPickle
|
|
import select
|
|
import audiotools
|
|
import time
|
|
import Queue
|
|
import threading
|
|
|
|
|
|
(RG_NO_REPLAYGAIN, RG_TRACK_GAIN, RG_ALBUM_GAIN) = range(3)
|
|
|
|
|
|
class Player:
|
|
"""A class for operating an audio player.
|
|
|
|
The player itself runs in a seperate thread,
|
|
which this sends commands to."""
|
|
|
|
def __init__(self, audio_output,
|
|
replay_gain=RG_NO_REPLAYGAIN,
|
|
next_track_callback=lambda: None):
|
|
"""audio_output is an AudioOutput subclass.
|
|
replay_gain is RG_NO_REPLAYGAIN, RG_TRACK_GAIN or RG_ALBUM_GAIN,
|
|
indicating how the player should apply ReplayGain.
|
|
next_track_callback is a function with no arguments
|
|
which is called by the player when the current track is finished."""
|
|
|
|
self.command_queue = Queue.Queue()
|
|
self.worker = PlayerThread(audio_output,
|
|
self.command_queue,
|
|
replay_gain)
|
|
self.thread = threading.Thread(target=self.worker.run,
|
|
args=(next_track_callback,))
|
|
self.thread.daemon = True
|
|
self.thread.start()
|
|
|
|
def open(self, track):
|
|
"""opens the given AudioFile for playing
|
|
|
|
stops playing the current file, if any"""
|
|
|
|
self.track = track
|
|
self.command_queue.put(("open", [track]))
|
|
|
|
def play(self):
|
|
"""begins or resumes playing an opened AudioFile, if any"""
|
|
|
|
self.command_queue.put(("play", []))
|
|
|
|
def set_replay_gain(self, replay_gain):
|
|
"""sets the given ReplayGain level to apply during playback
|
|
|
|
Choose from RG_NO_REPLAYGAIN, RG_TRACK_GAIN or RG_ALBUM_GAIN
|
|
ReplayGain cannot be applied mid-playback.
|
|
One must stop() and play() a file for it to take effect."""
|
|
|
|
self.command_queue.put(("set_replay_gain", [replay_gain]))
|
|
|
|
def pause(self):
|
|
"""pauses playback of the current file
|
|
|
|
Playback may be resumed with play() or toggle_play_pause()"""
|
|
|
|
self.command_queue.put(("pause", []))
|
|
|
|
def toggle_play_pause(self):
|
|
"""pauses the file if playing, play the file if paused"""
|
|
|
|
self.command_queue.put(("toggle_play_pause", []))
|
|
|
|
def stop(self):
|
|
"""stops playback of the current file
|
|
|
|
If play() is called, playback will start from the beginning."""
|
|
|
|
self.command_queue.put(("stop", []))
|
|
|
|
def close(self):
|
|
"""closes the player for playback
|
|
|
|
The player thread is halted and the AudioOutput is closed."""
|
|
|
|
self.command_queue.put(("exit", []))
|
|
|
|
def progress(self):
|
|
"""returns a (pcm_frames_played, pcm_frames_total) tuple
|
|
|
|
This indicates the current playback status in PCM frames."""
|
|
|
|
return (self.worker.frames_played, self.worker.total_frames)
|
|
|
|
|
|
(PLAYER_STOPPED, PLAYER_PAUSED, PLAYER_PLAYING) = range(3)
|
|
|
|
|
|
class PlayerThread:
|
|
"""The Player class' subthread.
|
|
|
|
This should not be instantiated directly;
|
|
Player will do so automatically."""
|
|
|
|
def __init__(self, audio_output, command_queue,
|
|
replay_gain=RG_NO_REPLAYGAIN):
|
|
self.audio_output = audio_output
|
|
self.command_queue = command_queue
|
|
self.replay_gain = replay_gain
|
|
|
|
self.track = None
|
|
self.pcmconverter = None
|
|
self.frames_played = 0
|
|
self.total_frames = 0
|
|
self.state = PLAYER_STOPPED
|
|
|
|
def open(self, track):
|
|
self.stop()
|
|
self.track = track
|
|
self.frames_played = 0
|
|
self.total_frames = track.total_frames()
|
|
|
|
def pause(self):
|
|
if (self.state == PLAYER_PLAYING):
|
|
self.state = PLAYER_PAUSED
|
|
|
|
def play(self):
|
|
if (self.track is not None):
|
|
if (self.state == PLAYER_STOPPED):
|
|
if (self.replay_gain == RG_TRACK_GAIN):
|
|
from audiotools.replaygain import ReplayGainReader
|
|
replay_gain = self.track.replay_gain()
|
|
|
|
if (replay_gain is not None):
|
|
pcmreader = ReplayGainReader(
|
|
self.track.to_pcm(),
|
|
replay_gain.track_gain,
|
|
replay_gain.track_peak)
|
|
else:
|
|
pcmreader = self.track.to_pcm()
|
|
elif (self.replay_gain == RG_ALBUM_GAIN):
|
|
from audiotools.replaygain import ReplayGainReader
|
|
replay_gain = self.track.replay_gain()
|
|
|
|
if (replay_gain is not None):
|
|
pcmreader = ReplayGainReader(
|
|
self.track.to_pcm(),
|
|
replay_gain.album_gain,
|
|
replay_gain.album_peak)
|
|
else:
|
|
pcmreader = self.track.to_pcm()
|
|
else:
|
|
pcmreader = self.track.to_pcm()
|
|
|
|
if (not self.audio_output.compatible(pcmreader)):
|
|
self.audio_output.init(
|
|
sample_rate=pcmreader.sample_rate,
|
|
channels=pcmreader.channels,
|
|
channel_mask=pcmreader.channel_mask,
|
|
bits_per_sample=pcmreader.bits_per_sample)
|
|
self.pcmconverter = ThreadedPCMConverter(
|
|
pcmreader,
|
|
self.audio_output.framelist_converter())
|
|
self.frames_played = 0
|
|
self.state = PLAYER_PLAYING
|
|
elif (self.state == PLAYER_PAUSED):
|
|
self.state = PLAYER_PLAYING
|
|
elif (self.state == PLAYER_PLAYING):
|
|
pass
|
|
|
|
def set_replay_gain(self, replay_gain):
|
|
self.replay_gain = replay_gain
|
|
|
|
def toggle_play_pause(self):
|
|
if (self.state == PLAYER_PLAYING):
|
|
self.pause()
|
|
elif ((self.state == PLAYER_PAUSED) or
|
|
(self.state == PLAYER_STOPPED)):
|
|
self.play()
|
|
|
|
def stop(self):
|
|
if (self.pcmconverter is not None):
|
|
self.pcmconverter.close()
|
|
del(self.pcmconverter)
|
|
self.pcmconverter = None
|
|
self.frames_played = 0
|
|
self.state = PLAYER_STOPPED
|
|
|
|
def run(self, next_track_callback=lambda: None):
|
|
while (True):
|
|
if ((self.state == PLAYER_STOPPED) or
|
|
(self.state == PLAYER_PAUSED)):
|
|
(command, args) = self.command_queue.get(True)
|
|
if (command == "exit"):
|
|
self.audio_output.close()
|
|
return
|
|
else:
|
|
getattr(self, command)(*args)
|
|
else:
|
|
try:
|
|
(command, args) = self.command_queue.get_nowait()
|
|
if (command == "exit"):
|
|
return
|
|
else:
|
|
getattr(self, command)(*args)
|
|
except Queue.Empty:
|
|
if (self.frames_played < self.total_frames):
|
|
(data, frames) = self.pcmconverter.read()
|
|
if (frames > 0):
|
|
self.audio_output.play(data)
|
|
self.frames_played += frames
|
|
if (self.frames_played >= self.total_frames):
|
|
next_track_callback()
|
|
else:
|
|
self.frames_played = self.total_frames
|
|
next_track_callback()
|
|
else:
|
|
self.stop()
|
|
|
|
|
|
class CDPlayer:
|
|
"""A class for operating a CDDA player.
|
|
|
|
The player itself runs in a seperate thread,
|
|
which this sends commands to."""
|
|
|
|
def __init__(self, cdda, audio_output,
|
|
next_track_callback=lambda: None):
|
|
"""cdda is a audiotools.CDDA object.
|
|
audio_output is an AudioOutput subclass.
|
|
next_track_callback is a function with no arguments
|
|
which is called by the player when the current track is finished."""
|
|
|
|
self.command_queue = Queue.Queue()
|
|
self.worker = CDPlayerThread(cdda,
|
|
audio_output,
|
|
self.command_queue)
|
|
self.thread = threading.Thread(target=self.worker.run,
|
|
args=(next_track_callback,))
|
|
self.thread.daemon = True
|
|
self.thread.start()
|
|
|
|
def open(self, track_number):
|
|
"""track_number indicates which track to open, starting from 1
|
|
|
|
stops playing the current track, if any"""
|
|
|
|
self.command_queue.put(("open", [track_number]))
|
|
|
|
def play(self):
|
|
"""begins or resumes playing the currently open track, if any"""
|
|
|
|
self.command_queue.put(("play", []))
|
|
|
|
def pause(self):
|
|
"""pauses playback of the current track
|
|
|
|
Playback may be resumed with play() or toggle_play_pause()"""
|
|
|
|
self.command_queue.put(("pause", []))
|
|
|
|
def toggle_play_pause(self):
|
|
"""pauses the track if playing, play the track if paused"""
|
|
|
|
self.command_queue.put(("toggle_play_pause", []))
|
|
|
|
def stop(self):
|
|
"""stops playback of the current track
|
|
|
|
If play() is called, playback will start from the beginning."""
|
|
|
|
self.command_queue.put(("stop", []))
|
|
|
|
def close(self):
|
|
"""closes the player for playback
|
|
|
|
The player thread is halted and the AudioOutput is closed."""
|
|
|
|
self.command_queue.put(("exit", []))
|
|
|
|
def progress(self):
|
|
"""returns a (pcm_frames_played, pcm_frames_total) tuple
|
|
|
|
This indicates the current playback status in PCM frames."""
|
|
|
|
return (self.worker.frames_played, self.worker.total_frames)
|
|
|
|
|
|
class CDPlayerThread:
|
|
"""The CDPlayer class' subthread.
|
|
|
|
This should not be instantiated directly;
|
|
CDPlayer will do so automatically."""
|
|
|
|
def __init__(self, cdda, audio_output, command_queue):
|
|
self.cdda = cdda
|
|
self.audio_output = audio_output
|
|
self.command_queue = command_queue
|
|
|
|
self.audio_output.init(
|
|
sample_rate=44100,
|
|
channels=2,
|
|
channel_mask=3,
|
|
bits_per_sample=16)
|
|
self.framelist_converter = self.audio_output.framelist_converter()
|
|
|
|
self.track = None
|
|
self.pcmconverter = None
|
|
self.frames_played = 0
|
|
self.total_frames = 0
|
|
self.state = PLAYER_STOPPED
|
|
|
|
def open(self, track_number):
|
|
self.stop()
|
|
self.track = self.cdda[track_number]
|
|
self.frames_played = 0
|
|
self.total_frames = self.track.length() * 44100 / 75
|
|
|
|
def play(self):
|
|
if (self.track is not None):
|
|
if (self.state == PLAYER_STOPPED):
|
|
self.pcmconverter = ThreadedPCMConverter(
|
|
self.track,
|
|
self.framelist_converter)
|
|
self.frames_played = 0
|
|
self.state = PLAYER_PLAYING
|
|
elif (self.state == PLAYER_PAUSED):
|
|
self.state = PLAYER_PLAYING
|
|
elif (self.state == PLAYER_PLAYING):
|
|
pass
|
|
|
|
def pause(self):
|
|
if (self.state == PLAYER_PLAYING):
|
|
self.state = PLAYER_PAUSED
|
|
|
|
def toggle_play_pause(self):
|
|
if (self.state == PLAYER_PLAYING):
|
|
self.pause()
|
|
elif ((self.state == PLAYER_PAUSED) or
|
|
(self.state == PLAYER_STOPPED)):
|
|
self.play()
|
|
|
|
def stop(self):
|
|
if (self.pcmconverter is not None):
|
|
self.pcmconverter.close()
|
|
del(self.pcmconverter)
|
|
self.pcmconverter = None
|
|
self.frames_played = 0
|
|
self.state = PLAYER_STOPPED
|
|
|
|
def run(self, next_track_callback=lambda: None):
|
|
while (True):
|
|
if ((self.state == PLAYER_STOPPED) or
|
|
(self.state == PLAYER_PAUSED)):
|
|
(command, args) = self.command_queue.get(True)
|
|
if (command == "exit"):
|
|
self.audio_output.close()
|
|
return
|
|
else:
|
|
getattr(self, command)(*args)
|
|
else:
|
|
try:
|
|
(command, args) = self.command_queue.get_nowait()
|
|
if (command == "exit"):
|
|
return
|
|
else:
|
|
getattr(self, command)(*args)
|
|
except Queue.Empty:
|
|
if (self.frames_played < self.total_frames):
|
|
(data, frames) = self.pcmconverter.read()
|
|
if (frames > 0):
|
|
self.audio_output.play(data)
|
|
self.frames_played += frames
|
|
if (self.frames_played >= self.total_frames):
|
|
next_track_callback()
|
|
else:
|
|
self.frames_played = self.total_frames
|
|
next_track_callback()
|
|
else:
|
|
self.stop()
|
|
|
|
|
|
class ThreadedPCMConverter:
|
|
"""A class for decoding a PCMReader in a seperate thread.
|
|
|
|
PCMReader's data is queued such that even if decoding and
|
|
conversion are relatively time-consuming, read() will
|
|
continue smoothly."""
|
|
|
|
def __init__(self, pcmreader, converter):
|
|
"""pcmreader is a PCMReader object.
|
|
|
|
converter is a function which takes a FrameList
|
|
and returns an object suitable for the current AudioOutput object.
|
|
Upon conclusion, the PCMReader is automatically closed."""
|
|
|
|
self.decoded_data = Queue.Queue()
|
|
self.stop_decoding = threading.Event()
|
|
|
|
def convert(pcmreader, buffer_size, converter, decoded_data,
|
|
stop_decoding):
|
|
try:
|
|
frame = pcmreader.read(buffer_size)
|
|
while ((not stop_decoding.is_set()) and (len(frame) > 0)):
|
|
decoded_data.put((converter(frame), frame.frames))
|
|
frame = pcmreader.read(buffer_size)
|
|
else:
|
|
decoded_data.put((None, 0))
|
|
pcmreader.close()
|
|
except (ValueError, IOError):
|
|
decoded_data.put((None, 0))
|
|
pcmreader.close()
|
|
|
|
buffer_size = (pcmreader.sample_rate *
|
|
pcmreader.channels *
|
|
(pcmreader.bits_per_sample / 8)) / 20
|
|
|
|
self.thread = threading.Thread(
|
|
target=convert,
|
|
args=(pcmreader,
|
|
buffer_size,
|
|
converter,
|
|
self.decoded_data,
|
|
self.stop_decoding))
|
|
self.thread.daemon = True
|
|
self.thread.start()
|
|
|
|
def read(self):
|
|
"""returns a (converted_data, pcm_frame_count) tuple"""
|
|
|
|
return self.decoded_data.get(True)
|
|
|
|
def close(self):
|
|
"""stops the decoding thread and closes the PCMReader"""
|
|
|
|
self.stop_decoding.set()
|
|
self.thread.join()
|
|
|
|
|
|
class AudioOutput:
|
|
"""An abstract parent class for playing audio."""
|
|
|
|
def __init__(self):
|
|
self.sample_rate = 0
|
|
self.channels = 0
|
|
self.channel_mask = 0
|
|
self.bits_per_sample = 0
|
|
self.initialized = False
|
|
|
|
def compatible(self, pcmreader):
|
|
"""Returns True if the given pcmreader is compatible.
|
|
|
|
If False, one is expected to open a new output stream
|
|
which is compatible."""
|
|
|
|
return ((self.sample_rate == pcmreader.sample_rate) and
|
|
(self.channels == pcmreader.channels) and
|
|
(self.channel_mask == pcmreader.channel_mask) and
|
|
(self.bits_per_sample == pcmreader.bits_per_sample))
|
|
|
|
def framelist_converter(self):
|
|
"""Returns a function which converts framelist objects
|
|
|
|
to objects acceptable by our play() method."""
|
|
|
|
raise NotImplementedError()
|
|
|
|
def init(self, sample_rate, channels, channel_mask, bits_per_sample):
|
|
"""Initializes the output stream.
|
|
|
|
This *must* be called prior to play() and close().
|
|
The general flow of audio playing is:
|
|
|
|
>>> pcm = audiofile.to_pcm()
|
|
>>> player = AudioOutput()
|
|
>>> player.init(pcm.sample_rate,
|
|
... pcm.channels,
|
|
... pcm.channel_mask,
|
|
... pcm.bits_per_sample)
|
|
>>> convert = player.framelist_converter()
|
|
>>> frame = pcm.read(1024)
|
|
>>> while (len(frame) > 0):
|
|
... player.play(convert(frame))
|
|
... frame = pcm.read(1024)
|
|
>>> player.close()
|
|
"""
|
|
|
|
raise NotImplementedError()
|
|
|
|
def play(self, data):
|
|
"""plays a chunk of converted data"""
|
|
|
|
raise NotImplementedError()
|
|
|
|
def close(self):
|
|
"""closes the output stream"""
|
|
|
|
raise NotImplementedError()
|
|
|
|
@classmethod
|
|
def available(cls):
|
|
"""returns True if the AudioOutput is available on the system"""
|
|
|
|
return False
|
|
|
|
|
|
class NULLAudioOutput(AudioOutput):
|
|
"""An AudioOutput subclass which does not actually play anything.
|
|
|
|
Although this consumes audio output at the rate it would normally
|
|
play, it generates no output."""
|
|
|
|
NAME = "NULL"
|
|
|
|
def framelist_converter(self):
|
|
"""Returns a function which converts framelist objects
|
|
|
|
to objects acceptable by our play() method."""
|
|
|
|
return lambda f: f.frames
|
|
|
|
def init(self, sample_rate, channels, channel_mask, bits_per_sample):
|
|
"""Initializes the output stream.
|
|
|
|
This *must* be called prior to play() and close()."""
|
|
|
|
self.sample_rate = sample_rate
|
|
self.channels = channels
|
|
self.channel_mask = channel_mask
|
|
self.bits_per_sample = bits_per_sample
|
|
|
|
def play(self, data):
|
|
"""plays a chunk of converted data"""
|
|
|
|
time.sleep(float(data) / self.sample_rate)
|
|
|
|
def close(self):
|
|
"""closes the output stream"""
|
|
|
|
pass
|
|
|
|
@classmethod
|
|
def available(cls):
|
|
"""returns True"""
|
|
|
|
return True
|
|
|
|
|
|
class OSSAudioOutput(AudioOutput):
|
|
"""An AudioOutput subclass for OSS output."""
|
|
|
|
NAME = "OSS"
|
|
|
|
def init(self, sample_rate, channels, channel_mask, bits_per_sample):
|
|
"""Initializes the output stream.
|
|
|
|
This *must* be called prior to play() and close()."""
|
|
|
|
if (not self.initialized):
|
|
import ossaudiodev
|
|
|
|
self.sample_rate = sample_rate
|
|
self.channels = channels
|
|
self.channel_mask = channel_mask
|
|
self.bits_per_sample = bits_per_sample
|
|
|
|
self.ossaudio = ossaudiodev.open('w')
|
|
if (self.bits_per_sample == 8):
|
|
self.ossaudio.setfmt(ossaudiodev.AFMT_S8_LE)
|
|
elif (self.bits_per_sample == 16):
|
|
self.ossaudio.setfmt(ossaudiodev.AFMT_S16_LE)
|
|
elif (self.bits_per_sample == 24):
|
|
self.ossaudio.setfmt(ossaudiodev.AFMT_S16_LE)
|
|
else:
|
|
raise ValueError("Unsupported bits-per-sample")
|
|
|
|
self.ossaudio.channels(channels)
|
|
self.ossaudio.speed(sample_rate)
|
|
|
|
self.initialized = True
|
|
else:
|
|
self.close()
|
|
self.init(sample_rate=sample_rate,
|
|
channels=channels,
|
|
channel_mask=channel_mask,
|
|
bits_per_sample=bits_per_sample)
|
|
|
|
def framelist_converter(self):
|
|
"""Returns a function which converts framelist objects
|
|
|
|
to objects acceptable by our play() method."""
|
|
|
|
if (self.bits_per_sample == 8):
|
|
return lambda f: f.to_bytes(False, True)
|
|
elif (self.bits_per_sample == 16):
|
|
return lambda f: f.to_bytes(False, True)
|
|
elif (self.bits_per_sample == 24):
|
|
import audiotools.pcm
|
|
|
|
return lambda f: audiotools.pcm.from_list(
|
|
[i >> 8 for i in list(f)],
|
|
self.channels, 16, True).to_bytes(False, True)
|
|
else:
|
|
raise ValueError("Unsupported bits-per-sample")
|
|
|
|
def play(self, data):
|
|
"""plays a chunk of converted data"""
|
|
|
|
self.ossaudio.writeall(data)
|
|
|
|
def close(self):
|
|
"""closes the output stream"""
|
|
|
|
if (self.initialized):
|
|
self.initialized = False
|
|
self.ossaudio.close()
|
|
|
|
@classmethod
|
|
def available(cls):
|
|
"""returns True if OSS output is available on the system"""
|
|
|
|
try:
|
|
import ossaudiodev
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
|
|
class PulseAudioOutput(AudioOutput):
|
|
"""An AudioOutput subclass for PulseAudio output."""
|
|
|
|
NAME = "PulseAudio"
|
|
|
|
def init(self, sample_rate, channels, channel_mask, bits_per_sample):
|
|
"""Initializes the output stream.
|
|
|
|
This *must* be called prior to play() and close()."""
|
|
|
|
if (not self.initialized):
|
|
import subprocess
|
|
|
|
self.sample_rate = sample_rate
|
|
self.channels = channels
|
|
self.channel_mask = channel_mask
|
|
self.bits_per_sample = bits_per_sample
|
|
|
|
if (bits_per_sample == 8):
|
|
format = "u8"
|
|
elif (bits_per_sample == 16):
|
|
format = "s16le"
|
|
elif (bits_per_sample == 24):
|
|
format = "s24le"
|
|
else:
|
|
raise ValueError("Unsupported bits-per-sample")
|
|
|
|
self.pacat = subprocess.Popen(
|
|
[audiotools.BIN["pacat"],
|
|
"-n", "Python Audio Tools",
|
|
"--rate", str(sample_rate),
|
|
"--format", format,
|
|
"--channels", str(channels),
|
|
"--latency-msec", str(100)],
|
|
stdin=subprocess.PIPE)
|
|
|
|
self.initialized = True
|
|
else:
|
|
self.close()
|
|
self.init(sample_rate=sample_rate,
|
|
channels=channels,
|
|
channel_mask=channel_mask,
|
|
bits_per_sample=bits_per_sample)
|
|
|
|
def framelist_converter(self):
|
|
"""Returns a function which converts framelist objects
|
|
|
|
to objects acceptable by our play() method."""
|
|
|
|
if (self.bits_per_sample == 8):
|
|
return lambda f: f.to_bytes(True, False)
|
|
elif (self.bits_per_sample == 16):
|
|
return lambda f: f.to_bytes(False, True)
|
|
elif (self.bits_per_sample == 24):
|
|
return lambda f: f.to_bytes(False, True)
|
|
else:
|
|
raise ValueError("Unsupported bits-per-sample")
|
|
|
|
def play(self, data):
|
|
"""plays a chunk of converted data"""
|
|
|
|
self.pacat.stdin.write(data)
|
|
self.pacat.stdin.flush()
|
|
|
|
def close(self):
|
|
"""closes the output stream"""
|
|
|
|
if (self.initialized):
|
|
self.initialized = False
|
|
self.pacat.stdin.close()
|
|
self.pacat.wait()
|
|
|
|
@classmethod
|
|
def server_alive(cls):
|
|
import subprocess
|
|
|
|
dev = subprocess.Popen([audiotools.BIN["pactl"], "stat"],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
dev.stdout.read()
|
|
dev.stderr.read()
|
|
return (dev.wait() == 0)
|
|
|
|
@classmethod
|
|
def available(cls):
|
|
"""returns True if PulseAudio is available and running on the system"""
|
|
|
|
return (audiotools.BIN.can_execute(audiotools.BIN["pacat"]) and
|
|
audiotools.BIN.can_execute(audiotools.BIN["pactl"]) and
|
|
cls.server_alive())
|
|
|
|
|
|
class PortAudioOutput(AudioOutput):
|
|
"""An AudioOutput subclass for PortAudio output."""
|
|
|
|
NAME = "PortAudio"
|
|
|
|
def init(self, sample_rate, channels, channel_mask, bits_per_sample):
|
|
"""Initializes the output stream.
|
|
|
|
This *must* be called prior to play() and close()."""
|
|
|
|
if (not self.initialized):
|
|
import pyaudio
|
|
|
|
self.sample_rate = sample_rate
|
|
self.channels = channels
|
|
self.channel_mask = channel_mask
|
|
self.bits_per_sample = bits_per_sample
|
|
|
|
self.pyaudio = pyaudio.PyAudio()
|
|
self.stream = self.pyaudio.open(
|
|
format=self.pyaudio.get_format_from_width(
|
|
self.bits_per_sample / 8, False),
|
|
channels=self.channels,
|
|
rate=self.sample_rate,
|
|
output=True)
|
|
|
|
self.initialized = True
|
|
else:
|
|
self.close()
|
|
self.init(sample_rate=sample_rate,
|
|
channels=channels,
|
|
channel_mask=channel_mask,
|
|
bits_per_sample=bits_per_sample)
|
|
|
|
def framelist_converter(self):
|
|
"""Returns a function which converts framelist objects
|
|
|
|
to objects acceptable by our play() method."""
|
|
|
|
return lambda f: f.to_bytes(False, True)
|
|
|
|
def play(self, data):
|
|
"""plays a chunk of converted data"""
|
|
|
|
self.stream.write(data)
|
|
|
|
def close(self):
|
|
"""closes the output stream"""
|
|
|
|
if (self.initialized):
|
|
self.stream.close()
|
|
self.pyaudio.terminate()
|
|
self.initialized = False
|
|
|
|
@classmethod
|
|
def available(cls):
|
|
"""returns True if the AudioOutput is available on the system"""
|
|
|
|
try:
|
|
import pyaudio
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
AUDIO_OUTPUT = (PulseAudioOutput, OSSAudioOutput,
|
|
PortAudioOutput, NULLAudioOutput)
|