Melodia/Melodia/resources/audiotools/player.py

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)