Merge pull request #529 from jodal/feature/audio-mute
Add mute support to audio, core, and MPD layers
This commit is contained in:
commit
d1d5975a59
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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),
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user