Esempio n. 1
0
    def __init__(self, engine):

        AudioStream.idx += 1
        self.name = '%s-audiostream-%s' % (engine.name, self.idx)
        self.engine = engine

        self.logger = logging.getLogger('%s [%s-a%s]' %
                                        (__name__, engine.name, self.idx))

        #  track being played by this stream
        self.current_track = None
        self.buffered_track = None

        # This exists because if there is a sink error, it doesn't
        # really make sense to recreate the sink -- it'll just fail
        # again. Instead, wait for the user to try to play a track,
        # and maybe the issue has resolved itself (plugged device in?)
        self.needs_sink = True
        self.last_position = 0

        self.audio_filters = gst_utils.ProviderBin('gst_audio_filter',
                                                   '%s-filters' % self.name)

        self.playbin = Gst.ElementFactory.make("playbin",
                                               "%s-playbin" % self.name)
        if self.playbin is None:
            raise TypeError("gstreamer 1.x base plugins not installed!")

        gst_utils.disable_video_text(self.playbin)

        self.playbin.connect("about-to-finish", self.on_about_to_finish)

        video = Gst.ElementFactory.make("fakesink", '%s-fakevideo' % self.name)
        video.set_property('sync', True)
        self.playbin.set_property('video-sink', video)

        self.audio_sink = DynamicAudioSink('%s-sink' % self.name)
        self.playbin.set_property('audio-sink', self.audio_sink)

        # Setup the bus
        bus = self.playbin.get_bus()
        bus.add_signal_watch()
        bus.connect('message', self.on_message)

        # priority boost hack if needed
        priority_boost(self.playbin)

        # Pulsesink changes volume behind our back, track it
        self.playbin.connect('notify::volume', self.on_volume_change)

        self.fader = TrackFader(self, self.on_fade_out_begin,
                                '%s-fade-%s' % (engine.name, self.idx))
Esempio n. 2
0
def test_calculate_fades():
    fader = TrackFader(None, None, None)

    # fin, fout, start_off, stop_off, tracklen;
    # start, start+fade, end-fade, end
    calcs = [
        # fmt: off

        # one is zero/none
        (0, 4, 0, 0, 10, 0, 0, 6, 10),
        (None, 4, 0, 0, 10, 0, 0, 6, 10),

        # other is zero/none
        (4, 0, 0, 0, 10, 0, 4, 10, 10),
        (4, None, 0, 0, 10, 0, 4, 10, 10),

        # both are equal
        (4, 4, 0, 0, 10, 0, 4, 6, 10),

        # both are none
        (0, 0, 0, 0, 10, 0, 0, 10, 10),
        (None, None, 0, 0, 10, 0, 0, 10, 10),

        # Bigger than playlen: all three cases
        (0, 4, 0, 0, 2, 0, 0, 0, 2),
        (4, 0, 0, 0, 2, 0, 2, 2, 2),
        (4, 4, 0, 0, 2, 0, 1, 1, 2),

        # With start offset
        (4, 4, 1, 0, 10, 1, 5, 6, 10),

        # With stop offset
        (4, 4, 0, 9, 10, 0, 4, 5, 9),

        # With both
        (2, 2, 1, 9, 10, 1, 3, 7, 9),

        # With both, constrained
        (4, 4, 4, 8, 10, 4, 6, 6, 8),
        (2, 4, 4, 7, 10, 4, 5, 5, 7),
        (4, 2, 4, 7, 10, 4, 6, 6, 7),

        # fmt: on
    ]

    i = 0
    for fin, fout, start, stop, tlen, t0, t1, t2, t3 in calcs:
        print('%2d: Fade In: %s; Fade Out: %s; start: %s; stop: %s; Len: %s' %
              (i, fin, fout, start, stop, tlen))
        track = FakeTrack(start, stop, tlen)
        assert fader.calculate_fades(track, fin, fout) == (t0, t1, t2, t3)
        i += 1
Esempio n. 3
0
def test_calculate_fades():
    fader = TrackFader(None, None, None)

    # fin, fout, start_off, stop_off, tracklen;
    # start, start+fade, end-fade, end
    calcs = [
        # fmt: off
        
        # one is zero/none
        (0, 4, 0, 0, 10,        0, 0, 6, 10),
        (None, 4, 0, 0, 10,     0, 0, 6, 10),

        # other is zero/none
        (4, 0, 0, 0, 10,        0, 4, 10, 10),
        (4, None, 0, 0, 10,     0, 4, 10, 10),

        # both are equal
        (4, 4, 0, 0, 10,        0, 4, 6, 10),

        # both are none
        (0, 0, 0, 0, 10,        0, 0, 10, 10),
        (None, None, 0, 0, 10,  0, 0, 10, 10),

        # Bigger than playlen: all three cases
        (0, 4, 0, 0, 2,         0, 0, 0, 2),
        (4, 0, 0, 0, 2,         0, 2, 2, 2),
        (4, 4, 0, 0, 2,         0, 1, 1, 2),

        # With start offset
        (4, 4, 1, 0, 10,        1, 5, 6, 10),

        # With stop offset
        (4, 4, 0, 9, 10,        0, 4, 5, 9),

        # With both
        (2, 2, 1, 9, 10,        1, 3, 7, 9),

        # With both, constrained
        (4, 4, 4, 8, 10,        4, 6, 6, 8),
        (2, 4, 4, 7, 10,        4, 5, 5, 7),
        (4, 2, 4, 7, 10,        4, 6, 6, 7),
        
        # fmt: on
    ]

    i = 0
    for fin, fout, start, stop, tlen, t0, t1, t2, t3 in calcs:
        print('%2d: Fade In: %s; Fade Out: %s; start: %s; stop: %s; Len: %s' %
              (i, fin, fout, start, stop, tlen))
        track = FakeTrack(start, stop, tlen)
        assert fader.calculate_fades(track, fin, fout) == (t0, t1, t2, t3)
        i += 1
Esempio n. 4
0
    def __init__(self, engine):

        AudioStream.idx += 1
        self.name = '%s-audiostream-%s' % (engine.name, self.idx)
        self.engine = engine

        self.logger = logging.getLogger(
            '%s [%s-a%s]' % (__name__, engine.name, self.idx)
        )

        #  track being played by this stream
        self.current_track = None
        self.buffered_track = None

        # This exists because if there is a sink error, it doesn't
        # really make sense to recreate the sink -- it'll just fail
        # again. Instead, wait for the user to try to play a track,
        # and maybe the issue has resolved itself (plugged device in?)
        self.needs_sink = True
        self.last_position = 0

        self.audio_filters = gst_utils.ProviderBin(
            'gst_audio_filter', '%s-filters' % self.name
        )

        self.playbin = Gst.ElementFactory.make("playbin", "%s-playbin" % self.name)
        if self.playbin is None:
            raise TypeError("gstreamer 1.x base plugins not installed!")

        gst_utils.disable_video_text(self.playbin)

        self.playbin.connect("about-to-finish", self.on_about_to_finish)

        video = Gst.ElementFactory.make("fakesink", '%s-fakevideo' % self.name)
        video.set_property('sync', True)
        self.playbin.set_property('video-sink', video)

        self.audio_sink = DynamicAudioSink('%s-sink' % self.name)
        self.playbin.set_property('audio-sink', self.audio_sink)

        # Setup the bus
        bus = self.playbin.get_bus()
        bus.add_signal_watch()
        bus.connect('message', self.on_message)

        # priority boost hack if needed
        priority_boost(self.playbin)

        # Pulsesink changes volume behind our back, track it
        self.playbin.connect('notify::volume', self.on_volume_change)

        self.fader = TrackFader(
            self, self.on_fade_out_begin, '%s-fade-%s' % (engine.name, self.idx)
        )
Esempio n. 5
0
def check_fader(test):

    stream = FakeStream()
    fader = TrackFader(stream, stream.on_fade_out, 'test')

    for data in test:
        print(data)
        now = data[0]
        stream.position = int(now * TrackFader.SECOND)
        print(stream.position)
        volume = data[1]
        state = data[2]
        timer_id = data[3]

        if len(data) > 4:
            action = data[4]
            args = data[5:] if len(data) > 5 else ()

            if action == 'start':
                action = '_on_fade_start'
            elif action == 'execute':
                action = '_execute_fade'
                args = timeout_args[0]
                fader.now = now - 0.010

            # Call the function
            getattr(fader, action)(*args)

        # Check to see if timer id exists
        if timer_id is None:
            assert fader.timer_id is None
        elif timer_id == TmSt:
            assert fader.timer_id == fader._on_fade_start
        elif timer_id == TmEx:
            assert fader.timer_id == fader._execute_fade
        else:
            assert False

        assert fader.state == state
        assert stream.volume == volume
Esempio n. 6
0
def check_fader(test):

    stream = FakeStream()
    fader = TrackFader(stream, stream.on_fade_out, 'test')
    
    for data in test:
        print data
        now = data[0]
        stream.position = int(now*TrackFader.SECOND)
        print stream.position
        volume = data[1]
        state = data[2]
        timer_id = data[3]
        
        if len(data) > 4:
            action = data[4]
            args = data[5:] if len(data) > 5 else ()
            
            if action == 'start':
                action = '_on_fade_start'
            elif action == 'execute':
                action = '_execute_fade'
                args = timeout_args[0]
                fader.now = now - 0.010
                        
            # Call the function
            getattr(fader, action)(*args)
        
        # Check to see if timer id exists
        if timer_id is None:
            assert fader.timer_id is None
        elif timer_id == TmSt:
            assert fader.timer_id == fader._on_fade_start
        elif timer_id == TmEx:
            assert fader.timer_id == fader._execute_fade
        else:
            assert False
                
        assert fader.state == state
        assert stream.volume == volume
Esempio n. 7
0
class AudioStream(object):
    '''
        An object that can play one or more tracks
    '''
    
    idx = 0
    
    def __init__(self, engine):
    
        AudioStream.idx += 1
        self.name = '%s-audiostream-%s' % (engine.name, self.idx)
        self.engine = engine
        
        self.logger = logging.getLogger('%s [%s-a%s]' % (__name__, engine.name, self.idx))
        
        #  track being played by this stream
        self.current_track = None
        self.buffered_track = None
        
        # This exists because if there is a sink error, it doesn't
        # really make sense to recreate the sink -- it'll just fail
        # again. Instead, wait for the user to try to play a track,
        # and maybe the issue has resolved itself (plugged device in?)
        self.needs_sink = True
        self.last_position = 0
        
        self.audio_filters = gst_utils.ProviderBin('gst_audio_filter',
                                                   '%s-filters' % self.name)
        
        self.playbin = Gst.ElementFactory.make("playbin", "%s-playbin" % self.name)
        if self.playbin is None:
            raise TypeError("gstreamer 1.x base plugins not installed!")
        
        gst_utils.disable_video_text(self.playbin)
        
        self.playbin.connect("about-to-finish", self.on_about_to_finish)
        
        video = Gst.ElementFactory.make("fakesink", '%s-fakevideo' % self.name)
        video.set_property('sync', True)
        self.playbin.set_property('video-sink', video)
        
        self.audio_sink = DynamicAudioSink('%s-sink' % self.name)
        self.playbin.set_property('audio-sink', self.audio_sink)
        
        # Setup the bus
        bus = self.playbin.get_bus()
        bus.add_signal_watch()
        bus.connect('message', self.on_message)
        
        # Pulsesink changes volume behind our back, track it
        self.playbin.connect('notify::volume', self.on_volume_change)
        
        self.fader = TrackFader(self, self.on_fade_out_begin,
                                '%s-fade-%s' %(engine.name, self.idx))
    
    def destroy(self):
        
        self.fader.stop()
        self.playbin.set_state(Gst.State.NULL)
        self.playbin.get_bus().remove_signal_watch()
        
    def reconfigure_sink(self):
        self.needs_sink = False
        sink = create_device(self.engine.name)
        self.audio_sink.reconfigure(sink)
        
    def reconfigure_fader(self, fade_in_duration, fade_out_duration):
        if self.get_gst_state() != Gst.State.NULL:
            self.fader.setup_track(self.current_track,
                                   fade_in_duration, fade_out_duration,
                                   is_update=True)
    
    def get_gst_state(self):
        return self.playbin.get_state(timeout=50*Gst.MSECOND)[1]
    
    def get_position(self):
        # TODO: This only works when pipeline is prerolled/ready?
        if not self.get_gst_state() == Gst.State.PAUSED:
            res, self.last_position = \
                self.playbin.query_position(Gst.Format.TIME)
            
            if res is False:
                self.last_position = 0
        
        return self.last_position
    
    def get_volume(self):
        return self.playbin.props.volume
    
    def get_user_volume(self):
        return self.fader.get_user_volume()
    
    def pause(self):
        # This caches the current last position before pausing
        self.get_position()
        self.playbin.set_state(Gst.State.PAUSED)
        self.fader.pause()
    
    def play(self, track, start_at, paused, already_queued,
             fade_in_duration=None, fade_out_duration=None):
        '''fade duration is in seconds'''
        
        if not already_queued:
            self.stop(emit_eos=False)
            
            # For the moment, the only safe time to add/remove elements
            # is when the playbin is NULL, so do that here..
            if self.audio_filters.setup_elements():
                self.logger.debug("Applying audio filters")
                self.playbin.props.audio_filter = self.audio_filters
            else:
                self.logger.debug("Not applying audio filters")
                self.playbin.props.audio_filter = None
        
        if self.needs_sink:
            self.reconfigure_sink()
        
        self.current_track = track
        self.last_position = 0
        self.buffered_track = None
        
        uri = track.get_loc_for_io()
        self.logger.info("Playing %s", common.sanitize_url(uri))
        
        
        # This is only set for gapless playback
        if not already_queued:
            self.playbin.set_property("uri", uri)
            if urlparse.urlsplit(uri)[0] == "cdda":
                self.notify_id = self.playbin.connect('source-setup',
                        self.on_source_setup, track)
            
        # Start in paused mode if we need to seek
        if paused or start_at is not None:
            self.playbin.set_state(Gst.State.PAUSED)
        elif not already_queued:
            self.playbin.set_state(Gst.State.PLAYING)
        
        self.fader.setup_track(track, fade_in_duration, fade_out_duration, now=0)
        
        if start_at is not None:
            self.seek(start_at)
            if not paused:
                self.playbin.set_state(Gst.State.PLAYING)
        
        if paused:
            self.fader.pause()
    
    def seek(self, value):
        '''value is in seconds'''
        
        # TODO: Make sure that we're in a valid seekable state before seeking?
        
        # wait up to 1s for the state to switch, else this fails
        if self.playbin.get_state(timeout=1000*Gst.MSECOND)[0] != Gst.StateChangeReturn.SUCCESS:
            # TODO: This error message is misleading, when does this ever happen?
            # TODO: if the sink is incorrectly specified, this error happens first.
            #self.engine._error_func(self, "Could not start at specified offset")
            self.logger.warning("Error seeking to specified offset")
            return False
        
        new_position = int(Gst.SECOND * value)
        seek_event = Gst.Event.new_seek(1.0, Gst.Format.TIME,
            Gst.SeekFlags.FLUSH, Gst.SeekType.SET,
            new_position,
            Gst.SeekType.NONE, 0)
        
        self.last_position = new_position
        self.fader.seek(value)
        
        return self.playbin.send_event(seek_event)
    
    def set_volume(self, volume):
        #self.logger.debug("Set playbin volume: %.2f", volume)
        # TODO: strange issue where pulse sets the system audio volume
        #       when exaile starts up...
        self.playbin.props.volume = volume
        
    def set_user_volume(self, volume):
        self.logger.debug("Set user volume: %.2f", volume)
        self.fader.set_user_volume(volume)
    
    def stop(self, emit_eos=True):
        prior_track = self.current_track
        self.current_track = None
        self.playbin.set_state(Gst.State.NULL)
        self.fader.stop()
        
        if emit_eos:
            self.engine._eos_func(self)
        
        return prior_track
    
    def unpause(self):
        
        # gstreamer does not buffer paused network streams, so if the user
        # is unpausing a stream, just restart playback
        current = self.current_track
        if not (current.is_local() or current.get_tag_raw('__length')):
            self.playbin.set_state(Gst.State.READY)

        self.playbin.set_state(Gst.State.PLAYING)
        self.fader.unpause()
    
    #
    # Events
    #
    
    def on_about_to_finish(self, *args):
        '''
            This function exists solely to allow gapless playback for audio
            formats that support it. Setting the URI property of the playbin
            will queue the track for playback immediately after the previous 
            track.
            
            .. note:: This is called from the gstreamer thread
        '''
        
        if self.engine.crossfade_enabled:
            return
        
        track = self.engine.player.engine_autoadvance_get_next_track(gapless=True)
        if track:
            uri = track.get_loc_for_io()
            self.playbin.set_property('uri', uri)
            self.buffered_track = track
            
            self.logger.debug("Gapless transition: queuing %s", common.sanitize_url(uri))
    
    def on_fade_out_begin(self):
        
        if self.engine.crossfade_enabled:
            self.engine._autoadvance_track(still_fading=True)
    

    def on_message(self, bus, message):
        '''
            This is called on the main thread
        '''
        
        if message.type == Gst.MessageType.BUFFERING:
            percent = message.parse_buffering()
            if not percent < 100:
                self.logger.info('Buffering complete')
            if percent % 5 == 0:
                event.log_event('playback_buffering', self.engine.player, percent)
        
        elif message.type == Gst.MessageType.TAG:
            """ Update track length and optionally metadata from gstreamer's parser.
                Useful for streams and files mutagen doesn't understand. """
            
            current = self.current_track
            
            if not current.is_local():
                gst_utils.parse_stream_tags(current, message.parse_tag())
            
            if current and not current.get_tag_raw('__length'):
                res, raw_duration = self.playbin.query_duration(Gst.Format.TIME)
                if not res:
                    self.logger.error("Couldn't query duration")
                    raw_duration = 0
                duration = float(raw_duration)/Gst.SECOND
                if duration > 0:
                    current.set_tag_raw('__length', duration)
        
        elif message.type == Gst.MessageType.EOS and \
            not self.get_gst_state() == Gst.State.PAUSED:
            self.engine._eos_func(self)
        
        elif message.type == Gst.MessageType.STREAM_START and \
                message.src == self.playbin and \
                self.buffered_track is not None:
            
            # This handles starting the next track during gapless transition
            buffered_track = self.buffered_track
            self.buffered_track = None
            play_args = self.engine.player.engine_autoadvance_notify_next(buffered_track) + (True, True)
            self.engine._next_track(*play_args)
        
        elif message.type == Gst.MessageType.STATE_CHANGED:
            
            # This idea from quodlibet: pulsesink will not notify us when
            # volume changes if the stream is paused, so do it when the
            # state changes.
            if message.src == self.audio_sink:
                self.playbin.notify("volume")
        
        elif message.type == Gst.MessageType.ERROR:
            
            # Error handling code is from quodlibet
            gerror, debug_info = message.parse_error()
            message_text = ""
            if gerror:
                message_text = gerror.message.rstrip(".")
                 
            if message_text == "":
                # The most readable part is always the last..
                message_text = debug_info[debug_info.rfind(':') + 1:]
                
                # .. unless there's nothing in it.
                if ' ' not in message_text:
                    if debug_info.startswith('playsink'):
                        message_text += _(': Possible audio device error, is it plugged in?')
            
            self.logger.error("Playback error: %s", message_text)
            self.logger.debug("- Extra error info: %s", debug_info)
            
            envname = 'GST_DEBUG_DUMP_DOT_DIR'
            if envname not in os.environ:
                import xl.xdg
                os.environ[envname] = xl.xdg.get_logs_dir()
             
            Gst.debug_bin_to_dot_file(self.playbin, Gst.DebugGraphDetails.ALL, self.name)
            self.logger.debug("- Pipeline debug info written to file '%s/%s.dot'",
                              os.environ[envname], self.name)
            
            self.engine._error_func(self, message_text)
        
        # TODO: Missing plugin error handling from quod libet
        # -- http://cgit.freedesktop.org/gstreamer/gstreamer/tree/docs/design/part-missing-plugins.txt
        
        return True
        
    def on_source_setup(self, playbin, source, track):
        # this is for handling multiple CD devices properly
        device = track.get_loc_for_io().split("#")[-1]
        source.props.device = device
        playbin.disconnect(self.notify_id)
    
    def on_volume_change(self, e, p):
        real = self.playbin.props.volume
        vol, is_same = self.fader.calculate_user_volume(real)
        if not is_same:
            GLib.idle_add(self.engine.player.engine_notify_user_volume_change, vol)
Esempio n. 8
0
class AudioStream(object):
    '''
        An object that can play one or more tracks
    '''

    idx = 0

    def __init__(self, engine):

        AudioStream.idx += 1
        self.name = '%s-audiostream-%s' % (engine.name, self.idx)
        self.engine = engine

        self.logger = logging.getLogger('%s [%s-a%s]' %
                                        (__name__, engine.name, self.idx))

        #  track being played by this stream
        self.current_track = None
        self.buffered_track = None

        # This exists because if there is a sink error, it doesn't
        # really make sense to recreate the sink -- it'll just fail
        # again. Instead, wait for the user to try to play a track,
        # and maybe the issue has resolved itself (plugged device in?)
        self.needs_sink = True
        self.last_position = 0

        self.audio_filters = gst_utils.ProviderBin('gst_audio_filter',
                                                   '%s-filters' % self.name)

        self.playbin = Gst.ElementFactory.make("playbin",
                                               "%s-playbin" % self.name)
        if self.playbin is None:
            raise TypeError("gstreamer 1.x base plugins not installed!")

        gst_utils.disable_video_text(self.playbin)

        self.playbin.connect("about-to-finish", self.on_about_to_finish)

        video = Gst.ElementFactory.make("fakesink", '%s-fakevideo' % self.name)
        video.set_property('sync', True)
        self.playbin.set_property('video-sink', video)

        self.audio_sink = DynamicAudioSink('%s-sink' % self.name)
        self.playbin.set_property('audio-sink', self.audio_sink)

        # Setup the bus
        bus = self.playbin.get_bus()
        bus.add_signal_watch()
        bus.connect('message', self.on_message)

        # priority boost hack if needed
        priority_boost(self.playbin)

        # Pulsesink changes volume behind our back, track it
        self.playbin.connect('notify::volume', self.on_volume_change)

        self.fader = TrackFader(self, self.on_fade_out_begin,
                                '%s-fade-%s' % (engine.name, self.idx))

    def destroy(self):

        self.fader.stop()
        self.playbin.set_state(Gst.State.NULL)
        self.playbin.get_bus().remove_signal_watch()

    def reconfigure_sink(self):
        self.needs_sink = False
        sink = create_device(self.engine.name)

        # Works for pulsesink, but not other sinks
        # -> Not a perfect solution, still some audio blip is heard. Unfortunately,
        #    can't do better without direct support from gstreamer
        if self.engine.disable_autoswitch and hasattr(sink.props,
                                                      'current_device'):
            self.selected_sink = sink.props.device
            sink.connect('notify::current-device', self._on_sink_change_notify)

        self.audio_sink.reconfigure(sink)

    def _on_sink_change_notify(self, sink, param):
        if self.selected_sink != sink.props.current_device:
            domain = GLib.quark_from_string("g-exaile-error")
            err = GLib.Error.new_literal(domain, "Audio device disconnected",
                                         0)
            self.playbin.get_bus().post(
                Gst.Message.new_error(None, err, "Disconnected"))
            self.logger.info("Detected device disconnect, stopping playback")

    def reconfigure_fader(self, fade_in_duration, fade_out_duration):
        if self.get_gst_state() != Gst.State.NULL:
            self.fader.setup_track(self.current_track,
                                   fade_in_duration,
                                   fade_out_duration,
                                   is_update=True)

    def get_gst_state(self):
        return self.playbin.get_state(timeout=50 * Gst.MSECOND)[1]

    def get_position(self):
        # TODO: This only works when pipeline is prerolled/ready?
        if not self.get_gst_state() == Gst.State.PAUSED:
            res, self.last_position = self.playbin.query_position(
                Gst.Format.TIME)

            if res is False:
                self.last_position = 0

        return self.last_position

    def get_volume(self):
        return self.playbin.props.volume

    def get_user_volume(self):
        return self.fader.get_user_volume()

    def pause(self):
        # This caches the current last position before pausing
        self.get_position()
        self.playbin.set_state(Gst.State.PAUSED)
        self.fader.pause()

    def play(
        self,
        track,
        start_at,
        paused,
        already_queued,
        fade_in_duration=None,
        fade_out_duration=None,
    ):
        '''fade duration is in seconds'''

        if not already_queued:
            self.stop(emit_eos=False)

            # For the moment, the only safe time to add/remove elements
            # is when the playbin is NULL, so do that here..
            if self.audio_filters.setup_elements():
                self.logger.debug("Applying audio filters")
                self.playbin.props.audio_filter = self.audio_filters
            else:
                self.logger.debug("Not applying audio filters")
                self.playbin.props.audio_filter = None

        if self.needs_sink:
            self.reconfigure_sink()

        self.current_track = track
        self.last_position = 0
        self.buffered_track = None

        uri = track.get_loc_for_io()
        self.logger.info("Playing %s", common.sanitize_url(uri))

        # This is only set for gapless playback
        if not already_queued:
            self.playbin.set_property("uri", uri)
            if urlparse.urlsplit(uri)[0] == "cdda":
                self.notify_id = self.playbin.connect('source-setup',
                                                      self.on_source_setup,
                                                      track)

        # Start in paused mode if we need to seek
        if paused or start_at is not None:
            self.playbin.set_state(Gst.State.PAUSED)
        elif not already_queued:
            self.playbin.set_state(Gst.State.PLAYING)

        self.fader.setup_track(track,
                               fade_in_duration,
                               fade_out_duration,
                               now=0)

        if start_at is not None:
            self.seek(start_at)
            if not paused:
                self.playbin.set_state(Gst.State.PLAYING)

        if paused:
            self.fader.pause()

    def seek(self, value):
        '''value is in seconds'''

        # TODO: Make sure that we're in a valid seekable state before seeking?

        # wait up to 1s for the state to switch, else this fails
        if (self.playbin.get_state(timeout=1000 * Gst.MSECOND)[0] !=
                Gst.StateChangeReturn.SUCCESS):
            # TODO: This error message is misleading, when does this ever happen?
            # TODO: if the sink is incorrectly specified, this error happens first.
            # self.engine._error_func(self, "Could not start at specified offset")
            self.logger.warning("Error seeking to specified offset")
            return False

        new_position = int(Gst.SECOND * value)
        seek_event = Gst.Event.new_seek(
            1.0,
            Gst.Format.TIME,
            Gst.SeekFlags.FLUSH,
            Gst.SeekType.SET,
            new_position,
            Gst.SeekType.NONE,
            0,
        )

        self.last_position = new_position
        self.fader.seek(value)

        return self.playbin.send_event(seek_event)

    def set_volume(self, volume):
        # self.logger.debug("Set playbin volume: %.2f", volume)
        # TODO: strange issue where pulse sets the system audio volume
        #       when exaile starts up...
        self.playbin.props.volume = volume

    def set_user_volume(self, volume):
        self.logger.debug("Set user volume: %.2f", volume)
        self.fader.set_user_volume(volume)

    def stop(self, emit_eos=True):
        prior_track = self.current_track
        self.current_track = None
        self.playbin.set_state(Gst.State.NULL)
        self.fader.stop()

        if emit_eos:
            self.engine._eos_func(self)

        return prior_track

    def unpause(self):

        # gstreamer does not buffer paused network streams, so if the user
        # is unpausing a stream, just restart playback
        current = self.current_track
        if not (current.is_local() or current.get_tag_raw('__length')):
            self.playbin.set_state(Gst.State.READY)

        self.playbin.set_state(Gst.State.PLAYING)
        self.fader.unpause()

    #
    # Events
    #

    def on_about_to_finish(self, *args):
        '''
            This function exists solely to allow gapless playback for audio
            formats that support it. Setting the URI property of the playbin
            will queue the track for playback immediately after the previous
            track.

            .. note:: This is called from the gstreamer thread
        '''

        if self.engine.crossfade_enabled:
            return

        track = self.engine.player.engine_autoadvance_get_next_track(
            gapless=True)
        if track:
            uri = track.get_loc_for_io()
            self.playbin.set_property('uri', uri)
            self.buffered_track = track

            self.logger.debug("Gapless transition: queuing %s",
                              common.sanitize_url(uri))

    def on_fade_out_begin(self):

        if self.engine.crossfade_enabled:
            self.engine._autoadvance_track(still_fading=True)

    def on_message(self, bus, message):
        '''
            This is called on the main thread
        '''

        if message.type == Gst.MessageType.BUFFERING:
            percent = message.parse_buffering()
            if not percent < 100:
                self.logger.info('Buffering complete')
            if percent % 5 == 0:
                event.log_event('playback_buffering', self.engine.player,
                                percent)

        elif message.type == Gst.MessageType.TAG:
            """ Update track length and optionally metadata from gstreamer's parser.
                Useful for streams and files mutagen doesn't understand. """

            current = self.current_track

            if not current.is_local():
                gst_utils.parse_stream_tags(current, message.parse_tag())

            if current and not current.get_tag_raw('__length'):
                res, raw_duration = self.playbin.query_duration(
                    Gst.Format.TIME)
                if not res:
                    self.logger.error("Couldn't query duration")
                    raw_duration = 0
                duration = float(raw_duration) / Gst.SECOND
                if duration > 0:
                    current.set_tag_raw('__length', duration)

        elif (message.type == Gst.MessageType.EOS
              and not self.get_gst_state() == Gst.State.PAUSED):
            self.engine._eos_func(self)

        elif (message.type == Gst.MessageType.STREAM_START
              and message.src == self.playbin
              and self.buffered_track is not None):

            # This handles starting the next track during gapless transition
            buffered_track = self.buffered_track
            self.buffered_track = None
            play_args = self.engine.player.engine_autoadvance_notify_next(
                buffered_track) + (True, True)
            self.engine._next_track(*play_args)

        elif message.type == Gst.MessageType.STATE_CHANGED:

            # This idea from quodlibet: pulsesink will not notify us when
            # volume changes if the stream is paused, so do it when the
            # state changes.
            if message.src == self.audio_sink:
                self.playbin.notify("volume")

        elif message.type == Gst.MessageType.ERROR:
            self.__handle_error_message(message)

        elif message.type == Gst.MessageType.ELEMENT:
            if not missing_plugin.handle_message(message, self.engine):
                logger.debug(
                    "Unexpected element-specific GstMessage received from %s: %s",
                    message.src,
                    message,
                )

        elif message.type == Gst.MessageType.WARNING:
            # TODO there might be some useful warnings we ignore for now.
            gerror, debug_text = Gst.Message.parse_warning(message)
            logger.warn(
                "Unhandled GStreamer warning received:\n\tGError: %s\n\tDebug text: %s",
                gerror,
                debug_text,
            )

        else:
            # TODO there might be some useful messages we ignore for now.
            logger.debug("Unhandled GstMessage of type %s received: %s",
                         message.type, message)

    def __handle_error_message(self, message):
        # Error handling code is from quodlibet
        gerror, debug_info = message.parse_error()
        message_text = ""
        if gerror:
            message_text = gerror.message.rstrip(".")

        if message_text == "":
            # The most readable part is always the last..
            message_text = debug_info[debug_info.rfind(':') + 1:]

            # .. unless there's nothing in it.
            if ' ' not in message_text:
                if debug_info.startswith('playsink'):
                    message_text += _(
                        ': Possible audio device error, is it plugged in?')

        self.logger.error("Playback error: %s", message_text)
        self.logger.debug("- Extra error info: %s", debug_info)

        envname = 'GST_DEBUG_DUMP_DOT_DIR'
        if envname not in os.environ:
            import xl.xdg

            os.environ[envname] = xl.xdg.get_logs_dir()

        Gst.debug_bin_to_dot_file(self.playbin, Gst.DebugGraphDetails.ALL,
                                  self.name)
        self.logger.debug(
            "- Pipeline debug info written to file '%s/%s.dot'",
            os.environ[envname],
            self.name,
        )

        self.engine._error_func(self, message_text)

    def on_source_setup(self, playbin, source, track):
        # this is for handling multiple CD devices properly
        device = track.get_loc_for_io().split("#")[-1]
        source.props.device = device
        playbin.disconnect(self.notify_id)

    def on_volume_change(self, e, p):
        real = self.playbin.props.volume
        vol, is_same = self.fader.calculate_user_volume(real)
        if not is_same:
            GLib.idle_add(self.engine.player.engine_notify_user_volume_change,
                          vol)