Merge pull request #529 from jodal/feature/audio-mute

Add mute support to audio, core, and MPD layers
This commit is contained in:
Stein Magnus Jodal 2013-10-10 13:51:21 -07:00
commit d1d5975a59
10 changed files with 154 additions and 12 deletions

View File

@ -34,6 +34,22 @@ of the following extensions as well:
This was causing divide by zero errors when scaling volumes to a zero to
hundred scale. (Fixes: :issue:`525`)
- Added support for muting audio without setting the volume to 0. This works
both for the software and hardware mixers. (Fixes: :issue:`186`)
**Core**
- Added :attr:`mopidy.core.PlaybackController.mute` for muting and unmuting
audio. (Fixes: :issue:`186`)
- Added :meth:`mopidy.core.CoreListener.mute_changed` event that is triggered
when the mute state changes.
**MPD frontend**
- Made the formerly unused commands ``outputs``, ``enableoutput``, and
``disableoutput`` mute/unmute audio. (Related to: :issue:`186`)
**Extension support**
- A cookiecutter project for quickly creating new Mopidy extensions have been

View File

@ -549,6 +549,37 @@ class Audio(pykka.ThreadingActor):
scaling = float(new_max - new_min) / (old_max - old_min)
return int(round(scaling * (value - old_min) + new_min))
def get_mute(self):
"""
Get mute status of the installed mixer.
:rtype: :class:`True` if muted, :class:`False` if unmuted,
:class:`None` if no mixer is installed.
"""
if self._software_mixing:
return self._playbin.get_property('mute')
if self._mixer_track is None:
return None
return bool(self._mixer_track.flags & gst.interfaces.MIXER_TRACK_MUTE)
def set_mute(self, mute):
"""
Mute or unmute of the installed mixer.
:param mute: Wether to mute the mixer or not.
:type mute: bool
:rtype: :class:`True` if successful, else :class:`False`
"""
if self._software_mixing:
return self._playbin.set_property('mute', bool(mute))
if self._mixer_track is None:
return False
return self._mixer.set_mute(self._mixer_track, bool(mute))
def set_metadata(self, track):
"""
Set track metadata for currently playing song.

View File

@ -132,11 +132,25 @@ class CoreListener(object):
"""
pass
def volume_changed(self):
def volume_changed(self, volume):
"""
Called whenever the volume is changed.
*MAY* be implemented by actor.
:param volume: the new volume in the range [0..100]
:type volume: int
"""
pass
def mute_changed(self, mute):
"""
Called whenever the mute state is changed.
*MAY* be implemented by actor.
:param mute: the new mute state
:type mute: boolean
"""
pass

View File

@ -24,6 +24,7 @@ class PlaybackController(object):
self._shuffled = []
self._first_shuffle = True
self._volume = None
self._mute = False
def _get_backend(self):
if self.current_tl_track is None:
@ -288,6 +289,26 @@ class PlaybackController(object):
volume = property(get_volume, set_volume)
"""Volume as int in range [0..100] or :class:`None`"""
def get_mute(self):
if self.audio:
return self.audio.get_mute().get()
else:
# For testing
return self._mute
def set_mute(self, value):
value = bool(value)
if self.audio:
self.audio.set_mute(value)
else:
# For testing
self._mute = value
self._trigger_mute_changed(value)
mute = property(get_mute, set_mute)
"""Mute state as a :class:`True` if muted, :class:`False` otherwise"""
### Methods
def change_track(self, tl_track, on_error_step=1):
@ -518,6 +539,10 @@ class PlaybackController(object):
logger.debug('Triggering volume changed event')
listener.CoreListener.send('volume_changed', volume=volume)
def _trigger_mute_changed(self, mute):
logger.debug('Triggering mute changed event')
listener.CoreListener.send('mute_changed', mute=mute)
def _trigger_seeked(self, time_position):
logger.debug('Triggering seeked event')
listener.CoreListener.send('seeked', time_position=time_position)

View File

@ -55,3 +55,6 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
def volume_changed(self, volume):
self.send_idle('mixer')
def mute_changed(self, mute):
self.send_idle('output')

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals
from mopidy.frontends.mpd.exceptions import MpdNoExistError
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_request(r'^disableoutput "(?P<outputid>\d+)"$')
@ -13,7 +13,10 @@ def disableoutput(context, outputid):
Turns an output off.
"""
raise MpdNotImplemented # TODO
if int(outputid) == 0:
context.core.playback.set_mute(True)
else:
raise MpdNoExistError('No such audio output', command='disableoutput')
@handle_request(r'^enableoutput "(?P<outputid>\d+)"$')
@ -25,7 +28,10 @@ def enableoutput(context, outputid):
Turns an output on.
"""
raise MpdNotImplemented # TODO
if int(outputid) == 0:
context.core.playback.set_mute(False)
else:
raise MpdNoExistError('No such audio output', command='enableoutput')
@handle_request(r'^outputs$')
@ -37,8 +43,9 @@ def outputs(context):
Shows information about all outputs.
"""
muted = 1 if context.core.playback.get_mute().get() else 0
return [
('outputid', 0),
('outputname', 'Default'),
('outputenabled', 1),
('outputname', 'Mute'),
('outputenabled', muted),
]

View File

@ -95,6 +95,10 @@ class AudioTest(unittest.TestCase):
self.audio = audio.Audio.start(config=config).proxy()
self.assertEqual(0, self.audio.get_volume().get())
@unittest.SkipTest
def test_set_mute(self):
pass # TODO Probably needs a fakemixer with a mixer track
@unittest.SkipTest
def test_set_state_encapsulation(self):
pass # TODO

View File

@ -49,7 +49,10 @@ class CoreListenerTest(unittest.TestCase):
self.listener.options_changed()
def test_listener_has_default_impl_for_volume_changed(self):
self.listener.volume_changed()
self.listener.volume_changed(70)
def test_listener_has_default_impl_for_mute_changed(self):
self.listener.mute_changed(True)
def test_listener_has_default_impl_for_seeked(self):
self.listener.seeked(0)

View File

@ -177,3 +177,10 @@ class CorePlaybackTest(unittest.TestCase):
self.assertEqual(result, 0)
self.assertFalse(self.playback1.get_time_position.called)
self.assertFalse(self.playback2.get_time_position.called)
def test_mute(self):
self.assertEqual(self.core.playback.mute, False)
self.core.playback.mute = True
self.assertEqual(self.core.playback.mute, True)

View File

@ -5,16 +5,48 @@ from tests.frontends.mpd import protocol
class AudioOutputHandlerTest(protocol.BaseTestCase):
def test_enableoutput(self):
self.core.playback.mute = True
self.sendRequest('enableoutput "0"')
self.assertInResponse('ACK [0@0] {} Not implemented')
self.assertInResponse('OK')
self.assertEqual(self.core.playback.mute.get(), False)
def test_enableoutput_unknown_outputid(self):
self.sendRequest('enableoutput "7"')
self.assertInResponse('ACK [50@0] {enableoutput} No such audio output')
def test_disableoutput(self):
self.sendRequest('disableoutput "0"')
self.assertInResponse('ACK [0@0] {} Not implemented')
self.core.playback.mute = False
self.sendRequest('disableoutput "0"')
self.assertInResponse('OK')
self.assertEqual(self.core.playback.mute.get(), True)
def test_disableoutput_unknown_outputid(self):
self.sendRequest('disableoutput "7"')
self.assertInResponse(
'ACK [50@0] {disableoutput} No such audio output')
def test_outputs_when_unmuted(self):
self.core.playback.mute = False
def test_outputs(self):
self.sendRequest('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Default')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 0')
self.assertInResponse('OK')
def test_outputs_when_muted(self):
self.core.playback.mute = True
self.sendRequest('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 1')
self.assertInResponse('OK')