diff --git a/docs/changes.rst b/docs/changes.rst index d3ed3db2..12028a17 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -34,6 +34,7 @@ greatly improved MPD client support. ``SPOTIFY_LIB_APPKEY`` setting is thus removed. - Added new :mod:`mopidy.mixers.GStreamerSoftwareMixer` which now is the default mixer on all platforms. +- New setting ``MIXER_MAX_VOLUME`` for capping the maximum output volume. - MPD frontend: - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py index 3ef1b645..c9543863 100644 --- a/mopidy/mixers/__init__.py +++ b/mopidy/mixers/__init__.py @@ -1,3 +1,5 @@ +from mopidy import settings + class BaseMixer(object): """ :param backend: a backend instance @@ -6,6 +8,7 @@ class BaseMixer(object): def __init__(self, backend, *args, **kwargs): self.backend = backend + self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0 @property def volume(self): @@ -15,11 +18,13 @@ class BaseMixer(object): Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is equal to 0. Values above 100 is equal to 100. """ - return self._get_volume() + if self._get_volume() is None: + return None + return int(self._get_volume() / self.amplification_factor) @volume.setter def volume(self, volume): - volume = int(volume) + volume = int(int(volume) * self.amplification_factor) if volume < 0: volume = 0 elif volume > 100: diff --git a/mopidy/settings.py b/mopidy/settings.py index 232adda8..67b0c24f 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -115,6 +115,16 @@ MIXER_EXT_SPEAKERS_A = None #: Default: :class:`None`. MIXER_EXT_SPEAKERS_B = None +#: The maximum volume. Integer in the range 0 to 100. +#: +#: If this settings is set to 80, the mixer will set the actual volume to 80 +#: when asked to set it to 100. +#: +#: Default:: +#: +#: MIXER_MAX_VOLUME = 100 +MIXER_MAX_VOLUME = 100 + #: Audio output handler to use. #: #: Default:: diff --git a/tests/backends/base.py b/tests/backends/base.py index 64ca7797..19f28ba5 100644 --- a/tests/backends/base.py +++ b/tests/backends/base.py @@ -811,6 +811,14 @@ class BasePlaybackControllerTest(object): self.playback.next() self.assert_(self.tracks[0] not in self.backend.current_playlist.tracks) + @populate_playlist + def test_next_with_single_and_repeat(self): + self.playback.single = True + self.playback.repeat = True + self.playback.play() + self.playback.next() + self.assertEqual(self.playback.current_track, self.tracks[1]) + @populate_playlist def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self): self.playback.consume = True @@ -851,6 +859,14 @@ class BasePlaybackControllerTest(object): self.playback.end_of_track_callback() self.assertEqual(self.playback.current_track, self.tracks[1]) + @populate_playlist + def test_end_of_song_with_single_and_repeat_starts_same(self): + self.playback.single = True + self.playback.repeat = True + self.playback.play() + self.playback.end_of_track_callback() + self.assertEqual(self.playback.current_track, self.tracks[0]) + @populate_playlist def test_end_of_playlist_stops(self): self.playback.play(self.current_playlist.cp_tracks[-1]) diff --git a/tests/mixers/base_test.py b/tests/mixers/base_test.py index 292edcda..d6129ad5 100644 --- a/tests/mixers/base_test.py +++ b/tests/mixers/base_test.py @@ -1,18 +1,16 @@ -import unittest - -from mopidy.mixers.dummy import DummyMixer - -class BaseMixerTest(unittest.TestCase): +class BaseMixerTest(object): MIN = 0 MAX = 100 - ACTUAL_MIN = MIN ACTUAL_MAX = MAX - INITIAL = None + mixer_class = None + def setUp(self): - self.mixer = DummyMixer(None) + assert self.mixer_class is not None, \ + "mixer_class must be set in subclass" + self.mixer = self.mixer_class(None) def test_initial_volume(self): self.assertEqual(self.mixer.volume, self.INITIAL) diff --git a/tests/mixers/denon_test.py b/tests/mixers/denon_test.py index e0f59705..5370f155 100644 --- a/tests/mixers/denon_test.py +++ b/tests/mixers/denon_test.py @@ -1,3 +1,5 @@ +import unittest + from mopidy.mixers.denon import DenonMixer from tests.mixers.base_test import BaseMixerTest @@ -22,11 +24,12 @@ class DenonMixerDeviceMock(object): def open(self): self._open = True -class DenonMixerTest(BaseMixerTest): +class DenonMixerTest(BaseMixerTest, unittest.TestCase): ACTUAL_MAX = 99 - INITIAL = 1 + mixer_class = DenonMixer + def setUp(self): self.device = DenonMixerDeviceMock() self.mixer = DenonMixer(None, device=self.device) diff --git a/tests/mixers/dummy_test.py b/tests/mixers/dummy_test.py new file mode 100644 index 00000000..334dc8a1 --- /dev/null +++ b/tests/mixers/dummy_test.py @@ -0,0 +1,17 @@ +import unittest + +from mopidy.mixers.dummy import DummyMixer +from tests.mixers.base_test import BaseMixerTest + +class DenonMixerTest(BaseMixerTest, unittest.TestCase): + mixer_class = DummyMixer + + def test_set_volume_is_capped(self): + self.mixer.amplification_factor = 0.5 + self.mixer.volume = 100 + self.assertEquals(self.mixer._volume, 50) + + def test_get_volume_does_not_show_that_the_volume_is_capped(self): + self.mixer.amplification_factor = 0.5 + self.mixer._volume = 50 + self.assertEquals(self.mixer.volume, 100)