From 9ca4dae16792f9c584b12943f70f025be8025fd4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 22 Jun 2014 11:28:39 +0200 Subject: [PATCH 01/40] mixer: Add mixer API --- docs/api/index.rst | 1 + docs/api/mixer.rst | 17 ++++++++++++++++ mopidy/mixer.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 docs/api/mixer.rst create mode 100644 mopidy/mixer.py diff --git a/docs/api/index.rst b/docs/api/index.rst index 444b6ece..5aac825c 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -21,6 +21,7 @@ API reference backends core audio + mixer frontends commands ext diff --git a/docs/api/mixer.rst b/docs/api/mixer.rst new file mode 100644 index 00000000..5f4c2882 --- /dev/null +++ b/docs/api/mixer.rst @@ -0,0 +1,17 @@ +.. _mixer-api: + +*************** +Audio mixer API +*************** + +.. module:: mopidy.mixer + :synopsis: The audio mixer API + +.. autoclass:: mopidy.mixer.Mixer + :members: + + +Mixer implementations +===================== + +- TODO diff --git a/mopidy/mixer.py b/mopidy/mixer.py new file mode 100644 index 00000000..1adae665 --- /dev/null +++ b/mopidy/mixer.py @@ -0,0 +1,51 @@ +from __future__ import unicode_literals + + +class Mixer(object): + """Audio mixer API""" + + def get_volume(self): + """ + Get volume level of the mixer. + + Example values: + + 0: + Minimum volume, usually silent. + 100: + Max volume. + :class:`None`: + Volume is unknown. + + :rtype: int in range [0..100] or :class:`None` + """ + return None + + def set_volume(self, volume): + """ + Set volume level of the mixer. + + :param volume: Volume in the range [0..100] + :type volume: int + :rtype: :class:`True` if success, :class:`False` if failure + """ + return False + + def get_mute(self): + """ + Get mute status of the mixer. + + :rtype: :class:`True` if muted, :class:`False` if unmuted, + :class:`None` if unknown. + """ + return None + + def set_mute(self, muted): + """ + Mute or unmute the mixer. + + :param muted: :class:`True` to mute, :class:`False` to unmute + :type muted: bool + :rtype: :class:`True` if success, :class:`False` if failure + """ + return False From 297aac4f5a5cb3b8637349d78fd18fc32de09898 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 6 Jul 2014 21:33:51 +0200 Subject: [PATCH 02/40] docs: Add Mopidy-ALSAMixer to mixer impl list --- docs/api/mixer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/mixer.rst b/docs/api/mixer.rst index 5f4c2882..6f764210 100644 --- a/docs/api/mixer.rst +++ b/docs/api/mixer.rst @@ -14,4 +14,4 @@ Audio mixer API Mixer implementations ===================== -- TODO +- `Mopidy-ALSAMixer `_ From 50d008ae6ae497ab658feadf8dcf1174e644e2c6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Jul 2014 20:46:56 +0200 Subject: [PATCH 03/40] mixer: Add name attr to mixer API --- mopidy/mixer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mopidy/mixer.py b/mopidy/mixer.py index 1adae665..e522823b 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -4,6 +4,14 @@ from __future__ import unicode_literals class Mixer(object): """Audio mixer API""" + name = None + """Name of the mixer. + + Used when configuring what mixer to use. Should usually match the + :attr:`~mopidy.ext.Extension.ext_name` of the extension providing the + mixer. + """ + def get_volume(self): """ Get volume level of the mixer. From 4f53521fea87f5fec75a347a6c695fa25504793e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Jul 2014 21:03:57 +0200 Subject: [PATCH 04/40] main: Start/stop the selected mixer --- mopidy/commands.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/mopidy/commands.py b/mopidy/commands.py index 022419a8..7e66753f 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -261,13 +261,15 @@ class RootCommand(Command): def run(self, args, config): loop = gobject.MainLoop() + mixer_classes = args.registry['mixer'] backend_classes = args.registry['backend'] frontend_classes = args.registry['frontend'] try: audio = self.start_audio(config) + mixer = self.start_mixer(config, audio, mixer_classes) backends = self.start_backends(config, backend_classes, audio) - core = self.start_core(audio, backends) + core = self.start_core(audio, mixer, backends) self.start_frontends(config, frontend_classes, core) loop.run() except KeyboardInterrupt: @@ -277,6 +279,7 @@ class RootCommand(Command): loop.quit() self.stop_frontends(frontend_classes) self.stop_core() + self.stop_mixer(mixer_classes) self.stop_backends(backend_classes) self.stop_audio() process.stop_remaining_actors() @@ -297,7 +300,23 @@ class RootCommand(Command): return backends - def start_core(self, audio, backends): + def start_mixer(self, config, audio, mixer_classes): + logger.debug( + 'Available Mopidy mixers: %s', + ', '.join(m.__name__ for m in mixer_classes) or 'none') + selected_mixers = [ + m for m in mixer_classes if m.name == config['audio']['mixer']] + if len(selected_mixers) != 1: + logger.error( + 'Did not find unique mixer "%s". Alternatives are: %s', + config['audio']['mixer'], + ', '.join([m.name for m in mixer_classes])) + process.exit_process() + mixer_class = selected_mixers[0] + logger.info('Starting Mopidy mixer: %s', mixer_class.__name__) + return mixer_class.start(config=config, audio=audio).proxy() + + def start_core(self, audio, mixer, backends): logger.info('Starting Mopidy core') return Core.start(audio=audio, backends=backends).proxy() @@ -318,6 +337,11 @@ class RootCommand(Command): logger.info('Stopping Mopidy core') process.stop_actors_by_class(Core) + def stop_mixer(self, mixer_classes): + logger.info('Stopping Mopidy mixer') + for mixer_class in mixer_classes: + process.stop_actors_by_class(mixer_class) + def stop_backends(self, backend_classes): logger.info('Stopping Mopidy backends') for backend_class in backend_classes: From 14d0433aaeabba96eb7b29b72772e41c94b6dfea Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 00:47:39 +0200 Subject: [PATCH 05/40] mixer: Add bundled Mopidy-SoftwareMixer extension --- mopidy/softwaremixer/__init__.py | 25 +++++++++++++++++++++++ mopidy/softwaremixer/ext.conf | 2 ++ mopidy/softwaremixer/mixer.py | 34 ++++++++++++++++++++++++++++++++ setup.py | 1 + 4 files changed, 62 insertions(+) create mode 100644 mopidy/softwaremixer/__init__.py create mode 100644 mopidy/softwaremixer/ext.conf create mode 100644 mopidy/softwaremixer/mixer.py diff --git a/mopidy/softwaremixer/__init__.py b/mopidy/softwaremixer/__init__.py new file mode 100644 index 00000000..242069eb --- /dev/null +++ b/mopidy/softwaremixer/__init__.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals + +import os + +import mopidy +from mopidy import config, ext + + +class Extension(ext.Extension): + + dist_name = 'Mopidy-SoftwareMixer' + ext_name = 'softwaremixer' + version = mopidy.__version__ + + def get_default_config(self): + conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf') + return config.read(conf_file) + + def get_config_schema(self): + schema = super(Extension, self).get_config_schema() + return schema + + def setup(self, registry): + from .mixer import SoftwareMixer + registry.add('mixer', SoftwareMixer) diff --git a/mopidy/softwaremixer/ext.conf b/mopidy/softwaremixer/ext.conf new file mode 100644 index 00000000..47a98ba7 --- /dev/null +++ b/mopidy/softwaremixer/ext.conf @@ -0,0 +1,2 @@ +[softwaremixer] +enabled = true diff --git a/mopidy/softwaremixer/mixer.py b/mopidy/softwaremixer/mixer.py new file mode 100644 index 00000000..b489730f --- /dev/null +++ b/mopidy/softwaremixer/mixer.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals + +import logging + +import pykka + +from mopidy import mixer + + +logger = logging.getLogger(__name__) + + +class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): + + name = 'software' + + def __init__(self, config, audio): + super(SoftwareMixer, self).__init__() + self.config = config + self.audio = audio + + logger.info('Mixing using GStreamer software mixing') + + def get_volume(self): + return self.audio.get_volume().get() + + def set_volume(self, volume): + self.audio.set_volume(volume) + + def get_mute(self): + return self.audio.get_mute().get() + + def set_mute(self, muted): + self.audio.set_mute(muted) diff --git a/setup.py b/setup.py index 437fe121..3f69591d 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ setup( 'http = mopidy.http:Extension', 'local = mopidy.local:Extension', 'mpd = mopidy.mpd:Extension', + 'softwaremixer = mopidy.softwaremixer:Extension', 'stream = mopidy.stream:Extension', ], }, From 6d6bc4b808b2a57cff43443c2a5a7da48b5184e8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 01:01:55 +0200 Subject: [PATCH 06/40] core: Use new mixer API --- mopidy/commands.py | 6 +++--- mopidy/core/actor.py | 4 ++-- mopidy/core/playback.py | 20 ++++++++++---------- tests/core/test_actor.py | 4 ++-- tests/core/test_library.py | 2 +- tests/core/test_playback.py | 2 +- tests/core/test_playlists.py | 2 +- tests/core/test_tracklist.py | 2 +- tests/local/test_tracklist.py | 2 +- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/mopidy/commands.py b/mopidy/commands.py index 7e66753f..514407b6 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -269,7 +269,7 @@ class RootCommand(Command): audio = self.start_audio(config) mixer = self.start_mixer(config, audio, mixer_classes) backends = self.start_backends(config, backend_classes, audio) - core = self.start_core(audio, mixer, backends) + core = self.start_core(mixer, backends) self.start_frontends(config, frontend_classes, core) loop.run() except KeyboardInterrupt: @@ -316,9 +316,9 @@ class RootCommand(Command): logger.info('Starting Mopidy mixer: %s', mixer_class.__name__) return mixer_class.start(config=config, audio=audio).proxy() - def start_core(self, audio, mixer, backends): + def start_core(self, mixer, backends): logger.info('Starting Mopidy core') - return Core.start(audio=audio, backends=backends).proxy() + return Core.start(mixer=mixer, backends=backends).proxy() def start_frontends(self, config, frontend_classes, core): logger.info( diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index a3dba245..e8c600ed 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -32,7 +32,7 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener): """The tracklist controller. An instance of :class:`mopidy.core.TracklistController`.""" - def __init__(self, audio=None, backends=None): + def __init__(self, mixer=None, backends=None): super(Core, self).__init__() self.backends = Backends(backends) @@ -40,7 +40,7 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener): self.library = LibraryController(backends=self.backends, core=self) self.playback = PlaybackController( - audio=audio, backends=self.backends, core=self) + mixer=mixer, backends=self.backends, core=self) self.playlists = PlaylistsController( backends=self.backends, core=self) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 7f4a31f4..8988aa9e 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -13,8 +13,8 @@ logger = logging.getLogger(__name__) class PlaybackController(object): pykka_traversable = True - def __init__(self, audio, backends, core): - self.audio = audio + def __init__(self, mixer, backends, core): + self.mixer = mixer self.backends = backends self.core = core @@ -88,15 +88,15 @@ class PlaybackController(object): """Time position in milliseconds.""" def get_volume(self): - if self.audio: - return self.audio.get_volume().get() + if self.mixer: + return self.mixer.get_volume().get() else: # For testing return self._volume def set_volume(self, volume): - if self.audio: - self.audio.set_volume(volume) + if self.mixer: + self.mixer.set_volume(volume) else: # For testing self._volume = volume @@ -107,16 +107,16 @@ class PlaybackController(object): """Volume as int in range [0..100] or :class:`None`""" def get_mute(self): - if self.audio: - return self.audio.get_mute().get() + if self.mixer: + return self.mixer.get_mute().get() else: # For testing return self._mute def set_mute(self, value): value = bool(value) - if self.audio: - self.audio.set_mute(value) + if self.mixer: + self.mixer.set_mute(value) else: # For testing self._mute = value diff --git a/tests/core/test_actor.py b/tests/core/test_actor.py index 47ac0168..79d778af 100644 --- a/tests/core/test_actor.py +++ b/tests/core/test_actor.py @@ -20,7 +20,7 @@ class CoreActorTest(unittest.TestCase): self.backend2.uri_schemes.get.return_value = ['dummy2'] self.backend2.actor_ref.actor_class.__name__ = b'B2' - self.core = Core(audio=None, backends=[self.backend1, self.backend2]) + self.core = Core(mixer=None, backends=[self.backend1, self.backend2]) def tearDown(self): pykka.ActorRegistry.stop_all() @@ -37,7 +37,7 @@ class CoreActorTest(unittest.TestCase): self.assertRaisesRegexp( AssertionError, 'Cannot add URI scheme dummy1 for B2, it is already handled by B1', - Core, audio=None, backends=[self.backend1, self.backend2]) + Core, mixer=None, backends=[self.backend1, self.backend2]) def test_version(self): self.assertEqual(self.core.version, versioning.get_version()) diff --git a/tests/core/test_library.py b/tests/core/test_library.py index 1db22688..9eac3ebd 100644 --- a/tests/core/test_library.py +++ b/tests/core/test_library.py @@ -30,7 +30,7 @@ class CoreLibraryTest(unittest.TestCase): self.backend3.has_library().get.return_value = False self.backend3.has_library_browse().get.return_value = False - self.core = core.Core(audio=None, backends=[ + self.core = core.Core(mixer=None, backends=[ self.backend1, self.backend2, self.backend3]) def test_browse_root_returns_dir_ref_for_each_lib_with_root_dir_name(self): diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 7a97b3d7..09012139 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -36,7 +36,7 @@ class CorePlaybackTest(unittest.TestCase): Track(uri='dummy1:b', length=40000), ] - self.core = core.Core(audio=None, backends=[ + self.core = core.Core(mixer=None, backends=[ self.backend1, self.backend2, self.backend3]) self.core.tracklist.add(self.tracks) diff --git a/tests/core/test_playlists.py b/tests/core/test_playlists.py index 73b4f486..49f617b5 100644 --- a/tests/core/test_playlists.py +++ b/tests/core/test_playlists.py @@ -34,7 +34,7 @@ class PlaylistsTest(unittest.TestCase): self.pl2b = Playlist(name='B', tracks=[Track(uri='dummy2:b')]) self.sp2.playlists.get.return_value = [self.pl2a, self.pl2b] - self.core = core.Core(audio=None, backends=[ + self.core = core.Core(mixer=None, backends=[ self.backend3, self.backend1, self.backend2]) def test_get_playlists_combines_result_from_backends(self): diff --git a/tests/core/test_tracklist.py b/tests/core/test_tracklist.py index 9f4dc9c0..963a4bb7 100644 --- a/tests/core/test_tracklist.py +++ b/tests/core/test_tracklist.py @@ -21,7 +21,7 @@ class TracklistTest(unittest.TestCase): self.library = mock.Mock(spec=backend.LibraryProvider) self.backend.library = self.library - self.core = core.Core(audio=None, backends=[self.backend]) + self.core = core.Core(mixer=None, backends=[self.backend]) self.tl_tracks = self.core.tracklist.add(self.tracks) def test_add_by_uri_looks_up_uri_in_library(self): diff --git a/tests/local/test_tracklist.py b/tests/local/test_tracklist.py index 7717f1a5..af07a4e6 100644 --- a/tests/local/test_tracklist.py +++ b/tests/local/test_tracklist.py @@ -30,7 +30,7 @@ class LocalTracklistProviderTest(unittest.TestCase): self.audio = audio.DummyAudio.start().proxy() self.backend = actor.LocalBackend.start( config=self.config, audio=self.audio).proxy() - self.core = core.Core(audio=self.audio, backends=[self.backend]) + self.core = core.Core(mixer=None, backends=[self.backend]) self.controller = self.core.tracklist self.playback = self.core.playback From 93ffde39c211b6be8274089894ccdff358279abb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 01:18:38 +0200 Subject: [PATCH 07/40] mixer: Use initial volume from audio/mixer_volume --- mopidy/commands.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mopidy/commands.py b/mopidy/commands.py index 514407b6..226f1227 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -304,6 +304,7 @@ class RootCommand(Command): logger.debug( 'Available Mopidy mixers: %s', ', '.join(m.__name__ for m in mixer_classes) or 'none') + selected_mixers = [ m for m in mixer_classes if m.name == config['audio']['mixer']] if len(selected_mixers) != 1: @@ -313,8 +314,18 @@ class RootCommand(Command): ', '.join([m.name for m in mixer_classes])) process.exit_process() mixer_class = selected_mixers[0] + logger.info('Starting Mopidy mixer: %s', mixer_class.__name__) - return mixer_class.start(config=config, audio=audio).proxy() + mixer = mixer_class.start(config=config, audio=audio).proxy() + + volume = config['audio']['mixer_volume'] + if volume is not None: + mixer.set_volume(volume) + logger.info('Mixer volume set to %d', volume) + else: + logger.debug('Mixer volume left unchanged') + + return mixer def start_core(self, mixer, backends): logger.info('Starting Mopidy core') From 9da716935cd7b1701037c504145eb83d04c221b2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 01:28:02 +0200 Subject: [PATCH 08/40] audio: Only expose GStreamer's software mixer --- mopidy/audio/actor.py | 177 +++----------------------------------- tests/audio/test_actor.py | 36 +------- 2 files changed, 18 insertions(+), 195 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 08c634e9..25a3a44c 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -10,7 +10,7 @@ import gst # noqa import pykka -from mopidy.audio import mixers, playlists, utils +from mopidy.audio import playlists, utils from mopidy.audio.constants import PlaybackState from mopidy.audio.listener import AudioListener from mopidy.utils import process @@ -18,8 +18,6 @@ from mopidy.utils import process logger = logging.getLogger(__name__) -mixers.register_mixers() - playlists.register_typefinders() playlists.register_elements() @@ -60,12 +58,6 @@ class Audio(pykka.ThreadingActor): self._playbin = None self._signal_ids = {} # {(element, event): signal_id} - self._mixer = None - self._mixer_track = None - self._mixer_scale = None - self._software_mixing = False - self._volume_set = None - self._appsrc = None self._appsrc_caps = None self._appsrc_need_data_callback = None @@ -77,7 +69,6 @@ class Audio(pykka.ThreadingActor): self._setup_playbin() self._setup_output() self._setup_visualizer() - self._setup_mixer() self._setup_message_processor() except gobject.GError as ex: logger.exception(ex) @@ -85,7 +76,6 @@ class Audio(pykka.ThreadingActor): def on_stop(self): self._teardown_message_processor() - self._teardown_mixer() self._teardown_playbin() def _connect(self, element, event, *args): @@ -204,86 +194,6 @@ class Audio(pykka.ThreadingActor): 'Failed to create audio visualizer "%s": %s', visualizer_element, ex) - def _setup_mixer(self): - mixer_desc = self._config['audio']['mixer'] - track_desc = self._config['audio']['mixer_track'] - volume = self._config['audio']['mixer_volume'] - - if mixer_desc is None: - logger.info('Not setting up audio mixer') - return - - if mixer_desc == 'software': - self._software_mixing = True - logger.info('Audio mixer is using software mixing') - if volume is not None: - self.set_volume(volume) - logger.info('Audio mixer volume set to %d', volume) - return - - try: - mixerbin = gst.parse_bin_from_description( - mixer_desc, ghost_unconnected_pads=False) - except gobject.GError as ex: - logger.warning( - 'Failed to create audio mixer "%s": %s', mixer_desc, ex) - return - - # We assume that the bin will contain a single mixer. - mixer = mixerbin.get_by_interface(b'GstMixer') - if not mixer: - logger.warning( - 'Did not find any audio mixers in "%s"', mixer_desc) - return - - if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: - logger.warning( - 'Setting audio mixer "%s" to READY failed', mixer_desc) - return - - track = self._select_mixer_track(mixer, track_desc) - if not track: - logger.warning('Could not find usable audio mixer track') - return - - self._mixer = mixer - self._mixer_track = track - self._mixer_scale = ( - self._mixer_track.min_volume, self._mixer_track.max_volume) - - logger.info( - 'Audio mixer set to "%s" using track "%s"', - str(mixer.get_factory().get_name()).decode('utf-8'), - str(track.label).decode('utf-8')) - - if volume is not None: - self.set_volume(volume) - logger.info('Audio mixer volume set to %d', volume) - - def _select_mixer_track(self, mixer, track_label): - # Ignore tracks without volumes, then look for track with - # label equal to the audio/mixer_track config value, otherwise fallback - # to first usable track hoping the mixer gave them to us in a sensible - # order. - - usable_tracks = [] - for track in mixer.list_tracks(): - if not mixer.get_volume(track): - continue - - if track_label and track.label == track_label: - return track - elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER | - gst.interfaces.MIXER_TRACK_OUTPUT): - usable_tracks.append(track) - - if usable_tracks: - return usable_tracks[0] - - def _teardown_mixer(self): - if self._mixer is not None: - self._mixer.set_state(gst.STATE_NULL) - def _setup_message_processor(self): bus = self._playbin.get_bus() bus.add_signal_watch() @@ -514,108 +424,49 @@ class Audio(pykka.ThreadingActor): def get_volume(self): """ - Get volume level of the installed mixer. + Get volume level of the software mixer. Example values: 0: - Muted. + Minimum volume. 100: - Max volume for given system. - :class:`None`: - No mixer present, so the volume is unknown. + Max volume. - :rtype: int in range [0..100] or :class:`None` + :rtype: int in range [0..100] """ - if self._software_mixing: - return int(round(self._playbin.get_property('volume') * 100)) - - if self._mixer is None: - return None - - volumes = self._mixer.get_volume(self._mixer_track) - avg_volume = float(sum(volumes)) / len(volumes) - - internal_scale = (0, 100) - - if self._volume_set is not None: - volume_set_on_mixer_scale = self._rescale( - self._volume_set, old=internal_scale, new=self._mixer_scale) - else: - volume_set_on_mixer_scale = None - - if volume_set_on_mixer_scale == avg_volume: - return self._volume_set - else: - return self._rescale( - avg_volume, old=self._mixer_scale, new=internal_scale) + return int(round(self._playbin.get_property('volume') * 100)) def set_volume(self, volume): """ - Set volume level of the installed mixer. + Set volume level of the software mixer. :param volume: the volume in the range [0..100] :type volume: int :rtype: :class:`True` if successful, else :class:`False` """ - if self._software_mixing: - self._playbin.set_property('volume', volume / 100.0) - return True - - if self._mixer is None: - return False - - self._volume_set = volume - - internal_scale = (0, 100) - - volume = self._rescale( - volume, old=internal_scale, new=self._mixer_scale) - - volumes = (volume,) * self._mixer_track.num_channels - self._mixer.set_volume(self._mixer_track, volumes) - - return self._mixer.get_volume(self._mixer_track) == volumes - - def _rescale(self, value, old=None, new=None): - """Convert value between scales.""" - new_min, new_max = new - old_min, old_max = old - if old_min == old_max: - return old_max - scaling = float(new_max - new_min) / (old_max - old_min) - return int(round(scaling * (value - old_min) + new_min)) + self._playbin.set_property('volume', volume / 100.0) + return True def get_mute(self): """ - Get mute status of the installed mixer. + Get mute status of the software mixer. :rtype: :class:`True` if muted, :class:`False` if unmuted, :class:`None` if no mixer is installed. """ - if self._software_mixing: - return self._playbin.get_property('mute') - - if self._mixer_track is None: - return None - - return bool(self._mixer_track.flags & gst.interfaces.MIXER_TRACK_MUTE) + return self._playbin.get_property('mute') def set_mute(self, mute): """ - Mute or unmute of the installed mixer. + Mute or unmute of the software mixer. :param mute: Wether to mute the mixer or not. :type mute: bool :rtype: :class:`True` if successful, else :class:`False` """ - if self._software_mixing: - return self._playbin.set_property('mute', bool(mute)) - - if self._mixer_track is None: - return False - - return self._mixer.set_mute(self._mixer_track, bool(mute)) + self._playbin.set_property('mute', bool(mute)) + return True def set_metadata(self, track): """ diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 1df4ff18..bcb7ca2b 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -23,8 +23,7 @@ class AudioTest(unittest.TestCase): def setUp(self): config = { 'audio': { - 'mixer': 'fakemixer track_max_volume=65536', - 'mixer_track': None, + 'mixer': 'software', 'mixer_volume': None, 'output': 'fakesink', 'visualizer': None, @@ -74,38 +73,11 @@ class AudioTest(unittest.TestCase): self.assertTrue(self.audio.set_volume(value).get()) self.assertEqual(value, self.audio.get_volume().get()) - def test_set_volume_with_mixer_max_below_100(self): - config = { - 'audio': { - 'mixer': 'fakemixer track_max_volume=40', - 'mixer_track': None, - 'mixer_volume': None, - 'output': 'fakesink', - 'visualizer': None, - } - } - self.audio = audio.Audio.start(config=config).proxy() - - for value in range(0, 101): - self.assertTrue(self.audio.set_volume(value).get()) - self.assertEqual(value, self.audio.get_volume().get()) - - def test_set_volume_with_mixer_min_equal_max(self): - config = { - 'audio': { - 'mixer': 'fakemixer track_max_volume=0', - 'mixer_track': None, - 'mixer_volume': None, - 'output': 'fakesink', - 'visualizer': None, - } - } - self.audio = audio.Audio.start(config=config).proxy() - self.assertEqual(0, self.audio.get_volume().get()) - @unittest.SkipTest def test_set_mute(self): - pass # TODO Probably needs a fakemixer with a mixer track + for value in (True, False): + self.assertTrue(self.audio.set_mute(value).get()) + self.assertEqual(value, self.audio.get_mute().get()) @unittest.SkipTest def test_set_state_encapsulation(self): From 810429a4495394fd9af403fed7d4207d4c5a817d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 01:29:58 +0200 Subject: [PATCH 09/40] audio: Remove custom GStreamer mixers --- mopidy/audio/mixers/__init__.py | 21 --------- mopidy/audio/mixers/auto.py | 77 --------------------------------- mopidy/audio/mixers/fake.py | 50 --------------------- mopidy/audio/mixers/utils.py | 38 ---------------- 4 files changed, 186 deletions(-) delete mode 100644 mopidy/audio/mixers/__init__.py delete mode 100644 mopidy/audio/mixers/auto.py delete mode 100644 mopidy/audio/mixers/fake.py delete mode 100644 mopidy/audio/mixers/utils.py diff --git a/mopidy/audio/mixers/__init__.py b/mopidy/audio/mixers/__init__.py deleted file mode 100644 index cf763de3..00000000 --- a/mopidy/audio/mixers/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import unicode_literals - -import gobject - -import pygst -pygst.require('0.10') -import gst # noqa - -from mopidy.audio.mixers.auto import AutoAudioMixer -from mopidy.audio.mixers.fake import FakeMixer - - -def register_mixer(mixer_class): - gobject.type_register(mixer_class) - gst.element_register( - mixer_class, mixer_class.__name__.lower(), gst.RANK_MARGINAL) - - -def register_mixers(): - register_mixer(AutoAudioMixer) - register_mixer(FakeMixer) diff --git a/mopidy/audio/mixers/auto.py b/mopidy/audio/mixers/auto.py deleted file mode 100644 index 01a16e42..00000000 --- a/mopidy/audio/mixers/auto.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Mixer element that automatically selects the real mixer to use. - -Set the :confval:`audio/mixer` config value to ``autoaudiomixer`` to use this -mixer. -""" - -from __future__ import unicode_literals - -import logging - -import pygst -pygst.require('0.10') -import gst # noqa - - -logger = logging.getLogger(__name__) - - -# 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.', - 'Mopidy') - - 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 implement mixing. - if factory.get_klass() != 'Generic/Audio': - continue - # Avoid anything that doesn't implement 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 diff --git a/mopidy/audio/mixers/fake.py b/mopidy/audio/mixers/fake.py deleted file mode 100644 index 404f6298..00000000 --- a/mopidy/audio/mixers/fake.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Fake mixer for use in tests. - -Set the :confval:`audio/mixer:` config value to ``fakemixer`` to use this -mixer. -""" - -from __future__ import unicode_literals - -import gobject - -import pygst -pygst.require('0.10') -import gst # noqa - -from mopidy.audio.mixers import utils - - -class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): - __gstdetails__ = ( - 'FakeMixer', - 'Mixer', - 'Fake mixer for use in tests.', - 'Mopidy') - - 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 list_tracks(self): - track = utils.create_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 diff --git a/mopidy/audio/mixers/utils.py b/mopidy/audio/mixers/utils.py deleted file mode 100644 index b3b08f19..00000000 --- a/mopidy/audio/mixers/utils.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import unicode_literals - -import gobject - -import pygst -pygst.require('0.10') -import gst # noqa - - -def create_track(label, initial_volume, min_volume, max_volume, - num_channels, flags): - - class Track(gst.interfaces.MixerTrack): - def __init__(self): - super(Track, self).__init__() - self.volumes = (initial_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() From 3daea856b19deca2a7e89048cf70ea4897a46c0a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 01:44:30 +0200 Subject: [PATCH 10/40] config: Deprecate audio/mixer_track --- docs/config.rst | 8 -------- mopidy/config/__init__.py | 2 +- mopidy/config/default.conf | 1 - 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 692204d9..e51ee273 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -91,14 +91,6 @@ Audio configuration Setting the config value to blank leaves the audio mixer volume unchanged. For the software mixer blank means 100. -.. confval:: audio/mixer_track - - Audio mixer track to use. - - Name of the mixer track to use. If this is not set we will try to find the - master output track. As an example, using ``alsamixer`` you would typically - set this to ``Master`` or ``PCM``. - .. confval:: audio/output Audio output to use. diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 1f9f5e2d..c23707d8 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -24,7 +24,7 @@ _loglevels_schema = LogLevelConfigSchema('loglevels') _audio_schema = ConfigSchema('audio') _audio_schema['mixer'] = String() -_audio_schema['mixer_track'] = String(optional=True) +_audio_schema['mixer_track'] = Deprecated() _audio_schema['mixer_volume'] = Integer(optional=True, minimum=0, maximum=100) _audio_schema['output'] = String() _audio_schema['visualizer'] = String(optional=True) diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index 839c983d..d56b7b28 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -6,7 +6,6 @@ config_file = [audio] mixer = software -mixer_track = mixer_volume = output = autoaudiosink visualizer = From e0acae231090bd5b942f5e42ee8509cbba4476b6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 01:45:28 +0200 Subject: [PATCH 11/40] docs: Remove MIXER_TRACK_* attribute workaround --- docs/conf.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 533f3fd5..52e84e06 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,8 +35,6 @@ class Mock(object): # glib.get_user_config_dir() return str elif (name[0] == name[0].upper() - # gst.interfaces.MIXER_TRACK_* - and not name.startswith('MIXER_TRACK_') # gst.PadTemplate and not name.startswith('PadTemplate') # dbus.String() From 1f8512e07dc16aa3602a7cff459cc33a9157def8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 01:46:48 +0200 Subject: [PATCH 12/40] docs: Update changelog with mixer changes --- docs/changelog.rst | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 157f0d73..9fd629bf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,44 @@ Changelog This changelog is used to track all major changes to Mopidy. +v0.20.0 (UNRELEASED) +==================== + +**Audio** + +- Removed support for GStreamer mixers. GStreamer 1.x does not support volume + control, so we changed to use software mixing by default in v0.17.0. Now, + we're removing support for all other GStreamer mixers and are reintroducing + mixers as something extensions can provide independently of GStreamer. + (Fixes: :issue:`665`, PR: :issue:`760`) + +- Changed the :confval:`audio/mixer` config value to refer to Mopidy mixer + extensions instead of GStreamer mixers. The default value, ``software``, + still has the same behavior. All other values will either no longer work or + will at the very least require you to install an additional extension. + +- Changed the :confval:`audio/mixer_volume` config value behavior from + affecting GStreamer mixers to affecting Mopidy mixer extensions instead. The + end result should be the same without any changes to this config value. + +- Deprecated the :confval:`audio/mixer_track` config value. This config value + is no longer in use. Mixer extensions that needs additional configuration + handle this themselves. + +**Mixers** + +- Added new :class:`mopidy.mixer.Mixer` API which can be implemented by + extensions. + +- Created a bundled extension, :mod:`mopidy.softwaremixer`, for controlling + volume in software in GStreamer's pipeline. This is Mopidy's default mixer. + To use this mixer, set :confval:`audio/mixer` to ``software``. + +- Created an external extension, `Mopidy-ALSAMixer + `_, for controlling volume with + hardware through ALSA. To use this mixer, install the extension, and set + :confval:`audio/mixer` to ``alsamixer``. + v0.19.0 (UNRELEASED) ==================== From aa2cb12b9c02805289c969eb6dab376e40e0eda4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 01:49:44 +0200 Subject: [PATCH 13/40] docs: Add Mopidy-SoftwareMixer to the mixer impl list --- docs/api/mixer.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api/mixer.rst b/docs/api/mixer.rst index 6f764210..0428dab6 100644 --- a/docs/api/mixer.rst +++ b/docs/api/mixer.rst @@ -15,3 +15,5 @@ Mixer implementations ===================== - `Mopidy-ALSAMixer `_ + +- Mopidy-SoftwareMixer (see :mod:`mopidy.softwaremixer` in the source code) From 71a66f21756dac76218f91fac5d14eeea073e6fb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Jul 2014 01:56:43 +0200 Subject: [PATCH 14/40] docs: Update audio/* config docs --- docs/config.rst | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index e51ee273..35acc10d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -64,23 +64,15 @@ Audio configuration Audio mixer to use. - Expects a GStreamer mixer to use, typical values are: ``software``, - ``autoaudiomixer``, ``alsamixer``, ``pulsemixer``, ``ossmixer``, and - ``oss4mixer``. - The default is ``software``, which does volume control inside Mopidy before the audio is sent to the audio output. This mixer does not affect the volume of any other audio playback on the system. It is the only mixer that will affect the audio volume if you're streaming the audio from Mopidy through Shoutcast. - If you want to use a hardware mixer, try ``autoaudiomixer``. It attempts to - select a sane hardware mixer for you automatically. When Mopidy is started, - it will log what mixer ``autoaudiomixer`` selected, for example:: - - INFO Audio mixer set to "alsamixer" using track "Master" - - Setting the config value to blank turns off volume control. + If you want to use a hardware mixer, you need to install a Mopidy extension + which integrates with your sound subsystem. E.g. for ALSA, install + `Mopidy-ALSAMixer `_. .. confval:: audio/mixer_volume From 4807bd275ad4940a8df85ecb939ad0c199c5b01e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 21:09:04 +0200 Subject: [PATCH 15/40] mixer: Remove 'should' from docstring --- mopidy/mixer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/mixer.py b/mopidy/mixer.py index e522823b..814681f1 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -7,7 +7,7 @@ class Mixer(object): name = None """Name of the mixer. - Used when configuring what mixer to use. Should usually match the + Used when configuring what mixer to use. Should match the :attr:`~mopidy.ext.Extension.ext_name` of the extension providing the mixer. """ From 64ecd7643a4520e5fe0d7dd885c44904a746779f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 21:16:09 +0200 Subject: [PATCH 16/40] main: Stop only the mixer actor that we now is running --- mopidy/commands.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/mopidy/commands.py b/mopidy/commands.py index 226f1227..b9e07677 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -261,13 +261,13 @@ class RootCommand(Command): def run(self, args, config): loop = gobject.MainLoop() - mixer_classes = args.registry['mixer'] + mixer_class = self.get_mixer_class(config, args.registry['mixer']) backend_classes = args.registry['backend'] frontend_classes = args.registry['frontend'] try: audio = self.start_audio(config) - mixer = self.start_mixer(config, audio, mixer_classes) + mixer = self.start_mixer(config, mixer_class, audio) backends = self.start_backends(config, backend_classes, audio) core = self.start_core(mixer, backends) self.start_frontends(config, frontend_classes, core) @@ -279,11 +279,26 @@ class RootCommand(Command): loop.quit() self.stop_frontends(frontend_classes) self.stop_core() - self.stop_mixer(mixer_classes) + self.stop_mixer(mixer_class) self.stop_backends(backend_classes) self.stop_audio() process.stop_remaining_actors() + def get_mixer_class(self, config, mixer_classes): + logger.debug( + 'Available Mopidy mixers: %s', + ', '.join(m.__name__ for m in mixer_classes) or 'none') + + selected_mixers = [ + m for m in mixer_classes if m.name == config['audio']['mixer']] + if len(selected_mixers) != 1: + logger.error( + 'Did not find unique mixer "%s". Alternatives are: %s', + config['audio']['mixer'], + ', '.join([m.name for m in mixer_classes])) + process.exit_process() + return selected_mixers[0] + def start_audio(self, config): logger.info('Starting Mopidy audio') return Audio.start(config=config).proxy() @@ -300,21 +315,7 @@ class RootCommand(Command): return backends - def start_mixer(self, config, audio, mixer_classes): - logger.debug( - 'Available Mopidy mixers: %s', - ', '.join(m.__name__ for m in mixer_classes) or 'none') - - selected_mixers = [ - m for m in mixer_classes if m.name == config['audio']['mixer']] - if len(selected_mixers) != 1: - logger.error( - 'Did not find unique mixer "%s". Alternatives are: %s', - config['audio']['mixer'], - ', '.join([m.name for m in mixer_classes])) - process.exit_process() - mixer_class = selected_mixers[0] - + def start_mixer(self, config, mixer_class, audio): logger.info('Starting Mopidy mixer: %s', mixer_class.__name__) mixer = mixer_class.start(config=config, audio=audio).proxy() @@ -348,10 +349,9 @@ class RootCommand(Command): logger.info('Stopping Mopidy core') process.stop_actors_by_class(Core) - def stop_mixer(self, mixer_classes): + def stop_mixer(self, mixer_class): logger.info('Stopping Mopidy mixer') - for mixer_class in mixer_classes: - process.stop_actors_by_class(mixer_class) + process.stop_actors_by_class(mixer_class) def stop_backends(self, backend_classes): logger.info('Stopping Mopidy backends') From 395019e857b3028878a155aa54b8a6157bc59881 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 21:18:20 +0200 Subject: [PATCH 17/40] swmixer: Remove unused attribute --- mopidy/softwaremixer/mixer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/softwaremixer/mixer.py b/mopidy/softwaremixer/mixer.py index b489730f..a589ff76 100644 --- a/mopidy/softwaremixer/mixer.py +++ b/mopidy/softwaremixer/mixer.py @@ -16,7 +16,6 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): def __init__(self, config, audio): super(SoftwareMixer, self).__init__() - self.config = config self.audio = audio logger.info('Mixing using GStreamer software mixing') From 8f59dd69adca2e6c1595159e5350bf5a4e69988f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 22:15:45 +0200 Subject: [PATCH 18/40] exc: Test ScannerError --- tests/test_exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index da8fed90..3452a06b 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -15,3 +15,7 @@ class ExceptionsTest(unittest.TestCase): def test_extension_error_is_a_mopidy_exception(self): self.assert_(issubclass( exceptions.ExtensionError, exceptions.MopidyException)) + + def test_scanner_error_is_a_mopidy_exception(self): + self.assert_(issubclass( + exceptions.ScannerError, exceptions.MopidyException)) From bf8307f329ce03b7a311d62aa50fdd7833048f32 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 22:17:46 +0200 Subject: [PATCH 19/40] exc: Add {Backend,Frontend,Mixer}Error exceptions --- mopidy/exceptions.py | 12 ++++++++++++ tests/test_exceptions.py | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/mopidy/exceptions.py b/mopidy/exceptions.py index 025d8fad..bf9b6dd9 100644 --- a/mopidy/exceptions.py +++ b/mopidy/exceptions.py @@ -16,9 +16,21 @@ class MopidyException(Exception): self._message = message +class BackendError(MopidyException): + pass + + class ExtensionError(MopidyException): pass +class FrontendError(MopidyException): + pass + + +class MixerError(MopidyException): + pass + + class ScannerError(MopidyException): pass diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 3452a06b..47b3080d 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -12,10 +12,22 @@ class ExceptionsTest(unittest.TestCase): self.assertEqual(exc.message, 'foo') self.assertEqual(str(exc), 'foo') + def test_backend_error_is_a_mopidy_exception(self): + self.assert_(issubclass( + exceptions.BackendError, exceptions.MopidyException)) + def test_extension_error_is_a_mopidy_exception(self): self.assert_(issubclass( exceptions.ExtensionError, exceptions.MopidyException)) + def test_frontend_error_is_a_mopidy_exception(self): + self.assert_(issubclass( + exceptions.FrontendError, exceptions.MopidyException)) + + def test_mixer_error_is_a_mopidy_exception(self): + self.assert_(issubclass( + exceptions.MixerError, exceptions.MopidyException)) + def test_scanner_error_is_a_mopidy_exception(self): self.assert_(issubclass( exceptions.ScannerError, exceptions.MopidyException)) From bb269688c65d1119f3e944b53cc887e7caeaf14a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 22:19:34 +0200 Subject: [PATCH 20/40] mixer: Mark methods that may be implemented by subclasses --- mopidy/mixer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mopidy/mixer.py b/mopidy/mixer.py index 814681f1..edda8455 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -25,6 +25,8 @@ class Mixer(object): :class:`None`: Volume is unknown. + *MAY be implemented by subclass.* + :rtype: int in range [0..100] or :class:`None` """ return None @@ -33,6 +35,8 @@ class Mixer(object): """ Set volume level of the mixer. + *MAY be implemented by subclass.* + :param volume: Volume in the range [0..100] :type volume: int :rtype: :class:`True` if success, :class:`False` if failure @@ -43,6 +47,8 @@ class Mixer(object): """ Get mute status of the mixer. + *MAY be implemented by subclass.* + :rtype: :class:`True` if muted, :class:`False` if unmuted, :class:`None` if unknown. """ @@ -52,6 +58,8 @@ class Mixer(object): """ Mute or unmute the mixer. + *MAY be implemented by subclass.* + :param muted: :class:`True` to mute, :class:`False` to unmute :type muted: bool :rtype: :class:`True` if success, :class:`False` if failure From 95bddf666bedf1c8fd644042ae6fbf8adca43027 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 23:20:10 +0200 Subject: [PATCH 21/40] main: Log and exit if {Backend,Frontend,Mixer}Error is raised --- mopidy/commands.py | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/mopidy/commands.py b/mopidy/commands.py index b9e07677..87d35056 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -10,7 +10,7 @@ import glib import gobject -from mopidy import config as config_lib +from mopidy import config as config_lib, exceptions from mopidy.audio import Audio from mopidy.core import Core from mopidy.utils import deps, process, versioning @@ -272,9 +272,12 @@ class RootCommand(Command): core = self.start_core(mixer, backends) self.start_frontends(config, frontend_classes, core) loop.run() + except (exceptions.BackendError, + exceptions.FrontendError, + exceptions.MixerError): + logger.info('Initialization error. Exiting...') except KeyboardInterrupt: logger.info('Interrupted. Exiting...') - return finally: loop.quit() self.stop_frontends(frontend_classes) @@ -310,15 +313,31 @@ class RootCommand(Command): backends = [] for backend_class in backend_classes: - backend = backend_class.start(config=config, audio=audio).proxy() - backends.append(backend) + try: + backend = backend_class.start( + config=config, audio=audio).proxy() + backends.append(backend) + except exceptions.BackendError as exc: + logger.error( + 'Backend (%s) initialization error: %s', + backend_class.__name__, exc.message) + raise return backends def start_mixer(self, config, mixer_class, audio): - logger.info('Starting Mopidy mixer: %s', mixer_class.__name__) - mixer = mixer_class.start(config=config, audio=audio).proxy() + try: + logger.info('Starting Mopidy mixer: %s', mixer_class.__name__) + mixer = mixer_class.start(config=config, audio=audio).proxy() + self.configure_mixer(config, mixer) + return mixer + except exceptions.MixerError as exc: + logger.error( + 'Mixer (%s) initialization error: %s', + mixer_class.__name__, exc.message) + raise + def configure_mixer(self, config, mixer): volume = config['audio']['mixer_volume'] if volume is not None: mixer.set_volume(volume) @@ -326,8 +345,6 @@ class RootCommand(Command): else: logger.debug('Mixer volume left unchanged') - return mixer - def start_core(self, mixer, backends): logger.info('Starting Mopidy core') return Core.start(mixer=mixer, backends=backends).proxy() @@ -338,7 +355,13 @@ class RootCommand(Command): ', '.join(f.__name__ for f in frontend_classes) or 'none') for frontend_class in frontend_classes: - frontend_class.start(config=config, core=core) + try: + frontend_class.start(config=config, core=core) + except exceptions.FrontendError as exc: + logger.error( + 'Frontend (%s) initialization error: %s', + frontend_class.__name__, exc.message) + raise def stop_frontends(self, frontend_classes): logger.info('Stopping Mopidy frontends') From 83f1d00944eaa2d22bffd9fc35ee3a9d990dfe41 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 23:20:48 +0200 Subject: [PATCH 22/40] docs: Add {Backend,Frontend,Mixer}Error guidelines to backend/frontend/mixer APIs --- docs/api/frontends.rst | 8 ++++---- mopidy/backend/__init__.py | 8 ++++++++ mopidy/mixer.py | 8 +++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/api/frontends.rst b/docs/api/frontends.rst index 5e2f8d6c..b843692d 100644 --- a/docs/api/frontends.rst +++ b/docs/api/frontends.rst @@ -29,12 +29,12 @@ The following requirements applies to any frontend implementation: - The main actor MUST be able to start and stop the frontend when the main actor is started and stopped. -- The frontend MAY require additional settings to be set for it to - work. +- The frontend MAY require additional config values to be set for it to work. -- Such settings MUST be documented. +- Such config values MUST be documented. -- The main actor MUST stop itself if the defined settings are not adequate for +- The main actor MUST raise the :exc:`~mopidy.exceptions.FrontendError` with a + descriptive error message if the defined config values are not adequate for the frontend to work properly. - Any actor which is part of the frontend MAY implement the diff --git a/mopidy/backend/__init__.py b/mopidy/backend/__init__.py index 6f895985..68aad778 100644 --- a/mopidy/backend/__init__.py +++ b/mopidy/backend/__init__.py @@ -6,6 +6,14 @@ from mopidy import listener class Backend(object): + """Backend API + + If the backend has problems during initialization it should raise + :exc:`~mopidy.exceptions.BackendError` with a descriptive error message. + This will make Mopidy print the error message and exit so that the user can + fix the issue. + """ + #: Actor proxy to an instance of :class:`mopidy.audio.Audio`. #: #: Should be passed to the backend constructor as the kwarg ``audio``, diff --git a/mopidy/mixer.py b/mopidy/mixer.py index edda8455..01216186 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -2,7 +2,13 @@ from __future__ import unicode_literals class Mixer(object): - """Audio mixer API""" + """Audio mixer API + + If the mixer has problems during initialization it should raise + :exc:`~mopidy.exceptions.MixerError` with a descriptive error message. This + will make Mopidy print the error message and exit so that the user can fix + the issue. + """ name = None """Name of the mixer. From 4d0fa17c852410d7a14b2a0d69aa79f833cafdb6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Jul 2014 23:25:00 +0200 Subject: [PATCH 23/40] docs: Describe backend/mixer __init__ args --- mopidy/backend/__init__.py | 5 +++++ mopidy/mixer.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/mopidy/backend/__init__.py b/mopidy/backend/__init__.py index 68aad778..d6474c43 100644 --- a/mopidy/backend/__init__.py +++ b/mopidy/backend/__init__.py @@ -12,6 +12,11 @@ class Backend(object): :exc:`~mopidy.exceptions.BackendError` with a descriptive error message. This will make Mopidy print the error message and exit so that the user can fix the issue. + + :param config: the entire Mopidy configuration + :type config: dict + :param audio: actor proxy for the audio subsystem + :type audio: :class:`pykka.ActorProxy` for :class:`mopidy.audio.Audio` """ #: Actor proxy to an instance of :class:`mopidy.audio.Audio`. diff --git a/mopidy/mixer.py b/mopidy/mixer.py index 01216186..6b15efb2 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -8,6 +8,11 @@ class Mixer(object): :exc:`~mopidy.exceptions.MixerError` with a descriptive error message. This will make Mopidy print the error message and exit so that the user can fix the issue. + + :param config: the entire Mopidy configuration + :type config: dict + :param audio: actor proxy for the audio subsystem + :type audio: :class:`pykka.ActorProxy` for :class:`mopidy.audio.Audio` """ name = None From b8de7fa75cfcafb84e1417a87ed0911014ab7def Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 10 Jul 2014 00:18:22 +0200 Subject: [PATCH 24/40] mixer: Add MixerListener --- docs/api/mixer.rst | 3 ++ mopidy/mixer.py | 76 +++++++++++++++++++++++++++++++++++++++++++-- tests/test_mixer.py | 26 ++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 tests/test_mixer.py diff --git a/docs/api/mixer.rst b/docs/api/mixer.rst index 0428dab6..4697a9d5 100644 --- a/docs/api/mixer.rst +++ b/docs/api/mixer.rst @@ -10,6 +10,9 @@ Audio mixer API .. autoclass:: mopidy.mixer.Mixer :members: +.. autoclass:: mopidy.mixer.MixerListener + :members: + Mixer implementations ===================== diff --git a/mopidy/mixer.py b/mopidy/mixer.py index 6b15efb2..c000eb41 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -1,8 +1,16 @@ from __future__ import unicode_literals +import logging + +from mopidy import listener + + +logger = logging.getLogger(__name__) + class Mixer(object): - """Audio mixer API + """ + Audio mixer API If the mixer has problems during initialization it should raise :exc:`~mopidy.exceptions.MixerError` with a descriptive error message. This @@ -16,7 +24,8 @@ class Mixer(object): """ name = None - """Name of the mixer. + """ + Name of the mixer. Used when configuring what mixer to use. Should match the :attr:`~mopidy.ext.Extension.ext_name` of the extension providing the @@ -54,9 +63,20 @@ class Mixer(object): """ return False + def trigger_volume_changed(self, volume): + """ + Send ``volume_changed`` event to all mixer listeners. + + This method should be called by subclasses when the volume is changed, + either because of a call to :meth:`set_volume` or because or any + external entity changing the volume. + """ + logger.debug('Mixer event: volume_changed(volume=%d)', volume) + MixerListener.send('volume_changed', volume=volume) + def get_mute(self): """ - Get mute status of the mixer. + Get mute state of the mixer. *MAY be implemented by subclass.* @@ -76,3 +96,53 @@ class Mixer(object): :rtype: :class:`True` if success, :class:`False` if failure """ return False + + def trigger_mute_changed(self, muted): + """ + Send ``mute_changed`` event to all mixer listeners. + + This method should be called by subclasses when the mute state is + changed, either because of a call to :meth:`set_mute` or because or + any external entity changing the mute state. + """ + logger.debug('Mixer event: mute_changed(muted=%s)', muted) + MixerListener.send('mute_changed', muted=muted) + + +class MixerListener(listener.Listener): + """ + Marker interface for recipients of events sent by the mixer actor. + + Any Pykka actor that mixes in this class will receive calls to the methods + defined here when the corresponding events happen in the mixer actor. This + interface is used both for looking up what actors to notify of the events, + and for providing default implementations for those listeners that are not + interested in all events. + """ + + @staticmethod + def send(event, **kwargs): + """Helper to allow calling of audio listener events""" + listener.send_async(MixerListener, event, **kwargs) + + def volume_changed(self, volume): + """ + Called after the volume has changed. + + *MAY* be implemented by actor. + + :param volume: the new volume + :type volume: int in range [0..100] + """ + pass + + def mute_changed(self, muted): + """ + Called after the mute state has changed. + + *MAY* be implemented by actor. + + :param muted: :class:`True` if muted, :class:`False` if not muted + :type muted: bool + """ + pass diff --git a/tests/test_mixer.py b/tests/test_mixer.py new file mode 100644 index 00000000..a214eba7 --- /dev/null +++ b/tests/test_mixer.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals + +import unittest + +import mock + +from mopidy import mixer + + +class MixerListenerTest(unittest.TestCase): + def setUp(self): + self.listener = mixer.MixerListener() + + def test_on_event_forwards_to_specific_handler(self): + self.listener.volume_changed = mock.Mock() + + self.listener.on_event( + 'volume_changed', volume=60) + + self.listener.volume_changed.assert_called_with(volume=60) + + def test_listener_has_default_impl_for_volume_changed(self): + self.listener.volume_changed(volume=60) + + def test_listener_has_default_impl_for_mute_changed(self): + self.listener.mute_changed(muted=True) From 84ad6db546b5164f2eb475b9985249a7cf22b44f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 10 Jul 2014 00:19:59 +0200 Subject: [PATCH 25/40] swmixer: Trigger {volume,mute}_changed on set_{volume,mute}() --- mopidy/softwaremixer/mixer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/softwaremixer/mixer.py b/mopidy/softwaremixer/mixer.py index a589ff76..3da90007 100644 --- a/mopidy/softwaremixer/mixer.py +++ b/mopidy/softwaremixer/mixer.py @@ -25,9 +25,11 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): def set_volume(self, volume): self.audio.set_volume(volume) + self.trigger_volume_changed(volume) def get_mute(self): return self.audio.get_mute().get() def set_mute(self, muted): self.audio.set_mute(muted) + self.trigger_mute_changed(muted) From a3dc763b29420becffc164eebafd8f2b3a935d07 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 10 Jul 2014 01:15:44 +0200 Subject: [PATCH 26/40] core: Forward {volume,mute}_changed events, don't trigger them ourselves --- mopidy/core/actor.py | 15 +++++++++++++-- mopidy/core/listener.py | 1 + mopidy/core/playback.py | 12 ------------ tests/core/test_events.py | 14 +++++++++++++- tests/core/test_playback.py | 15 +++++++-------- 5 files changed, 34 insertions(+), 23 deletions(-) diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index e8c600ed..61b5ec4a 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -5,7 +5,7 @@ import itertools import pykka -from mopidy import audio, backend +from mopidy import audio, backend, mixer from mopidy.audio import PlaybackState from mopidy.core.library import LibraryController from mopidy.core.listener import CoreListener @@ -15,7 +15,10 @@ from mopidy.core.tracklist import TracklistController from mopidy.utils import versioning -class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener): +class Core( + pykka.ThreadingActor, audio.AudioListener, backend.BackendListener, + mixer.MixerListener): + library = None """The library controller. An instance of :class:`mopidy.core.LibraryController`.""" @@ -81,6 +84,14 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener): # Forward event from backend to frontends CoreListener.send('playlists_loaded') + def volume_changed(self, volume): + # Forward event from mixer to frontends + CoreListener.send('volume_changed', volume=volume) + + def mute_changed(self, muted): + # Forward event from mixer to frontends + CoreListener.send('mute_changed', mute=muted) + class Backends(list): def __init__(self, backends): diff --git a/mopidy/core/listener.py b/mopidy/core/listener.py index f0bb1ea3..41c697a3 100644 --- a/mopidy/core/listener.py +++ b/mopidy/core/listener.py @@ -150,6 +150,7 @@ class CoreListener(listener.Listener): :param mute: the new mute state :type mute: boolean """ + # TODO Change 'mute' arg to 'muted' next time we break this API pass def seeked(self, time_position): diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 8988aa9e..bf645227 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -101,8 +101,6 @@ class PlaybackController(object): # For testing self._volume = volume - self._trigger_volume_changed(volume) - volume = property(get_volume, set_volume) """Volume as int in range [0..100] or :class:`None`""" @@ -121,8 +119,6 @@ class PlaybackController(object): # For testing self._mute = value - self._trigger_mute_changed(value) - mute = property(get_mute, set_mute) """Mute state as a :class:`True` if muted, :class:`False` otherwise""" @@ -351,14 +347,6 @@ class PlaybackController(object): 'playback_state_changed', old_state=old_state, new_state=new_state) - def _trigger_volume_changed(self, volume): - logger.debug('Triggering volume changed event') - listener.CoreListener.send('volume_changed', volume=volume) - - def _trigger_mute_changed(self, mute): - logger.debug('Triggering mute changed event') - listener.CoreListener.send('mute_changed', mute=mute) - def _trigger_seeked(self, time_position): logger.debug('Triggering seeked event') listener.CoreListener.send('seeked', time_position=time_position) diff --git a/tests/core/test_events.py b/tests/core/test_events.py index 87abd9f9..6ea6704a 100644 --- a/tests/core/test_events.py +++ b/tests/core/test_events.py @@ -20,11 +20,23 @@ class BackendEventsTest(unittest.TestCase): def tearDown(self): pykka.ActorRegistry.stop_all() - def test_backends_playlists_loaded_forwards_event_to_frontends(self, send): + def test_forwards_backend_playlists_loaded_event_to_frontends(self, send): self.core.playlists_loaded().get() self.assertEqual(send.call_args[0][0], 'playlists_loaded') + def test_forwards_mixer_volume_changed_event_to_frontends(self, send): + self.core.volume_changed(volume=60).get() + + self.assertEqual(send.call_args[0][0], 'volume_changed') + self.assertEqual(send.call_args[1]['volume'], 60) + + def test_forwards_mixer_mute_changed_event_to_frontends(self, send): + self.core.mute_changed(muted=True).get() + + self.assertEqual(send.call_args[0][0], 'mute_changed') + self.assertEqual(send.call_args[1]['mute'], True) + def test_tracklist_add_sends_tracklist_changed_event(self, send): send.reset_mock() diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 09012139..ce6c8571 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -376,17 +376,16 @@ class CorePlaybackTest(unittest.TestCase): # TODO Test on_tracklist_change - # TODO Test volume + def test_volume(self): + self.assertEqual(self.core.playback.volume, None) - @mock.patch( - 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) - def test_set_volume_emits_volume_changed_event(self, listener_mock): - self.core.playback.set_volume(10) - listener_mock.reset_mock() + self.core.playback.volume = 30 - self.core.playback.set_volume(20) + self.assertEqual(self.core.playback.volume, 30) - listener_mock.send.assert_called_once_with('volume_changed', volume=20) + self.core.playback.volume = 70 + + self.assertEqual(self.core.playback.volume, 70) def test_mute(self): self.assertEqual(self.core.playback.mute, False) From 5f091c10c25328fba087a973946d5025c3244a95 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Jul 2014 01:53:43 +0200 Subject: [PATCH 27/40] mixer: Inject audio into software mixer Instead of giving all mixers access to the audio actor. --- mopidy/audio/actor.py | 15 ++++++++- mopidy/commands.py | 58 +++++++++++++++++------------------ mopidy/mixer.py | 4 +-- mopidy/softwaremixer/mixer.py | 15 +++++++-- 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 25a3a44c..5c8c3c9f 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -50,10 +50,11 @@ class Audio(pykka.ThreadingActor): state = PlaybackState.STOPPED _target_state = gst.STATE_NULL - def __init__(self, config): + def __init__(self, config, mixer): super(Audio, self).__init__() self._config = config + self._mixer = mixer self._playbin = None self._signal_ids = {} # {(element, event): signal_id} @@ -68,6 +69,7 @@ class Audio(pykka.ThreadingActor): try: self._setup_playbin() self._setup_output() + self._setup_mixer() self._setup_visualizer() self._setup_message_processor() except gobject.GError as ex: @@ -76,6 +78,7 @@ class Audio(pykka.ThreadingActor): def on_stop(self): self._teardown_message_processor() + self._teardown_mixer() self._teardown_playbin() def _connect(self, element, event, *args): @@ -180,6 +183,16 @@ class Audio(pykka.ThreadingActor): 'Failed to create audio output "%s": %s', output_desc, ex) process.exit_process() + def _setup_mixer(self): + if self._config['audio']['mixer'] != 'software': + return + self._mixer.audio = self.actor_ref.proxy() + + def _teardown_mixer(self): + if self._config['audio']['mixer'] != 'software': + return + self._mixer.audio = None + def _setup_visualizer(self): visualizer_element = self._config['audio']['visualizer'] if not visualizer_element: diff --git a/mopidy/commands.py b/mopidy/commands.py index 87d35056..e43f182e 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -266,8 +266,8 @@ class RootCommand(Command): frontend_classes = args.registry['frontend'] try: - audio = self.start_audio(config) - mixer = self.start_mixer(config, mixer_class, audio) + mixer = self.start_mixer(config, mixer_class) + audio = self.start_audio(config, mixer) backends = self.start_backends(config, backend_classes, audio) core = self.start_core(mixer, backends) self.start_frontends(config, frontend_classes, core) @@ -282,9 +282,9 @@ class RootCommand(Command): loop.quit() self.stop_frontends(frontend_classes) self.stop_core() - self.stop_mixer(mixer_class) self.stop_backends(backend_classes) self.stop_audio() + self.stop_mixer(mixer_class) process.stop_remaining_actors() def get_mixer_class(self, config, mixer_classes): @@ -302,9 +302,29 @@ class RootCommand(Command): process.exit_process() return selected_mixers[0] - def start_audio(self, config): + def start_mixer(self, config, mixer_class): + try: + logger.info('Starting Mopidy mixer: %s', mixer_class.__name__) + mixer = mixer_class.start(config=config).proxy() + self.configure_mixer(config, mixer) + return mixer + except exceptions.MixerError as exc: + logger.error( + 'Mixer (%s) initialization error: %s', + mixer_class.__name__, exc.message) + raise + + def configure_mixer(self, config, mixer): + volume = config['audio']['mixer_volume'] + if volume is not None: + mixer.set_volume(volume) + logger.info('Mixer volume set to %d', volume) + else: + logger.debug('Mixer volume left unchanged') + + def start_audio(self, config, mixer): logger.info('Starting Mopidy audio') - return Audio.start(config=config).proxy() + return Audio.start(config=config, mixer=mixer).proxy() def start_backends(self, config, backend_classes, audio): logger.info( @@ -325,26 +345,6 @@ class RootCommand(Command): return backends - def start_mixer(self, config, mixer_class, audio): - try: - logger.info('Starting Mopidy mixer: %s', mixer_class.__name__) - mixer = mixer_class.start(config=config, audio=audio).proxy() - self.configure_mixer(config, mixer) - return mixer - except exceptions.MixerError as exc: - logger.error( - 'Mixer (%s) initialization error: %s', - mixer_class.__name__, exc.message) - raise - - def configure_mixer(self, config, mixer): - volume = config['audio']['mixer_volume'] - if volume is not None: - mixer.set_volume(volume) - logger.info('Mixer volume set to %d', volume) - else: - logger.debug('Mixer volume left unchanged') - def start_core(self, mixer, backends): logger.info('Starting Mopidy core') return Core.start(mixer=mixer, backends=backends).proxy() @@ -372,10 +372,6 @@ class RootCommand(Command): logger.info('Stopping Mopidy core') process.stop_actors_by_class(Core) - def stop_mixer(self, mixer_class): - logger.info('Stopping Mopidy mixer') - process.stop_actors_by_class(mixer_class) - def stop_backends(self, backend_classes): logger.info('Stopping Mopidy backends') for backend_class in backend_classes: @@ -385,6 +381,10 @@ class RootCommand(Command): logger.info('Stopping Mopidy audio') process.stop_actors_by_class(Audio) + def stop_mixer(self, mixer_class): + logger.info('Stopping Mopidy mixer') + process.stop_actors_by_class(mixer_class) + class ConfigCommand(Command): help = 'Show currently active configuration.' diff --git a/mopidy/mixer.py b/mopidy/mixer.py index c000eb41..eaea290d 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -19,8 +19,6 @@ class Mixer(object): :param config: the entire Mopidy configuration :type config: dict - :param audio: actor proxy for the audio subsystem - :type audio: :class:`pykka.ActorProxy` for :class:`mopidy.audio.Audio` """ name = None @@ -122,7 +120,7 @@ class MixerListener(listener.Listener): @staticmethod def send(event, **kwargs): - """Helper to allow calling of audio listener events""" + """Helper to allow calling of mixer listener events""" listener.send_async(MixerListener, event, **kwargs) def volume_changed(self, volume): diff --git a/mopidy/softwaremixer/mixer.py b/mopidy/softwaremixer/mixer.py index 3da90007..c50166c5 100644 --- a/mopidy/softwaremixer/mixer.py +++ b/mopidy/softwaremixer/mixer.py @@ -14,22 +14,33 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): name = 'software' - def __init__(self, config, audio): + def __init__(self, config): super(SoftwareMixer, self).__init__() - self.audio = audio + + self.audio = None logger.info('Mixing using GStreamer software mixing') def get_volume(self): + if self.audio is None: + return None return self.audio.get_volume().get() def set_volume(self, volume): + if self.audio is None: + return False self.audio.set_volume(volume) self.trigger_volume_changed(volume) + return True def get_mute(self): + if self.audio is None: + return None return self.audio.get_mute().get() def set_mute(self, muted): + if self.audio is None: + return False self.audio.set_mute(muted) self.trigger_mute_changed(muted) + return True From 2c4ba8b6a12ecabc266a514ac3ad259e32b541c6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Jul 2014 02:49:33 +0200 Subject: [PATCH 28/40] mixer: Add trigger_events_for_any_changes() Lifted from Mopidy-ALSAMixer, so SoftwareMixer can use it too. --- mopidy/mixer.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/mopidy/mixer.py b/mopidy/mixer.py index eaea290d..6cb5f299 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -106,6 +106,29 @@ class Mixer(object): logger.debug('Mixer event: mute_changed(muted=%s)', muted) MixerListener.send('mute_changed', muted=muted) + def trigger_events_for_any_changes(self): + """ + Checks current volume and mute, compares with old values, and emits + events if anything has changed. + + This method should be called by subclasses when they know something has + changed, and events needs to be sent. + """ + + if not hasattr(self, '_last_volume'): + self._last_volume = None + if not hasattr(self, '_last_muted'): + self._last_muted = None + + old_volume, self._last_volume = self._last_volume, self.get_volume() + old_muted, self._last_muted = self._last_muted, self.get_mute() + + if old_volume != self._last_volume: + self.trigger_volume_changed(self._last_volume) + + if old_muted != self._last_muted: + self.trigger_mute_changed(self._last_muted) + class MixerListener(listener.Listener): """ From 16f65270954e2a12802ef403a9ef7bed094b6a39 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Jul 2014 02:50:40 +0200 Subject: [PATCH 29/40] swmixer: Listen for volume/mute changes in GStreamer E.g. when using pulsesink, external changes to the application-volume and appliction-mute state will now immediately be reflected in the SoftwareMixer. --- mopidy/audio/actor.py | 7 +++++++ mopidy/softwaremixer/mixer.py | 4 +--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 5c8c3c9f..8757832e 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -187,10 +187,17 @@ class Audio(pykka.ThreadingActor): if self._config['audio']['mixer'] != 'software': return self._mixer.audio = self.actor_ref.proxy() + self._connect(self._playbin, 'notify::volume', self._on_mixer_change) + self._connect(self._playbin, 'notify::mute', self._on_mixer_change) + + def _on_mixer_change(self, element, gparamspec): + self._mixer.trigger_events_for_any_changes() def _teardown_mixer(self): if self._config['audio']['mixer'] != 'software': return + self._disconnect(self._playbin, 'notify::volume') + self._disconnect(self._playbin, 'notify::mute') self._mixer.audio = None def _setup_visualizer(self): diff --git a/mopidy/softwaremixer/mixer.py b/mopidy/softwaremixer/mixer.py index c50166c5..52d36713 100644 --- a/mopidy/softwaremixer/mixer.py +++ b/mopidy/softwaremixer/mixer.py @@ -15,7 +15,7 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): name = 'software' def __init__(self, config): - super(SoftwareMixer, self).__init__() + super(SoftwareMixer, self).__init__(config) self.audio = None @@ -30,7 +30,6 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): if self.audio is None: return False self.audio.set_volume(volume) - self.trigger_volume_changed(volume) return True def get_mute(self): @@ -42,5 +41,4 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): if self.audio is None: return False self.audio.set_mute(muted) - self.trigger_mute_changed(muted) return True From 249114b051d17cca8de92ae84a3c3f8ecbfe782c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Jul 2014 03:12:03 +0200 Subject: [PATCH 30/40] audio: Update tests broken by 5f091c10 --- tests/audio/test_actor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index bcb7ca2b..869d8ac6 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -23,7 +23,7 @@ class AudioTest(unittest.TestCase): def setUp(self): config = { 'audio': { - 'mixer': 'software', + 'mixer': 'foomixer', 'mixer_volume': None, 'output': 'fakesink', 'visualizer': None, @@ -33,7 +33,7 @@ class AudioTest(unittest.TestCase): }, } self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) - self.audio = audio.Audio.start(config=config).proxy() + self.audio = audio.Audio.start(config=config, mixer=None).proxy() def tearDown(self): pykka.ActorRegistry.stop_all() @@ -94,7 +94,7 @@ class AudioTest(unittest.TestCase): class AudioStateTest(unittest.TestCase): def setUp(self): - self.audio = audio.Audio(config=None) + self.audio = audio.Audio(config=None, mixer=None) def test_state_starts_as_stopped(self): self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) @@ -139,7 +139,7 @@ class AudioStateTest(unittest.TestCase): class AudioBufferingTest(unittest.TestCase): def setUp(self): - self.audio = audio.Audio(config=None) + self.audio = audio.Audio(config=None, mixer=None) self.audio._playbin = mock.Mock(spec=['set_state']) self.buffer_full_message = mock.Mock() From d15d66f070b613cb0ddee99e2048ff74d2c0f8b8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Jul 2014 03:14:19 +0200 Subject: [PATCH 31/40] Use 'mute' instead of 'muted', to avoid changing old APIs --- mopidy/core/actor.py | 4 ++-- mopidy/core/listener.py | 1 - mopidy/mixer.py | 28 ++++++++++++++-------------- mopidy/softwaremixer/mixer.py | 4 ++-- tests/core/test_events.py | 2 +- tests/test_mixer.py | 2 +- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index 61b5ec4a..8e4408f7 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -88,9 +88,9 @@ class Core( # Forward event from mixer to frontends CoreListener.send('volume_changed', volume=volume) - def mute_changed(self, muted): + def mute_changed(self, mute): # Forward event from mixer to frontends - CoreListener.send('mute_changed', mute=muted) + CoreListener.send('mute_changed', mute=mute) class Backends(list): diff --git a/mopidy/core/listener.py b/mopidy/core/listener.py index 41c697a3..f0bb1ea3 100644 --- a/mopidy/core/listener.py +++ b/mopidy/core/listener.py @@ -150,7 +150,6 @@ class CoreListener(listener.Listener): :param mute: the new mute state :type mute: boolean """ - # TODO Change 'mute' arg to 'muted' next time we break this API pass def seeked(self, time_position): diff --git a/mopidy/mixer.py b/mopidy/mixer.py index 6cb5f299..cd70d78a 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -83,19 +83,19 @@ class Mixer(object): """ return None - def set_mute(self, muted): + def set_mute(self, mute): """ Mute or unmute the mixer. *MAY be implemented by subclass.* - :param muted: :class:`True` to mute, :class:`False` to unmute - :type muted: bool + :param mute: :class:`True` to mute, :class:`False` to unmute + :type mute: bool :rtype: :class:`True` if success, :class:`False` if failure """ return False - def trigger_mute_changed(self, muted): + def trigger_mute_changed(self, mute): """ Send ``mute_changed`` event to all mixer listeners. @@ -103,8 +103,8 @@ class Mixer(object): changed, either because of a call to :meth:`set_mute` or because or any external entity changing the mute state. """ - logger.debug('Mixer event: mute_changed(muted=%s)', muted) - MixerListener.send('mute_changed', muted=muted) + logger.debug('Mixer event: mute_changed(mute=%s)', mute) + MixerListener.send('mute_changed', mute=mute) def trigger_events_for_any_changes(self): """ @@ -117,17 +117,17 @@ class Mixer(object): if not hasattr(self, '_last_volume'): self._last_volume = None - if not hasattr(self, '_last_muted'): - self._last_muted = None + if not hasattr(self, '_last_mute'): + self._last_mute = None old_volume, self._last_volume = self._last_volume, self.get_volume() - old_muted, self._last_muted = self._last_muted, self.get_mute() + old_mute, self._last_mute = self._last_mute, self.get_mute() if old_volume != self._last_volume: self.trigger_volume_changed(self._last_volume) - if old_muted != self._last_muted: - self.trigger_mute_changed(self._last_muted) + if old_mute != self._last_mute: + self.trigger_mute_changed(self._last_mute) class MixerListener(listener.Listener): @@ -157,13 +157,13 @@ class MixerListener(listener.Listener): """ pass - def mute_changed(self, muted): + def mute_changed(self, mute): """ Called after the mute state has changed. *MAY* be implemented by actor. - :param muted: :class:`True` if muted, :class:`False` if not muted - :type muted: bool + :param mute: :class:`True` if muted, :class:`False` if not muted + :type mute: bool """ pass diff --git a/mopidy/softwaremixer/mixer.py b/mopidy/softwaremixer/mixer.py index 52d36713..cf42fc26 100644 --- a/mopidy/softwaremixer/mixer.py +++ b/mopidy/softwaremixer/mixer.py @@ -37,8 +37,8 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): return None return self.audio.get_mute().get() - def set_mute(self, muted): + def set_mute(self, mute): if self.audio is None: return False - self.audio.set_mute(muted) + self.audio.set_mute(mute) return True diff --git a/tests/core/test_events.py b/tests/core/test_events.py index 6ea6704a..ab7906a8 100644 --- a/tests/core/test_events.py +++ b/tests/core/test_events.py @@ -32,7 +32,7 @@ class BackendEventsTest(unittest.TestCase): self.assertEqual(send.call_args[1]['volume'], 60) def test_forwards_mixer_mute_changed_event_to_frontends(self, send): - self.core.mute_changed(muted=True).get() + self.core.mute_changed(mute=True).get() self.assertEqual(send.call_args[0][0], 'mute_changed') self.assertEqual(send.call_args[1]['mute'], True) diff --git a/tests/test_mixer.py b/tests/test_mixer.py index a214eba7..53c10292 100644 --- a/tests/test_mixer.py +++ b/tests/test_mixer.py @@ -23,4 +23,4 @@ class MixerListenerTest(unittest.TestCase): self.listener.volume_changed(volume=60) def test_listener_has_default_impl_for_mute_changed(self): - self.listener.mute_changed(muted=True) + self.listener.mute_changed(mute=True) From 0e826d0086abc9b7b672bd0d708e9e5b203cee8d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Jul 2014 22:56:00 +0200 Subject: [PATCH 32/40] mixer: Use mangled-private variables to avoid name collisions with subclasses --- mopidy/mixer.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mopidy/mixer.py b/mopidy/mixer.py index cd70d78a..34e006a4 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -115,19 +115,19 @@ class Mixer(object): changed, and events needs to be sent. """ - if not hasattr(self, '_last_volume'): - self._last_volume = None - if not hasattr(self, '_last_mute'): - self._last_mute = None + if not hasattr(self, '__last_volume'): + self.__last_volume = None + if not hasattr(self, '__last_mute'): + self.__last_mute = None - old_volume, self._last_volume = self._last_volume, self.get_volume() - old_mute, self._last_mute = self._last_mute, self.get_mute() + old_volume, self.__last_volume = self.__last_volume, self.get_volume() + old_mute, self.__last_mute = self.__last_mute, self.get_mute() - if old_volume != self._last_volume: - self.trigger_volume_changed(self._last_volume) + if old_volume != self.__last_volume: + self.trigger_volume_changed(self.__last_volume) - if old_mute != self._last_mute: - self.trigger_mute_changed(self._last_mute) + if old_mute != self.__last_mute: + self.trigger_mute_changed(self.__last_mute) class MixerListener(listener.Listener): From 8602ea24f1b38a322511597e48116b37771c20e4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Jul 2014 23:09:57 +0200 Subject: [PATCH 33/40] docs: Volume use a linear scale --- mopidy/core/playback.py | 4 +++- mopidy/mixer.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index bf645227..097a9401 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -102,7 +102,9 @@ class PlaybackController(object): self._volume = volume volume = property(get_volume, set_volume) - """Volume as int in range [0..100] or :class:`None`""" + """Volume as int in range [0..100] or :class:`None` if unknown. The volume + scale is linear. + """ def get_mute(self): if self.mixer: diff --git a/mopidy/mixer.py b/mopidy/mixer.py index 34e006a4..3c8c7639 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -32,7 +32,7 @@ class Mixer(object): def get_volume(self): """ - Get volume level of the mixer. + Get volume level of the mixer on a linear scale from 0 to 100. Example values: From dd555ed828b857393a57a5ffd5a7400b6385431b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Jul 2014 23:10:52 +0200 Subject: [PATCH 34/40] docs: Fix typo --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9fd629bf..e2c705e1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,7 +25,7 @@ v0.20.0 (UNRELEASED) end result should be the same without any changes to this config value. - Deprecated the :confval:`audio/mixer_track` config value. This config value - is no longer in use. Mixer extensions that needs additional configuration + is no longer in use. Mixer extensions that need additional configuration handle this themselves. **Mixers** From d1c0d48be67f991e8d581d1faecb26eb50d36a4d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Jul 2014 23:18:55 +0200 Subject: [PATCH 35/40] docs: Fix typo --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 8757832e..e48e3d4b 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -481,7 +481,7 @@ class Audio(pykka.ThreadingActor): """ Mute or unmute of the software mixer. - :param mute: Wether to mute the mixer or not. + :param mute: Whether to mute the mixer or not. :type mute: bool :rtype: :class:`True` if successful, else :class:`False` """ From a981be22920b4802b5e5199c5b29dcd6c6ed0ba4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Jul 2014 23:47:15 +0200 Subject: [PATCH 36/40] doc: Fix typos --- mopidy/audio/actor.py | 2 +- mopidy/mixer.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index e48e3d4b..b88eafd8 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -451,7 +451,7 @@ class Audio(pykka.ThreadingActor): 0: Minimum volume. 100: - Max volume. + Maximum volume. :rtype: int in range [0..100] """ diff --git a/mopidy/mixer.py b/mopidy/mixer.py index 3c8c7639..7e9fc9ee 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -39,7 +39,7 @@ class Mixer(object): 0: Minimum volume, usually silent. 100: - Max volume. + Maximum volume. :class:`None`: Volume is unknown. @@ -66,7 +66,7 @@ class Mixer(object): Send ``volume_changed`` event to all mixer listeners. This method should be called by subclasses when the volume is changed, - either because of a call to :meth:`set_volume` or because or any + either because of a call to :meth:`set_volume` or because of any external entity changing the volume. """ logger.debug('Mixer event: volume_changed(volume=%d)', volume) @@ -100,7 +100,7 @@ class Mixer(object): Send ``mute_changed`` event to all mixer listeners. This method should be called by subclasses when the mute state is - changed, either because of a call to :meth:`set_mute` or because or + changed, either because of a call to :meth:`set_mute` or because of any external entity changing the mute state. """ logger.debug('Mixer event: mute_changed(mute=%s)', mute) From 9ca5b4af39473a1ba3fac3ddeb71b1a4d85f6723 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 15 Jul 2014 00:07:11 +0200 Subject: [PATCH 37/40] docs: Remove reference to mopidy.audio.mixers --- docs/extensiondev.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 55731a7d..27bf5e03 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -431,9 +431,6 @@ Basically, you just implement your GStreamer element in Python and then make your :meth:`~mopidy.ext.Extension.setup` method register all your custom GStreamer elements. -For examples of custom GStreamer elements implemented in Python, see -:mod:`mopidy.audio.mixers`. - Python conventions ================== From c234281c7b7ba684f32c5ddb7b8aa28d53e568cd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 15 Jul 2014 10:34:32 +0200 Subject: [PATCH 38/40] mixer: Remove trigger_events_for_any_change() helper from API --- mopidy/audio/actor.py | 2 +- mopidy/mixer.py | 23 ----------------------- mopidy/softwaremixer/mixer.py | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index b88eafd8..ae6d280d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -191,7 +191,7 @@ class Audio(pykka.ThreadingActor): self._connect(self._playbin, 'notify::mute', self._on_mixer_change) def _on_mixer_change(self, element, gparamspec): - self._mixer.trigger_events_for_any_changes() + self._mixer.trigger_events_for_changed_values() def _teardown_mixer(self): if self._config['audio']['mixer'] != 'software': diff --git a/mopidy/mixer.py b/mopidy/mixer.py index 7e9fc9ee..76c6e92a 100644 --- a/mopidy/mixer.py +++ b/mopidy/mixer.py @@ -106,29 +106,6 @@ class Mixer(object): logger.debug('Mixer event: mute_changed(mute=%s)', mute) MixerListener.send('mute_changed', mute=mute) - def trigger_events_for_any_changes(self): - """ - Checks current volume and mute, compares with old values, and emits - events if anything has changed. - - This method should be called by subclasses when they know something has - changed, and events needs to be sent. - """ - - if not hasattr(self, '__last_volume'): - self.__last_volume = None - if not hasattr(self, '__last_mute'): - self.__last_mute = None - - old_volume, self.__last_volume = self.__last_volume, self.get_volume() - old_mute, self.__last_mute = self.__last_mute, self.get_mute() - - if old_volume != self.__last_volume: - self.trigger_volume_changed(self.__last_volume) - - if old_mute != self.__last_mute: - self.trigger_mute_changed(self.__last_mute) - class MixerListener(listener.Listener): """ diff --git a/mopidy/softwaremixer/mixer.py b/mopidy/softwaremixer/mixer.py index cf42fc26..abb5592c 100644 --- a/mopidy/softwaremixer/mixer.py +++ b/mopidy/softwaremixer/mixer.py @@ -42,3 +42,18 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): return False self.audio.set_mute(mute) return True + + def trigger_events_for_changed_values(self): + if not hasattr(self, '_last_volume'): + self._last_volume = None + if not hasattr(self, '_last_mute'): + self._last_mute = None + + old_volume, self._last_volume = self._last_volume, self.get_volume() + old_mute, self._last_mute = self._last_mute, self.get_mute() + + if old_volume != self._last_volume: + self.trigger_volume_changed(self._last_volume) + + if old_mute != self._last_mute: + self.trigger_mute_changed(self._last_mute) From e36228a1cc84bf246439b29902c88b5dff1bd613 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 16 Jul 2014 10:25:29 +0200 Subject: [PATCH 39/40] docs: Add page for Mopidy-SoftwareMixer --- docs/changelog.rst | 6 +++--- docs/ext/external.rst | 1 + docs/ext/softwaremixer.rst | 35 +++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 docs/ext/softwaremixer.rst diff --git a/docs/changelog.rst b/docs/changelog.rst index e2c705e1..3509b93f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -33,9 +33,9 @@ v0.20.0 (UNRELEASED) - Added new :class:`mopidy.mixer.Mixer` API which can be implemented by extensions. -- Created a bundled extension, :mod:`mopidy.softwaremixer`, for controlling - volume in software in GStreamer's pipeline. This is Mopidy's default mixer. - To use this mixer, set :confval:`audio/mixer` to ``software``. +- Created a bundled extension, :ref:`ext-softwaremixer`, for controlling volume + in software in GStreamer's pipeline. This is Mopidy's default mixer. To use + this mixer, set :confval:`audio/mixer` to ``software``. - Created an external extension, `Mopidy-ALSAMixer `_, for controlling volume with diff --git a/docs/ext/external.rst b/docs/ext/external.rst index 82b5a9d2..897d25cf 100644 --- a/docs/ext/external.rst +++ b/docs/ext/external.rst @@ -13,6 +13,7 @@ Mopidy also bundles some extensions: - :ref:`ext-stream` - :ref:`ext-http` - :ref:`ext-mpd` +- :ref:`ext-softwaremixer` Mopidy-API-Explorer diff --git a/docs/ext/softwaremixer.rst b/docs/ext/softwaremixer.rst new file mode 100644 index 00000000..22badde8 --- /dev/null +++ b/docs/ext/softwaremixer.rst @@ -0,0 +1,35 @@ +.. _ext-softwaremixer: + +******************** +Mopidy-SoftwareMixer +******************** + +Mopidy-SoftwareMixer is an extension for controlling audio volume in software +through GStreamer. It is the only mixer bundled with Mopidy and is enabled by +default. + +If you use PulseAudio, the software mixer will control the per-application +volume for Mopidy in PulseAudio, and any changes to the per-application volume +done from outside Mopidy will be reflected by the software mixer. + +If you don't use PulseAudio, the mixer will adjust the volume internally in +Mopidy's GStreamer pipeline. + + +Configuration +============= + +Multiple mixers can be installed and enabled at the same time, but only the +mixer pointed to by the :confval:`audio/mixer` config value will actually be +used. + +See :ref:`config` for general help on configuring Mopidy. + +.. literalinclude:: ../../mopidy/stream/ext.conf + :language: ini + +.. confval:: softwaremixer/enabled + + If the software mixer should be enabled or not. Usually you don't want to + change this, but instead change the :confval:`audio/mixer` config value to + decide which mixer is actually used. diff --git a/docs/index.rst b/docs/index.rst index b117abc0..3d453741 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,6 +51,7 @@ Extensions ext/stream ext/http ext/mpd + ext/softwaremixer ext/external From b5fd6a6e9f280c1c07871c34de89986fec54db51 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 16 Jul 2014 22:11:17 +0200 Subject: [PATCH 40/40] swmixer: Move attr init to init method --- mopidy/softwaremixer/mixer.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mopidy/softwaremixer/mixer.py b/mopidy/softwaremixer/mixer.py index abb5592c..0ebbfeb7 100644 --- a/mopidy/softwaremixer/mixer.py +++ b/mopidy/softwaremixer/mixer.py @@ -18,6 +18,8 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): super(SoftwareMixer, self).__init__(config) self.audio = None + self._last_volume = None + self._last_mute = None logger.info('Mixing using GStreamer software mixing') @@ -44,11 +46,6 @@ class SoftwareMixer(pykka.ThreadingActor, mixer.Mixer): return True def trigger_events_for_changed_values(self): - if not hasattr(self, '_last_volume'): - self._last_volume = None - if not hasattr(self, '_last_mute'): - self._last_mute = None - old_volume, self._last_volume = self._last_volume, self.get_volume() old_mute, self._last_mute = self._last_mute, self.get_mute()