Merge pull request #1015 from ZenithDK/feature/mixer_none

core/mpd: Allow empty or 'none' as audio.mixer value
This commit is contained in:
Thomas Adamcik 2015-03-13 17:57:51 +01:00
commit 083ec130f9
14 changed files with 274 additions and 26 deletions

View File

@ -57,6 +57,9 @@ v0.20.0 (UNRELEASED)
- Add debug logging of unknown sections. (Fixes: :issue:`694`, PR: :issue:`1002`)
- Add support for configuring :confval:`audio/mixer` to ``none``. (Fixes:
:issue:`936`)
**Logging**
- Add custom log level ``TRACE`` (numerical level 5), which can be used by
@ -117,6 +120,10 @@ v0.20.0 (UNRELEASED)
- Switch the ``list`` command over to using
:meth:`mopidy.core.LibraryController.get_distinct`. (Fixes: :issue:`913`)
- Add support for ``toggleoutput`` command. The ``mixrampdb`` and
``mixrampdelay`` commands are now supported but throw a NotImplemented
exception.
**HTTP frontend**
- Prevent race condition in webservice broadcast from breaking the server.

View File

@ -70,6 +70,8 @@ Audio configuration
will affect the audio volume if you're streaming the audio from Mopidy
through Shoutcast.
If you want to disable audio mixing set the value to ``none``.
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 <https://github.com/mopidy/mopidy-alsamixer>`_.

View File

@ -276,7 +276,9 @@ class RootCommand(Command):
exit_status_code = 0
try:
mixer = self.start_mixer(config, mixer_class)
mixer = None
if mixer_class is not None:
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, audio)
@ -297,7 +299,8 @@ class RootCommand(Command):
self.stop_core()
self.stop_backends(backend_classes)
self.stop_audio()
self.stop_mixer(mixer_class)
if mixer_class is not None:
self.stop_mixer(mixer_class)
process.stop_remaining_actors()
return exit_status_code
@ -306,13 +309,18 @@ class RootCommand(Command):
'Available Mopidy mixers: %s',
', '.join(m.__name__ for m in mixer_classes) or 'none')
if config['audio']['mixer'] == 'none':
logger.debug('Mixer disabled')
return 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]))
', '.join([m.name for m in mixer_classes]) + ', none' or
'none')
process.exit_process()
return selected_mixers[0]

View File

@ -11,8 +11,6 @@ class MixerController(object):
def __init__(self, mixer):
self._mixer = mixer
self._volume = None
self._mute = False
def get_volume(self):
"""Get the volume.
@ -27,12 +25,15 @@ class MixerController(object):
def set_volume(self, volume):
"""Set the volume.
The volume is defined as an integer in range [0..100].
The volume is defined as an integer in range [0..100] or :class:`None`
if the mixer is disabled.
The volume scale is linear.
"""
if self._mixer is not None:
self._mixer.set_volume(volume)
if self._mixer is None:
return False
else:
return self._mixer.set_volume(volume).get()
def get_mute(self):
"""Get mute state.
@ -40,13 +41,19 @@ class MixerController(object):
:class:`True` if muted, :class:`False` unmuted, :class:`None` if
unknown.
"""
if self._mixer is not None:
if self._mixer is None:
return False
else:
return self._mixer.get_mute().get()
def set_mute(self, mute):
"""Set mute state.
:class:`True` to mute, :class:`False` to unmute.
Returns :class:`True` if call is successful, otherwise :class:`False`.
"""
if self._mixer is not None:
self._mixer.set_mute(bool(mute))
if self._mixer is None:
return False
else:
return self._mixer.set_mute(bool(mute)).get()

View File

@ -13,7 +13,9 @@ def disableoutput(context, outputid):
Turns an output off.
"""
if outputid == 0:
context.core.mixer.set_mute(False)
success = context.core.mixer.set_mute(False).get()
if success is False:
raise exceptions.MpdSystemError('problems disabling output')
else:
raise exceptions.MpdNoExistError('No such audio output')
@ -28,13 +30,14 @@ def enableoutput(context, outputid):
Turns an output on.
"""
if outputid == 0:
context.core.mixer.set_mute(True)
success = context.core.mixer.set_mute(True).get()
if success is False:
raise exceptions.MpdSystemError('problems enabling output')
else:
raise exceptions.MpdNoExistError('No such audio output')
# TODO: implement and test
# @protocol.commands.add('toggleoutput', outputid=protocol.UINT)
@protocol.commands.add('toggleoutput', outputid=protocol.UINT)
def toggleoutput(context, outputid):
"""
*musicpd.org, audio output section:*
@ -43,7 +46,13 @@ def toggleoutput(context, outputid):
Turns an output on or off, depending on the current state.
"""
pass
if outputid == 0:
mute_status = context.core.mixer.get_mute().get()
success = context.core.mixer.set_mute(not mute_status)
if success is False:
raise exceptions.MpdSystemError('problems toggling output')
else:
raise exceptions.MpdNoExistError('No such audio output')
@protocol.commands.add('outputs')

View File

@ -32,8 +32,7 @@ def crossfade(context, seconds):
raise exceptions.MpdNotImplemented # TODO
# TODO: add at least reflection tests before adding NotImplemented version
# @protocol.commands.add('mixrampdb')
@protocol.commands.add('mixrampdb')
def mixrampdb(context, decibels):
"""
*musicpd.org, playback section:*
@ -46,11 +45,10 @@ def mixrampdb(context, decibels):
volume so use negative values, I prefer -17dB. In the absence of mixramp
tags crossfading will be used. See http://sourceforge.net/projects/mixramp
"""
pass
raise exceptions.MpdNotImplemented # TODO
# TODO: add at least reflection tests before adding NotImplemented version
# @protocol.commands.add('mixrampdelay', seconds=protocol.UINT)
@protocol.commands.add('mixrampdelay', seconds=protocol.UINT)
def mixrampdelay(context, seconds):
"""
*musicpd.org, playback section:*
@ -61,7 +59,7 @@ def mixrampdelay(context, seconds):
value of "nan" disables MixRamp overlapping and falls back to
crossfading.
"""
pass
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add('next')
@ -397,7 +395,10 @@ def setvol(context, volume):
- issues ``setvol 50`` without quotes around the argument.
"""
# NOTE: we use INT as clients can pass in +N etc.
context.core.mixer.set_volume(min(max(0, volume), 100))
value = min(max(0, volume), 100)
success = context.core.mixer.set_volume(value).get()
if success is False:
raise exceptions.MpdSystemError('problems setting volume')
@protocol.commands.add('single', state=protocol.BOOL)

View File

@ -57,3 +57,6 @@ class CoreListenerTest(unittest.TestCase):
def test_listener_has_default_impl_for_seeked(self):
self.listener.seeked(0)
def test_listener_has_default_impl_for_current_metadata_changed(self):
self.listener.current_metadata_changed()

View File

@ -4,7 +4,10 @@ import unittest
import mock
import pykka
from mopidy import core, mixer
from tests import dummy_mixer
class CoreMixerTest(unittest.TestCase):
@ -33,3 +36,55 @@ class CoreMixerTest(unittest.TestCase):
self.core.mixer.set_mute(True)
self.mixer.set_mute.assert_called_once_with(True)
class CoreNoneMixerTest(unittest.TestCase):
def setUp(self): # noqa: N802
self.core = core.Core(mixer=None, backends=[])
def test_get_volume_return_none(self):
self.assertEqual(self.core.mixer.get_volume(), None)
def test_set_volume_return_false(self):
self.assertEqual(self.core.mixer.set_volume(30), False)
def test_get_set_mute_return_proper_state(self):
self.assertEqual(self.core.mixer.set_mute(False), False)
self.assertEqual(self.core.mixer.get_mute(), False)
self.assertEqual(self.core.mixer.set_mute(True), False)
self.assertEqual(self.core.mixer.get_mute(), False)
@mock.patch.object(mixer.MixerListener, 'send')
class CoreMixerListenerTest(unittest.TestCase):
def setUp(self): # noqa: N802
self.mixer = dummy_mixer.create_proxy()
self.core = core.Core(mixer=self.mixer, backends=[])
def tearDown(self): # noqa: N802
pykka.ActorRegistry.stop_all()
def test_forwards_mixer_volume_changed_event_to_frontends(self, send):
self.assertEqual(self.core.mixer.set_volume(volume=60), True)
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.mixer.set_mute(mute=True)
self.assertEqual(send.call_args[0][0], 'mute_changed')
self.assertEqual(send.call_args[1]['mute'], True)
@mock.patch.object(mixer.MixerListener, 'send')
class CoreNoneMixerListenerTest(unittest.TestCase):
def setUp(self): # noqa: N802
self.core = core.Core(mixer=None, backends=[])
def test_forwards_mixer_volume_changed_event_to_frontends(self, send):
self.assertEqual(self.core.mixer.set_volume(volume=60), False)
self.assertEqual(send.call_count, 0)
def test_forwards_mixer_mute_changed_event_to_frontends(self, send):
self.core.mixer.set_mute(mute=True)
self.assertEqual(send.call_count, 0)

View File

@ -21,9 +21,13 @@ class DummyMixer(pykka.ThreadingActor, mixer.Mixer):
def set_volume(self, volume):
self._volume = volume
self.trigger_volume_changed(volume=volume)
return True
def get_mute(self):
return self._mute
def set_mute(self, mute):
self._mute = mute
self.trigger_mute_changed(mute=mute)
return True

View File

@ -25,6 +25,8 @@ class MockConnection(mock.Mock):
class BaseTestCase(unittest.TestCase):
enable_mixer = True
def get_config(self):
return {
'mpd': {
@ -33,7 +35,10 @@ class BaseTestCase(unittest.TestCase):
}
def setUp(self): # noqa: N802
self.mixer = dummy_mixer.create_proxy()
if self.enable_mixer:
self.mixer = dummy_mixer.create_proxy()
else:
self.mixer = None
self.backend = dummy_backend.create_proxy()
self.core = core.Core.start(
mixer=self.mixer, backends=[self.backend]).proxy()

View File

@ -4,6 +4,7 @@ from tests.mpd import protocol
class AudioOutputHandlerTest(protocol.BaseTestCase):
def test_enableoutput(self):
self.core.mixer.set_mute(False)
@ -50,3 +51,95 @@ class AudioOutputHandlerTest(protocol.BaseTestCase):
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 1')
self.assertInResponse('OK')
def test_outputs_toggleoutput(self):
self.core.mixer.set_mute(False)
self.send_request('toggleoutput "0"')
self.send_request('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 1')
self.assertInResponse('OK')
self.send_request('toggleoutput "0"')
self.send_request('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 0')
self.assertInResponse('OK')
self.send_request('toggleoutput "0"')
self.send_request('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 1')
self.assertInResponse('OK')
def test_outputs_toggleoutput_unknown_outputid(self):
self.send_request('toggleoutput "7"')
self.assertInResponse(
'ACK [50@0] {toggleoutput} No such audio output')
class AudioOutputHandlerNoneMixerTest(protocol.BaseTestCase):
enable_mixer = False
def test_enableoutput(self):
self.core.mixer.set_mute(False)
self.send_request('enableoutput "0"')
self.assertInResponse(
'ACK [52@0] {enableoutput} problems enabling output')
self.assertEqual(self.core.mixer.get_mute().get(), False)
def test_disableoutput(self):
self.core.mixer.set_mute(True)
self.send_request('disableoutput "0"')
self.assertInResponse(
'ACK [52@0] {disableoutput} problems disabling output')
self.assertEqual(self.core.mixer.get_mute().get(), False)
def test_outputs_when_unmuted(self):
self.core.mixer.set_mute(False)
self.send_request('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 0')
self.assertInResponse('OK')
def test_outputs_when_muted(self):
self.core.mixer.set_mute(True)
self.send_request('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 0')
self.assertInResponse('OK')
def test_outputs_toggleoutput(self):
self.core.mixer.set_mute(False)
self.send_request('toggleoutput "0"')
self.send_request('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 0')
self.assertInResponse('OK')
self.send_request('toggleoutput "0"')
self.send_request('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 0')
self.assertInResponse('OK')

View File

@ -50,6 +50,12 @@ class IdleHandlerTest(protocol.BaseTestCase):
self.assertNoEvents()
self.assertNoResponse()
def test_idle_output(self):
self.send_request('idle output')
self.assertEqualSubscriptions(['output'])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_player_playlist(self):
self.send_request('idle player playlist')
self.assertEqualSubscriptions(['player', 'playlist'])
@ -102,6 +108,22 @@ class IdleHandlerTest(protocol.BaseTestCase):
self.assertOnceInResponse('changed: player')
self.assertOnceInResponse('OK')
def test_idle_then_output(self):
self.send_request('idle')
self.idle_event('output')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse('changed: output')
self.assertOnceInResponse('OK')
def test_idle_output_then_event_output(self):
self.send_request('idle output')
self.idle_event('output')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse('changed: output')
self.assertOnceInResponse('OK')
def test_idle_player_then_noidle(self):
self.send_request('idle player')
self.send_request('noidle')
@ -206,3 +228,11 @@ class IdleHandlerTest(protocol.BaseTestCase):
self.assertNotInResponse('changed: player')
self.assertOnceInResponse('changed: playlist')
self.assertOnceInResponse('OK')
def test_output_then_idle_toggleoutput(self):
self.idle_event('output')
self.send_request('idle output')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse('changed: output')
self.assertOnceInResponse('OK')

View File

@ -150,6 +150,14 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
self.assertInResponse('off')
def test_mixrampdb(self):
self.send_request('mixrampdb "10"')
self.assertInResponse('ACK [0@0] {mixrampdb} Not implemented')
def test_mixrampdelay(self):
self.send_request('mixrampdelay "10"')
self.assertInResponse('ACK [0@0] {mixrampdelay} Not implemented')
@unittest.SkipTest
def test_replay_gain_status_off(self):
pass
@ -463,3 +471,11 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.send_request('stop')
self.assertEqual(STOPPED, self.core.playback.state.get())
self.assertInResponse('OK')
class PlaybackOptionsHandlerNoneMixerTest(protocol.BaseTestCase):
enable_mixer = False
def test_setvol_max_error(self):
self.send_request('setvol "100"')
self.assertInResponse('ACK [52@0] {setvol} problems setting volume')

View File

@ -3,8 +3,8 @@ from __future__ import absolute_import, unicode_literals
import unittest
from mopidy.mpd.exceptions import (
MpdAckError, MpdNoCommand, MpdNotImplemented, MpdPermissionError,
MpdSystemError, MpdUnknownCommand)
MpdAckError, MpdNoCommand, MpdNoExistError, MpdNotImplemented,
MpdPermissionError, MpdSystemError, MpdUnknownCommand)
class MpdExceptionsTest(unittest.TestCase):
@ -61,3 +61,11 @@ class MpdExceptionsTest(unittest.TestCase):
self.assertEqual(
e.get_mpd_ack(),
'ACK [4@0] {foo} you don\'t have permission for "foo"')
def test_mpd_noexist_error(self):
try:
raise MpdNoExistError(command='foo')
except MpdNoExistError as e:
self.assertEqual(
e.get_mpd_ack(),
'ACK [50@0] {foo} ')