From ebead0d0d2c9c89807f0e687e40bddd16a519ec6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 01:00:56 +0200 Subject: [PATCH 01/19] Fix typo in variable name --- mopidy/mixers/fake.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/mixers/fake.py b/mopidy/mixers/fake.py index b697956a..83bde6fc 100644 --- a/mopidy/mixers/fake.py +++ b/mopidy/mixers/fake.py @@ -4,12 +4,12 @@ import gobject import gst -def create_fake_track(label, intial_volume, min_volume, max_volume, +def create_fake_track(label, initial_volume, min_volume, max_volume, num_channels, flags): class Track(gst.interfaces.MixerTrack): def __init__(self): super(Track, self).__init__() - self.volumes = (intial_volume,) * self.num_channels + self.volumes = (initial_volume,) * self.num_channels @gobject.property def label(self): From e3ba389996f986c0e87139594b5cd07143d5944b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 01:24:48 +0200 Subject: [PATCH 02/19] Make MPRIS frontend handle unknown volume --- mopidy/frontends/mpris/objects.py | 5 +++-- tests/frontends/mpris/player_interface_test.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index fa5f9614..6815c0d2 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -371,8 +371,9 @@ class MprisObject(dbus.service.Object): def get_Volume(self): volume = self.backend.playback.volume.get() - if volume is not None: - return volume / 100.0 + if volume is None: + return 0 + return volume / 100.0 def set_Volume(self, value): if not self.get_CanControl(): diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index d09d4f6b..b7ad1b60 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -205,6 +205,10 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(result['xesam:trackNumber'], 7) def test_get_volume_should_return_volume_between_zero_and_one(self): + self.backend.playback.volume = None + result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') + self.assertEquals(result, 0) + self.backend.playback.volume = 0 result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEquals(result, 0) From 641f8d2e2db60149d9ece571053e2ef27808d5aa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 00:39:22 +0200 Subject: [PATCH 03/19] Port NAD hardware mixer to the GStreamer mixer API Fixes #179 --- docs/changes.rst | 20 ++++ mopidy/mixers/__init__.py | 1 + mopidy/mixers/nad.py | 243 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 mopidy/mixers/nad.py diff --git a/docs/changes.rst b/docs/changes.rst index 963802d4..6298c8e3 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -42,6 +42,26 @@ v0.8 (in development) protocol support for volume has also been updated to return -1 when we have no mixer set. +- Removed the Denon hardware mixer, as it is not maintained. + +- Updated the NAD hardware mixer to work in the new GStreamer based mixing + regime. Settings are now passed as GStreamer element properties. In practice + that means that the following old-style config: + + MIXER = u'mopidy.mixers.nad.NadMixer' + MIXER_EXT_PORT = u'/dev/ttyUSB0' + MIXER_EXT_SOURCE = u'Aux' + MIXER_EXT_SPEAKERS_A = u'On' + MIXER_EXT_SPEAKERS_B = u'Off' + + Now is reduced to simply: + + MIXER = u'nadmixer port=/dev/ttyUSB0 source=Aux speakers-a=On speakers-b=Off' + + The ``port`` property defaults to ``/dev/ttyUSB0``, and the rest of the + properties may be left out if you don't want the mixer to adjust the settings + on your NAD amplifier when Mopidy is started. + v0.7.3 (2012-08-11) =================== diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py index cf282a03..259557d1 100644 --- a/mopidy/mixers/__init__.py +++ b/mopidy/mixers/__init__.py @@ -1,2 +1,3 @@ from mopidy.mixers.auto import AutoAudioMixer from mopidy.mixers.fake import FakeMixer +from mopidy.mixers.nad import NadMixer diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py new file mode 100644 index 00000000..e0bfa2d3 --- /dev/null +++ b/mopidy/mixers/nad.py @@ -0,0 +1,243 @@ +import logging + +import pygst +pygst.require('0.10') +import gobject +import gst + +try: + import serial +except ImportError: + serial = None + +from pykka.actor import ThreadingActor + +from mopidy.mixers.fake import create_fake_track + + +logger = logging.getLogger('mopidy.mixers.nad') + + +class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): + __gstdetails__ = ('NadMixer', + 'Mixer', + 'Mixer to control NAD amplifiers using a serial link', + 'Stein Magnus Jodal') + + 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) + + def __init__(self): + gst.Element.__init__(self) + self._volume_cache = 0 + self._nad_talker = None + + def list_tracks(self): + track = create_fake_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): + if mute: + self._nad_talker.mute() + else: + self._nad_talker.unmute() + + def do_change_state(self, transition): + if transition == gst.STATE_CHANGE_NULL_TO_READY: + if serial is None: + logger.warning(u'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() + + +gobject.type_register(NadMixer) +gst.element_register(NadMixer, 'nadmixer', gst.RANK_MARGINAL) + + +class NadTalker(ThreadingActor): + """ + Independent thread which does the communication with the NAD device. + + 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 device's volume. + """ + + # Serial link settings + 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 device 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 + if speakers_a in ('On', 'Off'): + self.speakers_a = speakers_a + else: + logger.warning('speakers-a must be "On" or "Off", or unset') + self.speakers_a = None + if speakers_b in ('On', 'Off'): + self.speakers_b = speakers_b + else: + logger.warning('speakers-b must be "On" or "Off", or unset') + self.speakers_b = None + + # 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(u'Connecting to NAD amplifier using serial device "%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._unmute() + self._calibrate_volume() + + def _get_device_model(self): + model = self._ask_device('Main.Model') + logger.info(u'Connected to NAD amplifier 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 self.speakers_a is not None: + while (self._ask_device('Main.SpeakerA') != self.speakers_a): + logger.info(u'Setting speakers A to "%s"', self.speakers_a) + self._command_device('Main.SpeakerA', self.speakers_a) + if self.speakers_b is not None: + while (self._ask_device('Main.SpeakerB') != self.speakers_b): + logger.info(u'Setting speakers B to "%s"', self.speakers_b) + self._command_device('Main.SpeakerB', self.speakers_b) + + def _select_input_source(self): + if self.source is not None: + while self._ask_device('Main.Source') != self.source: + logger.info(u'Selecting input source "%s"', self.source) + self._command_device('Main.Source', self.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 by setting volume to 0') + self._nad_volume = self.VOLUME_LEVELS + self.set_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 From cd021cc8198932969c055d55b6e4a5c0daadcbf6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 22:03:30 +0200 Subject: [PATCH 04/19] Move create_track() out of the fakemixer as it is useful for other mixers --- mopidy/mixers/__init__.py | 37 ++++++++++++++++++++++++++++++ mopidy/mixers/fake.py | 48 +++++++-------------------------------- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py index cf282a03..87fbd52f 100644 --- a/mopidy/mixers/__init__.py +++ b/mopidy/mixers/__init__.py @@ -1,2 +1,39 @@ +import pygst +pygst.require('0.10') +import gst +import gobject + + +def create_track(label, initial_volume, min_volume, max_volume, + num_channels, flags): + class Track(gst.interfaces.MixerTrack): + def __init__(self): + super(Track, self).__init__() + self.volumes = (initial_volume,) * self.num_channels + + @gobject.property + def label(self): + return label + + @gobject.property + def min_volume(self): + return min_volume + + @gobject.property + def max_volume(self): + return max_volume + + @gobject.property + def num_channels(self): + return num_channels + + @gobject.property + def flags(self): + return flags + + return Track() + + +# Import all mixers so that they are registered with GStreamer from mopidy.mixers.auto import AutoAudioMixer from mopidy.mixers.fake import FakeMixer diff --git a/mopidy/mixers/fake.py b/mopidy/mixers/fake.py index 83bde6fc..3c47ef33 100644 --- a/mopidy/mixers/fake.py +++ b/mopidy/mixers/fake.py @@ -3,35 +3,7 @@ pygst.require('0.10') import gobject import gst - -def create_fake_track(label, initial_volume, min_volume, max_volume, - num_channels, flags): - class Track(gst.interfaces.MixerTrack): - def __init__(self): - super(Track, self).__init__() - self.volumes = (initial_volume,) * self.num_channels - - @gobject.property - def label(self): - return label - - @gobject.property - def min_volume(self): - return min_volume - - @gobject.property - def max_volume(self): - return max_volume - - @gobject.property - def num_channels(self): - return num_channels - - @gobject.property - def flags(self): - return flags - - return Track() +from mopidy.mixers import create_track class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): @@ -41,15 +13,10 @@ class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): 'Thomas Adamcik') track_label = gobject.property(type=str, default='Master') - track_initial_volume = gobject.property(type=int, default=0) - track_min_volume = gobject.property(type=int, default=0) - track_max_volume = gobject.property(type=int, default=100) - track_num_channels = gobject.property(type=int, default=2) - track_flags = gobject.property(type=int, default=(gst.interfaces.MIXER_TRACK_MASTER | gst.interfaces.MIXER_TRACK_OUTPUT)) @@ -58,12 +25,13 @@ class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): gst.Element.__init__(self) def list_tracks(self): - track = create_fake_track(self.track_label, - self.track_initial_volume, - self.track_min_volume, - self.track_max_volume, - self.track_num_channels, - self.track_flags) + track = create_track( + self.track_label, + self.track_initial_volume, + self.track_min_volume, + self.track_max_volume, + self.track_num_channels, + self.track_flags) return [track] def get_volume(self, track): From f97ce0f06a60018ddc8f9cb850b25f5fa925197f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 22:17:09 +0200 Subject: [PATCH 05/19] Add note about how to avoid cyclic imports --- mopidy/mixers/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py index 87fbd52f..2067a4b5 100644 --- a/mopidy/mixers/__init__.py +++ b/mopidy/mixers/__init__.py @@ -34,6 +34,9 @@ def create_track(label, initial_volume, min_volume, max_volume, return Track() -# Import all mixers so that they are registered with GStreamer +# Import all mixers so that they are registered with GStreamer. +# +# Keep these imports at the bottom of the file to avoid cyclic import problems +# when mixers use the above code. from mopidy.mixers.auto import AutoAudioMixer from mopidy.mixers.fake import FakeMixer From 1ccdb08420eb2af253f4efa319703605e7db15ad Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 22:18:23 +0200 Subject: [PATCH 06/19] Use create_track() from new location --- mopidy/mixers/nad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index e0bfa2d3..49af8f06 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -12,7 +12,7 @@ except ImportError: from pykka.actor import ThreadingActor -from mopidy.mixers.fake import create_fake_track +from mopidy.mixers import create_track logger = logging.getLogger('mopidy.mixers.nad') @@ -35,7 +35,7 @@ class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): self._nad_talker = None def list_tracks(self): - track = create_fake_track( + track = create_track( label='Master', initial_volume=0, min_volume=0, From 5368f75f3a41cca0fe333f5f75dea272d88968d2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 22:22:05 +0200 Subject: [PATCH 07/19] Fix set_mute() implementation --- mopidy/mixers/nad.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 49af8f06..da205460 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -55,10 +55,7 @@ class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): self._nad_talker.set_volume(volume) def set_mute(self, track, mute): - if mute: - self._nad_talker.mute() - else: - self._nad_talker.unmute() + self._nad_talker.mute(mute) def do_change_state(self, transition): if transition == gst.STATE_CHANGE_NULL_TO_READY: @@ -147,7 +144,7 @@ class NadTalker(ThreadingActor): self._power_device_on() self._select_speakers() self._select_input_source() - self._unmute() + self.mute(False) self._calibrate_volume() def _get_device_model(self): @@ -176,10 +173,15 @@ class NadTalker(ThreadingActor): logger.info(u'Selecting input source "%s"', self.source) self._command_device('Main.Source', self.source) - def _unmute(self): - while self._ask_device('Main.Mute') != 'Off': - logger.info(u'Unmuting device') - self._command_device('Main.Mute', 'Off') + def mute(self, mute): + if mute: + while self._ask_device('Main.Mute') != 'On': + logger.info(u'Muting NAD amplifier') + self._command_device('Main.Mute', 'On') + else: + while self._ask_device('Main.Mute') != 'Off': + logger.info(u'Unmuting NAD amplifier') + self._command_device('Main.Mute', 'Off') def _ask_device(self, key): self._write('%s?' % key) From e741b4db7551795afb6c40ffb21bb31efa8a720d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 22:24:48 +0200 Subject: [PATCH 08/19] Prefer the word 'amplifier' over too generic 'device' in docs and log messages --- mopidy/mixers/nad.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index da205460..ce860d4a 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -80,11 +80,11 @@ gst.element_register(NadMixer, 'nadmixer', gst.RANK_MARGINAL) class NadTalker(ThreadingActor): """ - Independent thread which does the communication with the NAD device. + 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 device's volume. + calibrating the NAD amplifier's volume. """ # Serial link settings @@ -100,7 +100,7 @@ class NadTalker(ThreadingActor): # for NAD C 355BEE. TIMEOUT = 0.2 - # Number of volume levels the device supports. 40 for NAD C 355BEE. + # Number of volume levels the amplifier supports. 40 for NAD C 355BEE. VOLUME_LEVELS = 40 def __init__(self, port, source, speakers_a, speakers_b): @@ -129,7 +129,7 @@ class NadTalker(ThreadingActor): self._set_device_to_known_state() def _open_connection(self): - logger.info(u'Connecting to NAD amplifier using serial device "%s"', + logger.info(u'Connecting to NAD amplifier using "%s"', self.port) self._device = serial.Serial( port=self.port, @@ -154,7 +154,7 @@ class NadTalker(ThreadingActor): def _power_device_on(self): while self._ask_device('Main.Power') != 'On': - logger.info(u'Powering device on') + logger.info(u'Powering NAD amplifier on') self._command_device('Main.Power', 'On') def _select_speakers(self): From e57a71729a8bf937232e36ee70ac96ccb99cf124 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 23:01:08 +0200 Subject: [PATCH 09/19] Don't fail on GStreamer EOS if no backend is running This removes the printed AssertionError when running ScannerTest.test_data_is_set() --- mopidy/gstreamer.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 5adfd754..d9157a02 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -133,9 +133,7 @@ class GStreamer(ThreadingActor): def _on_message(self, bus, message): if message.type == gst.MESSAGE_EOS: - logger.debug(u'GStreamer signalled end-of-stream. ' - 'Telling backend ...') - self._get_backend().playback.on_end_of_track() + self._notify_backend_of_eos() elif message.type == gst.MESSAGE_ERROR: error, debug = message.parse_error() logger.error(u'%s %s', error, debug) @@ -144,10 +142,14 @@ class GStreamer(ThreadingActor): error, debug = message.parse_warning() logger.warning(u'%s %s', error, debug) - def _get_backend(self): + def _notify_backend_of_eos(self): backend_refs = ActorRegistry.get_by_class(Backend) - assert len(backend_refs) == 1, 'Expected exactly one running backend.' - return backend_refs[0].proxy() + assert len(backend_refs) <= 1, 'Expected at most one running backend.' + if backend_refs: + logger.debug(u'Notifying backend of end-of-stream.') + backend_refs[0].proxy().playback.on_end_of_track() + else: + logger.debug(u'No backend to notify of end-of-stream found.') def set_uri(self, uri): """ From e948a51310e8f914118b748811972f83b9a43db3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 23:05:31 +0200 Subject: [PATCH 10/19] Remove duplicate tearDown() --- tests/gstreamer_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index 62633e4f..790394f5 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -23,9 +23,6 @@ class GStreamerTest(unittest.TestCase): self.gstreamer.prepare_change() self.gstreamer.set_uri(uri) - def tearDown(self): - settings.runtime.clear() - def test_start_playback_existing_file(self): self.prepare_uri(self.song_uri) self.assertTrue(self.gstreamer.start_playback()) From 652f97054835366b932c02c00242dc9bdc6dd809 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 23:48:25 +0200 Subject: [PATCH 11/19] Remove old output docs --- docs/api/outputs.rst | 18 ------------------ docs/modules/outputs.rst | 11 ----------- 2 files changed, 29 deletions(-) delete mode 100644 docs/api/outputs.rst delete mode 100644 docs/modules/outputs.rst diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst deleted file mode 100644 index 7f487881..00000000 --- a/docs/api/outputs.rst +++ /dev/null @@ -1,18 +0,0 @@ -.. _output-api: - -********** -Output API -********** - -Outputs are used by :mod:`mopidy.gstreamer` to output audio in some way. - -.. autoclass:: mopidy.outputs.BaseOutput - :members: - - -Output implementations -====================== - -* :class:`mopidy.outputs.custom.CustomOutput` -* :class:`mopidy.outputs.local.LocalOutput` -* :class:`mopidy.outputs.shoutcast.ShoutcastOutput` diff --git a/docs/modules/outputs.rst b/docs/modules/outputs.rst deleted file mode 100644 index f80c16e3..00000000 --- a/docs/modules/outputs.rst +++ /dev/null @@ -1,11 +0,0 @@ -************************************************ -:mod:`mopidy.outputs` -- GStreamer audio outputs -************************************************ - -The following GStreamer audio outputs implements the :ref:`output-api`. - -.. autoclass:: mopidy.outputs.custom.CustomOutput - -.. autoclass:: mopidy.outputs.local.LocalOutput - -.. autoclass:: mopidy.outputs.shoutcast.ShoutcastOutput From fd4d5b2f62c2d7ab99537e3fb2204a3f89afefe2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 23:48:55 +0200 Subject: [PATCH 12/19] Update autodoc dependency mock --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a33a8f2d..d8aa118e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,17 +22,17 @@ class Mock(object): def __call__(self, *args, **kwargs): return Mock() + def __or__(self, other): + return Mock() + @classmethod def __getattr__(self, name): if name in ('__file__', '__path__'): return '/dev/null' - elif name[0] == name[0].upper(): - return type(name, (), {}) else: return Mock() MOCK_MODULES = [ - 'alsaaudio', 'dbus', 'dbus.mainloop', 'dbus.mainloop.glib', From 975c79c1e6868e67a99001fffe59bf5f2f49b77f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 8 Sep 2012 23:50:28 +0200 Subject: [PATCH 13/19] Remove invalid Sphinx text role --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 963802d4..204f1193 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,7 +18,7 @@ v0.8 (in development) Track position and CPID was intermixed, so it would cause a crash if a CPID matching the track position didn't exist. (Fixes: :issue:`162`) -- Added :option:`--list-deps` option to :cmd:`mopidy` command that lists +- Added :option:`--list-deps` option to the `mopidy` command that lists required and optional dependencies, their current versions, and some other information useful for debugging. (Fixes: :issue:`74`) From ccf2c12a189229d9f456cabeafcb713d081c0a2b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Sep 2012 12:52:41 +0200 Subject: [PATCH 14/19] Reuse set-and-check logic --- mopidy/mixers/nad.py | 55 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index ce860d4a..eecf9aab 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -153,45 +153,23 @@ class NadTalker(ThreadingActor): return model def _power_device_on(self): - while self._ask_device('Main.Power') != 'On': - logger.info(u'Powering NAD amplifier on') - self._command_device('Main.Power', 'On') + self._check_and_set('Main.Power', 'On') def _select_speakers(self): if self.speakers_a is not None: - while (self._ask_device('Main.SpeakerA') != self.speakers_a): - logger.info(u'Setting speakers A to "%s"', self.speakers_a) - self._command_device('Main.SpeakerA', self.speakers_a) + self._check_and_set('Main.SpeakerA', self.speakers_a) if self.speakers_b is not None: - while (self._ask_device('Main.SpeakerB') != self.speakers_b): - logger.info(u'Setting speakers B to "%s"', self.speakers_b) - self._command_device('Main.SpeakerB', self.speakers_b) + self._check_and_set('Main.SpeakerB', self.speakers_b) def _select_input_source(self): if self.source is not None: - while self._ask_device('Main.Source') != self.source: - logger.info(u'Selecting input source "%s"', self.source) - self._command_device('Main.Source', self.source) + self._check_and_set('Main.Source', self.source) def mute(self, mute): if mute: - while self._ask_device('Main.Mute') != 'On': - logger.info(u'Muting NAD amplifier') - self._command_device('Main.Mute', 'On') + self._check_and_set('Main.Mute', 'On') else: - while self._ask_device('Main.Mute') != 'Off': - logger.info(u'Unmuting NAD amplifier') - 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() + self._check_and_set('Main.Mute', 'Off') def _calibrate_volume(self): # The NAD C 355BEE amplifier has 40 different volume levels. We have no @@ -226,6 +204,27 @@ class NadTalker(ThreadingActor): 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(u'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(u'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. From affe7795694f5a96e190cee7c083af1032369e12 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Sep 2012 12:53:36 +0200 Subject: [PATCH 15/19] Cleanup log messages --- mopidy/mixers/nad.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index eecf9aab..dad3e853 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -129,7 +129,7 @@ class NadTalker(ThreadingActor): self._set_device_to_known_state() def _open_connection(self): - logger.info(u'Connecting to NAD amplifier using "%s"', + logger.info(u'NAD amplifier: Connecting through "%s"', self.port) self._device = serial.Serial( port=self.port, @@ -149,7 +149,7 @@ class NadTalker(ThreadingActor): def _get_device_model(self): model = self._ask_device('Main.Model') - logger.info(u'Connected to NAD amplifier model "%s"', model) + logger.info(u'NAD amplifier: Connected to model "%s"', model) return model def _power_device_on(self): @@ -175,10 +175,10 @@ class NadTalker(ThreadingActor): # 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 by setting volume to 0') + logger.info(u'NAD amplifier: Calibrating by setting volume to 0') self._nad_volume = self.VOLUME_LEVELS self.set_volume(0) - logger.info(u'Done calibrating NAD amplifier') + logger.info(u'NAD amplifier: Done calibrating') def set_volume(self, volume): # Increase or decrease the amplifier volume until it matches the given From 297b8db3cd858a3faa4219b0c0d9e466e6866f9c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Sep 2012 12:54:17 +0200 Subject: [PATCH 16/19] Titlecase source and speaker settings --- docs/changes.rst | 2 +- mopidy/mixers/nad.py | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 6298c8e3..922eba13 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -56,7 +56,7 @@ v0.8 (in development) Now is reduced to simply: - MIXER = u'nadmixer port=/dev/ttyUSB0 source=Aux speakers-a=On speakers-b=Off' + MIXER = u'nadmixer port=/dev/ttyUSB0 source=aux speakers-a=on speakers-b=off' The ``port`` property defaults to ``/dev/ttyUSB0``, and the rest of the properties may be left out if you don't want the mixer to adjust the settings diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index dad3e853..de959d41 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -108,16 +108,8 @@ class NadTalker(ThreadingActor): self.port = port self.source = source - if speakers_a in ('On', 'Off'): - self.speakers_a = speakers_a - else: - logger.warning('speakers-a must be "On" or "Off", or unset') - self.speakers_a = None - if speakers_b in ('On', 'Off'): - self.speakers_b = speakers_b - else: - logger.warning('speakers-b must be "On" or "Off", or unset') - self.speakers_b = None + self.speakers_a = speakers_a + self.speakers_b = speakers_b # Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration. self._nad_volume = None @@ -157,13 +149,13 @@ class NadTalker(ThreadingActor): def _select_speakers(self): if self.speakers_a is not None: - self._check_and_set('Main.SpeakerA', self.speakers_a) + 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) + 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) + self._check_and_set('Main.Source', self.source.title()) def mute(self, mute): if mute: From ab7d0c4cc22289b44552722a062240865058827c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 9 Sep 2012 21:36:52 +0200 Subject: [PATCH 17/19] Don't block gobject event thread, fixes #150. --- mopidy/utils/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 4b8a9ac9..7d97daf8 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -150,7 +150,7 @@ class Connection(object): logger.log(level, reason) try: - self.actor_ref.stop() + self.actor_ref.stop(block=False) except ActorDeadError: pass From db3a201795f11a3df06b657408140babe3304793 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 9 Sep 2012 22:09:23 +0200 Subject: [PATCH 18/19] Fix tests and update docs with #150 fix. --- docs/changes.rst | 6 ++++++ tests/utils/network/connection_test.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index aae792fa..57224300 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -62,6 +62,12 @@ v0.8 (in development) properties may be left out if you don't want the mixer to adjust the settings on your NAD amplifier when Mopidy is started. +- Fixed :issue:`150` which caused some clients to block Mopidy completely. Bug + was caused by some clients sending ``close`` and then shutting down the + connection right away. This trigged a situation in which the connection + cleanup code would wait for an response that would never come inside the + event loop, blocking everything else. + v0.7.3 (2012-08-11) =================== diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py index aa1be2b6..96ddb833 100644 --- a/tests/utils/network/connection_test.py +++ b/tests/utils/network/connection_test.py @@ -91,7 +91,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) - self.mock.actor_ref.stop.assert_called_once_with() + self.mock.actor_ref.stop.assert_called_once_with(block=False) def test_stop_handles_actor_already_being_stopped(self): self.mock.stopping = False @@ -100,7 +100,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) - self.mock.actor_ref.stop.assert_called_once_with() + self.mock.actor_ref.stop.assert_called_once_with(block=False) def test_stop_sets_stopping_to_true(self): self.mock.stopping = False From 911b45dce8f83b1abbb24df927de61bafef5eaf6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 9 Sep 2012 23:27:03 +0200 Subject: [PATCH 19/19] Document debug-proxy's existance. --- docs/development/contributing.rst | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst index 373da1a0..74e2f0b5 100644 --- a/docs/development/contributing.rst +++ b/docs/development/contributing.rst @@ -125,6 +125,47 @@ statistics and uses pylint to check for errors and possible improvements in our code. So, if you're out of work, the code coverage and pylint data at the CI server should give you a place to start. +Protocol debugging +================== + +Since the main interface provided to Mopidy is through the MPD protocol, it is +crucial that we try and stay in sync with protocol developments. In an attempt +to make it easier to debug differences Mopidy and MPD protocol handling we have +created ``tools/debug-proxy.py``. + +This tool is proxy that sits in front of two MPD protocol aware servers and +sends all requests to both, returning the primary response to the client and +then printing any diff in the two responses. + +Note that this tool depends on ``gevent`` unlike the rest of Mopidy at the time +of writing. See ``--help`` for available options. Sample session:: + + [127.0.0.1]:59714 + listallinfo + --- Reference response + +++ Actual response + @@ -1,16 +1,1 @@ + -file: uri1 + -Time: 4 + -Artist: artist1 + -Title: track1 + -Album: album1 + -file: uri2 + -Time: 4 + -Artist: artist2 + -Title: track2 + -Album: album2 + -file: uri3 + -Time: 4 + -Artist: artist3 + -Title: track3 + -Album: album3 + -OK + +ACK [2@0] {listallinfo} incorrect arguments + +To ensure that Mopidy and MPD have comparable state it is suggested you setup +both to use ``tests/data/library_tag_cache`` for their tag cache and +``tests/data`` for music/playlist folders. Writing documentation =====================