diff --git a/docs/changes.rst b/docs/changes.rst index 1c516a0d..5df46066 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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) =================== diff --git a/mopidy/audio/__init__.py b/mopidy/audio/__init__.py index 448412b4..df5efb92 100644 --- a/mopidy/audio/__init__.py +++ b/mopidy/audio/__init__.py @@ -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) diff --git a/mopidy/settings.py b/mopidy/settings.py index a2270707..98f7e05e 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -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:: #: