Allow 'none' as audio.mixer value

To disable mixing altogether, you can now set the configuration value
 audio/mixer to 'none'.
This commit is contained in:
Lasse Bigum 2015-02-07 22:54:02 +01:00
parent 29b00cabf9
commit cb19b2c48c
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 debug logging of unknown sections. (Fixes: :issue:`694`, PR: :issue:`1002`)
- Add support for configuring :confval:`audio/mixer` to ``none``. (Fixes:
:issue:`936`)
**Logging** **Logging**
- Add custom log level ``TRACE`` (numerical level 5), which can be used by - Add custom log level ``TRACE`` (numerical level 5), which can be used by
@ -114,6 +117,10 @@ v0.20.0 (UNRELEASED)
- Switch the ``list`` command over to using - Switch the ``list`` command over to using
:meth:`mopidy.core.LibraryController.get_distinct`. (Fixes: :issue:`913`) :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** **HTTP frontend**
- Prevent race condition in webservice broadcast from breaking the server. - 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 will affect the audio volume if you're streaming the audio from Mopidy
through Shoutcast. 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 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 which integrates with your sound subsystem. E.g. for ALSA, install
`Mopidy-ALSAMixer <https://github.com/mopidy/mopidy-alsamixer>`_. `Mopidy-ALSAMixer <https://github.com/mopidy/mopidy-alsamixer>`_.

View File

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

View File

@ -11,8 +11,6 @@ class MixerController(object):
def __init__(self, mixer): def __init__(self, mixer):
self._mixer = mixer self._mixer = mixer
self._volume = None
self._mute = False
def get_volume(self): def get_volume(self):
"""Get the volume. """Get the volume.
@ -27,12 +25,15 @@ class MixerController(object):
def set_volume(self, volume): def set_volume(self, volume):
"""Set the 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. The volume scale is linear.
""" """
if self._mixer is not None: if self._mixer is None:
self._mixer.set_volume(volume) return False
else:
return self._mixer.set_volume(volume).get()
def get_mute(self): def get_mute(self):
"""Get mute state. """Get mute state.
@ -40,13 +41,19 @@ class MixerController(object):
:class:`True` if muted, :class:`False` unmuted, :class:`None` if :class:`True` if muted, :class:`False` unmuted, :class:`None` if
unknown. unknown.
""" """
if self._mixer is not None: if self._mixer is None:
return False
else:
return self._mixer.get_mute().get() return self._mixer.get_mute().get()
def set_mute(self, mute): def set_mute(self, mute):
"""Set mute state. """Set mute state.
:class:`True` to mute, :class:`False` to unmute. :class:`True` to mute, :class:`False` to unmute.
Returns :class:`True` if call is successful, otherwise :class:`False`.
""" """
if self._mixer is not None: if self._mixer is None:
self._mixer.set_mute(bool(mute)) 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. Turns an output off.
""" """
if outputid == 0: 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: else:
raise exceptions.MpdNoExistError('No such audio output') raise exceptions.MpdNoExistError('No such audio output')
@ -28,13 +30,14 @@ def enableoutput(context, outputid):
Turns an output on. Turns an output on.
""" """
if outputid == 0: 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: else:
raise exceptions.MpdNoExistError('No such audio output') 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): def toggleoutput(context, outputid):
""" """
*musicpd.org, audio output section:* *musicpd.org, audio output section:*
@ -43,7 +46,13 @@ def toggleoutput(context, outputid):
Turns an output on or off, depending on the current state. 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') @protocol.commands.add('outputs')

View File

@ -32,8 +32,7 @@ def crossfade(context, seconds):
raise exceptions.MpdNotImplemented # TODO 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): def mixrampdb(context, decibels):
""" """
*musicpd.org, playback section:* *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 volume so use negative values, I prefer -17dB. In the absence of mixramp
tags crossfading will be used. See http://sourceforge.net/projects/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): def mixrampdelay(context, seconds):
""" """
*musicpd.org, playback section:* *musicpd.org, playback section:*
@ -61,7 +59,7 @@ def mixrampdelay(context, seconds):
value of "nan" disables MixRamp overlapping and falls back to value of "nan" disables MixRamp overlapping and falls back to
crossfading. crossfading.
""" """
pass raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add('next') @protocol.commands.add('next')
@ -397,7 +395,10 @@ def setvol(context, volume):
- issues ``setvol 50`` without quotes around the argument. - issues ``setvol 50`` without quotes around the argument.
""" """
# NOTE: we use INT as clients can pass in +N etc. # 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) @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): def test_listener_has_default_impl_for_seeked(self):
self.listener.seeked(0) 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 mock
import pykka
from mopidy import core, mixer from mopidy import core, mixer
from tests import dummy_mixer
class CoreMixerTest(unittest.TestCase): class CoreMixerTest(unittest.TestCase):
@ -33,3 +36,55 @@ class CoreMixerTest(unittest.TestCase):
self.core.mixer.set_mute(True) self.core.mixer.set_mute(True)
self.mixer.set_mute.assert_called_once_with(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): def set_volume(self, volume):
self._volume = volume self._volume = volume
self.trigger_volume_changed(volume=volume)
return True
def get_mute(self): def get_mute(self):
return self._mute return self._mute
def set_mute(self, mute): def set_mute(self, mute):
self._mute = 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): class BaseTestCase(unittest.TestCase):
enable_mixer = True
def get_config(self): def get_config(self):
return { return {
'mpd': { 'mpd': {
@ -33,7 +35,10 @@ class BaseTestCase(unittest.TestCase):
} }
def setUp(self): # noqa: N802 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.backend = dummy_backend.create_proxy()
self.core = core.Core.start( self.core = core.Core.start(
mixer=self.mixer, backends=[self.backend]).proxy() mixer=self.mixer, backends=[self.backend]).proxy()

View File

@ -4,6 +4,7 @@ from tests.mpd import protocol
class AudioOutputHandlerTest(protocol.BaseTestCase): class AudioOutputHandlerTest(protocol.BaseTestCase):
def test_enableoutput(self): def test_enableoutput(self):
self.core.mixer.set_mute(False) self.core.mixer.set_mute(False)
@ -50,3 +51,95 @@ class AudioOutputHandlerTest(protocol.BaseTestCase):
self.assertInResponse('outputname: Mute') self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 1') self.assertInResponse('outputenabled: 1')
self.assertInResponse('OK') 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.assertNoEvents()
self.assertNoResponse() 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): def test_idle_player_playlist(self):
self.send_request('idle player playlist') self.send_request('idle player playlist')
self.assertEqualSubscriptions(['player', 'playlist']) self.assertEqualSubscriptions(['player', 'playlist'])
@ -102,6 +108,22 @@ class IdleHandlerTest(protocol.BaseTestCase):
self.assertOnceInResponse('changed: player') self.assertOnceInResponse('changed: player')
self.assertOnceInResponse('OK') 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): def test_idle_player_then_noidle(self):
self.send_request('idle player') self.send_request('idle player')
self.send_request('noidle') self.send_request('noidle')
@ -206,3 +228,11 @@ class IdleHandlerTest(protocol.BaseTestCase):
self.assertNotInResponse('changed: player') self.assertNotInResponse('changed: player')
self.assertOnceInResponse('changed: playlist') self.assertOnceInResponse('changed: playlist')
self.assertOnceInResponse('OK') 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('OK')
self.assertInResponse('off') 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 @unittest.SkipTest
def test_replay_gain_status_off(self): def test_replay_gain_status_off(self):
pass pass
@ -463,3 +471,11 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.send_request('stop') self.send_request('stop')
self.assertEqual(STOPPED, self.core.playback.state.get()) self.assertEqual(STOPPED, self.core.playback.state.get())
self.assertInResponse('OK') 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 import unittest
from mopidy.mpd.exceptions import ( from mopidy.mpd.exceptions import (
MpdAckError, MpdNoCommand, MpdNotImplemented, MpdPermissionError, MpdAckError, MpdNoCommand, MpdNoExistError, MpdNotImplemented,
MpdSystemError, MpdUnknownCommand) MpdPermissionError, MpdSystemError, MpdUnknownCommand)
class MpdExceptionsTest(unittest.TestCase): class MpdExceptionsTest(unittest.TestCase):
@ -61,3 +61,11 @@ class MpdExceptionsTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
e.get_mpd_ack(), e.get_mpd_ack(),
'ACK [4@0] {foo} you don\'t have permission for "foo"') '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} ')