From 11aa75796c7b08abd399aa403434f6dffb6b82cb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 4 Aug 2011 00:51:26 +0200 Subject: [PATCH 01/28] Get rid of current volume element (fixes #115) --- mopidy/gstreamer.py | 18 +++++++++++++----- mopidy/outputs/local.py | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index edcb3084..4ded2f95 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -38,7 +38,6 @@ class GStreamer(ThreadingActor): self._source = None self._tee = None self._uridecodebin = None - self._volume = None self._outputs = [] self._handlers = {} @@ -51,14 +50,12 @@ class GStreamer(ThreadingActor): description = ' ! '.join([ 'uridecodebin name=uri', 'audioconvert name=convert', - 'volume name=volume', 'tee name=tee']) logger.debug(u'Setting up base GStreamer pipeline: %s', description) self._pipeline = gst.parse_launch(description) self._tee = self._pipeline.get_by_name('tee') - self._volume = self._pipeline.get_by_name('volume') self._uridecodebin = self._pipeline.get_by_name('uri') self._uridecodebin.connect('notify::source', self._on_new_source) @@ -247,7 +244,14 @@ class GStreamer(ThreadingActor): :rtype: int in range [0..100] """ - return int(self._volume.get_property('volume') * 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) def set_volume(self, volume): """ @@ -257,7 +261,11 @@ class GStreamer(ThreadingActor): :type volume: int :rtype: :class:`True` if successful, else :class:`False` """ - self._volume.set_property('volume', volume / 100.0) + 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 def set_metadata(self, track): diff --git a/mopidy/outputs/local.py b/mopidy/outputs/local.py index 8101e026..62b26e3f 100644 --- a/mopidy/outputs/local.py +++ b/mopidy/outputs/local.py @@ -17,4 +17,4 @@ class LocalOutput(BaseOutput): """ def describe_bin(self): - return 'autoaudiosink' + return 'volume ! autoaudiosink' From 6e3e1f997f9ece725910dc6aeafd5a91d2b045ac Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 26 Aug 2012 14:30:13 +0200 Subject: [PATCH 02/28] 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', From 5785087c95a1a6997a5b683958a68e5e9b70aa6a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 26 Aug 2012 16:15:55 +0200 Subject: [PATCH 03/28] Add AutoAudioMixer that auto selects a suitable mixer. --- mopidy/gstreamer.py | 81 +++++++++++++++++++++++++++++++++++++++++++++ mopidy/settings.py | 5 ++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index cf47308e..1a80bffc 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -1,5 +1,6 @@ import pygst pygst.require('0.10') +import gobject import gst import logging @@ -13,6 +14,86 @@ from mopidy.backends.base import Backend logger = logging.getLogger('mopidy.gstreamer') +# TODO: we might want to add some ranking to the mixers we know about? +# TODO: move to mixers module and do from mopidy.mixers import * to install +# elements. +class AutoAudioMixer(gst.Element): + __gstdetails__ = ('AutoAudioMixer', + 'Mixer', + 'Element automatically selects a mixer.', + 'Thomas Adamcik') + + def __init__(self): + gst.Element.__init__(self) + self._mixer = self._find_mixer() + self._mixer.set_state(gst.STATE_READY) + logger.debug('AutoAudioMixer choose: %s', self._mixer.get_name()) + + def _find_mixer(self): + registry = gst.registry_get_default() + + factories = registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY) + factories.sort(key: lambda f: (-f.get_rank(), f.get_name()) + + for factory in factories: + # Avoid sink/srcs that implment mixing. + if factory.get_klass() != 'Generic/Audio': + continue + # Avoid anything that doesn't implment mixing. + elif not factory.has_interface('GstMixer'): + continue + + element = factory.create() + if not element: + continue + + # Element has devices, try each one. + if hasattr(element, 'probe_get_values_name'): + devices = element.probe_get_values_name('device') + + for device in devices: + element.set_property('device', device) + if self._check_mixer(element): + return element + + # Otherwise just test it as is. + elif self._check_mixer(element): + return element + + def _check_mixer(self, element): + try: + # Only allow elements that succesfully become ready. + result = element.set_state(gst.STATE_READY) + if result != gst.STATE_CHANGE_SUCCESS: + return False + + # Only allow elements that have a least one output track. + output_flag = gst.interfaces.MIXER_TRACK_OUTPUT + return bool(self._find_track(element, output_flag)) + finally: + element.set_state(gst.STATE_NULL) + + def _find_track(self, element, flags): + # Return first track that matches flags. + for track in element.list_tracks(): + if track.flags & flags: + return track + return None + + def list_tracks(self): + return self._mixer.list_tracks() + + def get_volume(self, track): + return self._mixer.get_volume(track) + + def set_volume(self, track, volumes): + return self._mixer.set_volume(track, volumes) + + +gobject.type_register(AutoAudioMixer) +gst.element_register (AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL) + + class GStreamer(ThreadingActor): """ Audio output through `GStreamer `_. diff --git a/mopidy/settings.py b/mopidy/settings.py index e8cedff6..bebcd24d 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -112,9 +112,8 @@ LOCAL_TAG_CACHE_FILE = None #: #: Default:: #: -#: MIXER = u'alsamixer' -# TODO: update to an automixer that tries to select correct mixer. -MIXER = u'alsamixer' +#: MIXER = u'autoaudiomixer' +MIXER = u'autoaudiomixer' #: Sound mixer track to use. #: From b7e59c9cef9cc3cbce49b1fa608d88d9cb69f9eb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 26 Aug 2012 18:10:49 +0200 Subject: [PATCH 04/28] Fix comments from review. --- mopidy/gstreamer.py | 16 ++++++++++------ mopidy/settings.py | 5 +++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index d157ee9d..15346939 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -36,7 +36,7 @@ class AutoAudioMixer(gst.Element): factories.sort(key: lambda f: (-f.get_rank(), f.get_name()) for factory in factories: - # Avoid sink/srcs that implment mixing. + # Avoid sink/srcs that implment mixing. if factory.get_klass() != 'Generic/Audio': continue # Avoid anything that doesn't implment mixing. @@ -353,6 +353,10 @@ class GStreamer(ThreadingActor): """ Get volume level of the installed mixer. + 0 == muted. + 100 == max volume for given system. + -1 == no mixer present, i.e. volume unknown. + :rtype: int in range [-1..100] """ if self._mixer is None: @@ -364,8 +368,7 @@ class GStreamer(ThreadingActor): 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)) + old=(track.min_volume, track.max_volume), new=(0, 100)) def set_volume(self, volume): """ @@ -380,11 +383,12 @@ class GStreamer(ThreadingActor): mixer, track = self._mixer - volume = utils.rescale(volume, old=(0, 100), - new=(track.min_volume, track.max_volume)) - volumes = (volume,) * track.num_channels + 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 bebcd24d..1d5ec330 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -108,7 +108,7 @@ LOCAL_TAG_CACHE_FILE = None #: Expects a GStreamer mixer to use, typical values are: #: alsamixer, pulsemixer, oss4mixer, ossmixer. #: -#: Setting this to ``None`` means no volume controll. +#: Setting this to ``None`` means no volume control. #: #: Default:: #: @@ -118,7 +118,8 @@ MIXER = u'autoaudiomixer' #: Sound mixer track to use. #: #: Name of the mixer track to use. If this is not set we will try to find the -#: output track with master set. +#: output track with master set. As an example, using ``alsamixer`` you would +#: typically set this to ``Master`` or ``PCM``. #: #: Default:: #: From b2caad4d8c82d33f2c0e1a5e2a749d5998e1f0e4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 26 Aug 2012 19:05:59 +0200 Subject: [PATCH 05/28] Implement Mixer interface properly. --- mopidy/gstreamer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 15346939..2a377443 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -17,7 +17,7 @@ logger = logging.getLogger('mopidy.gstreamer') # TODO: we might want to add some ranking to the mixers we know about? # TODO: move to mixers module and do from mopidy.mixers import * to install # elements. -class AutoAudioMixer(gst.Element): +class AutoAudioMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): __gstdetails__ = ('AutoAudioMixer', 'Mixer', 'Element automatically selects a mixer.', @@ -89,6 +89,9 @@ class AutoAudioMixer(gst.Element): def set_volume(self, track, volumes): return self._mixer.set_volume(track, volumes) + def set_record(self, track, record): + pass + gobject.type_register(AutoAudioMixer) gst.element_register (AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL) From b7734f6a766b88cdb5a506829d1330d30615ccc8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 26 Aug 2012 19:47:45 +0200 Subject: [PATCH 06/28] Return None for unknown volume. --- mopidy/gstreamer.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 2a377443..e5d7337b 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -356,15 +356,14 @@ class GStreamer(ThreadingActor): """ Get volume level of the installed mixer. - 0 == muted. - 100 == max volume for given system. - -1 == no mixer present, i.e. volume unknown. + 0 == muted. + 100 == max volume for given system. + None == no mixer present, i.e. volume unknown. - :rtype: int in range [-1..100] + :rtype: int in range [0..100] """ if self._mixer is None: - # TODO: add tests for this case and check we propagate change - return -1 + return None mixer, track = self._mixer From 0f5bf655a0215f9f0d467096853511124a671884 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 31 Aug 2012 00:52:30 +0200 Subject: [PATCH 07/28] Fix import and factory sort code. --- mopidy/gstreamer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index e5d7337b..84b540a6 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -8,7 +8,7 @@ import logging from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy import settings +from mopidy import settings, utils from mopidy.backends.base import Backend logger = logging.getLogger('mopidy.gstreamer') @@ -33,7 +33,7 @@ class AutoAudioMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer) registry = gst.registry_get_default() factories = registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY) - factories.sort(key: lambda f: (-f.get_rank(), f.get_name()) + factories.sort(key=lambda f: (-f.get_rank(), f.get_name())) for factory in factories: # Avoid sink/srcs that implment mixing. From 2d5ba154ed93f80569906051d9abd562fdc949b5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 13:33:41 +0200 Subject: [PATCH 08/28] Switch to module imports and with assertRaises in init_test. --- tests/utils/init_test.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/utils/init_test.py b/tests/utils/init_test.py index 2097e3e6..f232e2ef 100644 --- a/tests/utils/init_test.py +++ b/tests/utils/init_test.py @@ -1,24 +1,27 @@ -from mopidy.utils import get_class +from mopidy import utils from tests import unittest class GetClassTest(unittest.TestCase): def test_loading_module_that_does_not_exist(self): - self.assertRaises(ImportError, get_class, 'foo.bar.Baz') + with self.assertRaises(ImportError): + utils.get_class('foo.bar.Baz') def test_loading_class_that_does_not_exist(self): - self.assertRaises(ImportError, get_class, 'unittest.FooBarBaz') + with self.assertRaises(ImportError): + utils.get_class('unittest.FooBarBaz') def test_loading_incorrect_class_path(self): - self.assertRaises(ImportError, get_class, 'foobarbaz') + with self.assertRaises(ImportError): + utils.get_class('foobarbaz') def test_import_error_message_contains_complete_class_path(self): try: - get_class('foo.bar.Baz') + utils.get_class('foo.bar.Baz') except ImportError as e: self.assert_('foo.bar.Baz' in str(e)) def test_loading_existing_class(self): - cls = get_class('unittest.TestCase') + cls = utils.get_class('unittest.TestCase') self.assertEqual(cls.__name__, 'TestCase') From b796d7c859b7a8f8208aa02d766ac66cb8b0c68f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 13:34:16 +0200 Subject: [PATCH 09/28] Add create fakemixer element for testing. - GStreamer tests now use this instead of a real mixer. - fakemixer and the autoaudiomixer still need to be moved. - We should probably use a fakesink as output as well. --- mopidy/gstreamer.py | 75 +++++++++++++++++++++++++++++++++++++++- mopidy/utils/__init__.py | 2 +- tests/gstreamer_test.py | 1 + 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 60e601f3..17657729 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -21,6 +21,8 @@ class GStreamerError(Exception): # TODO: we might want to add some ranking to the mixers we know about? # TODO: move to mixers module and do from mopidy.mixers import * to install # elements. +# TODO: use gst.Bin so we can add the real mixer and have state sync +# automatically. class AutoAudioMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): __gstdetails__ = ('AutoAudioMixer', 'Mixer', @@ -31,7 +33,7 @@ class AutoAudioMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer) gst.Element.__init__(self) self._mixer = self._find_mixer() self._mixer.set_state(gst.STATE_READY) - logger.debug('AutoAudioMixer choose: %s', self._mixer.get_name()) + logger.debug('AutoAudioMixer chose: %s', self._mixer.get_name()) def _find_mixer(self): registry = gst.registry_get_default() @@ -101,6 +103,77 @@ gobject.type_register(AutoAudioMixer) gst.element_register (AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL) +def create_fake_track(label, min_volume, max_volume, num_channels, flags): + class Track(gst.interfaces.MixerTrack): + def __init__(self): + super(Track, self).__init__() + self.volumes = (100,) * self.num_channels + + @gobject.property + def label(self): + return label + + @gobject.property + def min_volume(self): + return min_volume + + @gobject.property + def max_volume(self): + return max_volume + + @gobject.property + def num_channels(self): + return num_channels + + @gobject.property + def flags(self): + return flags + + return Track() + +class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): + __gstdetails__ = ('FakeMixer', + 'Mixer', + 'Fake mixer for use in tests.', + 'Thomas Adamcik') + + track_label = gobject.property(type=str, default='Master') + + track_min_volume = gobject.property(type=int, default=0) + + track_max_volume = gobject.property(type=int, default=100) + + track_num_channels = gobject.property(type=int, default=2) + + track_flags = gobject.property(type=int, + default=(gst.interfaces.MIXER_TRACK_MASTER | + gst.interfaces.MIXER_TRACK_OUTPUT)) + + def __init__(self): + gst.Element.__init__(self) + + def list_tracks(self): + track = create_fake_track(self.track_label, + self.track_min_volume, + self.track_max_volume, + self.track_num_channels, + self.track_flags) + return [track] + + def get_volume(self, track): + return track.volumes + + def set_volume(self, track, volumes): + track.volumes = volumes + + def set_record(self, track, record): + pass + + +gobject.type_register(FakeMixer) +gst.element_register (FakeMixer, 'fakemixer', gst.RANK_MARGINAL) + + class GStreamer(ThreadingActor): """ Audio output through `GStreamer `_. diff --git a/mopidy/utils/__init__.py b/mopidy/utils/__init__.py index e35c98a4..35ec916a 100644 --- a/mopidy/utils/__init__.py +++ b/mopidy/utils/__init__.py @@ -8,7 +8,7 @@ import sys logger = logging.getLogger('mopidy.utils') -# TODO: user itertools.chain.from_iterable(the_list)? +# TODO: use itertools.chain.from_iterable(the_list)? def flatten(the_list): result = [] for element in the_list: diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index b370981a..9d33901d 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -12,6 +12,7 @@ from tests import unittest, path_to_data_dir class GStreamerTest(unittest.TestCase): def setUp(self): settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) + settings.MIXER = 'fakemixer' self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) self.gstreamer = GStreamer() From 2b018606805e611e298573e730bc25d12a530244 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 14:01:11 +0200 Subject: [PATCH 10/28] Make it possible to override GStreamer settings in tests. - Specifically you can now pass in values instead of relying on global settings. --- mopidy/gstreamer.py | 34 +++++++++++++++++++--------------- tests/gstreamer_test.py | 5 +++-- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 17657729..74b66410 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -131,6 +131,7 @@ def create_fake_track(label, min_volume, max_volume, num_channels, flags): return Track() + class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): __gstdetails__ = ('FakeMixer', 'Mixer', @@ -181,10 +182,12 @@ class GStreamer(ThreadingActor): **Settings:** - :attr:`mopidy.settings.OUTPUT` + - :attr:`mopidy.settings.MIXER` + - :attr:`mopidy.settings.MIXER_TRACK` """ - def __init__(self): + def __init__(self, output=None, mixer=None, mixer_track=None): super(GStreamer, self).__init__() self._default_caps = gst.Caps(""" audio/x-raw-int, @@ -201,8 +204,9 @@ class GStreamer(ThreadingActor): self._mixer = None self._setup_pipeline() - self._setup_output() - self._setup_mixer() + self._setup_output(output or settings.OUTPUT) + self._setup_mixer(mixer or settings.MIXER, + mixer_track or settings.MIXER_TRACK) self._setup_message_processor() def _setup_pipeline(self): @@ -223,29 +227,29 @@ class GStreamer(ThreadingActor): self._uridecodebin.connect('pad-added', self._on_new_pad, self._pipeline.get_by_name('queue').get_pad('sink')) - def _setup_output(self): + def _setup_output(self, output_description): try: - self._output = gst.parse_bin_from_description(settings.OUTPUT, True) + self._output = gst.parse_bin_from_description(output_description, True) except gobject.GError as e: raise GStreamerError('%r while creating %r' % (e.message, - settings.OUTPUT)) + output_description)) self._pipeline.add(self._output) gst.element_link_many(self._pipeline.get_by_name('queue'), self._output) - logger.debug('Output set to %s', settings.OUTPUT) + logger.debug('Output set to %s', output_description) - def _setup_mixer(self): - if not settings.MIXER: + def _setup_mixer(self, mixer_element, track_label): + if not mixer_element: logger.debug('Not adding mixer.') return - mixer = gst.element_factory_make(settings.MIXER) + mixer = gst.element_factory_make(mixer_element) if mixer.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: - logger.warning('Adding mixer %r failed.', settings.MIXER) + logger.warning('Adding mixer %r failed.', mixer_element) return - track = self._select_mixer_track(mixer) + track = self._select_mixer_track(mixer, track_label) if not track: logger.warning('Could not find usable mixer track.') return @@ -254,12 +258,12 @@ class GStreamer(ThreadingActor): logger.info('Mixer set to %s using %s', mixer.get_factory().get_name(), track.label) - def _select_mixer_track(self, mixer): + def _select_mixer_track(self, mixer, track_label): # 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: + if track_label: + if track.label == track_label: return track elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER | gst.interfaces.MIXER_TRACK_OUTPUT): diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index 9d33901d..a4f740b4 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -11,10 +11,11 @@ from tests import unittest, path_to_data_dir 'Our Windows build server does not support GStreamer yet') class GStreamerTest(unittest.TestCase): def setUp(self): + # TODO: does this modify global settings without reseting it? + # TODO: should use a fake backend stub for this test? settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) - settings.MIXER = 'fakemixer' self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) - self.gstreamer = GStreamer() + self.gstreamer = GStreamer(mixer='fakemixer') def prepare_uri(self, uri): self.gstreamer.prepare_change() From 9c30fab959b3a07d9f14d9309bf4a736923f28f5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 14:33:32 +0200 Subject: [PATCH 11/28] Switch to using a mixerbin instead of element. - This allows us to set values like ``alsasink device=hw:1`` etc. - Adds an intial volume to our fakemixer. - Minor code cleanup for rescale() calls. --- mopidy/gstreamer.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 74b66410..bc0058dd 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -103,11 +103,12 @@ gobject.type_register(AutoAudioMixer) gst.element_register (AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL) -def create_fake_track(label, min_volume, max_volume, num_channels, flags): +def create_fake_track(label, intial_volume, min_volume, max_volume, + num_channels, flags): class Track(gst.interfaces.MixerTrack): def __init__(self): super(Track, self).__init__() - self.volumes = (100,) * self.num_channels + self.volumes = (intial_volume,) * self.num_channels @gobject.property def label(self): @@ -140,6 +141,8 @@ class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): track_label = gobject.property(type=str, default='Master') + track_initial_volume = gobject.property(type=int, default=0) + track_min_volume = gobject.property(type=int, default=0) track_max_volume = gobject.property(type=int, default=100) @@ -155,6 +158,7 @@ class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): def list_tracks(self): track = create_fake_track(self.track_label, + self.track_initial_volume, self.track_min_volume, self.track_max_volume, self.track_num_channels, @@ -244,11 +248,12 @@ class GStreamer(ThreadingActor): logger.debug('Not adding mixer.') return - mixer = gst.element_factory_make(mixer_element) - if mixer.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: + mixerbin = gst.parse_bin_from_description(mixer_element, False) + if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: logger.warning('Adding mixer %r failed.', mixer_element) return + mixer = mixerbin.get_by_interface('GstMixer') track = self._select_mixer_track(mixer, track_label) if not track: logger.warning('Could not find usable mixer track.') @@ -454,10 +459,13 @@ class GStreamer(ThreadingActor): 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)) + avg_volume = float(sum(volumes)) / len(volumes) + + new_scale = (0, 100) + old_scale = (track.min_volume, track.max_volume) + return utils.rescale(avg_volume, old=old_scale, new=new_scale) def set_volume(self, volume): """ @@ -472,8 +480,10 @@ class GStreamer(ThreadingActor): mixer, track = self._mixer - volume = utils.rescale(volume, - old=(0, 100), new=(track.min_volume, track.max_volume)) + old_scale = (0, 100) + new_scale = (track.min_volume, track.max_volume) + + volume = utils.rescale(volume, old=old_scale, new=new_scale) volumes = (volume,) * track.num_channels mixer.set_volume(track, volumes) From 40502e41e5860bd3d787c0c738fbf16699611d72 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 14:35:20 +0200 Subject: [PATCH 12/28] Update tests to catch scaling problem in mixer. - Fixes problem where 60% became 59% due to bad rounding. - Tests assume scale of 0-65536 which matches ALSA. - Check all possible values of set_volume and ensure we the right value out. --- mopidy/utils/__init__.py | 4 ++-- tests/gstreamer_test.py | 18 ++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/mopidy/utils/__init__.py b/mopidy/utils/__init__.py index 35ec916a..aacc2e85 100644 --- a/mopidy/utils/__init__.py +++ b/mopidy/utils/__init__.py @@ -23,8 +23,8 @@ 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) + scaling = float(new_max - new_min) / (old_max - old_min) + return round(scaling * (v - old_min) + new_min) def import_module(name): diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index a4f740b4..f30b672b 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -15,7 +15,7 @@ class GStreamerTest(unittest.TestCase): # TODO: should use a fake backend stub for this test? settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) - self.gstreamer = GStreamer(mixer='fakemixer') + self.gstreamer = GStreamer(mixer='fakemixer track_max_volume=65536') def prepare_uri(self, uri): self.gstreamer.prepare_change() @@ -50,20 +50,10 @@ class GStreamerTest(unittest.TestCase): def test_end_of_data_stream(self): pass # TODO - def test_default_get_volume_result(self): - self.assertEqual(100, self.gstreamer.get_volume()) - def test_set_volume(self): - self.assertTrue(self.gstreamer.set_volume(50)) - self.assertEqual(50, self.gstreamer.get_volume()) - - def test_set_volume_to_zero(self): - self.assertTrue(self.gstreamer.set_volume(0)) - self.assertEqual(0, self.gstreamer.get_volume()) - - def test_set_volume_to_one_hundred(self): - self.assertTrue(self.gstreamer.set_volume(100)) - self.assertEqual(100, self.gstreamer.get_volume()) + for value in range(0, 101): + self.assertTrue(self.gstreamer.set_volume(value)) + self.assertEqual(value, self.gstreamer.get_volume()) @unittest.SkipTest def test_set_state_encapsulation(self): From 03b836ed64d7e6d04f6e70084e04f5cbf3ea7eb9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 15:04:22 +0200 Subject: [PATCH 13/28] Removed autoaudiosink's device selection. The way this code was testing devices locked the element to using the wrong device. The incorrect device had a max volume of 39 on the Master track, really making accurate volume changes impossible. Instead of trying to make any guesses about this I'm leaving it to the element to have sensible defaults. Code will also ensure that it returns a newly created copy of the mixer, not one we have already used. --- mopidy/gstreamer.py | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index bc0058dd..3c5f4ed3 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -49,42 +49,34 @@ class AutoAudioMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer) elif not factory.has_interface('GstMixer'): continue - element = factory.create() - if not element: - continue + if self._test_mixer(factory): + return factory.create() - # Element has devices, try each one. - if hasattr(element, 'probe_get_values_name'): - devices = element.probe_get_values_name('device') + return None - for device in devices: - element.set_property('device', device) - if self._check_mixer(element): - return element + def _test_mixer(self, factory): + element = factory.create() + if not element: + return False - # Otherwise just test it as is. - elif self._check_mixer(element): - return element - - def _check_mixer(self, element): try: - # Only allow elements that succesfully become ready. result = element.set_state(gst.STATE_READY) if result != gst.STATE_CHANGE_SUCCESS: return False - # Only allow elements that have a least one output track. - output_flag = gst.interfaces.MIXER_TRACK_OUTPUT - return bool(self._find_track(element, output_flag)) + # Trust that the default device is sane and just check tracks. + return self._test_tracks(element) finally: element.set_state(gst.STATE_NULL) - def _find_track(self, element, flags): - # Return first track that matches flags. + def _test_tracks(self, element): + # Only allow elements that have a least one output track. + flags = gst.interfaces.MIXER_TRACK_OUTPUT + for track in element.list_tracks(): if track.flags & flags: - return track - return None + return True + return False def list_tracks(self): return self._mixer.list_tracks() @@ -452,7 +444,7 @@ class GStreamer(ThreadingActor): 100 == max volume for given system. None == no mixer present, i.e. volume unknown. - :rtype: int in range [0..100] + :rtype: int in range [0..100] or :class:`None` """ if self._mixer is None: return None From eee3edf7273bb660be70be1fe9b055111be80c33 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 15:21:13 +0200 Subject: [PATCH 14/28] Turn autoaudiomixer into a bin. This allows us to add our sub mixer that we are proxing (not sure if GstChildProxy can be used in Python) so that state changes to the parent propagates nicely. --- mopidy/gstreamer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 3c5f4ed3..de63b702 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -23,16 +23,16 @@ class GStreamerError(Exception): # elements. # TODO: use gst.Bin so we can add the real mixer and have state sync # automatically. -class AutoAudioMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): +class AutoAudioMixer(gst.Bin, gst.ImplementsInterface, gst.interfaces.Mixer): __gstdetails__ = ('AutoAudioMixer', 'Mixer', 'Element automatically selects a mixer.', 'Thomas Adamcik') def __init__(self): - gst.Element.__init__(self) + gst.Bin.__init__(self) self._mixer = self._find_mixer() - self._mixer.set_state(gst.STATE_READY) + self.add(self._mixer) logger.debug('AutoAudioMixer chose: %s', self._mixer.get_name()) def _find_mixer(self): From 036bf2ab24d80b974a290a955913e2b5e41f353e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 15:49:24 +0200 Subject: [PATCH 15/28] Update code to handle case where AutoAudioMixer fails to find a mixer. This change implies removing the GstMixer interface from the autoaudiomixer, which allows us to check if a mixer was found in a generic way by using get_by_interface. This also means we get direct access to the child mixer so the proxying code is no longer needed. --- mopidy/gstreamer.py | 50 +++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index de63b702..e14f4f78 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -21,9 +21,7 @@ class GStreamerError(Exception): # TODO: we might want to add some ranking to the mixers we know about? # TODO: move to mixers module and do from mopidy.mixers import * to install # elements. -# TODO: use gst.Bin so we can add the real mixer and have state sync -# automatically. -class AutoAudioMixer(gst.Bin, gst.ImplementsInterface, gst.interfaces.Mixer): +class AutoAudioMixer(gst.Bin): __gstdetails__ = ('AutoAudioMixer', 'Mixer', 'Element automatically selects a mixer.', @@ -31,9 +29,12 @@ class AutoAudioMixer(gst.Bin, gst.ImplementsInterface, gst.interfaces.Mixer): def __init__(self): gst.Bin.__init__(self) - self._mixer = self._find_mixer() - self.add(self._mixer) - logger.debug('AutoAudioMixer chose: %s', self._mixer.get_name()) + mixer = self._find_mixer() + if mixer: + self.add(mixer) + logger.debug('AutoAudioMixer chose: %s', mixer.get_name()) + else: + logger.debug('AutoAudioMixer did not find any usable mixers') def _find_mixer(self): registry = gst.registry_get_default() @@ -78,18 +79,6 @@ class AutoAudioMixer(gst.Bin, gst.ImplementsInterface, gst.interfaces.Mixer): return True return False - def list_tracks(self): - return self._mixer.list_tracks() - - def get_volume(self, track): - return self._mixer.get_volume(track) - - def set_volume(self, track, volumes): - return self._mixer.set_volume(track, volumes) - - def set_record(self, track, record): - pass - gobject.type_register(AutoAudioMixer) gst.element_register (AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL) @@ -233,26 +222,34 @@ class GStreamer(ThreadingActor): self._pipeline.add(self._output) gst.element_link_many(self._pipeline.get_by_name('queue'), self._output) - logger.debug('Output set to %s', output_description) + logger.info('Output set to %s', output_description) - def _setup_mixer(self, mixer_element, track_label): - if not mixer_element: + def _setup_mixer(self, mixer_bin_description, track_label): + if not mixer_bin_description: logger.debug('Not adding mixer.') return - mixerbin = gst.parse_bin_from_description(mixer_element, False) - if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: - logger.warning('Adding mixer %r failed.', mixer_element) + mixerbin = gst.parse_bin_from_description(mixer_bin_description, False) + + # We assume that the bin will contain a single mixer. + mixer = mixerbin.get_by_interface('GstMixer') + if not mixer: + logger.warning('Did not find any mixers in %r', + mixer_bin_description) + return + + if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: + logger.warning('Setting mixer %r to READY failed.', + mixer_bin_description) return - mixer = mixerbin.get_by_interface('GstMixer') track = self._select_mixer_track(mixer, track_label) if not track: logger.warning('Could not find usable mixer track.') return self._mixer = (mixer, track) - logger.info('Mixer set to %s using %s', + logger.info('Mixer set to %s using track called %s', mixer.get_factory().get_name(), track.label) def _select_mixer_track(self, mixer, track_label): @@ -451,7 +448,6 @@ class GStreamer(ThreadingActor): mixer, track = self._mixer - volumes = mixer.get_volume(track) avg_volume = float(sum(volumes)) / len(volumes) From 7574862491d4b8e04316c26ecda427907daf02f7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 15:59:38 +0200 Subject: [PATCH 16/28] Cleanup error handling of bad GStreamer settings. No need to have a GStreamerError, we can just let the GErrors bubble now that we are initialising in the main thread. --- mopidy/core.py | 4 +--- mopidy/gstreamer.py | 24 +++++++++--------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index 4012359f..128b4723 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -20,7 +20,7 @@ sys.argv[1:] = gstreamer_args from mopidy import (get_version, settings, OptionalDependencyError, SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE) -from mopidy.gstreamer import GStreamer, GStreamerError +from mopidy.gstreamer import GStreamer from mopidy.utils import get_class from mopidy.utils.log import setup_logging from mopidy.utils.path import get_or_create_folder, get_or_create_file @@ -45,8 +45,6 @@ def main(): loop.run() except SettingsError as e: logger.error(e.message) - except GStreamerError as e: - logger.error(e) except KeyboardInterrupt: logger.info(u'Interrupted. Exiting...') except Exception as e: diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index e14f4f78..4eb94e91 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -14,10 +14,6 @@ from mopidy.backends.base import Backend logger = logging.getLogger('mopidy.gstreamer') -class GStreamerError(Exception): - pass - - # TODO: we might want to add some ranking to the mixers we know about? # TODO: move to mixers module and do from mopidy.mixers import * to install # elements. @@ -213,34 +209,32 @@ class GStreamer(ThreadingActor): self._pipeline.get_by_name('queue').get_pad('sink')) def _setup_output(self, output_description): - try: - self._output = gst.parse_bin_from_description(output_description, True) - except gobject.GError as e: - raise GStreamerError('%r while creating %r' % (e.message, - output_description)) + # This will raise a gobject.GError if the description is bad. + self._output = gst.parse_bin_from_description(output_description, True) self._pipeline.add(self._output) gst.element_link_many(self._pipeline.get_by_name('queue'), self._output) logger.info('Output set to %s', output_description) - def _setup_mixer(self, mixer_bin_description, track_label): - if not mixer_bin_description: - logger.debug('Not adding mixer.') + def _setup_mixer(self, mixer_description, track_label): + if not mixer_description: + logger.info('Not setting up mixer.') return - mixerbin = gst.parse_bin_from_description(mixer_bin_description, False) + # This will raise a gobject.GError if the description is bad. + mixerbin = gst.parse_bin_from_description(mixer_description, False) # We assume that the bin will contain a single mixer. mixer = mixerbin.get_by_interface('GstMixer') if not mixer: logger.warning('Did not find any mixers in %r', - mixer_bin_description) + mixer_description) return if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: logger.warning('Setting mixer %r to READY failed.', - mixer_bin_description) + mixer_description) return track = self._select_mixer_track(mixer, track_label) From 5a0199ac200ac1707cc0b4ab6f2e7454c1685f66 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 16:12:38 +0200 Subject: [PATCH 17/28] Remove MIXER_MAX_VOLUME setting. --- mopidy/mixers/base.py | 9 ++------- mopidy/settings.py | 11 ----------- mopidy/utils/settings.py | 1 + 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index 82783be1..a387c143 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -5,13 +5,8 @@ from mopidy import listeners, settings logger = logging.getLogger('mopidy.mixers') class BaseMixer(object): - """ - **Settings:** - - - :attr:`mopidy.settings.MIXER_MAX_VOLUME` - """ - - amplification_factor = settings.MIXER_MAX_VOLUME / 100.0 + # TODO: remove completly + amplification_factor = 1.0 @property def volume(self): diff --git a/mopidy/settings.py b/mopidy/settings.py index 4e8370e6..72e805bf 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -126,17 +126,6 @@ MIXER = u'autoaudiomixer' #: MIXER_TRACK = None MIXER_TRACK = None -#: The maximum volume. Integer in the range 0 to 100. -#: -#: If this settings is set to 80, the mixer will set the actual volume to 80 -#: when asked to set it to 100. -#: -#: Default:: -#: -#: MIXER_MAX_VOLUME = 100 -# TODO: re-add support for this. -MIXER_MAX_VOLUME = 100 - #: Which address Mopidy's MPD server should bind to. #: #:Examples: diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 372dd8a0..70d45721 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -125,6 +125,7 @@ def validate_settings(defaults, settings): 'MIXER_EXT_PORT': None, 'MIXER_EXT_SPEAKERS_A': None, 'MIXER_EXT_SPEAKERS_B': None, + 'MIXER_MAX_VOLUME': None, 'SERVER': None, 'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME', 'SERVER_PORT': 'MPD_SERVER_PORT', From f6b96680ae849d87510a6d52faa348084bffdb3a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 1 Sep 2012 22:07:15 +0200 Subject: [PATCH 18/28] Fix MPD volume command. The command should return -1 when the volume is not known. --- mopidy/frontends/mpd/protocol/status.py | 4 ++-- tests/frontends/mpd/status_test.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index f32c46c8..81b68dd6 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -137,7 +137,7 @@ def status(context): Reports the current status of the player and the volume level. - - ``volume``: 0-100 + - ``volume``: 0-100 or -1 - ``repeat``: 0 or 1 - ``single``: 0 or 1 - ``consume``: 0 or 1 @@ -267,7 +267,7 @@ def _status_volume(futures): if volume is not None: return volume else: - return 0 + return -1 def _status_xfade(futures): return 0 # Not supported diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index bdd2dab8..9fa62321 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -42,10 +42,10 @@ class StatusHandlerTest(unittest.TestCase): self.assert_('playtime' in result) self.assert_(int(result['playtime']) >= 0) - def test_status_method_contains_volume_which_defaults_to_0(self): + def test_status_method_contains_volume_with_na_value(self): result = dict(status.status(self.context)) self.assert_('volume' in result) - self.assertEqual(int(result['volume']), 0) + self.assertEqual(int(result['volume']), -1) def test_status_method_contains_volume(self): self.mixer.volume = 17 From 4c509c2e2ce23708928e9d54d64744dde5f740a2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 4 Sep 2012 00:21:00 +0200 Subject: [PATCH 19/28] Add volume to playback API of backends. - Add volume get/setter to the playback provider. - Add volume property to the playback controller. --- mopidy/backends/base/playback.py | 32 +++++++++++++++++++++++++++++ mopidy/backends/dummy/__init__.py | 10 +++++++++ mopidy/backends/local/__init__.py | 6 ++++++ mopidy/backends/spotify/playback.py | 6 ++++++ 4 files changed, 54 insertions(+) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 16ac75d1..778e7a54 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -319,6 +319,14 @@ class PlaybackController(object): def _current_wall_time(self): return int(time.time() * 1000) + @property + def volume(self): + return self.provider.get_volume() + + @volume.setter + def volume(self, volume): + self.provider.set_volume(volume) + def change_track(self, cp_track, on_error_step=1): """ Change to the given track, keeping the current playback state. @@ -604,3 +612,27 @@ class BasePlaybackProvider(object): :rtype: :class:`True` if successful, else :class:`False` """ raise NotImplementedError + + # TODO: having these in the provider is stupid, but since we currently + # don't have gstreamer exposed in a sensible way for this... + # On the bright side it makes testing volume stuff less painful. + def get_volume(self): + """ + Get current volume + + *MUST be implemented by subclass.* + + :rtype: int [0..100] or :class:`None` + """ + raise NotImplementedError + + def set_volume(self, volume): + """ + Get current volume + + *MUST be implemented by subclass.* + + :param: volume + :type volume: int [0..100] + """ + raise NotImplementedError diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 70efb028..2234242c 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -56,6 +56,10 @@ class DummyLibraryProvider(BaseLibraryProvider): class DummyPlaybackProvider(BasePlaybackProvider): + def __init__(self, *args, **kwargs): + super(DummyPlaybackProvider, self).__init__(*args, **kwargs) + self._volume = None + def pause(self): return True @@ -72,6 +76,12 @@ class DummyPlaybackProvider(BasePlaybackProvider): def stop(self): return True + def get_volume(self): + return self._volume + + def set_volume(self, volume): + self._volume = volume + class DummyStoredPlaylistsProvider(BaseStoredPlaylistsProvider): def create(self, name): diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index e8638a3a..1b1f9730 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -102,6 +102,12 @@ class LocalPlaybackProvider(BasePlaybackProvider): def stop(self): return self.backend.gstreamer.stop_playback().get() + def get_volume(self): + return self.backend.gstreamer.get_volume().get() + + def set_volume(self, volume): + self.backend.gstreamer.set_volume(volume).get() + class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): def __init__(self, *args, **kwargs): diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index dc328fc9..116be285 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -41,3 +41,9 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): result = self.backend.gstreamer.stop_playback() self.backend.spotify.session.play(0) return result + + def get_volume(self): + return self.backend.gstreamer.get_volume() + + def set_volume(self, volume): + self.backend.gstreamer.set_volume(volume) From 14eeb20226b32bebfa657f7c5029a4310cf50257 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 4 Sep 2012 00:30:02 +0200 Subject: [PATCH 20/28] Update MPRIS to use playback.volume API. --- mopidy/frontends/mpris/objects.py | 18 ++++------------ .../frontends/mpris/player_interface_test.py | 21 ++++++++----------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 9ed1fe2c..fa5f9614 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -17,7 +17,6 @@ from pykka.registry import ActorRegistry from mopidy import settings from mopidy.backends.base import Backend from mopidy.backends.base.playback import PlaybackController -from mopidy.mixers.base import BaseMixer from mopidy.utils.process import exit_process # Must be done before dbus.SessionBus() is called @@ -37,7 +36,6 @@ class MprisObject(dbus.service.Object): def __init__(self): self._backend = None - self._mixer = None self.properties = { ROOT_IFACE: self._get_root_iface_properties(), PLAYER_IFACE: self._get_player_iface_properties(), @@ -95,14 +93,6 @@ class MprisObject(dbus.service.Object): self._backend = backend_refs[0].proxy() return self._backend - @property - def mixer(self): - if self._mixer is None: - mixer_refs = ActorRegistry.get_by_class(BaseMixer) - assert len(mixer_refs) == 1, 'Expected exactly one running mixer.' - self._mixer = mixer_refs[0].proxy() - return self._mixer - def _get_track_id(self, cp_track): return '/com/mopidy/track/%d' % cp_track.cpid @@ -380,7 +370,7 @@ class MprisObject(dbus.service.Object): return dbus.Dictionary(metadata, signature='sv') def get_Volume(self): - volume = self.mixer.volume.get() + volume = self.backend.playback.volume.get() if volume is not None: return volume / 100.0 @@ -391,11 +381,11 @@ class MprisObject(dbus.service.Object): if value is None: return elif value < 0: - self.mixer.volume = 0 + self.backend.playback.volume = 0 elif value > 1: - self.mixer.volume = 100 + self.backend.playback.volume = 100 elif 0 <= value <= 1: - self.mixer.volume = int(value * 100) + self.backend.playback.volume = int(value * 100) def get_Position(self): return self.backend.playback.time_position.get() * 1000 diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 24c426fb..d09d4f6b 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -5,7 +5,6 @@ import mock from mopidy import OptionalDependencyError from mopidy.backends.dummy import DummyBackend from mopidy.backends.base.playback import PlaybackController -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Album, Artist, Track try: @@ -24,14 +23,12 @@ STOPPED = PlaybackController.STOPPED class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() - self.mixer = DummyMixer.start().proxy() self.backend = DummyBackend.start().proxy() self.mpris = objects.MprisObject() self.mpris._backend = self.backend def tearDown(self): self.backend.stop() - self.mixer.stop() def test_get_playback_status_is_playing_when_playing(self): self.backend.playback.state = PLAYING @@ -208,36 +205,36 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(result['xesam:trackNumber'], 7) def test_get_volume_should_return_volume_between_zero_and_one(self): - self.mixer.volume = 0 + self.backend.playback.volume = 0 result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEquals(result, 0) - self.mixer.volume = 50 + self.backend.playback.volume = 50 result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEquals(result, 0.5) - self.mixer.volume = 100 + self.backend.playback.volume = 100 result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEquals(result, 1) def test_set_volume_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False - self.mixer.volume = 0 + self.backend.playback.volume = 0 self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0) - self.assertEquals(self.mixer.volume.get(), 0) + self.assertEquals(self.backend.playback.volume.get(), 0) def test_set_volume_to_one_should_set_mixer_volume_to_100(self): self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0) - self.assertEquals(self.mixer.volume.get(), 100) + self.assertEquals(self.backend.playback.volume.get(), 100) def test_set_volume_to_anything_above_one_should_set_mixer_volume_to_100(self): self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 2.0) - self.assertEquals(self.mixer.volume.get(), 100) + self.assertEquals(self.backend.playback.volume.get(), 100) def test_set_volume_to_anything_not_a_number_does_not_change_volume(self): - self.mixer.volume = 10 + self.backend.playback.volume = 10 self.mpris.Set(objects.PLAYER_IFACE, 'Volume', None) - self.assertEquals(self.mixer.volume.get(), 10) + self.assertEquals(self.backend.playback.volume.get(), 10) def test_get_position_returns_time_position_in_microseconds(self): self.backend.current_playlist.append([Track(uri='a', length=40000)]) From 4ffd06736e7afd9e8b1e6ee4b7ff0175876cbb4d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 4 Sep 2012 00:52:12 +0200 Subject: [PATCH 21/28] Update MPD frontend to use playback volume API. --- mopidy/frontends/mpd/dispatcher.py | 13 ------------- mopidy/frontends/mpd/protocol/playback.py | 2 +- mopidy/frontends/mpd/protocol/status.py | 4 ++-- tests/frontends/mpd/dispatcher_test.py | 3 --- tests/frontends/mpd/protocol/__init__.py | 3 --- tests/frontends/mpd/protocol/playback_test.py | 14 +++++++------- tests/frontends/mpd/status_test.py | 5 +---- 7 files changed, 11 insertions(+), 33 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 2b012c7c..94ac6bf9 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -15,7 +15,6 @@ from mopidy.frontends.mpd.protocol import (audio_output, command_list, connection, current_playlist, empty, music_db, playback, reflection, status, stickers, stored_playlists) # pylint: enable = W0611 -from mopidy.mixers.base import BaseMixer from mopidy.utils import flatten logger = logging.getLogger('mopidy.frontends.mpd.dispatcher') @@ -235,7 +234,6 @@ class MpdContext(object): self.events = set() self.subscriptions = set() self._backend = None - self._mixer = None @property def backend(self): @@ -248,14 +246,3 @@ class MpdContext(object): 'Expected exactly one running backend.' self._backend = backend_refs[0].proxy() return self._backend - - @property - def mixer(self): - """ - The mixer. An instance of :class:`mopidy.mixers.base.BaseMixer`. - """ - if self._mixer is None: - mixer_refs = ActorRegistry.get_by_class(BaseMixer) - assert len(mixer_refs) == 1, 'Expected exactly one running mixer.' - self._mixer = mixer_refs[0].proxy() - return self._mixer diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 948083a8..4cf33266 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -353,7 +353,7 @@ def setvol(context, volume): volume = 0 if volume > 100: volume = 100 - context.mixer.volume = volume + context.backend.playback.volume = volume @handle_request(r'^single (?P[01])$') @handle_request(r'^single "(?P[01])"$') diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 81b68dd6..4a9ad9a1 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -168,7 +168,7 @@ def status(context): futures = { 'current_playlist.length': context.backend.current_playlist.length, 'current_playlist.version': context.backend.current_playlist.version, - 'mixer.volume': context.mixer.volume, + 'playback.volume': context.backend.playback.volume, 'playback.consume': context.backend.playback.consume, 'playback.random': context.backend.playback.random, 'playback.repeat': context.backend.playback.repeat, @@ -263,7 +263,7 @@ def _status_time_total(futures): return current_cp_track.track.length def _status_volume(futures): - volume = futures['mixer.volume'].get() + volume = futures['playback.volume'].get() if volume is not None: return volume else: diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index bfa7c548..63f6d299 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -2,7 +2,6 @@ from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd.exceptions import MpdAckError from mopidy.frontends.mpd.protocol import request_handlers, handle_request -from mopidy.mixers.dummy import DummyMixer from tests import unittest @@ -10,12 +9,10 @@ from tests import unittest class MpdDispatcherTest(unittest.TestCase): def setUp(self): self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() self.dispatcher = MpdDispatcher() def tearDown(self): self.backend.stop().get() - self.mixer.stop().get() def test_register_same_pattern_twice_fails(self): func = lambda: None diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index b54906be..b39ded01 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -3,7 +3,6 @@ import mock from mopidy import settings from mopidy.backends import dummy as backend from mopidy.frontends import mpd -from mopidy.mixers import dummy as mixer from tests import unittest @@ -23,7 +22,6 @@ class MockConnection(mock.Mock): class BaseTestCase(unittest.TestCase): def setUp(self): self.backend = backend.DummyBackend.start().proxy() - self.mixer = mixer.DummyMixer.start().proxy() self.connection = MockConnection() self.session = mpd.MpdSession(self.connection) @@ -32,7 +30,6 @@ class BaseTestCase(unittest.TestCase): def tearDown(self): self.backend.stop().get() - self.mixer.stop().get() settings.runtime.clear() def sendRequest(self, request): diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index 01658f6d..87c9bbb8 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -76,37 +76,37 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase): def test_setvol_below_min(self): self.sendRequest(u'setvol "-10"') - self.assertEqual(0, self.mixer.volume.get()) + self.assertEqual(0, self.backend.playback.volume.get()) self.assertInResponse(u'OK') def test_setvol_min(self): self.sendRequest(u'setvol "0"') - self.assertEqual(0, self.mixer.volume.get()) + self.assertEqual(0, self.backend.playback.volume.get()) self.assertInResponse(u'OK') def test_setvol_middle(self): self.sendRequest(u'setvol "50"') - self.assertEqual(50, self.mixer.volume.get()) + self.assertEqual(50, self.backend.playback.volume.get()) self.assertInResponse(u'OK') def test_setvol_max(self): self.sendRequest(u'setvol "100"') - self.assertEqual(100, self.mixer.volume.get()) + self.assertEqual(100, self.backend.playback.volume.get()) self.assertInResponse(u'OK') def test_setvol_above_max(self): self.sendRequest(u'setvol "110"') - self.assertEqual(100, self.mixer.volume.get()) + self.assertEqual(100, self.backend.playback.volume.get()) self.assertInResponse(u'OK') def test_setvol_plus_is_ignored(self): self.sendRequest(u'setvol "+10"') - self.assertEqual(10, self.mixer.volume.get()) + self.assertEqual(10, self.backend.playback.volume.get()) self.assertInResponse(u'OK') def test_setvol_without_quotes(self): self.sendRequest(u'setvol 50') - self.assertEqual(50, self.mixer.volume.get()) + self.assertEqual(50, self.backend.playback.volume.get()) self.assertInResponse(u'OK') def test_single_off(self): diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 9fa62321..3701faaf 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,7 +1,6 @@ from mopidy.backends import dummy as backend from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.protocol import status -from mopidy.mixers import dummy as mixer from mopidy.models import Track from tests import unittest @@ -17,13 +16,11 @@ STOPPED = backend.PlaybackController.STOPPED class StatusHandlerTest(unittest.TestCase): def setUp(self): self.backend = backend.DummyBackend.start().proxy() - self.mixer = mixer.DummyMixer.start().proxy() self.dispatcher = dispatcher.MpdDispatcher() self.context = self.dispatcher.context def tearDown(self): self.backend.stop().get() - self.mixer.stop().get() def test_stats_method(self): result = status.stats(self.context) @@ -48,7 +45,7 @@ class StatusHandlerTest(unittest.TestCase): self.assertEqual(int(result['volume']), -1) def test_status_method_contains_volume(self): - self.mixer.volume = 17 + self.backend.playback.volume = 17 result = dict(status.status(self.context)) self.assert_('volume' in result) self.assertEqual(int(result['volume']), 17) From c71202c2be48156264270a2af71d5ab100d865d4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 4 Sep 2012 00:54:28 +0200 Subject: [PATCH 22/28] Remove all of mopidy.mixers and tests.mixers modules. --- mopidy/core.py | 10 -- mopidy/mixers/__init__.py | 0 mopidy/mixers/alsa.py | 60 --------- mopidy/mixers/base.py | 63 --------- mopidy/mixers/denon.py | 58 -------- mopidy/mixers/dummy.py | 16 --- mopidy/mixers/gstreamer_software.py | 23 ---- mopidy/mixers/nad.py | 198 ---------------------------- mopidy/mixers/osa.py | 46 ------- tests/mixers/__init__.py | 0 tests/mixers/base_test.py | 38 ------ tests/mixers/denon_test.py | 42 ------ tests/mixers/dummy_test.py | 23 ---- 13 files changed, 577 deletions(-) delete mode 100644 mopidy/mixers/__init__.py delete mode 100644 mopidy/mixers/alsa.py delete mode 100644 mopidy/mixers/base.py delete mode 100644 mopidy/mixers/denon.py delete mode 100644 mopidy/mixers/dummy.py delete mode 100644 mopidy/mixers/gstreamer_software.py delete mode 100644 mopidy/mixers/nad.py delete mode 100644 mopidy/mixers/osa.py delete mode 100644 tests/mixers/__init__.py delete mode 100644 tests/mixers/base_test.py delete mode 100644 tests/mixers/denon_test.py delete mode 100644 tests/mixers/dummy_test.py diff --git a/mopidy/core.py b/mopidy/core.py index 7d2e01ff..ecded337 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -40,7 +40,6 @@ def main(): check_old_folders() setup_settings(options.interactive) setup_gstreamer() - setup_mixer() setup_backend() setup_frontends() loop.run() @@ -54,7 +53,6 @@ def main(): loop.quit() stop_frontends() stop_backend() - stop_mixer() stop_gstreamer() stop_remaining_actors() @@ -109,14 +107,6 @@ def setup_gstreamer(): def stop_gstreamer(): stop_actors_by_class(GStreamer) -def setup_mixer(): - # 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('mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer')) - def setup_backend(): get_class(settings.BACKENDS[0]).start() diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py deleted file mode 100644 index acb12e66..00000000 --- a/mopidy/mixers/alsa.py +++ /dev/null @@ -1,60 +0,0 @@ -import alsaaudio -import logging - -from pykka.actor import ThreadingActor - -from mopidy import settings -from mopidy.mixers.base import BaseMixer - -logger = logging.getLogger('mopidy.mixers.alsa') - -class AlsaMixer(ThreadingActor, BaseMixer): - """ - Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control - volume. - - **Dependencies:** - - - pyalsaaudio >= 0.2 (python-alsaaudio on Debian/Ubuntu) - - **Settings:** - - - :attr:`mopidy.settings.MIXER_ALSA_CONTROL` - """ - - def __init__(self): - super(AlsaMixer, self).__init__() - self._mixer = None - - def on_start(self): - self._mixer = alsaaudio.Mixer(self._get_mixer_control()) - assert self._mixer is not None - - def _get_mixer_control(self): - """Returns the first mixer control candidate that is known to ALSA""" - candidates = self._get_mixer_control_candidates() - for control in candidates: - if control in alsaaudio.mixers(): - logger.info(u'Mixer control in use: %s', control) - return control - else: - logger.debug(u'Mixer control not found, skipping: %s', control) - logger.warning(u'No working mixer controls found. Tried: %s', - candidates) - - def _get_mixer_control_candidates(self): - """ - A mixer named 'Master' does not always exist, so we fall back to using - 'PCM'. If this does not work for you, you may set - :attr:`mopidy.settings.MIXER_ALSA_CONTROL`. - """ - if settings.MIXER_ALSA_CONTROL: - return [settings.MIXER_ALSA_CONTROL] - return [u'Master', u'PCM'] - - def get_volume(self): - # FIXME does not seem to see external volume changes. - return self._mixer.getvolume()[0] - - def set_volume(self, volume): - self._mixer.setvolume(volume) diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py deleted file mode 100644 index a387c143..00000000 --- a/mopidy/mixers/base.py +++ /dev/null @@ -1,63 +0,0 @@ -import logging - -from mopidy import listeners, settings - -logger = logging.getLogger('mopidy.mixers') - -class BaseMixer(object): - # TODO: remove completly - amplification_factor = 1.0 - - @property - def volume(self): - """ - The audio volume - - Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is - equal to 0. Values above 100 is equal to 100. - """ - if not hasattr(self, '_user_volume'): - self._user_volume = 0 - volume = self.get_volume() - if volume is None or not self.amplification_factor < 1: - return volume - else: - user_volume = int(volume / self.amplification_factor) - if (user_volume - 1) <= self._user_volume <= (user_volume + 1): - return self._user_volume - else: - return user_volume - - @volume.setter - def volume(self, volume): - if not hasattr(self, '_user_volume'): - self._user_volume = 0 - volume = int(volume) - if volume < 0: - volume = 0 - elif volume > 100: - volume = 100 - self._user_volume = volume - real_volume = int(volume * self.amplification_factor) - self.set_volume(real_volume) - self._trigger_volume_changed() - - def get_volume(self): - """ - Return volume as integer in range [0, 100]. :class:`None` if unknown. - - *MUST be implemented by subclass.* - """ - raise NotImplementedError - - def set_volume(self, volume): - """ - Set volume as integer in range [0, 100]. - - *MUST be implemented by subclass.* - """ - raise NotImplementedError - - def _trigger_volume_changed(self): - logger.debug(u'Triggering volume changed event') - listeners.BackendListener.send('volume_changed') diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py deleted file mode 100644 index b0abbdb9..00000000 --- a/mopidy/mixers/denon.py +++ /dev/null @@ -1,58 +0,0 @@ -import logging - -from pykka.actor import ThreadingActor - -from mopidy import settings -from mopidy.mixers.base import BaseMixer - -logger = logging.getLogger(u'mopidy.mixers.denon') - -class DenonMixer(ThreadingActor, BaseMixer): - """ - Mixer for controlling Denon amplifiers and receivers using the RS-232 - protocol. - - The external mixer is the authoritative source for the current volume. - This allows the user to use his remote control the volume without Mopidy - cancelling the volume setting. - - **Dependencies** - - - pyserial (python-serial on Debian/Ubuntu) - - **Settings** - - - :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0`` - """ - - def __init__(self, device=None): - super(DenonMixer, self).__init__() - self._device = device - self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] - self._volume = 0 - - def on_start(self): - if self._device is None: - from serial import Serial - self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) - - def get_volume(self): - self._ensure_open_device() - self._device.write('MV?\r') - vol = str(self._device.readline()[2:4]) - logger.debug(u'_get_volume() = %s' % vol) - return self._levels.index(vol) - - def set_volume(self, volume): - # Clamp according to Denon-spec - if volume > 99: - volume = 99 - self._ensure_open_device() - self._device.write('MV%s\r'% self._levels[volume]) - vol = self._device.readline()[2:4] - self._volume = self._levels.index(vol) - - def _ensure_open_device(self): - if not self._device.isOpen(): - logger.debug(u'(re)connecting to Denon device') - self._device.open() diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py deleted file mode 100644 index 7262e83c..00000000 --- a/mopidy/mixers/dummy.py +++ /dev/null @@ -1,16 +0,0 @@ -from pykka.actor import ThreadingActor - -from mopidy.mixers.base import BaseMixer - -class DummyMixer(ThreadingActor, BaseMixer): - """Mixer which just stores and reports the chosen volume.""" - - def __init__(self): - super(DummyMixer, self).__init__() - self._volume = None - - def get_volume(self): - return self._volume - - def set_volume(self, volume): - self._volume = volume diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py deleted file mode 100644 index a38692db..00000000 --- a/mopidy/mixers/gstreamer_software.py +++ /dev/null @@ -1,23 +0,0 @@ -from pykka.actor import ThreadingActor -from pykka.registry import ActorRegistry - -from mopidy.mixers.base import BaseMixer -from mopidy.gstreamer import GStreamer - -class GStreamerSoftwareMixer(ThreadingActor, BaseMixer): - """Mixer which uses GStreamer to control volume in software.""" - - def __init__(self): - super(GStreamerSoftwareMixer, self).__init__() - self.output = None - - def on_start(self): - output_refs = ActorRegistry.get_by_class(GStreamer) - assert len(output_refs) == 1, 'Expected exactly one running output.' - self.output = output_refs[0].proxy() - - def get_volume(self): - return self.output.get_volume().get() - - def set_volume(self, volume): - self.output.set_volume(volume).get() diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py deleted file mode 100644 index 78473308..00000000 --- a/mopidy/mixers/nad.py +++ /dev/null @@ -1,198 +0,0 @@ -import logging -import serial - -from pykka.actor import ThreadingActor - -from mopidy import settings -from mopidy.mixers.base import BaseMixer - -logger = logging.getLogger('mopidy.mixers.nad') - -class NadMixer(ThreadingActor, BaseMixer): - """ - Mixer for controlling NAD amplifiers and receivers using the NAD RS-232 - protocol. - - The NAD mixer was created using a NAD C 355BEE amplifier, but should also - work with other NAD amplifiers supporting the same RS-232 protocol (v2.x). - The C 355BEE does not give you access to the current volume. It only - supports increasing or decreasing the volume one step at the time. Other - NAD amplifiers may support more advanced volume adjustment than what is - currently used by this mixer. - - Sadly, this means that if you use the remote control to change the volume - on the amplifier, Mopidy will no longer report the correct volume. - - **Dependencies** - - - pyserial (python-serial on Debian/Ubuntu) - - **Settings** - - - :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0`` - - :attr:`mopidy.settings.MIXER_EXT_SOURCE` -- Example: ``Aux`` - - :attr:`mopidy.settings.MIXER_EXT_SPEAKERS_A` -- Example: ``On`` - - :attr:`mopidy.settings.MIXER_EXT_SPEAKERS_B` -- Example: ``Off`` - - """ - - def __init__(self): - super(NadMixer, self).__init__() - self._volume_cache = None - self._nad_talker = NadTalker.start().proxy() - - def get_volume(self): - return self._volume_cache - - def set_volume(self, volume): - self._volume_cache = volume - self._nad_talker.set_volume(volume) - - -class NadTalker(ThreadingActor): - """ - Independent process which does the communication with the NAD device. - - Since the communication is done in an independent process, Mopidy won't - block other requests while doing rather time consuming work like - calibrating the NAD device's volume. - """ - - # Timeout in seconds used for read/write operations. - # If you set the timeout too low, the reads will never get complete - # confirmations and calibration will decrease volume forever. If you set - # the timeout too high, stuff takes more time. 0.2s seems like a good value - # for NAD C 355BEE. - TIMEOUT = 0.2 - - # Number of volume levels the device supports. 40 for NAD C 355BEE. - VOLUME_LEVELS = 40 - - # Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration. - _nad_volume = None - - def __init__(self): - super(NadTalker, self).__init__() - self._device = None - - def on_start(self): - self._open_connection() - self._set_device_to_known_state() - - def _open_connection(self): - # Opens serial connection to the device. - # Communication settings: 115200 bps 8N1 - logger.info(u'Connecting to serial device "%s"', - settings.MIXER_EXT_PORT) - self._device = serial.Serial(port=settings.MIXER_EXT_PORT, - baudrate=115200, timeout=self.TIMEOUT) - self._get_device_model() - - def _set_device_to_known_state(self): - self._power_device_on() - self._select_speakers() - self._select_input_source() - self._unmute() - self._calibrate_volume() - - def _get_device_model(self): - model = self._ask_device('Main.Model') - logger.info(u'Connected to device of model "%s"', model) - return model - - def _power_device_on(self): - while self._ask_device('Main.Power') != 'On': - logger.info(u'Powering device on') - self._command_device('Main.Power', 'On') - - def _select_speakers(self): - if settings.MIXER_EXT_SPEAKERS_A is not None: - while (self._ask_device('Main.SpeakerA') - != settings.MIXER_EXT_SPEAKERS_A): - logger.info(u'Setting speakers A "%s"', - settings.MIXER_EXT_SPEAKERS_A) - self._command_device('Main.SpeakerA', - settings.MIXER_EXT_SPEAKERS_A) - if settings.MIXER_EXT_SPEAKERS_B is not None: - while (self._ask_device('Main.SpeakerB') != - settings.MIXER_EXT_SPEAKERS_B): - logger.info(u'Setting speakers B "%s"', - settings.MIXER_EXT_SPEAKERS_B) - self._command_device('Main.SpeakerB', - settings.MIXER_EXT_SPEAKERS_B) - - def _select_input_source(self): - if settings.MIXER_EXT_SOURCE is not None: - while self._ask_device('Main.Source') != settings.MIXER_EXT_SOURCE: - logger.info(u'Selecting input source "%s"', - settings.MIXER_EXT_SOURCE) - self._command_device('Main.Source', settings.MIXER_EXT_SOURCE) - - def _unmute(self): - while self._ask_device('Main.Mute') != 'Off': - logger.info(u'Unmuting device') - self._command_device('Main.Mute', 'Off') - - def _ask_device(self, key): - self._write('%s?' % key) - return self._readline().replace('%s=' % key, '') - - def _command_device(self, key, value): - if type(value) == unicode: - value = value.encode('utf-8') - self._write('%s=%s' % (key, value)) - self._readline() - - def _calibrate_volume(self): - # The NAD C 355BEE amplifier has 40 different volume levels. We have no - # way of asking on which level we are. Thus, we must calibrate the - # mixer by decreasing the volume 39 times. - logger.info(u'Calibrating NAD amplifier') - steps_left = self.VOLUME_LEVELS - 1 - while steps_left: - if self._decrease_volume(): - steps_left -= 1 - self._nad_volume = 0 - logger.info(u'Done calibrating NAD amplifier') - - def set_volume(self, volume): - # Increase or decrease the amplifier volume until it matches the given - # target volume. - logger.debug(u'Setting volume to %d' % volume) - target_nad_volume = int(round(volume * self.VOLUME_LEVELS / 100.0)) - if self._nad_volume is None: - return # Calibration needed - while target_nad_volume > self._nad_volume: - if self._increase_volume(): - self._nad_volume += 1 - while target_nad_volume < self._nad_volume: - if self._decrease_volume(): - self._nad_volume -= 1 - - def _increase_volume(self): - # Increase volume. Returns :class:`True` if confirmed by device. - self._write('Main.Volume+') - return self._readline() == 'Main.Volume+' - - def _decrease_volume(self): - # Decrease volume. Returns :class:`True` if confirmed by device. - self._write('Main.Volume-') - return self._readline() == 'Main.Volume-' - - def _write(self, data): - # Write data to device. Prepends and appends a newline to the data, as - # recommended by the NAD documentation. - if not self._device.isOpen(): - self._device.open() - self._device.write('\n%s\n' % data) - logger.debug('Write: %s', data) - - def _readline(self): - # Read line from device. The result is stripped for leading and - # trailing whitespace. - if not self._device.isOpen(): - self._device.open() - result = self._device.readline().strip() - if result: - logger.debug('Read: %s', result) - return result diff --git a/mopidy/mixers/osa.py b/mopidy/mixers/osa.py deleted file mode 100644 index bd97d790..00000000 --- a/mopidy/mixers/osa.py +++ /dev/null @@ -1,46 +0,0 @@ -from subprocess import Popen, PIPE -import time - -from pykka.actor import ThreadingActor - -from mopidy.mixers.base import BaseMixer - -class OsaMixer(ThreadingActor, BaseMixer): - """ - Mixer which uses ``osascript`` on OS X to control volume. - - **Dependencies:** - - - None - - **Settings:** - - - None - """ - - CACHE_TTL = 30 - - _cache = None - _last_update = None - - def _valid_cache(self): - return (self._cache is not None - and self._last_update is not None - and (int(time.time() - self._last_update) < self.CACHE_TTL)) - - def get_volume(self): - if not self._valid_cache(): - try: - self._cache = int(Popen( - ['osascript', '-e', - 'output volume of (get volume settings)'], - stdout=PIPE).communicate()[0]) - except ValueError: - self._cache = None - self._last_update = int(time.time()) - return self._cache - - def set_volume(self, volume): - Popen(['osascript', '-e', 'set volume output volume %d' % volume]) - self._cache = volume - self._last_update = int(time.time()) diff --git a/tests/mixers/__init__.py b/tests/mixers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/mixers/base_test.py b/tests/mixers/base_test.py deleted file mode 100644 index 54cd8773..00000000 --- a/tests/mixers/base_test.py +++ /dev/null @@ -1,38 +0,0 @@ -class BaseMixerTest(object): - MIN = 0 - MAX = 100 - ACTUAL_MIN = MIN - ACTUAL_MAX = MAX - INITIAL = None - - mixer_class = None - - def setUp(self): - assert self.mixer_class is not None, \ - "mixer_class must be set in subclass" - # pylint: disable = E1102 - self.mixer = self.mixer_class() - # pylint: enable = E1102 - - def test_initial_volume(self): - self.assertEqual(self.mixer.volume, self.INITIAL) - - def test_volume_set_to_min(self): - self.mixer.volume = self.MIN - self.assertEqual(self.mixer.volume, self.ACTUAL_MIN) - - def test_volume_set_to_max(self): - self.mixer.volume = self.MAX - self.assertEqual(self.mixer.volume, self.ACTUAL_MAX) - - def test_volume_set_to_below_min_results_in_min(self): - self.mixer.volume = -10 - self.assertEqual(self.mixer.volume, self.ACTUAL_MIN) - - def test_volume_set_to_above_max_results_in_max(self): - self.mixer.volume = self.MAX + 10 - self.assertEqual(self.mixer.volume, self.ACTUAL_MAX) - - def test_volume_is_not_float(self): - self.mixer.volume = 1.0 / 3 * 100 - self.assertEqual(self.mixer.volume, 33) diff --git a/tests/mixers/denon_test.py b/tests/mixers/denon_test.py deleted file mode 100644 index cdfe0772..00000000 --- a/tests/mixers/denon_test.py +++ /dev/null @@ -1,42 +0,0 @@ -from mopidy.mixers.denon import DenonMixer -from tests.mixers.base_test import BaseMixerTest - -from tests import unittest - - -class DenonMixerDeviceMock(object): - def __init__(self): - self._open = True - self.ret_val = bytes('MV00\r') - - def write(self, x): - if x[2] != '?': - self.ret_val = bytes(x) - - def read(self, x): - return self.ret_val - - def readline(self): - return self.ret_val - - def isOpen(self): - return self._open - - def open(self): - self._open = True - - -class DenonMixerTest(BaseMixerTest, unittest.TestCase): - ACTUAL_MAX = 99 - INITIAL = 1 - - mixer_class = DenonMixer - - def setUp(self): - self.device = DenonMixerDeviceMock() - self.mixer = DenonMixer(device=self.device) - - def test_reopen_device(self): - self.device._open = False - self.mixer.volume = 10 - self.assertTrue(self.device.isOpen()) diff --git a/tests/mixers/dummy_test.py b/tests/mixers/dummy_test.py deleted file mode 100644 index f9418d7a..00000000 --- a/tests/mixers/dummy_test.py +++ /dev/null @@ -1,23 +0,0 @@ -from mopidy.mixers.dummy import DummyMixer - -from tests import unittest -from tests.mixers.base_test import BaseMixerTest - - -class DummyMixerTest(BaseMixerTest, unittest.TestCase): - mixer_class = DummyMixer - - def test_set_volume_is_capped(self): - self.mixer.amplification_factor = 0.5 - self.mixer.volume = 100 - self.assertEquals(self.mixer._volume, 50) - - def test_get_volume_does_not_show_that_the_volume_is_capped(self): - self.mixer.amplification_factor = 0.5 - self.mixer._volume = 50 - self.assertEquals(self.mixer.volume, 100) - - def test_get_volume_get_the_same_number_as_was_set(self): - self.mixer.amplification_factor = 0.5 - self.mixer.volume = 13 - self.assertEquals(self.mixer.volume, 13) From 8281128a2eff054a14229e4b1471ff1278bbe3b1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 4 Sep 2012 01:04:59 +0200 Subject: [PATCH 23/28] Move mixers out of gstreamer module. Mixers now live in mopidy.mixers.. Futhermore mopidy.mixers should now import each of the mixers so that doing import mopidy.mixers is enough to install the gstreamer elements. --- mopidy/gstreamer.py | 143 +------------------------------------- mopidy/mixers/__init__.py | 2 + mopidy/mixers/auto.py | 72 +++++++++++++++++++ mopidy/mixers/fake.py | 80 +++++++++++++++++++++ 4 files changed, 155 insertions(+), 142 deletions(-) create mode 100644 mopidy/mixers/__init__.py create mode 100644 mopidy/mixers/auto.py create mode 100644 mopidy/mixers/fake.py diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index de5f988e..7bf5f370 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -1,6 +1,5 @@ import pygst pygst.require('0.10') -import gobject import gst import logging @@ -10,151 +9,11 @@ from pykka.registry import ActorRegistry from mopidy import settings, utils from mopidy.backends.base import Backend +from mopidy import mixers # Trigger install of gst mixer plugins. logger = logging.getLogger('mopidy.gstreamer') -# TODO: we might want to add some ranking to the mixers we know about? -# TODO: move to mixers module and do from mopidy.mixers import * to install -# elements. -class AutoAudioMixer(gst.Bin): - __gstdetails__ = ('AutoAudioMixer', - 'Mixer', - 'Element automatically selects a mixer.', - 'Thomas Adamcik') - - def __init__(self): - gst.Bin.__init__(self) - mixer = self._find_mixer() - if mixer: - self.add(mixer) - logger.debug('AutoAudioMixer chose: %s', mixer.get_name()) - else: - logger.debug('AutoAudioMixer did not find any usable mixers') - - def _find_mixer(self): - registry = gst.registry_get_default() - - factories = registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY) - factories.sort(key=lambda f: (-f.get_rank(), f.get_name())) - - for factory in factories: - # Avoid sink/srcs that implment mixing. - if factory.get_klass() != 'Generic/Audio': - continue - # Avoid anything that doesn't implment mixing. - elif not factory.has_interface('GstMixer'): - continue - - if self._test_mixer(factory): - return factory.create() - - return None - - def _test_mixer(self, factory): - element = factory.create() - if not element: - return False - - try: - result = element.set_state(gst.STATE_READY) - if result != gst.STATE_CHANGE_SUCCESS: - return False - - # Trust that the default device is sane and just check tracks. - return self._test_tracks(element) - finally: - element.set_state(gst.STATE_NULL) - - def _test_tracks(self, element): - # Only allow elements that have a least one output track. - flags = gst.interfaces.MIXER_TRACK_OUTPUT - - for track in element.list_tracks(): - if track.flags & flags: - return True - return False - - -gobject.type_register(AutoAudioMixer) -gst.element_register (AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL) - - -def create_fake_track(label, intial_volume, min_volume, max_volume, - num_channels, flags): - class Track(gst.interfaces.MixerTrack): - def __init__(self): - super(Track, self).__init__() - self.volumes = (intial_volume,) * self.num_channels - - @gobject.property - def label(self): - return label - - @gobject.property - def min_volume(self): - return min_volume - - @gobject.property - def max_volume(self): - return max_volume - - @gobject.property - def num_channels(self): - return num_channels - - @gobject.property - def flags(self): - return flags - - return Track() - - -class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): - __gstdetails__ = ('FakeMixer', - 'Mixer', - 'Fake mixer for use in tests.', - 'Thomas Adamcik') - - track_label = gobject.property(type=str, default='Master') - - track_initial_volume = gobject.property(type=int, default=0) - - track_min_volume = gobject.property(type=int, default=0) - - track_max_volume = gobject.property(type=int, default=100) - - track_num_channels = gobject.property(type=int, default=2) - - track_flags = gobject.property(type=int, - default=(gst.interfaces.MIXER_TRACK_MASTER | - gst.interfaces.MIXER_TRACK_OUTPUT)) - - def __init__(self): - gst.Element.__init__(self) - - def list_tracks(self): - track = create_fake_track(self.track_label, - self.track_initial_volume, - self.track_min_volume, - self.track_max_volume, - self.track_num_channels, - self.track_flags) - return [track] - - def get_volume(self, track): - return track.volumes - - def set_volume(self, track, volumes): - track.volumes = volumes - - def set_record(self, track, record): - pass - -gobject.type_register(FakeMixer) -gst.element_register (FakeMixer, 'fakemixer', gst.RANK_MARGINAL) - - class GStreamer(ThreadingActor): """ Audio output through `GStreamer `_. diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py new file mode 100644 index 00000000..cf282a03 --- /dev/null +++ b/mopidy/mixers/__init__.py @@ -0,0 +1,2 @@ +from mopidy.mixers.auto import AutoAudioMixer +from mopidy.mixers.fake import FakeMixer diff --git a/mopidy/mixers/auto.py b/mopidy/mixers/auto.py new file mode 100644 index 00000000..4b4ce543 --- /dev/null +++ b/mopidy/mixers/auto.py @@ -0,0 +1,72 @@ +import pygst +pygst.require('0.10') +import gobject +import gst + +import logging + +logger = logging.getLogger('mopidy.mixers.auto') + + +# TODO: we might want to add some ranking to the mixers we know about? +class AutoAudioMixer(gst.Bin): + __gstdetails__ = ('AutoAudioMixer', + 'Mixer', + 'Element automatically selects a mixer.', + 'Thomas Adamcik') + + def __init__(self): + gst.Bin.__init__(self) + mixer = self._find_mixer() + if mixer: + self.add(mixer) + logger.debug('AutoAudioMixer chose: %s', mixer.get_name()) + else: + logger.debug('AutoAudioMixer did not find any usable mixers') + + def _find_mixer(self): + registry = gst.registry_get_default() + + factories = registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY) + factories.sort(key=lambda f: (-f.get_rank(), f.get_name())) + + for factory in factories: + # Avoid sink/srcs that implment mixing. + if factory.get_klass() != 'Generic/Audio': + continue + # Avoid anything that doesn't implment mixing. + elif not factory.has_interface('GstMixer'): + continue + + if self._test_mixer(factory): + return factory.create() + + return None + + def _test_mixer(self, factory): + element = factory.create() + if not element: + return False + + try: + result = element.set_state(gst.STATE_READY) + if result != gst.STATE_CHANGE_SUCCESS: + return False + + # Trust that the default device is sane and just check tracks. + return self._test_tracks(element) + finally: + element.set_state(gst.STATE_NULL) + + def _test_tracks(self, element): + # Only allow elements that have a least one output track. + flags = gst.interfaces.MIXER_TRACK_OUTPUT + + for track in element.list_tracks(): + if track.flags & flags: + return True + return False + + +gobject.type_register(AutoAudioMixer) +gst.element_register (AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL) diff --git a/mopidy/mixers/fake.py b/mopidy/mixers/fake.py new file mode 100644 index 00000000..de0f6a50 --- /dev/null +++ b/mopidy/mixers/fake.py @@ -0,0 +1,80 @@ +import pygst +pygst.require('0.10') +import gobject +import gst + + +def create_fake_track(label, intial_volume, min_volume, max_volume, + num_channels, flags): + class Track(gst.interfaces.MixerTrack): + def __init__(self): + super(Track, self).__init__() + self.volumes = (intial_volume,) * self.num_channels + + @gobject.property + def label(self): + return label + + @gobject.property + def min_volume(self): + return min_volume + + @gobject.property + def max_volume(self): + return max_volume + + @gobject.property + def num_channels(self): + return num_channels + + @gobject.property + def flags(self): + return flags + + return Track() + + +class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): + __gstdetails__ = ('FakeMixer', + 'Mixer', + 'Fake mixer for use in tests.', + 'Thomas Adamcik') + + track_label = gobject.property(type=str, default='Master') + + track_initial_volume = gobject.property(type=int, default=0) + + track_min_volume = gobject.property(type=int, default=0) + + track_max_volume = gobject.property(type=int, default=100) + + track_num_channels = gobject.property(type=int, default=2) + + track_flags = gobject.property(type=int, + default=(gst.interfaces.MIXER_TRACK_MASTER | + gst.interfaces.MIXER_TRACK_OUTPUT)) + + def __init__(self): + gst.Element.__init__(self) + + def list_tracks(self): + track = create_fake_track(self.track_label, + self.track_initial_volume, + self.track_min_volume, + self.track_max_volume, + self.track_num_channels, + self.track_flags) + return [track] + + def get_volume(self, track): + return track.volumes + + def set_volume(self, track, volumes): + track.volumes = volumes + + def set_record(self, track, record): + pass + + +gobject.type_register(FakeMixer) +gst.element_register (FakeMixer, 'fakemixer', gst.RANK_MARGINAL) From 562c25043ab213c149e07a885458fab26c0cfe37 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 5 Sep 2012 20:34:53 +0200 Subject: [PATCH 24/28] Remove comment about get/set_volume. --- mopidy/backends/base/playback.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 778e7a54..a615f936 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -613,9 +613,6 @@ class BasePlaybackProvider(object): """ raise NotImplementedError - # TODO: having these in the provider is stupid, but since we currently - # don't have gstreamer exposed in a sensible way for this... - # On the bright side it makes testing volume stuff less painful. def get_volume(self): """ Get current volume From 8c038bbae616180095a1a831346f17136e0790bf Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 5 Sep 2012 20:48:00 +0200 Subject: [PATCH 25/28] Review cleanup. --- mopidy/backends/spotify/playback.py | 2 +- mopidy/gstreamer.py | 4 ++-- mopidy/mixers/auto.py | 6 +++--- mopidy/mixers/fake.py | 2 +- mopidy/utils/settings.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index 116be285..70cc4617 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -43,7 +43,7 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): return result def get_volume(self): - return self.backend.gstreamer.get_volume() + return self.backend.gstreamer.get_volume().get() def set_volume(self, volume): self.backend.gstreamer.set_volume(volume) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 7bf5f370..3e538ccc 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -44,8 +44,8 @@ class GStreamer(ThreadingActor): self._setup_pipeline() self._setup_output(output or settings.OUTPUT) - self._setup_mixer(mixer or settings.MIXER, - mixer_track or settings.MIXER_TRACK) + self._setup_mixer( + mixer or settings.MIXER, mixer_track or settings.MIXER_TRACK) self._setup_message_processor() def _setup_pipeline(self): diff --git a/mopidy/mixers/auto.py b/mopidy/mixers/auto.py index 4b4ce543..f4bd0f92 100644 --- a/mopidy/mixers/auto.py +++ b/mopidy/mixers/auto.py @@ -31,10 +31,10 @@ class AutoAudioMixer(gst.Bin): factories.sort(key=lambda f: (-f.get_rank(), f.get_name())) for factory in factories: - # Avoid sink/srcs that implment mixing. + # Avoid sink/srcs that implement mixing. if factory.get_klass() != 'Generic/Audio': continue - # Avoid anything that doesn't implment mixing. + # Avoid anything that doesn't implement mixing. elif not factory.has_interface('GstMixer'): continue @@ -69,4 +69,4 @@ class AutoAudioMixer(gst.Bin): gobject.type_register(AutoAudioMixer) -gst.element_register (AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL) +gst.element_register(AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL) diff --git a/mopidy/mixers/fake.py b/mopidy/mixers/fake.py index de0f6a50..b697956a 100644 --- a/mopidy/mixers/fake.py +++ b/mopidy/mixers/fake.py @@ -77,4 +77,4 @@ class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): gobject.type_register(FakeMixer) -gst.element_register (FakeMixer, 'fakemixer', gst.RANK_MARGINAL) +gst.element_register(FakeMixer, 'fakemixer', gst.RANK_MARGINAL) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index edda0222..726917c6 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -152,7 +152,7 @@ def validate_settings(defaults, settings): elif setting == 'OUTPUTS': errors[setting] = ( - u'Deprecated setting, please change to OUTPUT. OUTPUT expectes ' + u'Deprecated setting, please change to OUTPUT. OUTPUT expects ' u'a GStreamer bin description string for your desired output.') elif setting == 'SPOTIFY_BITRATE': From b0d6dc3e0cbb80f9fa9fec0006d88461e0b0f8a1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 5 Sep 2012 22:42:09 +0200 Subject: [PATCH 26/28] Remove injected gstreamer settings. --- mopidy/gstreamer.py | 27 ++++++++++++--------------- tests/gstreamer_test.py | 10 ++++++---- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 3e538ccc..5adfd754 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -26,7 +26,7 @@ class GStreamer(ThreadingActor): """ - def __init__(self, output=None, mixer=None, mixer_track=None): + def __init__(self): super(GStreamer, self).__init__() self._default_caps = gst.Caps(""" audio/x-raw-int, @@ -43,9 +43,8 @@ class GStreamer(ThreadingActor): self._mixer = None self._setup_pipeline() - self._setup_output(output or settings.OUTPUT) - self._setup_mixer( - mixer or settings.MIXER, mixer_track or settings.MIXER_TRACK) + self._setup_output() + self._setup_mixer() self._setup_message_processor() def _setup_pipeline(self): @@ -66,37 +65,35 @@ class GStreamer(ThreadingActor): self._uridecodebin.connect('pad-added', self._on_new_pad, self._pipeline.get_by_name('queue').get_pad('sink')) - def _setup_output(self, output_description): + def _setup_output(self): # This will raise a gobject.GError if the description is bad. self._output = gst.parse_bin_from_description( - output_description, ghost_unconnected_pads=True) + settings.OUTPUT, ghost_unconnected_pads=True) self._pipeline.add(self._output) gst.element_link_many(self._pipeline.get_by_name('queue'), self._output) - logger.info('Output set to %s', output_description) + logger.info('Output set to %s', settings.OUTPUT) - def _setup_mixer(self, mixer_description, track_label): - if not mixer_description: + def _setup_mixer(self): + if not settings.MIXER: logger.info('Not setting up mixer.') return # This will raise a gobject.GError if the description is bad. - mixerbin = gst.parse_bin_from_description(mixer_description, False) + mixerbin = gst.parse_bin_from_description(settings.MIXER, False) # We assume that the bin will contain a single mixer. mixer = mixerbin.get_by_interface('GstMixer') if not mixer: - logger.warning('Did not find any mixers in %r', - mixer_description) + logger.warning('Did not find any mixers in %r', settings.MIXER) return if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: - logger.warning('Setting mixer %r to READY failed.', - mixer_description) + logger.warning('Setting mixer %r to READY failed.', settings.MIXER) return - track = self._select_mixer_track(mixer, track_label) + track = self._select_mixer_track(mixer, settings.MIXER_TRACK) if not track: logger.warning('Could not find usable mixer track.') return diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index f30b672b..62633e4f 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -11,11 +11,13 @@ from tests import unittest, path_to_data_dir 'Our Windows build server does not support GStreamer yet') class GStreamerTest(unittest.TestCase): def setUp(self): - # TODO: does this modify global settings without reseting it? - # TODO: should use a fake backend stub for this test? - settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) + settings.MIXER = 'fakemixer track_max_volume=65536' + settings.OUTPUT = 'fakesink' self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) - self.gstreamer = GStreamer(mixer='fakemixer track_max_volume=65536') + self.gstreamer = GStreamer() + + def tearDown(self): + settings.runtime.clear() def prepare_uri(self, uri): self.gstreamer.prepare_change() From 7e934401fb9ec0ec8a7b43dfffc8aa531b9024e2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 5 Sep 2012 22:55:57 +0200 Subject: [PATCH 27/28] Removed refrences to old mixers from docs. --- docs/api/backends/concepts.rst | 1 - docs/api/backends/controllers.rst | 10 +---- docs/api/mixers.rst | 43 ---------------------- docs/changes.rst | 8 ++++ docs/clients/mpd.rst | 5 --- docs/installation/index.rst | 3 -- docs/modules/mixers/alsa.rst | 7 ---- docs/modules/mixers/denon.rst | 7 ---- docs/modules/mixers/dummy.rst | 7 ---- docs/modules/mixers/gstreamer_software.rst | 7 ---- docs/modules/mixers/nad.rst | 7 ---- docs/modules/mixers/osa.rst | 7 ---- 12 files changed, 10 insertions(+), 102 deletions(-) delete mode 100644 docs/api/mixers.rst delete mode 100644 docs/modules/mixers/alsa.rst delete mode 100644 docs/modules/mixers/denon.rst delete mode 100644 docs/modules/mixers/dummy.rst delete mode 100644 docs/modules/mixers/gstreamer_software.rst delete mode 100644 docs/modules/mixers/nad.rst delete mode 100644 docs/modules/mixers/osa.rst diff --git a/docs/api/backends/concepts.rst b/docs/api/backends/concepts.rst index 0d476213..371e03bc 100644 --- a/docs/api/backends/concepts.rst +++ b/docs/api/backends/concepts.rst @@ -27,4 +27,3 @@ Providers: "Playback\ncontroller" -> "Playback\nproviders" Backend -> "Stored\nplaylists\ncontroller" "Stored\nplaylists\ncontroller" -> "Stored\nplaylist\nproviders" - Backend -> Mixer diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst index 20dc2d61..bb1e20f5 100644 --- a/docs/api/backends/controllers.rst +++ b/docs/api/backends/controllers.rst @@ -20,19 +20,13 @@ The backend Playback controller =================== -Manages playback, with actions like play, pause, stop, next, previous, and -seek. +Manages playback, with actions like play, pause, stop, next, previous, +seek and volume control. .. autoclass:: mopidy.backends.base.PlaybackController :members: -Mixer controller -================ - -Manages volume. See :class:`mopidy.mixers.base.BaseMixer`. - - Current playlist controller =========================== diff --git a/docs/api/mixers.rst b/docs/api/mixers.rst deleted file mode 100644 index 2459db8c..00000000 --- a/docs/api/mixers.rst +++ /dev/null @@ -1,43 +0,0 @@ -********* -Mixer API -********* - -Mixers are responsible for controlling volume. Clients of the mixers will -simply instantiate a mixer and read/write to the ``volume`` attribute:: - - >>> from mopidy.mixers.alsa import AlsaMixer - >>> mixer = AlsaMixer() - >>> mixer.volume - 100 - >>> mixer.volume = 80 - >>> mixer.volume - 80 - -Most users will use one of the internal mixers which controls the volume on the -computer running Mopidy. If you do not specify which mixer you want to use in -the settings, Mopidy will choose one for you based upon what OS you run. See -:attr:`mopidy.settings.MIXER` for the defaults. - -Mopidy also supports controlling volume on other hardware devices instead of on -the computer running Mopidy through the use of custom mixer implementations. To -enable one of the hardware device mixers, you must the set -:attr:`mopidy.settings.MIXER` setting to point to one of the classes found -below, and possibly add some extra settings required by the mixer you choose. - -All mixers should subclass :class:`mopidy.mixers.base.BaseMixer` and override -methods as described below. - -.. automodule:: mopidy.mixers.base - :synopsis: Mixer API - :members: - - -Mixer implementations -===================== - -* :mod:`mopidy.mixers.alsa` -* :mod:`mopidy.mixers.denon` -* :mod:`mopidy.mixers.dummy` -* :mod:`mopidy.mixers.gstreamer_software` -* :mod:`mopidy.mixers.osa` -* :mod:`mopidy.mixers.nad` diff --git a/docs/changes.rst b/docs/changes.rst index 1e767d1c..2664a083 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -34,6 +34,14 @@ v0.8 (in development) ``autoaudiosink``. (Fixes: :issue:`81`, :issue:`115`, :issue:`121`, :issue:`159`) +- Switch to pure GStreamer based mixing. This implies that users setup a + GStreamer bin with a mixer in it in :attr:`mopidy.setting.MIXER`. The default + value is ``autoaudiomixer``, a custom mixer that attempts to find a 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 support. MPD protocol + support for volume has also been updated to return -1 when we have no mixer + set. + v0.7.3 (2012-08-11) =================== diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 4c789eba..844eaee7 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -181,11 +181,6 @@ they artist and album tabs do not hang. The folder tab still freezes when ``lsinfo`` returns a list of stored playlists, though zero files. Thus, we've discovered a couple of bugs in Droid MPD Client. -The volume control is very slick, with a turn knob, just like on an amplifier. -It lends itself to showing off to friends when combined with Mopidy's external -amplifier mixers. Everybody loves turning a knob on a touch screen and see the -physical knob on the amplifier turn as well ;-) - Even though ``lsinfo`` returns the stored playlists for the folder tab, they are not displayed anywhere. Thus, we had to select an album in the album tab to complete the test procedure. diff --git a/docs/installation/index.rst b/docs/installation/index.rst index fae50a1b..766616ac 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -46,9 +46,6 @@ dependencies installed. sudo apt-get install python-dbus python-indicate - - Some custom mixers (but not the default one) require additional - dependencies. See the docs for each mixer. - Install latest stable release ============================= diff --git a/docs/modules/mixers/alsa.rst b/docs/modules/mixers/alsa.rst deleted file mode 100644 index e8b7ed6c..00000000 --- a/docs/modules/mixers/alsa.rst +++ /dev/null @@ -1,7 +0,0 @@ -************************************************* -:mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux -************************************************* - -.. automodule:: mopidy.mixers.alsa - :synopsis: ALSA mixer for Linux - :members: diff --git a/docs/modules/mixers/denon.rst b/docs/modules/mixers/denon.rst deleted file mode 100644 index 7fb2d6cc..00000000 --- a/docs/modules/mixers/denon.rst +++ /dev/null @@ -1,7 +0,0 @@ -***************************************************************** -:mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers -***************************************************************** - -.. automodule:: mopidy.mixers.denon - :synopsis: Hardware mixer for Denon amplifiers - :members: diff --git a/docs/modules/mixers/dummy.rst b/docs/modules/mixers/dummy.rst deleted file mode 100644 index 8ac18e10..00000000 --- a/docs/modules/mixers/dummy.rst +++ /dev/null @@ -1,7 +0,0 @@ -***************************************************** -:mod:`mopidy.mixers.dummy` -- Dummy mixer for testing -***************************************************** - -.. automodule:: mopidy.mixers.dummy - :synopsis: Dummy mixer for testing - :members: diff --git a/docs/modules/mixers/gstreamer_software.rst b/docs/modules/mixers/gstreamer_software.rst deleted file mode 100644 index 98e09f44..00000000 --- a/docs/modules/mixers/gstreamer_software.rst +++ /dev/null @@ -1,7 +0,0 @@ -*************************************************************************** -:mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms -*************************************************************************** - -.. automodule:: mopidy.mixers.gstreamer_software - :synopsis: Software mixer for all platforms - :members: diff --git a/docs/modules/mixers/nad.rst b/docs/modules/mixers/nad.rst deleted file mode 100644 index 56291cbb..00000000 --- a/docs/modules/mixers/nad.rst +++ /dev/null @@ -1,7 +0,0 @@ -************************************************************* -:mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers -************************************************************* - -.. automodule:: mopidy.mixers.nad - :synopsis: Hardware mixer for NAD amplifiers - :members: diff --git a/docs/modules/mixers/osa.rst b/docs/modules/mixers/osa.rst deleted file mode 100644 index a4363cb4..00000000 --- a/docs/modules/mixers/osa.rst +++ /dev/null @@ -1,7 +0,0 @@ -********************************************** -:mod:`mopidy.mixers.osa` -- Osa mixer for OS X -********************************************** - -.. automodule:: mopidy.mixers.osa - :synopsis: Osa mixer for OS X - :members: From 776a5040b1405f868b9bae77d42ece0468466fc3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 6 Sep 2012 00:10:24 +0200 Subject: [PATCH 28/28] Review nitpicks. --- docs/api/backends/controllers.rst | 2 +- docs/changes.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst index bb1e20f5..8d6687e2 100644 --- a/docs/api/backends/controllers.rst +++ b/docs/api/backends/controllers.rst @@ -21,7 +21,7 @@ Playback controller =================== Manages playback, with actions like play, pause, stop, next, previous, -seek and volume control. +seek, and volume control. .. autoclass:: mopidy.backends.base.PlaybackController :members: diff --git a/docs/changes.rst b/docs/changes.rst index 2664a083..963802d4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -38,9 +38,9 @@ v0.8 (in development) GStreamer bin with a mixer in it in :attr:`mopidy.setting.MIXER`. The default value is ``autoaudiomixer``, a custom mixer that attempts to find a 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 support. MPD protocol - support for volume has also been updated to return -1 when we have no mixer - set. + 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. v0.7.3 (2012-08-11)