From e22175ca98fd495b7915d67657d64d472f64f98c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 16 Oct 2012 15:49:10 +0200 Subject: [PATCH] Move Audio actor from __init__.py to actor.py --- mopidy/audio/__init__.py | 382 +-------------------------------------- mopidy/audio/actor.py | 381 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 383 insertions(+), 380 deletions(-) create mode 100644 mopidy/audio/actor.py diff --git a/mopidy/audio/__init__.py b/mopidy/audio/__init__.py index 4a0b0000..ba76bd84 100644 --- a/mopidy/audio/__init__.py +++ b/mopidy/audio/__init__.py @@ -1,381 +1,3 @@ -import pygst -pygst.require('0.10') -import gst -import gobject - -import logging - -from pykka.actor import ThreadingActor - -from mopidy import settings, utils -from mopidy.utils import process - -from . import mixers +# flake8: noqa +from .actor import Audio from .listener import AudioListener - -logger = logging.getLogger('mopidy.audio') - -mixers.register_mixers() - - -class Audio(ThreadingActor): - """ - Audio output through `GStreamer `_. - - **Settings:** - - - :attr:`mopidy.settings.OUTPUT` - - :attr:`mopidy.settings.MIXER` - - :attr:`mopidy.settings.MIXER_TRACK` - - """ - - def __init__(self): - super(Audio, self).__init__() - - self._playbin = None - self._mixer = None - self._mixer_track = None - self._software_mixing = False - - self._message_processor_set_up = False - - def on_start(self): - try: - self._setup_playbin() - self._setup_output() - self._setup_mixer() - self._setup_message_processor() - except gobject.GError as ex: - logger.exception(ex) - process.exit_process() - - def on_stop(self): - self._teardown_message_processor() - self._teardown_mixer() - self._teardown_playbin() - - def _setup_playbin(self): - self._playbin = gst.element_factory_make('playbin2') - - fakesink = gst.element_factory_make('fakesink') - self._playbin.set_property('video-sink', fakesink) - - def _teardown_playbin(self): - self._playbin.set_state(gst.STATE_NULL) - - def _setup_output(self): - try: - output = gst.parse_bin_from_description( - settings.OUTPUT, ghost_unconnected_pads=True) - self._playbin.set_property('audio-sink', output) - logger.info('Output set to %s', settings.OUTPUT) - except gobject.GError as ex: - logger.error( - 'Failed to create output "%s": %s', settings.OUTPUT, ex) - process.exit_process() - - def _setup_mixer(self): - if not settings.MIXER: - logger.info('Not setting up mixer.') - return - - if settings.MIXER == 'software': - self._software_mixing = True - logger.info('Mixer set to software mixing.') - return - - try: - mixerbin = gst.parse_bin_from_description( - settings.MIXER, ghost_unconnected_pads=False) - except gobject.GError as ex: - logger.warning( - 'Failed to create mixer "%s": %s', settings.MIXER, ex) - return - - # We assume that the bin will contain a single mixer. - mixer = mixerbin.get_by_interface('GstMixer') - if not mixer: - logger.warning('Did not find any mixers in %r', settings.MIXER) - return - - if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: - logger.warning('Setting mixer %r to READY failed.', settings.MIXER) - return - - track = self._select_mixer_track(mixer, settings.MIXER_TRACK) - if not track: - logger.warning('Could not find usable mixer track.') - return - - self._mixer = mixer - self._mixer_track = track - logger.info('Mixer set to %s using track called %s', - mixer.get_factory().get_name(), track.label) - - def _select_mixer_track(self, mixer, track_label): - # Look for track with label == MIXER_TRACK, otherwise fallback to - # master track which is also an output. - for track in mixer.list_tracks(): - if track_label: - if track.label == track_label: - return track - elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER | - gst.interfaces.MIXER_TRACK_OUTPUT): - return track - - def _teardown_mixer(self): - if self._mixer is not None: - self._mixer.set_state(gst.STATE_NULL) - - def _setup_message_processor(self): - bus = self._playbin.get_bus() - bus.add_signal_watch() - bus.connect('message', self._on_message) - self._message_processor_set_up = True - - def _teardown_message_processor(self): - if self._message_processor_set_up: - bus = self._playbin.get_bus() - bus.remove_signal_watch() - - def _on_message(self, bus, message): - if message.type == gst.MESSAGE_EOS: - self._trigger_reached_end_of_stream_event() - elif message.type == gst.MESSAGE_ERROR: - error, debug = message.parse_error() - logger.error(u'%s %s', error, debug) - self.stop_playback() - elif message.type == gst.MESSAGE_WARNING: - error, debug = message.parse_warning() - logger.warning(u'%s %s', error, debug) - - def _trigger_reached_end_of_stream_event(self): - logger.debug(u'Triggering reached end of stream event') - AudioListener.send('reached_end_of_stream') - - def set_uri(self, uri): - """ - Set URI of audio to be played. - - You *MUST* call :meth:`prepare_change` before calling this method. - - :param uri: the URI to play - :type uri: string - """ - self._playbin.set_property('uri', uri) - - def emit_data(self, capabilities, data): - """ - Call this to deliver raw audio data to be played. - - Note that the uri must be set to ``appsrc://`` for this to work. - - :param capabilities: a GStreamer capabilities string - :type capabilities: string - :param data: raw audio data to be played - """ - caps = gst.caps_from_string(capabilities) - buffer_ = gst.Buffer(buffer(data)) - buffer_.set_caps(caps) - - source = self._playbin.get_property('source') - source.set_property('caps', caps) - source.emit('push-buffer', buffer_) - - def emit_end_of_stream(self): - """ - Put an end-of-stream token on the playbin. This is typically used in - combination with :meth:`emit_data`. - - We will get a GStreamer message when the stream playback reaches the - token, and can then do any end-of-stream related tasks. - """ - self._playbin.get_property('source').emit('end-of-stream') - - def get_position(self): - """ - Get position in milliseconds. - - :rtype: int - """ - if self._playbin.get_state()[1] == gst.STATE_NULL: - return 0 - try: - position = self._playbin.query_position(gst.FORMAT_TIME)[0] - return position // gst.MSECOND - except gst.QueryError, e: - logger.error('time_position failed: %s', e) - return 0 - - def set_position(self, position): - """ - Set position in milliseconds. - - :param position: the position in milliseconds - :type volume: int - :rtype: :class:`True` if successful, else :class:`False` - """ - self._playbin.get_state() # block until state changes are done - handeled = self._playbin.seek_simple( - gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, - position * gst.MSECOND) - self._playbin.get_state() # block until seek is done - return handeled - - def start_playback(self): - """ - Notify GStreamer that it should start playback. - - :rtype: :class:`True` if successfull, else :class:`False` - """ - return self._set_state(gst.STATE_PLAYING) - - def pause_playback(self): - """ - Notify GStreamer that it should pause playback. - - :rtype: :class:`True` if successfull, else :class:`False` - """ - return self._set_state(gst.STATE_PAUSED) - - def prepare_change(self): - """ - Notify GStreamer that we are about to change state of playback. - - This function *MUST* be called before changing URIs or doing - changes like updating data that is being pushed. The reason for this - is that GStreamer will reset all its state when it changes to - :attr:`gst.STATE_READY`. - """ - return self._set_state(gst.STATE_READY) - - def stop_playback(self): - """ - Notify GStreamer that is should stop playback. - - :rtype: :class:`True` if successfull, else :class:`False` - """ - return self._set_state(gst.STATE_NULL) - - def _set_state(self, state): - """ - Internal method for setting the raw GStreamer state. - - .. digraph:: gst_state_transitions - - graph [rankdir="LR"]; - node [fontsize=10]; - - "NULL" -> "READY" - "PAUSED" -> "PLAYING" - "PAUSED" -> "READY" - "PLAYING" -> "PAUSED" - "READY" -> "NULL" - "READY" -> "PAUSED" - - :param state: State to set playbin to. One of: `gst.STATE_NULL`, - `gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`. - :type state: :class:`gst.State` - :rtype: :class:`True` if successfull, else :class:`False` - """ - result = self._playbin.set_state(state) - if result == gst.STATE_CHANGE_FAILURE: - logger.warning( - 'Setting GStreamer state to %s: failed', state.value_name) - return False - elif result == gst.STATE_CHANGE_ASYNC: - logger.debug( - 'Setting GStreamer state to %s: async', state.value_name) - return True - else: - logger.debug( - 'Setting GStreamer state to %s: OK', state.value_name) - return True - - def get_volume(self): - """ - Get volume level of the installed mixer. - - Example values: - - 0: - Muted. - 100: - Max volume for given system. - :class:`None`: - No mixer present, so the volume is unknown. - - :rtype: int in range [0..100] or :class:`None` - """ - if self._software_mixing: - return round(self._playbin.get_property('volume') * 100) - - if self._mixer is None: - return None - - volumes = self._mixer.get_volume(self._mixer_track) - avg_volume = float(sum(volumes)) / len(volumes) - - new_scale = (0, 100) - old_scale = ( - self._mixer_track.min_volume, self._mixer_track.max_volume) - return utils.rescale(avg_volume, old=old_scale, new=new_scale) - - def set_volume(self, volume): - """ - Set volume level of the installed mixer. - - :param volume: the volume in the range [0..100] - :type volume: int - :rtype: :class:`True` if successful, else :class:`False` - """ - if self._software_mixing: - self._playbin.set_property('volume', volume / 100.0) - return True - - if self._mixer is None: - return False - - old_scale = (0, 100) - new_scale = ( - self._mixer_track.min_volume, self._mixer_track.max_volume) - - volume = utils.rescale(volume, old=old_scale, new=new_scale) - - volumes = (volume,) * self._mixer_track.num_channels - self._mixer.set_volume(self._mixer_track, volumes) - - return self._mixer.get_volume(self._mixer_track) == volumes - - def set_metadata(self, track): - """ - Set track metadata for currently playing song. - - Only needs to be called by sources such as `appsrc` which do not - already inject tags in playbin, e.g. when using :meth:`emit_data` to - deliver raw audio data to GStreamer. - - :param track: the current track - :type track: :class:`mopidy.models.Track` - """ - taglist = gst.TagList() - artists = [a for a in (track.artists or []) if a.name] - - # Default to blank data to trick shoutcast into clearing any previous - # values it might have. - taglist[gst.TAG_ARTIST] = u' ' - taglist[gst.TAG_TITLE] = u' ' - taglist[gst.TAG_ALBUM] = u' ' - - if artists: - taglist[gst.TAG_ARTIST] = u', '.join([a.name for a in artists]) - - if track.name: - taglist[gst.TAG_TITLE] = track.name - - if track.album and track.album.name: - taglist[gst.TAG_ALBUM] = track.album.name - - event = gst.event_new_tag(taglist) - self._playbin.send_event(event) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py new file mode 100644 index 00000000..4a0b0000 --- /dev/null +++ b/mopidy/audio/actor.py @@ -0,0 +1,381 @@ +import pygst +pygst.require('0.10') +import gst +import gobject + +import logging + +from pykka.actor import ThreadingActor + +from mopidy import settings, utils +from mopidy.utils import process + +from . import mixers +from .listener import AudioListener + +logger = logging.getLogger('mopidy.audio') + +mixers.register_mixers() + + +class Audio(ThreadingActor): + """ + Audio output through `GStreamer `_. + + **Settings:** + + - :attr:`mopidy.settings.OUTPUT` + - :attr:`mopidy.settings.MIXER` + - :attr:`mopidy.settings.MIXER_TRACK` + + """ + + def __init__(self): + super(Audio, self).__init__() + + self._playbin = None + self._mixer = None + self._mixer_track = None + self._software_mixing = False + + self._message_processor_set_up = False + + def on_start(self): + try: + self._setup_playbin() + self._setup_output() + self._setup_mixer() + self._setup_message_processor() + except gobject.GError as ex: + logger.exception(ex) + process.exit_process() + + def on_stop(self): + self._teardown_message_processor() + self._teardown_mixer() + self._teardown_playbin() + + def _setup_playbin(self): + self._playbin = gst.element_factory_make('playbin2') + + fakesink = gst.element_factory_make('fakesink') + self._playbin.set_property('video-sink', fakesink) + + def _teardown_playbin(self): + self._playbin.set_state(gst.STATE_NULL) + + def _setup_output(self): + try: + output = gst.parse_bin_from_description( + settings.OUTPUT, ghost_unconnected_pads=True) + self._playbin.set_property('audio-sink', output) + logger.info('Output set to %s', settings.OUTPUT) + except gobject.GError as ex: + logger.error( + 'Failed to create output "%s": %s', settings.OUTPUT, ex) + process.exit_process() + + def _setup_mixer(self): + if not settings.MIXER: + logger.info('Not setting up mixer.') + return + + if settings.MIXER == 'software': + self._software_mixing = True + logger.info('Mixer set to software mixing.') + return + + try: + mixerbin = gst.parse_bin_from_description( + settings.MIXER, ghost_unconnected_pads=False) + except gobject.GError as ex: + logger.warning( + 'Failed to create mixer "%s": %s', settings.MIXER, ex) + return + + # We assume that the bin will contain a single mixer. + mixer = mixerbin.get_by_interface('GstMixer') + if not mixer: + logger.warning('Did not find any mixers in %r', settings.MIXER) + return + + if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS: + logger.warning('Setting mixer %r to READY failed.', settings.MIXER) + return + + track = self._select_mixer_track(mixer, settings.MIXER_TRACK) + if not track: + logger.warning('Could not find usable mixer track.') + return + + self._mixer = mixer + self._mixer_track = track + logger.info('Mixer set to %s using track called %s', + mixer.get_factory().get_name(), track.label) + + def _select_mixer_track(self, mixer, track_label): + # Look for track with label == MIXER_TRACK, otherwise fallback to + # master track which is also an output. + for track in mixer.list_tracks(): + if track_label: + if track.label == track_label: + return track + elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER | + gst.interfaces.MIXER_TRACK_OUTPUT): + return track + + def _teardown_mixer(self): + if self._mixer is not None: + self._mixer.set_state(gst.STATE_NULL) + + def _setup_message_processor(self): + bus = self._playbin.get_bus() + bus.add_signal_watch() + bus.connect('message', self._on_message) + self._message_processor_set_up = True + + def _teardown_message_processor(self): + if self._message_processor_set_up: + bus = self._playbin.get_bus() + bus.remove_signal_watch() + + def _on_message(self, bus, message): + if message.type == gst.MESSAGE_EOS: + self._trigger_reached_end_of_stream_event() + elif message.type == gst.MESSAGE_ERROR: + error, debug = message.parse_error() + logger.error(u'%s %s', error, debug) + self.stop_playback() + elif message.type == gst.MESSAGE_WARNING: + error, debug = message.parse_warning() + logger.warning(u'%s %s', error, debug) + + def _trigger_reached_end_of_stream_event(self): + logger.debug(u'Triggering reached end of stream event') + AudioListener.send('reached_end_of_stream') + + def set_uri(self, uri): + """ + Set URI of audio to be played. + + You *MUST* call :meth:`prepare_change` before calling this method. + + :param uri: the URI to play + :type uri: string + """ + self._playbin.set_property('uri', uri) + + def emit_data(self, capabilities, data): + """ + Call this to deliver raw audio data to be played. + + Note that the uri must be set to ``appsrc://`` for this to work. + + :param capabilities: a GStreamer capabilities string + :type capabilities: string + :param data: raw audio data to be played + """ + caps = gst.caps_from_string(capabilities) + buffer_ = gst.Buffer(buffer(data)) + buffer_.set_caps(caps) + + source = self._playbin.get_property('source') + source.set_property('caps', caps) + source.emit('push-buffer', buffer_) + + def emit_end_of_stream(self): + """ + Put an end-of-stream token on the playbin. This is typically used in + combination with :meth:`emit_data`. + + We will get a GStreamer message when the stream playback reaches the + token, and can then do any end-of-stream related tasks. + """ + self._playbin.get_property('source').emit('end-of-stream') + + def get_position(self): + """ + Get position in milliseconds. + + :rtype: int + """ + if self._playbin.get_state()[1] == gst.STATE_NULL: + return 0 + try: + position = self._playbin.query_position(gst.FORMAT_TIME)[0] + return position // gst.MSECOND + except gst.QueryError, e: + logger.error('time_position failed: %s', e) + return 0 + + def set_position(self, position): + """ + Set position in milliseconds. + + :param position: the position in milliseconds + :type volume: int + :rtype: :class:`True` if successful, else :class:`False` + """ + self._playbin.get_state() # block until state changes are done + handeled = self._playbin.seek_simple( + gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, + position * gst.MSECOND) + self._playbin.get_state() # block until seek is done + return handeled + + def start_playback(self): + """ + Notify GStreamer that it should start playback. + + :rtype: :class:`True` if successfull, else :class:`False` + """ + return self._set_state(gst.STATE_PLAYING) + + def pause_playback(self): + """ + Notify GStreamer that it should pause playback. + + :rtype: :class:`True` if successfull, else :class:`False` + """ + return self._set_state(gst.STATE_PAUSED) + + def prepare_change(self): + """ + Notify GStreamer that we are about to change state of playback. + + This function *MUST* be called before changing URIs or doing + changes like updating data that is being pushed. The reason for this + is that GStreamer will reset all its state when it changes to + :attr:`gst.STATE_READY`. + """ + return self._set_state(gst.STATE_READY) + + def stop_playback(self): + """ + Notify GStreamer that is should stop playback. + + :rtype: :class:`True` if successfull, else :class:`False` + """ + return self._set_state(gst.STATE_NULL) + + def _set_state(self, state): + """ + Internal method for setting the raw GStreamer state. + + .. digraph:: gst_state_transitions + + graph [rankdir="LR"]; + node [fontsize=10]; + + "NULL" -> "READY" + "PAUSED" -> "PLAYING" + "PAUSED" -> "READY" + "PLAYING" -> "PAUSED" + "READY" -> "NULL" + "READY" -> "PAUSED" + + :param state: State to set playbin to. One of: `gst.STATE_NULL`, + `gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`. + :type state: :class:`gst.State` + :rtype: :class:`True` if successfull, else :class:`False` + """ + result = self._playbin.set_state(state) + if result == gst.STATE_CHANGE_FAILURE: + logger.warning( + 'Setting GStreamer state to %s: failed', state.value_name) + return False + elif result == gst.STATE_CHANGE_ASYNC: + logger.debug( + 'Setting GStreamer state to %s: async', state.value_name) + return True + else: + logger.debug( + 'Setting GStreamer state to %s: OK', state.value_name) + return True + + def get_volume(self): + """ + Get volume level of the installed mixer. + + Example values: + + 0: + Muted. + 100: + Max volume for given system. + :class:`None`: + No mixer present, so the volume is unknown. + + :rtype: int in range [0..100] or :class:`None` + """ + if self._software_mixing: + return round(self._playbin.get_property('volume') * 100) + + if self._mixer is None: + return None + + volumes = self._mixer.get_volume(self._mixer_track) + avg_volume = float(sum(volumes)) / len(volumes) + + new_scale = (0, 100) + old_scale = ( + self._mixer_track.min_volume, self._mixer_track.max_volume) + return utils.rescale(avg_volume, old=old_scale, new=new_scale) + + def set_volume(self, volume): + """ + Set volume level of the installed mixer. + + :param volume: the volume in the range [0..100] + :type volume: int + :rtype: :class:`True` if successful, else :class:`False` + """ + if self._software_mixing: + self._playbin.set_property('volume', volume / 100.0) + return True + + if self._mixer is None: + return False + + old_scale = (0, 100) + new_scale = ( + self._mixer_track.min_volume, self._mixer_track.max_volume) + + volume = utils.rescale(volume, old=old_scale, new=new_scale) + + volumes = (volume,) * self._mixer_track.num_channels + self._mixer.set_volume(self._mixer_track, volumes) + + return self._mixer.get_volume(self._mixer_track) == volumes + + def set_metadata(self, track): + """ + Set track metadata for currently playing song. + + Only needs to be called by sources such as `appsrc` which do not + already inject tags in playbin, e.g. when using :meth:`emit_data` to + deliver raw audio data to GStreamer. + + :param track: the current track + :type track: :class:`mopidy.models.Track` + """ + taglist = gst.TagList() + artists = [a for a in (track.artists or []) if a.name] + + # Default to blank data to trick shoutcast into clearing any previous + # values it might have. + taglist[gst.TAG_ARTIST] = u' ' + taglist[gst.TAG_TITLE] = u' ' + taglist[gst.TAG_ALBUM] = u' ' + + if artists: + taglist[gst.TAG_ARTIST] = u', '.join([a.name for a in artists]) + + if track.name: + taglist[gst.TAG_TITLE] = track.name + + if track.album and track.album.name: + taglist[gst.TAG_ALBUM] = track.album.name + + event = gst.event_new_tag(taglist) + self._playbin.send_event(event)