Merge pull request #1015 from ZenithDK/feature/mixer_none
core/mpd: Allow empty or 'none' as audio.mixer value
This commit is contained in:
commit
083ec130f9
@ -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.
|
||||
|
||||
@ -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>`_.
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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} ')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user