Merge pull request #205 from adamcik/feature/software-mixer
Switch to playbin2 and re-add software mixer support.
This commit is contained in:
commit
bb589dc78b
@ -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)
|
||||
===================
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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::
|
||||
#:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user