Move NAD mixer to an external extension
This commit is contained in:
parent
f2b42c53d1
commit
3e366efb20
@ -47,6 +47,9 @@ and improved.
|
||||
as extensions, but they are still distributed together with Mopidy and are
|
||||
enabled by default.
|
||||
|
||||
- The NAD mixer have been moved out of Mopidy core to its own project,
|
||||
Mopidy-NAD. See :ref:`ext` for more information.
|
||||
|
||||
**Command line options**
|
||||
|
||||
- The command option :option:`mopidy --list-settings` is now named
|
||||
|
||||
@ -30,6 +30,21 @@ These extensions are maintained outside Mopidy's core, often by other
|
||||
developers.
|
||||
|
||||
|
||||
Mopidy-NAD
|
||||
----------
|
||||
|
||||
Extension for controlling volume using an external NAD amplifier.
|
||||
|
||||
Author:
|
||||
Stein Magnus Jodal
|
||||
PyPI:
|
||||
`Mopidy-NAD <https://pypi.python.org/pypi/Mopidy-NAD>`_
|
||||
GitHub:
|
||||
`mopidy/mopidy-nad <https://github.com/mopidy/mopidy-nad>`_
|
||||
Issues:
|
||||
`mopidy/mopidy <https://github.com/mopidy/mopidy/issues>`_
|
||||
|
||||
|
||||
Mopidy-SoundCloud
|
||||
-----------------
|
||||
|
||||
@ -42,3 +57,5 @@ PyPI:
|
||||
`Mopidy-SoundCloud <https://pypi.python.org/pypi/Mopidy-SoundCloud>`_
|
||||
GitHub:
|
||||
`dz0ny/mopidy-soundcloud <https://github.com/dz0ny/mopidy-soundcloud>`_
|
||||
Issues:
|
||||
`dz0ny/mopidy-soundcloud <https://github.com/dz0ny/mopidy-soundcloud/issues>`_
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
*********************************************
|
||||
:mod:`mopidy.audio.mixers.nad` -- NAD mixer
|
||||
*********************************************
|
||||
|
||||
.. automodule:: mopidy.audio.mixers.nad
|
||||
:synopsis: Mixer element for controlling volume on NAD amplifiers
|
||||
@ -7,7 +7,6 @@ import gobject
|
||||
|
||||
from .auto import AutoAudioMixer
|
||||
from .fake import FakeMixer
|
||||
from .nad import NadMixer
|
||||
|
||||
|
||||
def register_mixer(mixer_class):
|
||||
@ -19,4 +18,3 @@ def register_mixer(mixer_class):
|
||||
def register_mixers():
|
||||
register_mixer(AutoAudioMixer)
|
||||
register_mixer(FakeMixer)
|
||||
register_mixer(NadMixer)
|
||||
|
||||
@ -1,292 +0,0 @@
|
||||
"""Mixer that controls volume using a NAD amplifier.
|
||||
|
||||
The NAD amplifier must be connected to the machine running Mopidy using a
|
||||
serial cable.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
.. literalinclude:: ../../../../requirements/external_mixers.txt
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Set the :confval:`audio/mixer` config value to ``nadmixer`` to use it. You
|
||||
probably also needs to add some properties to the :confval:`audio/mixer` config
|
||||
value.
|
||||
|
||||
Supported properties includes:
|
||||
|
||||
``port``:
|
||||
The serial device to use, defaults to ``/dev/ttyUSB0``. This must be
|
||||
set correctly for the mixer to work.
|
||||
|
||||
``source``:
|
||||
The source that should be selected on the amplifier, like ``aux``,
|
||||
``disc``, ``tape``, ``tuner``, etc. Leave unset if you don't want the
|
||||
mixer to change it for you.
|
||||
|
||||
``speakers-a``:
|
||||
Set to ``on`` or ``off`` if you want the mixer to make sure that
|
||||
speaker set A is turned on or off. Leave unset if you don't want the
|
||||
mixer to change it for you.
|
||||
|
||||
``speakers-b``:
|
||||
See ``speakers-a``.
|
||||
|
||||
Configuration examples::
|
||||
|
||||
# Minimum configuration, if the amplifier is available at /dev/ttyUSB0
|
||||
mixer = nadmixer
|
||||
|
||||
# Minimum configuration, if the amplifier is available elsewhere
|
||||
mixer = nadmixer port=/dev/ttyUSB3
|
||||
|
||||
# Full configuration
|
||||
mixer = nadmixer port=/dev/ttyUSB0 source=aux speakers-a=on speakers-b=off
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gobject
|
||||
import gst
|
||||
|
||||
try:
|
||||
import serial
|
||||
except ImportError:
|
||||
serial = None # noqa
|
||||
|
||||
import pykka
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.audio.mixers.nad')
|
||||
|
||||
|
||||
class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
|
||||
__gstdetails__ = (
|
||||
'NadMixer',
|
||||
'Mixer',
|
||||
'Mixer to control NAD amplifiers using a serial link',
|
||||
'Mopidy')
|
||||
|
||||
port = gobject.property(type=str, default='/dev/ttyUSB0')
|
||||
source = gobject.property(type=str)
|
||||
speakers_a = gobject.property(type=str)
|
||||
speakers_b = gobject.property(type=str)
|
||||
|
||||
_volume_cache = 0
|
||||
_nad_talker = None
|
||||
|
||||
def list_tracks(self):
|
||||
track = utils.create_track(
|
||||
label='Master',
|
||||
initial_volume=0,
|
||||
min_volume=0,
|
||||
max_volume=100,
|
||||
num_channels=1,
|
||||
flags=(
|
||||
gst.interfaces.MIXER_TRACK_MASTER |
|
||||
gst.interfaces.MIXER_TRACK_OUTPUT))
|
||||
return [track]
|
||||
|
||||
def get_volume(self, track):
|
||||
return [self._volume_cache]
|
||||
|
||||
def set_volume(self, track, volumes):
|
||||
if len(volumes):
|
||||
volume = volumes[0]
|
||||
self._volume_cache = volume
|
||||
self._nad_talker.set_volume(volume)
|
||||
|
||||
def set_mute(self, track, mute):
|
||||
self._nad_talker.mute(mute)
|
||||
|
||||
def do_change_state(self, transition):
|
||||
if transition == gst.STATE_CHANGE_NULL_TO_READY:
|
||||
if serial is None:
|
||||
logger.warning('nadmixer dependency python-serial not found')
|
||||
return gst.STATE_CHANGE_FAILURE
|
||||
self._start_nad_talker()
|
||||
return gst.STATE_CHANGE_SUCCESS
|
||||
|
||||
def _start_nad_talker(self):
|
||||
self._nad_talker = NadTalker.start(
|
||||
port=self.port,
|
||||
source=self.source or None,
|
||||
speakers_a=self.speakers_a or None,
|
||||
speakers_b=self.speakers_b or None
|
||||
).proxy()
|
||||
|
||||
|
||||
class NadTalker(pykka.ThreadingActor):
|
||||
"""
|
||||
Independent thread which does the communication with the NAD amplifier.
|
||||
|
||||
Since the communication is done in an independent thread, Mopidy won't
|
||||
block other requests while doing rather time consuming work like
|
||||
calibrating the NAD amplifier's volume.
|
||||
"""
|
||||
|
||||
# Serial link config
|
||||
BAUDRATE = 115200
|
||||
BYTESIZE = 8
|
||||
PARITY = 'N'
|
||||
STOPBITS = 1
|
||||
|
||||
# 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 amplifier supports. 40 for NAD C 355BEE.
|
||||
VOLUME_LEVELS = 40
|
||||
|
||||
def __init__(self, port, source, speakers_a, speakers_b):
|
||||
super(NadTalker, self).__init__()
|
||||
|
||||
self.port = port
|
||||
self.source = source
|
||||
self.speakers_a = speakers_a
|
||||
self.speakers_b = speakers_b
|
||||
|
||||
# Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration.
|
||||
self._nad_volume = None
|
||||
|
||||
self._device = None
|
||||
|
||||
def on_start(self):
|
||||
self._open_connection()
|
||||
self._set_device_to_known_state()
|
||||
|
||||
def _open_connection(self):
|
||||
logger.info('NAD amplifier: Connecting through "%s"', self.port)
|
||||
self._device = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=self.BAUDRATE,
|
||||
bytesize=self.BYTESIZE,
|
||||
parity=self.PARITY,
|
||||
stopbits=self.STOPBITS,
|
||||
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.mute(False)
|
||||
self.calibrate_volume()
|
||||
|
||||
def _get_device_model(self):
|
||||
model = self._ask_device('Main.Model')
|
||||
logger.info('NAD amplifier: Connected to model "%s"', model)
|
||||
return model
|
||||
|
||||
def _power_device_on(self):
|
||||
self._check_and_set('Main.Power', 'On')
|
||||
|
||||
def _select_speakers(self):
|
||||
if self.speakers_a is not None:
|
||||
self._check_and_set('Main.SpeakerA', self.speakers_a.title())
|
||||
if self.speakers_b is not None:
|
||||
self._check_and_set('Main.SpeakerB', self.speakers_b.title())
|
||||
|
||||
def _select_input_source(self):
|
||||
if self.source is not None:
|
||||
self._check_and_set('Main.Source', self.source.title())
|
||||
|
||||
def mute(self, mute):
|
||||
if mute:
|
||||
self._check_and_set('Main.Mute', 'On')
|
||||
else:
|
||||
self._check_and_set('Main.Mute', 'Off')
|
||||
|
||||
def calibrate_volume(self, current_nad_volume=None):
|
||||
# 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.
|
||||
if current_nad_volume is None:
|
||||
current_nad_volume = self.VOLUME_LEVELS
|
||||
if current_nad_volume == self.VOLUME_LEVELS:
|
||||
logger.info('NAD amplifier: Calibrating by setting volume to 0')
|
||||
self._nad_volume = current_nad_volume
|
||||
if self._decrease_volume():
|
||||
current_nad_volume -= 1
|
||||
if current_nad_volume == 0:
|
||||
logger.info('NAD amplifier: Done calibrating')
|
||||
else:
|
||||
self.actor_ref.proxy().calibrate_volume(current_nad_volume)
|
||||
|
||||
def set_volume(self, volume):
|
||||
# Increase or decrease the amplifier volume until it matches the given
|
||||
# target volume.
|
||||
logger.debug('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 _check_and_set(self, key, value):
|
||||
for attempt in range(1, 4):
|
||||
if self._ask_device(key) == value:
|
||||
return
|
||||
logger.info(
|
||||
'NAD amplifier: Setting "%s" to "%s" (attempt %d/3)',
|
||||
key, value, attempt)
|
||||
self._command_device(key, value)
|
||||
if self._ask_device(key) != value:
|
||||
logger.info(
|
||||
'NAD amplifier: Gave up on setting "%s" to "%s"',
|
||||
key, value)
|
||||
|
||||
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 _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
|
||||
Loading…
Reference in New Issue
Block a user