From 523216d0fd9a04c19f396e09d4b77ec20df9c932 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Mar 2010 23:48:50 +0100 Subject: [PATCH] Add mixer API and rewrite ALSA mixer hack --- docs/api/mixers.rst | 8 ++++++ docs/installation.rst | 5 ++++ mopidy/__main__.py | 8 +++--- mopidy/backends/__init__.py | 15 +++++++----- mopidy/backends/despotify.py | 3 ++- mopidy/backends/dummy.py | 14 +++-------- mopidy/backends/libspotify.py | 3 ++- mopidy/mixers/__init__.py | 35 ++++++++++++++++++++++++++ mopidy/mixers/alsa.py | 13 ++++++++++ mopidy/mixers/dummy.py | 11 +++++++++ mopidy/settings/default.py | 6 ++++- tests/__main__.py | 1 + tests/mixers/dummytest.py | 26 ++++++++++++++++++++ tests/mpd/handlertest.py | 46 ++++++++++++++++++++++++----------- 14 files changed, 156 insertions(+), 38 deletions(-) create mode 100644 docs/api/mixers.rst create mode 100644 mopidy/mixers/__init__.py create mode 100644 mopidy/mixers/alsa.py create mode 100644 mopidy/mixers/dummy.py create mode 100644 tests/mixers/dummytest.py diff --git a/docs/api/mixers.rst b/docs/api/mixers.rst new file mode 100644 index 00000000..0bf94237 --- /dev/null +++ b/docs/api/mixers.rst @@ -0,0 +1,8 @@ +******************************** +:mod:`mopidy.mixer` -- Mixer API +******************************** + +.. automodule:: mopidy.mixers + :synopsis: Sound mixer interface. + :members: + :undoc-members: diff --git a/docs/installation.rst b/docs/installation.rst index dc495582..f0aca702 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -160,6 +160,11 @@ libspotify backend, copy the Spotify application key to BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',) +*OS X:* The default mixer does not work on OS X, so you must change to a dummy +mixer:: + + MIXER = u'mopidy.mixers.dummy.DummyMixer' + For a full list of available settings, see :mod:`mopidy.settings.default`. diff --git a/mopidy/__main__.py b/mopidy/__main__.py index edc5c808..2fb51f4e 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -14,7 +14,8 @@ logger = logging.getLogger('mopidy') def main(): _setup_logging(2) - backend = _get_backend(settings.BACKENDS[0]) + mixer = _get_class(settings.MIXER)() + backend = _get_class(settings.BACKENDS[0])(mixer=mixer) MpdServer(backend=backend) asyncore.loop() @@ -30,14 +31,13 @@ def _setup_logging(verbosity_level): level=level, ) -def _get_backend(name): +def _get_class(name): module_name = name[:name.rindex('.')] class_name = name[name.rindex('.') + 1:] logger.info('Loading: %s from %s', class_name, module_name) module = __import__(module_name, globals(), locals(), [class_name], -1) class_object = getattr(module, class_name) - instance = class_object() - return instance + return class_object if __name__ == '__main__': try: diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index 55da11c2..93b74498 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -3,13 +3,14 @@ import logging import random import time -import alsaaudio - from mopidy.models import Playlist logger = logging.getLogger('backends.base') class BaseBackend(object): + def __init__(self, mixer=None): + self.mixer = mixer + #: The current playlist controller. An instance of #: :class:`BaseCurrentPlaylistController`. current_playlist = None @@ -17,6 +18,9 @@ class BaseBackend(object): #: The library controller. An instance of :class:`BaseLibraryController`. library = None + #: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`. + mixer = None + #: The playback controller. An instance of :class:`BasePlaybackController`. playback = None @@ -255,10 +259,9 @@ class BasePlaybackController(object): #: Playback continues after current song. single = False - def __init__(self, backend, mixer=alsaaudio.Mixer): + def __init__(self, backend): self.backend = backend self._state = self.STOPPED - self._mixer = mixer() @property def next_track(self): @@ -370,11 +373,11 @@ class BasePlaybackController(object): :class:`None` if unknown. """ - return self._mixer.getvolume()[0] + return self.backend.mixer.volume @volume.setter def volume(self, volume): - self._mixer.setvolume(volume) + self.backend.mixer.volume = volume def new_playlist_loaded_callback(self): """Tell the playback controller that a new playlist has been loaded.""" diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index e8a78752..349f3a9a 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -14,7 +14,8 @@ logger = logging.getLogger(u'backends.despotify') ENCODING = 'utf-8' class DespotifyBackend(BaseBackend): - def __init__(self): + def __init__(self, *args, **kwargs): + super(DespotifyBackend, self).__init__(*args, **kwargs) self.current_playlist = DespotifyCurrentPlaylistController(backend=self) self.library = DespotifyLibraryController(backend=self) self.playback = DespotifyPlaybackController(backend=self) diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py index c2e828f9..96a87e7d 100644 --- a/mopidy/backends/dummy.py +++ b/mopidy/backends/dummy.py @@ -4,10 +4,11 @@ from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController, from mopidy.models import Playlist class DummyBackend(BaseBackend): - def __init__(self): + def __init__(self, *args, **kwargs): + super(DummyBackend, self).__init__(*args, **kwargs) self.current_playlist = DummyCurrentPlaylistController(backend=self) self.library = DummyLibraryController(backend=self) - self.playback = DummyPlaybackController(backend=self, mixer=DummyMixer) + self.playback = DummyPlaybackController(backend=self) self.stored_playlists = DummyStoredPlaylistsController(backend=self) self.uri_handlers = [u'dummy:'] @@ -46,12 +47,3 @@ class DummyPlaybackController(BasePlaybackController): class DummyStoredPlaylistsController(BaseStoredPlaylistsController): def search(self, query): return [Playlist(name=query)] - -class DummyMixer(object): - volume = 0 - - def getvolume(self): - return [self.volume, self.volume] - - def setvolume(self, volume): - self.volume = volume diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 8a2218b8..d2fabaf1 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -17,7 +17,8 @@ logger = logging.getLogger(u'backends.libspotify') ENCODING = 'utf-8' class LibspotifyBackend(BaseBackend): - def __init__(self): + def __init__(self, *args, **kwargs): + super(LibspotifyBackend, self).__init__(*args, **kwargs) self.current_playlist = LibspotifyCurrentPlaylistController( backend=self) self.library = LibspotifyLibraryController(backend=self) diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py new file mode 100644 index 00000000..786a32d0 --- /dev/null +++ b/mopidy/mixers/__init__.py @@ -0,0 +1,35 @@ +class BaseMixer(object): + @property + def volume(self): + """ + The audio volume + + Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is + equal to 0. Values above 100 is equal to 100. + """ + return self._get_volume() + + @volume.setter + def volume(self, volume): + volume = int(volume) + if volume < 0: + volume = 0 + elif volume > 100: + volume = 100 + self._set_volume(volume) + + def _get_volume(self): + """ + Return volume as integer in range [0, 100]. :class:`None` if unknown. + + *Must be implemented by subclass.* + """ + raise NotImplementedError + + def _set_volume(self, volume): + """ + Set volume as integer in range [0, 100]. + + *Must be implemented by subclass.* + """ + raise NotImplementedError diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py new file mode 100644 index 00000000..54c10a09 --- /dev/null +++ b/mopidy/mixers/alsa.py @@ -0,0 +1,13 @@ +import alsaaudio + +from mopidy.mixers import BaseMixer + +class AlsaMixer(BaseMixer): + def __init__(self): + self._mixer = alsaaudio.Mixer() + + def _get_volume(self): + return self._mixer.getvolume()[0] + + def _set_volume(self, volume): + self._mixer.setvolume(volume) diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py new file mode 100644 index 00000000..a08e8398 --- /dev/null +++ b/mopidy/mixers/dummy.py @@ -0,0 +1,11 @@ +from mopidy.mixers import BaseMixer + +class DummyMixer(BaseMixer): + def __init__(self): + self._volume = None + + def _get_volume(self): + return self._volume + + def _set_volume(self, volume): + self._volume = volume diff --git a/mopidy/settings/default.py b/mopidy/settings/default.py index c136bc2e..44f63dea 100644 --- a/mopidy/settings/default.py +++ b/mopidy/settings/default.py @@ -13,7 +13,6 @@ Available settings and their default values. #: #: .. note:: #: Currently only the first backend in the list is used. -#: BACKENDS = ( u'mopidy.backends.despotify.DespotifyBackend', #u'mopidy.backends.libspotify.LibspotifyBackend', @@ -24,6 +23,11 @@ BACKENDS = ( #: the format. CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s [%(threadName)s] %(name)s\n %(message)s' +#: Sound mixer to use. Default:: +#: +#: MIXER = u'mopidy.mixers.alsa.AlsaMixer' +MIXER = u'mopidy.mixers.alsa.AlsaMixer' + #: Which address Mopidy should bind to. Examples: #: #: ``localhost`` diff --git a/tests/__main__.py b/tests/__main__.py index e203582a..11677b0e 100644 --- a/tests/__main__.py +++ b/tests/__main__.py @@ -9,6 +9,7 @@ def main(): sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) r = CoverageTestRunner() + r.add_pair('mopidy/mixers/dummy.py', 'tests/mixers/dummytest.py') r.add_pair('mopidy/models.py', 'tests/modelstest.py') r.add_pair('mopidy/mpd/handler.py', 'tests/mpd/handlertest.py') r.run() diff --git a/tests/mixers/dummytest.py b/tests/mixers/dummytest.py new file mode 100644 index 00000000..00d748fe --- /dev/null +++ b/tests/mixers/dummytest.py @@ -0,0 +1,26 @@ +import unittest + +from mopidy.mixers.dummy import DummyMixer + +class BaseMixerTest(unittest.TestCase): + def setUp(self): + self.m = DummyMixer() + + def test_volume_is_None_initially(self): + self.assertEqual(self.m.volume, None) + + def test_volume_set_to_min(self): + self.m.volume = 0 + self.assertEqual(self.m.volume, 0) + + def test_volume_set_to_max(self): + self.m.volume = 100 + self.assertEqual(self.m.volume, 100) + + def test_volume_set_to_below_min_results_in_min(self): + self.m.volume = -10 + self.assertEqual(self.m.volume, 0) + + def test_volume_set_to_above_max_results_in_max(self): + self.m.volume = 110 + self.assertEqual(self.m.volume, 100) diff --git a/tests/mpd/handlertest.py b/tests/mpd/handlertest.py index fa3a2143..2274851a 100644 --- a/tests/mpd/handlertest.py +++ b/tests/mpd/handlertest.py @@ -3,6 +3,7 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.exceptions import MpdAckError +from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track, Playlist from mopidy.mpd import handler @@ -19,7 +20,9 @@ class DummySession(object): class RequestHandlerTest(unittest.TestCase): def setUp(self): - self.h = handler.MpdHandler(backend=DummyBackend()) + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) + self.h = handler.MpdHandler(backend=self.b) def test_register_same_pattern_twice_fails(self): func = lambda: None @@ -46,7 +49,9 @@ class RequestHandlerTest(unittest.TestCase): class CommandListsTest(unittest.TestCase): def setUp(self): - self.h = handler.MpdHandler(backend=DummyBackend()) + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) + self.h = handler.MpdHandler(backend=self.b) def test_command_list_begin(self): result = self.h.handle_request(u'command_list_begin') @@ -92,7 +97,8 @@ class CommandListsTest(unittest.TestCase): class StatusHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend() + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) self.s = DummySession() self.h = handler.MpdHandler(backend=self.b, session=self.s) @@ -158,7 +164,6 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_status_method_contains_volume_which_defaults_to_0(self): - self.b.playback.volume = None result = dict(self.h._status_status()) self.assert_('volume' in result) self.assertEquals(int(result['volume']), 0) @@ -302,7 +307,8 @@ class StatusHandlerTest(unittest.TestCase): class PlaybackOptionsHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend() + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) self.h = handler.MpdHandler(backend=self.b) def test_consume_off(self): @@ -421,7 +427,8 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): class PlaybackControlHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend() + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) self.h = handler.MpdHandler(backend=self.b) def test_next(self): @@ -499,7 +506,8 @@ class PlaybackControlHandlerTest(unittest.TestCase): class CurrentPlaylistHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend() + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) self.h = handler.MpdHandler(backend=self.b) def test_add(self): @@ -791,7 +799,8 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): class StoredPlaylistsHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend() + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) self.h = handler.MpdHandler(backend=self.b) def test_listplaylist(self): @@ -863,7 +872,8 @@ class StoredPlaylistsHandlerTest(unittest.TestCase): class MusicDatabaseHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend() + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) self.h = handler.MpdHandler(backend=self.b) def test_count(self): @@ -1018,7 +1028,9 @@ class MusicDatabaseHandlerTest(unittest.TestCase): class StickersHandlerTest(unittest.TestCase): def setUp(self): - self.h = handler.MpdHandler(backend=DummyBackend()) + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) + self.h = handler.MpdHandler(backend=self.b) def test_sticker_get(self): result = self.h.handle_request( @@ -1053,8 +1065,10 @@ class StickersHandlerTest(unittest.TestCase): class ConnectionHandlerTest(unittest.TestCase): def setUp(self): - self.h = handler.MpdHandler(session=DummySession(), - backend=DummyBackend()) + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) + self.s = DummySession() + self.h = handler.MpdHandler(backend=self.b, session=self.s) def test_close(self): result = self.h.handle_request(u'close') @@ -1079,7 +1093,9 @@ class ConnectionHandlerTest(unittest.TestCase): class AudioOutputHandlerTest(unittest.TestCase): def setUp(self): - self.h = handler.MpdHandler(backend=DummyBackend()) + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) + self.h = handler.MpdHandler(backend=self.b) def test_enableoutput(self): result = self.h.handle_request(u'enableoutput "0"') @@ -1099,7 +1115,9 @@ class AudioOutputHandlerTest(unittest.TestCase): class ReflectionHandlerTest(unittest.TestCase): def setUp(self): - self.h = handler.MpdHandler(backend=DummyBackend()) + self.m = DummyMixer() + self.b = DummyBackend(mixer=self.m) + self.h = handler.MpdHandler(backend=self.b) def test_commands(self): result = self.h.handle_request(u'commands')