Add mixer API and rewrite ALSA mixer hack

This commit is contained in:
Stein Magnus Jodal 2010-03-07 23:48:50 +01:00
parent 0af7c52544
commit 523216d0fd
14 changed files with 156 additions and 38 deletions

8
docs/api/mixers.rst Normal file
View File

@ -0,0 +1,8 @@
********************************
:mod:`mopidy.mixer` -- Mixer API
********************************
.. automodule:: mopidy.mixers
:synopsis: Sound mixer interface.
:members:
:undoc-members:

View File

@ -160,6 +160,11 @@ libspotify backend, copy the Spotify application key to
BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',) 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`. For a full list of available settings, see :mod:`mopidy.settings.default`.

View File

@ -14,7 +14,8 @@ logger = logging.getLogger('mopidy')
def main(): def main():
_setup_logging(2) _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) MpdServer(backend=backend)
asyncore.loop() asyncore.loop()
@ -30,14 +31,13 @@ def _setup_logging(verbosity_level):
level=level, level=level,
) )
def _get_backend(name): def _get_class(name):
module_name = name[:name.rindex('.')] module_name = name[:name.rindex('.')]
class_name = name[name.rindex('.') + 1:] class_name = name[name.rindex('.') + 1:]
logger.info('Loading: %s from %s', class_name, module_name) logger.info('Loading: %s from %s', class_name, module_name)
module = __import__(module_name, globals(), locals(), [class_name], -1) module = __import__(module_name, globals(), locals(), [class_name], -1)
class_object = getattr(module, class_name) class_object = getattr(module, class_name)
instance = class_object() return class_object
return instance
if __name__ == '__main__': if __name__ == '__main__':
try: try:

View File

@ -3,13 +3,14 @@ import logging
import random import random
import time import time
import alsaaudio
from mopidy.models import Playlist from mopidy.models import Playlist
logger = logging.getLogger('backends.base') logger = logging.getLogger('backends.base')
class BaseBackend(object): class BaseBackend(object):
def __init__(self, mixer=None):
self.mixer = mixer
#: The current playlist controller. An instance of #: The current playlist controller. An instance of
#: :class:`BaseCurrentPlaylistController`. #: :class:`BaseCurrentPlaylistController`.
current_playlist = None current_playlist = None
@ -17,6 +18,9 @@ class BaseBackend(object):
#: The library controller. An instance of :class:`BaseLibraryController`. #: The library controller. An instance of :class:`BaseLibraryController`.
library = None library = None
#: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`.
mixer = None
#: The playback controller. An instance of :class:`BasePlaybackController`. #: The playback controller. An instance of :class:`BasePlaybackController`.
playback = None playback = None
@ -255,10 +259,9 @@ class BasePlaybackController(object):
#: Playback continues after current song. #: Playback continues after current song.
single = False single = False
def __init__(self, backend, mixer=alsaaudio.Mixer): def __init__(self, backend):
self.backend = backend self.backend = backend
self._state = self.STOPPED self._state = self.STOPPED
self._mixer = mixer()
@property @property
def next_track(self): def next_track(self):
@ -370,11 +373,11 @@ class BasePlaybackController(object):
:class:`None` if unknown. :class:`None` if unknown.
""" """
return self._mixer.getvolume()[0] return self.backend.mixer.volume
@volume.setter @volume.setter
def volume(self, volume): def volume(self, volume):
self._mixer.setvolume(volume) self.backend.mixer.volume = volume
def new_playlist_loaded_callback(self): def new_playlist_loaded_callback(self):
"""Tell the playback controller that a new playlist has been loaded.""" """Tell the playback controller that a new playlist has been loaded."""

View File

@ -14,7 +14,8 @@ logger = logging.getLogger(u'backends.despotify')
ENCODING = 'utf-8' ENCODING = 'utf-8'
class DespotifyBackend(BaseBackend): class DespotifyBackend(BaseBackend):
def __init__(self): def __init__(self, *args, **kwargs):
super(DespotifyBackend, self).__init__(*args, **kwargs)
self.current_playlist = DespotifyCurrentPlaylistController(backend=self) self.current_playlist = DespotifyCurrentPlaylistController(backend=self)
self.library = DespotifyLibraryController(backend=self) self.library = DespotifyLibraryController(backend=self)
self.playback = DespotifyPlaybackController(backend=self) self.playback = DespotifyPlaybackController(backend=self)

View File

@ -4,10 +4,11 @@ from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController,
from mopidy.models import Playlist from mopidy.models import Playlist
class DummyBackend(BaseBackend): class DummyBackend(BaseBackend):
def __init__(self): def __init__(self, *args, **kwargs):
super(DummyBackend, self).__init__(*args, **kwargs)
self.current_playlist = DummyCurrentPlaylistController(backend=self) self.current_playlist = DummyCurrentPlaylistController(backend=self)
self.library = DummyLibraryController(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.stored_playlists = DummyStoredPlaylistsController(backend=self)
self.uri_handlers = [u'dummy:'] self.uri_handlers = [u'dummy:']
@ -46,12 +47,3 @@ class DummyPlaybackController(BasePlaybackController):
class DummyStoredPlaylistsController(BaseStoredPlaylistsController): class DummyStoredPlaylistsController(BaseStoredPlaylistsController):
def search(self, query): def search(self, query):
return [Playlist(name=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

View File

@ -17,7 +17,8 @@ logger = logging.getLogger(u'backends.libspotify')
ENCODING = 'utf-8' ENCODING = 'utf-8'
class LibspotifyBackend(BaseBackend): class LibspotifyBackend(BaseBackend):
def __init__(self): def __init__(self, *args, **kwargs):
super(LibspotifyBackend, self).__init__(*args, **kwargs)
self.current_playlist = LibspotifyCurrentPlaylistController( self.current_playlist = LibspotifyCurrentPlaylistController(
backend=self) backend=self)
self.library = LibspotifyLibraryController(backend=self) self.library = LibspotifyLibraryController(backend=self)

35
mopidy/mixers/__init__.py Normal file
View File

@ -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

13
mopidy/mixers/alsa.py Normal file
View File

@ -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)

11
mopidy/mixers/dummy.py Normal file
View File

@ -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

View File

@ -13,7 +13,6 @@ Available settings and their default values.
#: #:
#: .. note:: #: .. note::
#: Currently only the first backend in the list is used. #: Currently only the first backend in the list is used.
#:
BACKENDS = ( BACKENDS = (
u'mopidy.backends.despotify.DespotifyBackend', u'mopidy.backends.despotify.DespotifyBackend',
#u'mopidy.backends.libspotify.LibspotifyBackend', #u'mopidy.backends.libspotify.LibspotifyBackend',
@ -24,6 +23,11 @@ BACKENDS = (
#: the format. #: the format.
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s [%(threadName)s] %(name)s\n %(message)s' 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: #: Which address Mopidy should bind to. Examples:
#: #:
#: ``localhost`` #: ``localhost``

View File

@ -9,6 +9,7 @@ def main():
sys.path.insert(0, sys.path.insert(0,
os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
r = CoverageTestRunner() 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/models.py', 'tests/modelstest.py')
r.add_pair('mopidy/mpd/handler.py', 'tests/mpd/handlertest.py') r.add_pair('mopidy/mpd/handler.py', 'tests/mpd/handlertest.py')
r.run() r.run()

26
tests/mixers/dummytest.py Normal file
View File

@ -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)

View File

@ -3,6 +3,7 @@ import unittest
from mopidy.backends.dummy import DummyBackend from mopidy.backends.dummy import DummyBackend
from mopidy.exceptions import MpdAckError from mopidy.exceptions import MpdAckError
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track, Playlist from mopidy.models import Track, Playlist
from mopidy.mpd import handler from mopidy.mpd import handler
@ -19,7 +20,9 @@ class DummySession(object):
class RequestHandlerTest(unittest.TestCase): class RequestHandlerTest(unittest.TestCase):
def setUp(self): 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): def test_register_same_pattern_twice_fails(self):
func = lambda: None func = lambda: None
@ -46,7 +49,9 @@ class RequestHandlerTest(unittest.TestCase):
class CommandListsTest(unittest.TestCase): class CommandListsTest(unittest.TestCase):
def setUp(self): 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): def test_command_list_begin(self):
result = self.h.handle_request(u'command_list_begin') result = self.h.handle_request(u'command_list_begin')
@ -92,7 +97,8 @@ class CommandListsTest(unittest.TestCase):
class StatusHandlerTest(unittest.TestCase): class StatusHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend() self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.s = DummySession() self.s = DummySession()
self.h = handler.MpdHandler(backend=self.b, session=self.s) self.h = handler.MpdHandler(backend=self.b, session=self.s)
@ -158,7 +164,6 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result) self.assert_(u'OK' in result)
def test_status_method_contains_volume_which_defaults_to_0(self): def test_status_method_contains_volume_which_defaults_to_0(self):
self.b.playback.volume = None
result = dict(self.h._status_status()) result = dict(self.h._status_status())
self.assert_('volume' in result) self.assert_('volume' in result)
self.assertEquals(int(result['volume']), 0) self.assertEquals(int(result['volume']), 0)
@ -302,7 +307,8 @@ class StatusHandlerTest(unittest.TestCase):
class PlaybackOptionsHandlerTest(unittest.TestCase): class PlaybackOptionsHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend() self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.h = handler.MpdHandler(backend=self.b) self.h = handler.MpdHandler(backend=self.b)
def test_consume_off(self): def test_consume_off(self):
@ -421,7 +427,8 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
class PlaybackControlHandlerTest(unittest.TestCase): class PlaybackControlHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend() self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.h = handler.MpdHandler(backend=self.b) self.h = handler.MpdHandler(backend=self.b)
def test_next(self): def test_next(self):
@ -499,7 +506,8 @@ class PlaybackControlHandlerTest(unittest.TestCase):
class CurrentPlaylistHandlerTest(unittest.TestCase): class CurrentPlaylistHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend() self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.h = handler.MpdHandler(backend=self.b) self.h = handler.MpdHandler(backend=self.b)
def test_add(self): def test_add(self):
@ -791,7 +799,8 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
class StoredPlaylistsHandlerTest(unittest.TestCase): class StoredPlaylistsHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend() self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.h = handler.MpdHandler(backend=self.b) self.h = handler.MpdHandler(backend=self.b)
def test_listplaylist(self): def test_listplaylist(self):
@ -863,7 +872,8 @@ class StoredPlaylistsHandlerTest(unittest.TestCase):
class MusicDatabaseHandlerTest(unittest.TestCase): class MusicDatabaseHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.b = DummyBackend() self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.h = handler.MpdHandler(backend=self.b) self.h = handler.MpdHandler(backend=self.b)
def test_count(self): def test_count(self):
@ -1018,7 +1028,9 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
class StickersHandlerTest(unittest.TestCase): class StickersHandlerTest(unittest.TestCase):
def setUp(self): 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): def test_sticker_get(self):
result = self.h.handle_request( result = self.h.handle_request(
@ -1053,8 +1065,10 @@ class StickersHandlerTest(unittest.TestCase):
class ConnectionHandlerTest(unittest.TestCase): class ConnectionHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.h = handler.MpdHandler(session=DummySession(), self.m = DummyMixer()
backend=DummyBackend()) self.b = DummyBackend(mixer=self.m)
self.s = DummySession()
self.h = handler.MpdHandler(backend=self.b, session=self.s)
def test_close(self): def test_close(self):
result = self.h.handle_request(u'close') result = self.h.handle_request(u'close')
@ -1079,7 +1093,9 @@ class ConnectionHandlerTest(unittest.TestCase):
class AudioOutputHandlerTest(unittest.TestCase): class AudioOutputHandlerTest(unittest.TestCase):
def setUp(self): 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): def test_enableoutput(self):
result = self.h.handle_request(u'enableoutput "0"') result = self.h.handle_request(u'enableoutput "0"')
@ -1099,7 +1115,9 @@ class AudioOutputHandlerTest(unittest.TestCase):
class ReflectionHandlerTest(unittest.TestCase): class ReflectionHandlerTest(unittest.TestCase):
def setUp(self): 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): def test_commands(self):
result = self.h.handle_request(u'commands') result = self.h.handle_request(u'commands')