Merge pull request #205 from adamcik/feature/software-mixer

Switch to playbin2 and re-add software mixer support.
This commit is contained in:
Stein Magnus Jodal 2012-09-18 15:12:37 -07:00
commit bb589dc78b
3 changed files with 60 additions and 72 deletions

View File

@ -23,7 +23,7 @@ v0.8 (in development)
mixer that will work on your system. If this picks the wrong mixer you can of
course override it. Setting the mixer to :class:`None` is also supported. MPD
protocol support for volume has also been updated to return -1 when we have
no mixer set.
no mixer set. ``software`` can be used to force software mixing.
- Removed the Denon hardware mixer, as it is not maintained.
@ -73,6 +73,9 @@ v0.8 (in development)
``$XDG_MUSIC_DIR`` substitution. Defaults for such settings have been updated
to use this instead of hidden away defaults.
- Playback is no done using ``playbin2`` from GStreamer instead of rolling our
own. This is the first step towards resolving :issue:`171`.
**Bug fixes**
- :issue:`72`: Created a Spotify track proxy that will switch to using loaded
@ -96,6 +99,8 @@ v0.8 (in development)
- Fixed incorrect track URIs generated by M3U playlist parsing code. Generated
tracks are now relative to ``LOCAL_MUSIC_PATH``.
- :issue:`203`: Re-add support for software mixing.
v0.7.3 (2012-08-11)
===================

View File

@ -42,17 +42,16 @@ class Audio(ThreadingActor):
signed=(boolean)true,
rate=(int)44100""")
self._pipeline = None
self._source = None
self._uridecodebin = None
self._output = None
self._playbin = None
self._mixer = None
self._mixer_track = None
self._software_mixing = False
self._message_processor_set_up = False
def on_start(self):
try:
self._setup_pipeline()
self._setup_playbin()
self._setup_output()
self._setup_mixer()
self._setup_message_processor()
@ -63,36 +62,22 @@ class Audio(ThreadingActor):
def on_stop(self):
self._teardown_message_processor()
self._teardown_mixer()
self._teardown_pipeline()
self._teardown_playbin()
def _setup_pipeline(self):
# TODO: replace with and input bin so we simply have an input bin we
# connect to an output bin with a mixer on the side. set_uri on bin?
description = ' ! '.join([
'uridecodebin name=uri',
'audioconvert name=convert',
'audioresample name=resample',
'queue name=queue'])
def _setup_playbin(self):
self._playbin = gst.element_factory_make('playbin2')
logger.debug(u'Setting up base GStreamer pipeline: %s', description)
fakesink = gst.element_factory_make('fakesink')
self._playbin.set_property('video-sink', fakesink)
self._pipeline = gst.parse_launch(description)
self._uridecodebin = self._pipeline.get_by_name('uri')
self._uridecodebin.connect('notify::source', self._on_new_source)
self._uridecodebin.connect('pad-added', self._on_new_pad,
self._pipeline.get_by_name('queue').get_pad('sink'))
def _teardown_pipeline(self):
self._pipeline.set_state(gst.STATE_NULL)
def _teardown_playbin(self):
self._playbin.set_state(gst.STATE_NULL)
def _setup_output(self):
try:
self._output = gst.parse_bin_from_description(
output = gst.parse_bin_from_description(
settings.OUTPUT, ghost_unconnected_pads=True)
self._pipeline.add(self._output)
gst.element_link_many(self._pipeline.get_by_name('queue'),
self._output)
self._playbin.set_property('audio-sink', output)
logger.info('Output set to %s', settings.OUTPUT)
except gobject.GError as ex:
logger.error('Failed to create output "%s": %s',
@ -104,6 +89,11 @@ class Audio(ThreadingActor):
logger.info('Not setting up mixer.')
return
if settings.MIXER == 'software':
self._software_mixing = True
logger.info('Mixer set to software mixing.')
return
try:
mixerbin = gst.parse_bin_from_description(settings.MIXER,
ghost_unconnected_pads=False)
@ -127,7 +117,8 @@ class Audio(ThreadingActor):
logger.warning('Could not find usable mixer track.')
return
self._mixer = (mixer, track)
self._mixer = mixer
self._mixer_track = track
logger.info('Mixer set to %s using track called %s',
mixer.get_factory().get_name(), track.label)
@ -144,33 +135,19 @@ class Audio(ThreadingActor):
def _teardown_mixer(self):
if self._mixer is not None:
(mixer, track) = self._mixer
mixer.set_state(gst.STATE_NULL)
self._mixer.set_state(gst.STATE_NULL)
def _setup_message_processor(self):
bus = self._pipeline.get_bus()
bus = self._playbin.get_bus()
bus.add_signal_watch()
bus.connect('message', self._on_message)
self._message_processor_set_up = True
def _teardown_message_processor(self):
if self._message_processor_set_up:
bus = self._pipeline.get_bus()
bus = self._playbin.get_bus()
bus.remove_signal_watch()
def _on_new_source(self, element, pad):
self._source = element.get_property('source')
try:
self._source.set_property('caps', self._default_caps)
except TypeError:
pass
def _on_new_pad(self, source, pad, target_pad):
if not pad.is_linked():
if target_pad.is_linked():
target_pad.get_peer().unlink(target_pad)
pad.link(target_pad)
def _on_message(self, bus, message):
if message.type == gst.MESSAGE_EOS:
self._notify_backend_of_eos()
@ -200,7 +177,7 @@ class Audio(ThreadingActor):
:param uri: the URI to play
:type uri: string
"""
self._uridecodebin.set_property('uri', uri)
self._playbin.set_property('uri', uri)
def emit_data(self, capabilities, data):
"""
@ -215,18 +192,20 @@ class Audio(ThreadingActor):
caps = gst.caps_from_string(capabilities)
buffer_ = gst.Buffer(buffer(data))
buffer_.set_caps(caps)
self._source.set_property('caps', caps)
self._source.emit('push-buffer', buffer_)
source = self._playbin.get_property('source')
source.set_property('caps', caps)
source.emit('push-buffer', buffer_)
def emit_end_of_stream(self):
"""
Put an end-of-stream token on the pipeline. This is typically used in
Put an end-of-stream token on the playbin. This is typically used in
combination with :meth:`emit_data`.
We will get a GStreamer message when the stream playback reaches the
token, and can then do any end-of-stream related tasks.
"""
self._source.emit('end-of-stream')
self._playbin.get_property('source').emit('end-of-stream')
def get_position(self):
"""
@ -234,10 +213,10 @@ class Audio(ThreadingActor):
:rtype: int
"""
if self._pipeline.get_state()[1] == gst.STATE_NULL:
if self._playbin.get_state()[1] == gst.STATE_NULL:
return 0
try:
position = self._pipeline.query_position(gst.FORMAT_TIME)[0]
position = self._playbin.query_position(gst.FORMAT_TIME)[0]
return position // gst.MSECOND
except gst.QueryError, e:
logger.error('time_position failed: %s', e)
@ -251,10 +230,10 @@ class Audio(ThreadingActor):
:type volume: int
:rtype: :class:`True` if successful, else :class:`False`
"""
self._pipeline.get_state() # block until state changes are done
handeled = self._pipeline.seek_simple(gst.Format(gst.FORMAT_TIME),
self._playbin.get_state() # block until state changes are done
handeled = self._playbin.seek_simple(gst.Format(gst.FORMAT_TIME),
gst.SEEK_FLAG_FLUSH, position * gst.MSECOND)
self._pipeline.get_state() # block until seek is done
self._playbin.get_state() # block until seek is done
return handeled
def start_playback(self):
@ -308,12 +287,12 @@ class Audio(ThreadingActor):
"READY" -> "NULL"
"READY" -> "PAUSED"
:param state: State to set pipeline to. One of: `gst.STATE_NULL`,
:param state: State to set playbin to. One of: `gst.STATE_NULL`,
`gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`.
:type state: :class:`gst.State`
:rtype: :class:`True` if successfull, else :class:`False`
"""
result = self._pipeline.set_state(state)
result = self._playbin.set_state(state)
if result == gst.STATE_CHANGE_FAILURE:
logger.warning('Setting GStreamer state to %s: failed',
state.value_name)
@ -342,16 +321,17 @@ class Audio(ThreadingActor):
:rtype: int in range [0..100] or :class:`None`
"""
if self._software_mixing:
return round(self._playbin.get_property('volume') * 100)
if self._mixer is None:
return None
mixer, track = self._mixer
volumes = mixer.get_volume(track)
volumes = self._mixer.get_volume(self._mixer_track)
avg_volume = float(sum(volumes)) / len(volumes)
new_scale = (0, 100)
old_scale = (track.min_volume, track.max_volume)
old_scale = (self._mixer_track.min_volume, self._mixer_track.max_volume)
return utils.rescale(avg_volume, old=old_scale, new=new_scale)
def set_volume(self, volume):
@ -362,27 +342,29 @@ class Audio(ThreadingActor):
:type volume: int
:rtype: :class:`True` if successful, else :class:`False`
"""
if self._software_mixing:
self._playbin.set_property('volume', volume / 100.0)
return True
if self._mixer is None:
return False
mixer, track = self._mixer
old_scale = (0, 100)
new_scale = (track.min_volume, track.max_volume)
new_scale = (self._mixer_track.min_volume, self._mixer_track.max_volume)
volume = utils.rescale(volume, old=old_scale, new=new_scale)
volumes = (volume,) * track.num_channels
mixer.set_volume(track, volumes)
volumes = (volume,) * self._mixer_track.num_channels
self._mixer.set_volume(self._mixer_track, volumes)
return mixer.get_volume(track) == volumes
return self._mixer.get_volume(self._mixer_track) == volumes
def set_metadata(self, track):
"""
Set track metadata for currently playing song.
Only needs to be called by sources such as `appsrc` which do not
already inject tags in pipeline, e.g. when using :meth:`emit_data` to
already inject tags in playbin, e.g. when using :meth:`emit_data` to
deliver raw audio data to GStreamer.
:param track: the current track
@ -407,4 +389,4 @@ class Audio(ThreadingActor):
taglist[gst.TAG_ALBUM] = track.album.name
event = gst.event_new_tag(taglist)
self._pipeline.send_event(event)
self._playbin.send_event(event)

View File

@ -111,7 +111,8 @@ LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache'
#: Expects a GStreamer mixer to use, typical values are:
#: ``alsamixer``, ``pulsemixer``, ``ossmixer``, and ``oss4mixer``.
#:
#: Setting this to :class:`None` turns off volume control.
#: Setting this to :class:`None` turns off volume control. ``software``
#: can be used to force software mixing in the application.
#:
#: Default::
#: