From 3e366efb2005fca8c2a88f29ffb2f312a7ebe800 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 16 Apr 2013 10:46:22 +0200 Subject: [PATCH] Move NAD mixer to an external extension --- docs/changelog.rst | 3 + docs/ext/index.rst | 17 ++ docs/modules/audio/mixers/nad.rst | 6 - mopidy/audio/mixers/__init__.py | 2 - mopidy/audio/mixers/nad.py | 292 ------------------------------ setup.py | 1 - 6 files changed, 20 insertions(+), 301 deletions(-) delete mode 100644 docs/modules/audio/mixers/nad.rst delete mode 100644 mopidy/audio/mixers/nad.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 894b5883..a2a73dd3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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 diff --git a/docs/ext/index.rst b/docs/ext/index.rst index af51e9bb..691ee083 100644 --- a/docs/ext/index.rst +++ b/docs/ext/index.rst @@ -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 `_ +GitHub: + `mopidy/mopidy-nad `_ +Issues: + `mopidy/mopidy `_ + + Mopidy-SoundCloud ----------------- @@ -42,3 +57,5 @@ PyPI: `Mopidy-SoundCloud `_ GitHub: `dz0ny/mopidy-soundcloud `_ +Issues: + `dz0ny/mopidy-soundcloud `_ diff --git a/docs/modules/audio/mixers/nad.rst b/docs/modules/audio/mixers/nad.rst deleted file mode 100644 index 661dc723..00000000 --- a/docs/modules/audio/mixers/nad.rst +++ /dev/null @@ -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 diff --git a/mopidy/audio/mixers/__init__.py b/mopidy/audio/mixers/__init__.py index feaccc3d..9b14974f 100644 --- a/mopidy/audio/mixers/__init__.py +++ b/mopidy/audio/mixers/__init__.py @@ -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) diff --git a/mopidy/audio/mixers/nad.py b/mopidy/audio/mixers/nad.py deleted file mode 100644 index bbbaee71..00000000 --- a/mopidy/audio/mixers/nad.py +++ /dev/null @@ -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 diff --git a/setup.py b/setup.py index 58c043f8..6b7b2a6f 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ setup( 'spotify': ['pyspotify >= 1.9, < 1.11'], 'scrobbler': ['pylast >= 0.5.7'], 'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'], - 'external_mixers': ['pyserial'], }, test_suite='nose.collector', tests_require=[