Skip to content

Commit

Permalink
Allow 'none' as audio.mixer value
Browse files Browse the repository at this point in the history
To disable mixing altogether, you can now set the configuration value
 audio/mixer to 'none'.
  • Loading branch information
ZenithDK committed Mar 9, 2015
1 parent 29b00ca commit 5208489
Show file tree
Hide file tree
Showing 14 changed files with 273 additions and 26 deletions.
6 changes: 6 additions & 0 deletions docs/changelog.rst
Expand Up @@ -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
Expand Down Expand Up @@ -114,6 +117,9 @@ 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. ``mixrampdb`` and ``mixrampdelay``
have also been added but have not been implemented yet.

**HTTP frontend**

- Prevent race condition in webservice broadcast from breaking the server.
Expand Down
2 changes: 2 additions & 0 deletions docs/config.rst
Expand Up @@ -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>`_.
Expand Down
14 changes: 11 additions & 3 deletions mopidy/commands.py
Expand Up @@ -276,7 +276,9 @@ def run(self, args, config):

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)
Expand All @@ -297,7 +299,8 @@ def run(self, args, config):
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

Expand All @@ -306,13 +309,18 @@ def get_mixer_class(self, config, mixer_classes):
'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]

Expand Down
23 changes: 15 additions & 8 deletions mopidy/core/mixer.py
Expand Up @@ -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.
Expand All @@ -27,26 +25,35 @@ def get_volume(self):
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.
: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()
19 changes: 14 additions & 5 deletions mopidy/mpd/protocol/audio_output.py
Expand Up @@ -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')

Expand All @@ -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:*
Expand All @@ -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')
Expand Down
15 changes: 8 additions & 7 deletions mopidy/mpd/protocol/playback.py
Expand Up @@ -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:*
Expand All @@ -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:*
Expand All @@ -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')
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions tests/core/test_listener.py
Expand Up @@ -57,3 +57,6 @@ def test_listener_has_default_impl_for_mute_changed(self):

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()
55 changes: 55 additions & 0 deletions tests/core/test_mixer.py
Expand Up @@ -4,7 +4,10 @@

import mock

import pykka

from mopidy import core, mixer
from tests import dummy_mixer


class CoreMixerTest(unittest.TestCase):
Expand Down Expand Up @@ -33,3 +36,55 @@ def test_set_mute(self):
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)
4 changes: 4 additions & 0 deletions tests/dummy_mixer.py
Expand Up @@ -21,9 +21,13 @@ def get_volume(self):

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
7 changes: 6 additions & 1 deletion tests/mpd/protocol/__init__.py
Expand Up @@ -25,6 +25,8 @@ def queue_send(self, data):


class BaseTestCase(unittest.TestCase):
enable_mixer = True

def get_config(self):
return {
'mpd': {
Expand All @@ -33,7 +35,10 @@ def get_config(self):
}

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()
Expand Down

0 comments on commit 5208489

Please sign in to comment.