Remove all of mopidy.mixers and tests.mixers modules.
This commit is contained in:
parent
4ffd06736e
commit
c71202c2be
@ -40,7 +40,6 @@ def main():
|
||||
check_old_folders()
|
||||
setup_settings(options.interactive)
|
||||
setup_gstreamer()
|
||||
setup_mixer()
|
||||
setup_backend()
|
||||
setup_frontends()
|
||||
loop.run()
|
||||
@ -54,7 +53,6 @@ def main():
|
||||
loop.quit()
|
||||
stop_frontends()
|
||||
stop_backend()
|
||||
stop_mixer()
|
||||
stop_gstreamer()
|
||||
stop_remaining_actors()
|
||||
|
||||
@ -109,14 +107,6 @@ def setup_gstreamer():
|
||||
def stop_gstreamer():
|
||||
stop_actors_by_class(GStreamer)
|
||||
|
||||
def setup_mixer():
|
||||
# TODO: remove this hack which is just a stepping stone for our
|
||||
# refactoring.
|
||||
get_class('mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer').start()
|
||||
|
||||
def stop_mixer():
|
||||
stop_actors_by_class(get_class('mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer'))
|
||||
|
||||
def setup_backend():
|
||||
get_class(settings.BACKENDS[0]).start()
|
||||
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
import alsaaudio
|
||||
import logging
|
||||
|
||||
from pykka.actor import ThreadingActor
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
logger = logging.getLogger('mopidy.mixers.alsa')
|
||||
|
||||
class AlsaMixer(ThreadingActor, BaseMixer):
|
||||
"""
|
||||
Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control
|
||||
volume.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- pyalsaaudio >= 0.2 (python-alsaaudio on Debian/Ubuntu)
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.MIXER_ALSA_CONTROL`
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(AlsaMixer, self).__init__()
|
||||
self._mixer = None
|
||||
|
||||
def on_start(self):
|
||||
self._mixer = alsaaudio.Mixer(self._get_mixer_control())
|
||||
assert self._mixer is not None
|
||||
|
||||
def _get_mixer_control(self):
|
||||
"""Returns the first mixer control candidate that is known to ALSA"""
|
||||
candidates = self._get_mixer_control_candidates()
|
||||
for control in candidates:
|
||||
if control in alsaaudio.mixers():
|
||||
logger.info(u'Mixer control in use: %s', control)
|
||||
return control
|
||||
else:
|
||||
logger.debug(u'Mixer control not found, skipping: %s', control)
|
||||
logger.warning(u'No working mixer controls found. Tried: %s',
|
||||
candidates)
|
||||
|
||||
def _get_mixer_control_candidates(self):
|
||||
"""
|
||||
A mixer named 'Master' does not always exist, so we fall back to using
|
||||
'PCM'. If this does not work for you, you may set
|
||||
:attr:`mopidy.settings.MIXER_ALSA_CONTROL`.
|
||||
"""
|
||||
if settings.MIXER_ALSA_CONTROL:
|
||||
return [settings.MIXER_ALSA_CONTROL]
|
||||
return [u'Master', u'PCM']
|
||||
|
||||
def get_volume(self):
|
||||
# FIXME does not seem to see external volume changes.
|
||||
return self._mixer.getvolume()[0]
|
||||
|
||||
def set_volume(self, volume):
|
||||
self._mixer.setvolume(volume)
|
||||
@ -1,63 +0,0 @@
|
||||
import logging
|
||||
|
||||
from mopidy import listeners, settings
|
||||
|
||||
logger = logging.getLogger('mopidy.mixers')
|
||||
|
||||
class BaseMixer(object):
|
||||
# TODO: remove completly
|
||||
amplification_factor = 1.0
|
||||
|
||||
@property
|
||||
def volume(self):
|
||||
"""
|
||||
The audio volume
|
||||
|
||||
Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
|
||||
equal to 0. Values above 100 is equal to 100.
|
||||
"""
|
||||
if not hasattr(self, '_user_volume'):
|
||||
self._user_volume = 0
|
||||
volume = self.get_volume()
|
||||
if volume is None or not self.amplification_factor < 1:
|
||||
return volume
|
||||
else:
|
||||
user_volume = int(volume / self.amplification_factor)
|
||||
if (user_volume - 1) <= self._user_volume <= (user_volume + 1):
|
||||
return self._user_volume
|
||||
else:
|
||||
return user_volume
|
||||
|
||||
@volume.setter
|
||||
def volume(self, volume):
|
||||
if not hasattr(self, '_user_volume'):
|
||||
self._user_volume = 0
|
||||
volume = int(volume)
|
||||
if volume < 0:
|
||||
volume = 0
|
||||
elif volume > 100:
|
||||
volume = 100
|
||||
self._user_volume = volume
|
||||
real_volume = int(volume * self.amplification_factor)
|
||||
self.set_volume(real_volume)
|
||||
self._trigger_volume_changed()
|
||||
|
||||
def get_volume(self):
|
||||
"""
|
||||
Return volume as integer in range [0, 100]. :class:`None` if unknown.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""
|
||||
Set volume as integer in range [0, 100].
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _trigger_volume_changed(self):
|
||||
logger.debug(u'Triggering volume changed event')
|
||||
listeners.BackendListener.send('volume_changed')
|
||||
@ -1,58 +0,0 @@
|
||||
import logging
|
||||
|
||||
from pykka.actor import ThreadingActor
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
logger = logging.getLogger(u'mopidy.mixers.denon')
|
||||
|
||||
class DenonMixer(ThreadingActor, BaseMixer):
|
||||
"""
|
||||
Mixer for controlling Denon amplifiers and receivers using the RS-232
|
||||
protocol.
|
||||
|
||||
The external mixer is the authoritative source for the current volume.
|
||||
This allows the user to use his remote control the volume without Mopidy
|
||||
cancelling the volume setting.
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- pyserial (python-serial on Debian/Ubuntu)
|
||||
|
||||
**Settings**
|
||||
|
||||
- :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0``
|
||||
"""
|
||||
|
||||
def __init__(self, device=None):
|
||||
super(DenonMixer, self).__init__()
|
||||
self._device = device
|
||||
self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)]
|
||||
self._volume = 0
|
||||
|
||||
def on_start(self):
|
||||
if self._device is None:
|
||||
from serial import Serial
|
||||
self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2)
|
||||
|
||||
def get_volume(self):
|
||||
self._ensure_open_device()
|
||||
self._device.write('MV?\r')
|
||||
vol = str(self._device.readline()[2:4])
|
||||
logger.debug(u'_get_volume() = %s' % vol)
|
||||
return self._levels.index(vol)
|
||||
|
||||
def set_volume(self, volume):
|
||||
# Clamp according to Denon-spec
|
||||
if volume > 99:
|
||||
volume = 99
|
||||
self._ensure_open_device()
|
||||
self._device.write('MV%s\r'% self._levels[volume])
|
||||
vol = self._device.readline()[2:4]
|
||||
self._volume = self._levels.index(vol)
|
||||
|
||||
def _ensure_open_device(self):
|
||||
if not self._device.isOpen():
|
||||
logger.debug(u'(re)connecting to Denon device')
|
||||
self._device.open()
|
||||
@ -1,16 +0,0 @@
|
||||
from pykka.actor import ThreadingActor
|
||||
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
class DummyMixer(ThreadingActor, BaseMixer):
|
||||
"""Mixer which just stores and reports the chosen volume."""
|
||||
|
||||
def __init__(self):
|
||||
super(DummyMixer, self).__init__()
|
||||
self._volume = None
|
||||
|
||||
def get_volume(self):
|
||||
return self._volume
|
||||
|
||||
def set_volume(self, volume):
|
||||
self._volume = volume
|
||||
@ -1,23 +0,0 @@
|
||||
from pykka.actor import ThreadingActor
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
from mopidy.gstreamer import GStreamer
|
||||
|
||||
class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
|
||||
"""Mixer which uses GStreamer to control volume in software."""
|
||||
|
||||
def __init__(self):
|
||||
super(GStreamerSoftwareMixer, self).__init__()
|
||||
self.output = None
|
||||
|
||||
def on_start(self):
|
||||
output_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(output_refs) == 1, 'Expected exactly one running output.'
|
||||
self.output = output_refs[0].proxy()
|
||||
|
||||
def get_volume(self):
|
||||
return self.output.get_volume().get()
|
||||
|
||||
def set_volume(self, volume):
|
||||
self.output.set_volume(volume).get()
|
||||
@ -1,198 +0,0 @@
|
||||
import logging
|
||||
import serial
|
||||
|
||||
from pykka.actor import ThreadingActor
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
logger = logging.getLogger('mopidy.mixers.nad')
|
||||
|
||||
class NadMixer(ThreadingActor, BaseMixer):
|
||||
"""
|
||||
Mixer for controlling NAD amplifiers and receivers using the NAD RS-232
|
||||
protocol.
|
||||
|
||||
The NAD mixer was created using a NAD C 355BEE amplifier, but should also
|
||||
work with other NAD amplifiers supporting the same RS-232 protocol (v2.x).
|
||||
The C 355BEE does not give you access to the current volume. It only
|
||||
supports increasing or decreasing the volume one step at the time. Other
|
||||
NAD amplifiers may support more advanced volume adjustment than what is
|
||||
currently used by this mixer.
|
||||
|
||||
Sadly, this means that if you use the remote control to change the volume
|
||||
on the amplifier, Mopidy will no longer report the correct volume.
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- pyserial (python-serial on Debian/Ubuntu)
|
||||
|
||||
**Settings**
|
||||
|
||||
- :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0``
|
||||
- :attr:`mopidy.settings.MIXER_EXT_SOURCE` -- Example: ``Aux``
|
||||
- :attr:`mopidy.settings.MIXER_EXT_SPEAKERS_A` -- Example: ``On``
|
||||
- :attr:`mopidy.settings.MIXER_EXT_SPEAKERS_B` -- Example: ``Off``
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(NadMixer, self).__init__()
|
||||
self._volume_cache = None
|
||||
self._nad_talker = NadTalker.start().proxy()
|
||||
|
||||
def get_volume(self):
|
||||
return self._volume_cache
|
||||
|
||||
def set_volume(self, volume):
|
||||
self._volume_cache = volume
|
||||
self._nad_talker.set_volume(volume)
|
||||
|
||||
|
||||
class NadTalker(ThreadingActor):
|
||||
"""
|
||||
Independent process which does the communication with the NAD device.
|
||||
|
||||
Since the communication is done in an independent process, Mopidy won't
|
||||
block other requests while doing rather time consuming work like
|
||||
calibrating the NAD device's volume.
|
||||
"""
|
||||
|
||||
# Timeout in seconds used for read/write operations.
|
||||
# If you set the timeout too low, the reads will never get complete
|
||||
# confirmations and calibration will decrease volume forever. If you set
|
||||
# the timeout too high, stuff takes more time. 0.2s seems like a good value
|
||||
# for NAD C 355BEE.
|
||||
TIMEOUT = 0.2
|
||||
|
||||
# Number of volume levels the device supports. 40 for NAD C 355BEE.
|
||||
VOLUME_LEVELS = 40
|
||||
|
||||
# Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration.
|
||||
_nad_volume = None
|
||||
|
||||
def __init__(self):
|
||||
super(NadTalker, self).__init__()
|
||||
self._device = None
|
||||
|
||||
def on_start(self):
|
||||
self._open_connection()
|
||||
self._set_device_to_known_state()
|
||||
|
||||
def _open_connection(self):
|
||||
# Opens serial connection to the device.
|
||||
# Communication settings: 115200 bps 8N1
|
||||
logger.info(u'Connecting to serial device "%s"',
|
||||
settings.MIXER_EXT_PORT)
|
||||
self._device = serial.Serial(port=settings.MIXER_EXT_PORT,
|
||||
baudrate=115200, timeout=self.TIMEOUT)
|
||||
self._get_device_model()
|
||||
|
||||
def _set_device_to_known_state(self):
|
||||
self._power_device_on()
|
||||
self._select_speakers()
|
||||
self._select_input_source()
|
||||
self._unmute()
|
||||
self._calibrate_volume()
|
||||
|
||||
def _get_device_model(self):
|
||||
model = self._ask_device('Main.Model')
|
||||
logger.info(u'Connected to device of model "%s"', model)
|
||||
return model
|
||||
|
||||
def _power_device_on(self):
|
||||
while self._ask_device('Main.Power') != 'On':
|
||||
logger.info(u'Powering device on')
|
||||
self._command_device('Main.Power', 'On')
|
||||
|
||||
def _select_speakers(self):
|
||||
if settings.MIXER_EXT_SPEAKERS_A is not None:
|
||||
while (self._ask_device('Main.SpeakerA')
|
||||
!= settings.MIXER_EXT_SPEAKERS_A):
|
||||
logger.info(u'Setting speakers A "%s"',
|
||||
settings.MIXER_EXT_SPEAKERS_A)
|
||||
self._command_device('Main.SpeakerA',
|
||||
settings.MIXER_EXT_SPEAKERS_A)
|
||||
if settings.MIXER_EXT_SPEAKERS_B is not None:
|
||||
while (self._ask_device('Main.SpeakerB') !=
|
||||
settings.MIXER_EXT_SPEAKERS_B):
|
||||
logger.info(u'Setting speakers B "%s"',
|
||||
settings.MIXER_EXT_SPEAKERS_B)
|
||||
self._command_device('Main.SpeakerB',
|
||||
settings.MIXER_EXT_SPEAKERS_B)
|
||||
|
||||
def _select_input_source(self):
|
||||
if settings.MIXER_EXT_SOURCE is not None:
|
||||
while self._ask_device('Main.Source') != settings.MIXER_EXT_SOURCE:
|
||||
logger.info(u'Selecting input source "%s"',
|
||||
settings.MIXER_EXT_SOURCE)
|
||||
self._command_device('Main.Source', settings.MIXER_EXT_SOURCE)
|
||||
|
||||
def _unmute(self):
|
||||
while self._ask_device('Main.Mute') != 'Off':
|
||||
logger.info(u'Unmuting device')
|
||||
self._command_device('Main.Mute', 'Off')
|
||||
|
||||
def _ask_device(self, key):
|
||||
self._write('%s?' % key)
|
||||
return self._readline().replace('%s=' % key, '')
|
||||
|
||||
def _command_device(self, key, value):
|
||||
if type(value) == unicode:
|
||||
value = value.encode('utf-8')
|
||||
self._write('%s=%s' % (key, value))
|
||||
self._readline()
|
||||
|
||||
def _calibrate_volume(self):
|
||||
# The NAD C 355BEE amplifier has 40 different volume levels. We have no
|
||||
# way of asking on which level we are. Thus, we must calibrate the
|
||||
# mixer by decreasing the volume 39 times.
|
||||
logger.info(u'Calibrating NAD amplifier')
|
||||
steps_left = self.VOLUME_LEVELS - 1
|
||||
while steps_left:
|
||||
if self._decrease_volume():
|
||||
steps_left -= 1
|
||||
self._nad_volume = 0
|
||||
logger.info(u'Done calibrating NAD amplifier')
|
||||
|
||||
def set_volume(self, volume):
|
||||
# Increase or decrease the amplifier volume until it matches the given
|
||||
# target volume.
|
||||
logger.debug(u'Setting volume to %d' % volume)
|
||||
target_nad_volume = int(round(volume * self.VOLUME_LEVELS / 100.0))
|
||||
if self._nad_volume is None:
|
||||
return # Calibration needed
|
||||
while target_nad_volume > self._nad_volume:
|
||||
if self._increase_volume():
|
||||
self._nad_volume += 1
|
||||
while target_nad_volume < self._nad_volume:
|
||||
if self._decrease_volume():
|
||||
self._nad_volume -= 1
|
||||
|
||||
def _increase_volume(self):
|
||||
# Increase volume. Returns :class:`True` if confirmed by device.
|
||||
self._write('Main.Volume+')
|
||||
return self._readline() == 'Main.Volume+'
|
||||
|
||||
def _decrease_volume(self):
|
||||
# Decrease volume. Returns :class:`True` if confirmed by device.
|
||||
self._write('Main.Volume-')
|
||||
return self._readline() == 'Main.Volume-'
|
||||
|
||||
def _write(self, data):
|
||||
# Write data to device. Prepends and appends a newline to the data, as
|
||||
# recommended by the NAD documentation.
|
||||
if not self._device.isOpen():
|
||||
self._device.open()
|
||||
self._device.write('\n%s\n' % data)
|
||||
logger.debug('Write: %s', data)
|
||||
|
||||
def _readline(self):
|
||||
# Read line from device. The result is stripped for leading and
|
||||
# trailing whitespace.
|
||||
if not self._device.isOpen():
|
||||
self._device.open()
|
||||
result = self._device.readline().strip()
|
||||
if result:
|
||||
logger.debug('Read: %s', result)
|
||||
return result
|
||||
@ -1,46 +0,0 @@
|
||||
from subprocess import Popen, PIPE
|
||||
import time
|
||||
|
||||
from pykka.actor import ThreadingActor
|
||||
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
|
||||
class OsaMixer(ThreadingActor, BaseMixer):
|
||||
"""
|
||||
Mixer which uses ``osascript`` on OS X to control volume.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- None
|
||||
"""
|
||||
|
||||
CACHE_TTL = 30
|
||||
|
||||
_cache = None
|
||||
_last_update = None
|
||||
|
||||
def _valid_cache(self):
|
||||
return (self._cache is not None
|
||||
and self._last_update is not None
|
||||
and (int(time.time() - self._last_update) < self.CACHE_TTL))
|
||||
|
||||
def get_volume(self):
|
||||
if not self._valid_cache():
|
||||
try:
|
||||
self._cache = int(Popen(
|
||||
['osascript', '-e',
|
||||
'output volume of (get volume settings)'],
|
||||
stdout=PIPE).communicate()[0])
|
||||
except ValueError:
|
||||
self._cache = None
|
||||
self._last_update = int(time.time())
|
||||
return self._cache
|
||||
|
||||
def set_volume(self, volume):
|
||||
Popen(['osascript', '-e', 'set volume output volume %d' % volume])
|
||||
self._cache = volume
|
||||
self._last_update = int(time.time())
|
||||
@ -1,38 +0,0 @@
|
||||
class BaseMixerTest(object):
|
||||
MIN = 0
|
||||
MAX = 100
|
||||
ACTUAL_MIN = MIN
|
||||
ACTUAL_MAX = MAX
|
||||
INITIAL = None
|
||||
|
||||
mixer_class = None
|
||||
|
||||
def setUp(self):
|
||||
assert self.mixer_class is not None, \
|
||||
"mixer_class must be set in subclass"
|
||||
# pylint: disable = E1102
|
||||
self.mixer = self.mixer_class()
|
||||
# pylint: enable = E1102
|
||||
|
||||
def test_initial_volume(self):
|
||||
self.assertEqual(self.mixer.volume, self.INITIAL)
|
||||
|
||||
def test_volume_set_to_min(self):
|
||||
self.mixer.volume = self.MIN
|
||||
self.assertEqual(self.mixer.volume, self.ACTUAL_MIN)
|
||||
|
||||
def test_volume_set_to_max(self):
|
||||
self.mixer.volume = self.MAX
|
||||
self.assertEqual(self.mixer.volume, self.ACTUAL_MAX)
|
||||
|
||||
def test_volume_set_to_below_min_results_in_min(self):
|
||||
self.mixer.volume = -10
|
||||
self.assertEqual(self.mixer.volume, self.ACTUAL_MIN)
|
||||
|
||||
def test_volume_set_to_above_max_results_in_max(self):
|
||||
self.mixer.volume = self.MAX + 10
|
||||
self.assertEqual(self.mixer.volume, self.ACTUAL_MAX)
|
||||
|
||||
def test_volume_is_not_float(self):
|
||||
self.mixer.volume = 1.0 / 3 * 100
|
||||
self.assertEqual(self.mixer.volume, 33)
|
||||
@ -1,42 +0,0 @@
|
||||
from mopidy.mixers.denon import DenonMixer
|
||||
from tests.mixers.base_test import BaseMixerTest
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class DenonMixerDeviceMock(object):
|
||||
def __init__(self):
|
||||
self._open = True
|
||||
self.ret_val = bytes('MV00\r')
|
||||
|
||||
def write(self, x):
|
||||
if x[2] != '?':
|
||||
self.ret_val = bytes(x)
|
||||
|
||||
def read(self, x):
|
||||
return self.ret_val
|
||||
|
||||
def readline(self):
|
||||
return self.ret_val
|
||||
|
||||
def isOpen(self):
|
||||
return self._open
|
||||
|
||||
def open(self):
|
||||
self._open = True
|
||||
|
||||
|
||||
class DenonMixerTest(BaseMixerTest, unittest.TestCase):
|
||||
ACTUAL_MAX = 99
|
||||
INITIAL = 1
|
||||
|
||||
mixer_class = DenonMixer
|
||||
|
||||
def setUp(self):
|
||||
self.device = DenonMixerDeviceMock()
|
||||
self.mixer = DenonMixer(device=self.device)
|
||||
|
||||
def test_reopen_device(self):
|
||||
self.device._open = False
|
||||
self.mixer.volume = 10
|
||||
self.assertTrue(self.device.isOpen())
|
||||
@ -1,23 +0,0 @@
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
|
||||
from tests import unittest
|
||||
from tests.mixers.base_test import BaseMixerTest
|
||||
|
||||
|
||||
class DummyMixerTest(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)
|
||||
|
||||
def test_get_volume_get_the_same_number_as_was_set(self):
|
||||
self.mixer.amplification_factor = 0.5
|
||||
self.mixer.volume = 13
|
||||
self.assertEquals(self.mixer.volume, 13)
|
||||
Loading…
Reference in New Issue
Block a user