From 6e3e1f997f9ece725910dc6aeafd5a91d2b045ac Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 26 Aug 2012 14:30:13 +0200 Subject: [PATCH] Convert to only using GStreamer mixers. --- mopidy/core.py | 6 ++-- mopidy/gstreamer.py | 76 +++++++++++++++++++++++++++++++--------- mopidy/settings.py | 43 +++++++++-------------- mopidy/utils/__init__.py | 10 ++++++ mopidy/utils/settings.py | 4 +++ 5 files changed, 93 insertions(+), 46 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index 596e0fe5..128b4723 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -106,10 +106,12 @@ def stop_gstreamer(): stop_actors_by_class(GStreamer) def setup_mixer(): - get_class(settings.MIXER).start() + # TODO: remove this hack which is just a stepping stone for our + # refactoring. + get_class('mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer').start() def stop_mixer(): - stop_actors_by_class(get_class(settings.MIXER)) + stop_actors_by_class(get_class('mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer')) def setup_backend(): get_class(settings.BACKENDS[0]).start() diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 03d79265..cf47308e 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -37,13 +37,17 @@ class GStreamer(ThreadingActor): self._source = None self._uridecodebin = None self._output = None + self._mixer = None def on_start(self): self._setup_pipeline() self._setup_output() + self._setup_mixer() self._setup_message_processor() 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']) @@ -64,6 +68,36 @@ class GStreamer(ThreadingActor): self._output) logger.debug('Output set to %s', settings.OUTPUT) + def _setup_mixer(self): + if not settings.MIXER: + logger.debug('Not adding mixer.') + return + + mixer = gst.element_factory_make(settings.MIXER) + if mixer.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: + logger.warning('Adding mixer %r failed.', settings.MIXER) + return + + track = self._select_mixer_track(mixer) + if not track: + logger.warning('Could not find usable mixer track.') + return + + self._mixer = (mixer, track) + logger.info('Mixer set to %s using %s', + mixer.get_factory().get_name(), track.label) + + def _select_mixer_track(self, mixer): + # Look for track with label == MIXER_TRACK, otherwise fallback to + # master track which is also an output. + for track in mixer.list_tracks(): + if settings.MIXER_TRACK: + if track.label == settings.MIXER_TRACK: + return track + elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER | + gst.interfaces.MIXER_TRACK_OUTPUT): + return track + def _setup_message_processor(self): bus = self._pipeline.get_bus() bus.add_signal_watch() @@ -236,33 +270,41 @@ class GStreamer(ThreadingActor): def get_volume(self): """ - Get volume level of the GStreamer software mixer. + Get volume level of the installed mixer. - :rtype: int in range [0..100] + :rtype: int in range [-1..100] """ - mixers = self._pipeline.iterate_all_by_interface(gst.interfaces.Mixer) - try: - mixer = mixers.next() - except StopIteration: - return 0 - # FIXME this _will_ break for mixers that don't implement - # GstStreamVolume - return int(mixer.get_property('volume') * 100) + if self._mixer is None: + # TODO: add tests for this case and check we propagate change + return -1 + + mixer, track = self._mixer + + volumes = mixer.get_volume(track) + avg_volume = sum(volumes) / len(volumes) + return utils.rescale(avg_volume, + old=(track.min_volume, track.max_volume), + new=(0, 100)) def set_volume(self, volume): """ - Set volume level of the GStreamer software mixer. + Set volume level of the installed mixer. :param volume: the volume in the range [0..100] :type volume: int :rtype: :class:`True` if successful, else :class:`False` """ - mixers = self._pipeline.iterate_all_by_interface(gst.interfaces.Mixer) - for mixer in mixers: - # FIXME this _will_ break for mixers that don't implement - # GstStreamVolume - mixer.set_property('volume', volume / 100.0) - return True + if self._mixer is None: + return False + + mixer, track = self._mixer + + volume = utils.rescale(volume, old=(0, 100), + new=(track.min_volume, track.max_volume)) + volumes = (volume,) * track.num_channels + + mixer.set_volume(track, volumes) + return mixer.get_volume(track) == volumes def set_metadata(self, track): """ diff --git a/mopidy/settings.py b/mopidy/settings.py index 0bb04823..e8cedff6 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -103,40 +103,28 @@ LOCAL_PLAYLIST_PATH = None #: LOCAL_TAG_CACHE_FILE = None # Implies $XDG_DATA_DIR/mopidy/tag_cache LOCAL_TAG_CACHE_FILE = None -#: Sound mixer to use. See :mod:`mopidy.mixers` for all available mixers. +#: Sound mixer to use. +#: +#: Expects a GStreamer mixer to use, typical values are: +#: alsamixer, pulsemixer, oss4mixer, ossmixer. +#: +#: Setting this to ``None`` means no volume controll. #: #: Default:: #: -#: MIXER = u'mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer' -MIXER = u'mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer' +#: MIXER = u'alsamixer' +# TODO: update to an automixer that tries to select correct mixer. +MIXER = u'alsamixer' -#: ALSA mixer only. What mixer control to use. If set to :class:`False`, first -#: ``Master`` and then ``PCM`` will be tried. +#: Sound mixer track to use. #: -#: Example: ``Master Front``. Default: :class:`False` -MIXER_ALSA_CONTROL = False - -#: External mixers only. Which port the mixer is connected to. +#: Name of the mixer track to use. If this is not set we will try to find the +#: output track with master set. #: -#: This must point to the device port like ``/dev/ttyUSB0``. +#: Default:: #: -#: Default: :class:`None` -MIXER_EXT_PORT = None - -#: External mixers only. What input source the external mixer should use. -#: -#: Example: ``Aux``. Default: :class:`None` -MIXER_EXT_SOURCE = None - -#: External mixers only. What state Speakers A should be in. -#: -#: Default: :class:`None`. -MIXER_EXT_SPEAKERS_A = None - -#: External mixers only. What state Speakers B should be in. -#: -#: Default: :class:`None`. -MIXER_EXT_SPEAKERS_B = None +#: MIXER_TRACK = None +MIXER_TRACK = None #: The maximum volume. Integer in the range 0 to 100. #: @@ -146,6 +134,7 @@ MIXER_EXT_SPEAKERS_B = None #: Default:: #: #: MIXER_MAX_VOLUME = 100 +# TODO: re-add support for this. MIXER_MAX_VOLUME = 100 #: Which address Mopidy's MPD server should bind to. diff --git a/mopidy/utils/__init__.py b/mopidy/utils/__init__.py index 567c7301..e35c98a4 100644 --- a/mopidy/utils/__init__.py +++ b/mopidy/utils/__init__.py @@ -1,3 +1,5 @@ +from __future__ import division + import locale import logging import os @@ -17,6 +19,14 @@ def flatten(the_list): return result +def rescale(v, old=None, new=None): + """Convert value between scales.""" + new_min, new_max = new + old_min, old_max = old + scaled = (new_max - new_min) / (old_max - old_min) * (v - old_min) + new_min + return int(scaled) + + def import_module(name): __import__(name) return sys.modules[name] diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 8060c667..52320099 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -121,6 +121,10 @@ def validate_settings(defaults, settings): 'LOCAL_OUTPUT_OVERRIDE': 'CUSTOM_OUTPUT', 'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH', 'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE', + 'MIXER_ALSA_CONTROL': None, + 'MIXER_EXT_PORT': None, + 'MIXER_EXT_SPEAKERS_A': None, + 'MIXER_EXT_SPEAKERS_B': None, 'SERVER': None, 'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME', 'SERVER_PORT': 'MPD_SERVER_PORT',