def __init__(self, player, library): super(SeekBar, self).__init__() self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) scale.set_adjustment(Gtk.Adjustment.new(0, 0, 0, 3, 15, 0)) scale.set_draw_value(False) self._scale = scale self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self._id = self._scale.connect("value-changed", self._on_user_changed, player) self._scale.connect("value-changed", self._on_scale_value_changed, player) self._tracker = TimeTracker(player) self._tracker.connect("tick", self._on_tick, player) connect_destroy(player, "seek", self._on_player_seek) connect_destroy(player, "song-started", self._on_song_started) connect_destroy(player, "notify::seekable", self._on_seekable_changed) connect_destroy(library, "changed", self._on_song_changed, player) self.connect("destroy", self._on_destroy) with self._inhibit(): self._update(player) self._tracker.tick()
def __init__(self, player, library): super(WaveformSeekBar, self).__init__() self._player = player self._rms_vals = [] self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() self._waveform_scale = WaveformScale() self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(self._waveform_scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self._tracker = TimeTracker(player) self._tracker.connect('tick', self._on_tick, player) connect_destroy(player, 'seek', self._on_player_seek) connect_destroy(player, 'song-started', self._on_song_started) connect_destroy(player, 'song-ended', self._on_song_ended) connect_destroy(player, 'notify::seekable', self._on_seekable_changed) connect_destroy(library, 'changed', self._on_song_changed, player) self.connect('destroy', self._on_destroy) self._update(player) self._tracker.tick() if player.info: self._create_waveform(player.info, CONFIG.data_size)
def __init__(self, player, library): super(WaveformSeekBar, self).__init__() self._player = player self._rms_vals = [] self._hovering = False self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() self._waveform_scale = WaveformScale(player) self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(self._waveform_scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self.set_time_label_visibility(CONFIG.show_time_labels) self._waveform_scale.connect('size-allocate', self._update_redraw_interval) self._waveform_scale.connect('motion-notify-event', self._on_mouse_hover) self._waveform_scale.connect('leave-notify-event', self._on_mouse_leave) self._label_tracker = TimeTracker(player) self._label_tracker.connect('tick', self._on_tick_label, player) self._redraw_tracker = TimeTracker(player) self._redraw_tracker.connect('tick', self._on_tick_waveform, player) connect_destroy(player, 'seek', self._on_player_seek) connect_destroy(player, 'song-started', self._on_song_started) connect_destroy(player, 'song-ended', self._on_song_ended) connect_destroy(player, 'notify::seekable', self._on_seekable_changed) connect_destroy(library, 'changed', self._on_song_changed, player) self.connect('destroy', self._on_destroy) self._update(player) if player.info: self._create_waveform(player.info, CONFIG.max_data_points)
def __init__(self, player, library): hbox = gtk.HBox(spacing=3) l = gtk.Label("0:00") hbox.pack_start(l) hbox.pack_start( gtk.Arrow(gtk.ARROW_RIGHT, gtk.SHADOW_NONE), expand=False) super(SeekBar, self).__init__(hbox) self.scale.connect('button-press-event', self.__seek_lock) self.scale.connect('button-release-event', self.__seek_unlock, player) self.scale.connect('key-press-event', self.__seek_lock) self.scale.connect('key-release-event', self.__seek_unlock, player) self.connect('scroll-event', self.__scroll, player) self.scale.connect('value-changed', self.__update_time, l) m = gtk.Menu() c = ConfigCheckMenuItem( _("Display remaining time"), "player", "time_remaining") c.set_active(config.getboolean("player", "time_remaining")) c.connect_object('toggled', self.scale.emit, 'value-changed') self.__remaining = c m.append(c) m.append(gtk.SeparatorMenuItem()) i = qltk.MenuItem(_("_Edit Bookmarks..."), gtk.STOCK_EDIT) i.connect_object( 'activate', bookmarks.EditBookmarks, self, library, player) m.append(i) m.show_all() self.child.connect_object( 'button-press-event', self.__check_menu, m, player) self.connect_object('popup-menu', self.__popup_menu, m, player, self.child.child) timer = TimeTracker(player) timer.connect_object('tick', self.__check_time, player) player.connect('song-started', self.__song_changed, l, m) player.connect('seek', self.__seeked)
class WaveformSeekBar(Gtk.Box): """A widget containing labels and the seekbar.""" def __init__(self, player, library): super(WaveformSeekBar, self).__init__() self._player = player self._rms_vals = [] self._hovering = False self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() self._waveform_scale = WaveformScale(player) self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(self._waveform_scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self._waveform_scale.connect('size-allocate', self._update_redraw_interval) self._waveform_scale.connect('motion-notify-event', self._on_mouse_hover) self._waveform_scale.connect('leave-notify-event', self._on_mouse_leave) self._label_tracker = TimeTracker(player) self._label_tracker.connect('tick', self._on_tick_label, player) self._redraw_tracker = TimeTracker(player) self._redraw_tracker.connect('tick', self._on_tick_waveform, player) connect_destroy(player, 'seek', self._on_player_seek) connect_destroy(player, 'song-started', self._on_song_started) connect_destroy(player, 'song-ended', self._on_song_ended) connect_destroy(player, 'notify::seekable', self._on_seekable_changed) connect_destroy(library, 'changed', self._on_song_changed, player) self.connect('destroy', self._on_destroy) self._update(player) if player.info: self._create_waveform(player.info, CONFIG.max_data_points) def _create_waveform(self, song, points): # Close any existing pipeline to avoid leaks self._clean_pipeline() if not song.is_file: return command_template = """ uridecodebin name=uridec ! audioconvert ! level name=audiolevel interval={} post-messages=true ! fakesink sync=false""" interval = int(song("~#length") * 1E9 / points) print_d("Computing data for each %.3f seconds" % (interval / 1E9)) command = command_template.format(interval) pipeline = Gst.parse_launch(command) pipeline.get_by_name("uridec").set_property("uri", song("~uri")) bus = pipeline.get_bus() self._bus_id = bus.connect("message", self._on_bus_message) bus.add_signal_watch() pipeline.set_state(Gst.State.PLAYING) self._pipeline = pipeline self._new_rms_vals = [] def _on_bus_message(self, bus, message): if message.type == Gst.MessageType.ERROR: error, debug = message.parse_error() print_d("Error received from element {name}: {error}".format( name=message.src.get_name(), error=error)) print_d("Debugging information: {}".format(debug)) elif message.type == Gst.MessageType.ELEMENT: structure = message.get_structure() if structure.get_name() == "level": rms_db = structure.get_value("rms") if rms_db: # Calculate average of all channels (usually 2) rms_db_avg = sum(rms_db) / len(rms_db) # Normalize dB value to value between 0 and 1 rms = pow(10, (rms_db_avg / 20)) self._new_rms_vals.append(rms) else: print_w("Got unexpected message of type {}" .format(message.type)) elif message.type == Gst.MessageType.EOS: self._clean_pipeline() # Update the waveform with the new data self._rms_vals = self._new_rms_vals self._waveform_scale.reset(self._rms_vals) self._waveform_scale.set_placeholder(False) self._update_redraw_interval() # Clear temporary reference to the waveform data del self._new_rms_vals def _clean_pipeline(self): if hasattr(self, "_pipeline") and self._pipeline: self._pipeline.set_state(Gst.State.NULL) if self._bus_id: bus = self._pipeline.get_bus() bus.remove_signal_watch() bus.disconnect(self._bus_id) self._bus_id = None if self._pipeline: self._pipeline = None def _update_redraw_interval(self, *args): if self._player.info and self.is_visible(): # Must be recomputed when size is changed interval = self._waveform_scale.compute_redraw_interval() self._redraw_tracker.set_interval(interval) def _on_destroy(self, *args): self._clean_pipeline() self._label_tracker.destroy() self._redraw_tracker.destroy() def _on_tick_label(self, tracker, player): self._update_label(player) def _on_tick_waveform(self, tracker, player): self._update_waveform(player) def _on_seekable_changed(self, player, *args): self._update_label(player) def _on_player_seek(self, player, song, ms): self._update(player) def _on_song_changed(self, library, songs, player): if not player.info: return # Check that the currently playing song has changed if player.info in songs: # Trigger a re-computation of the waveform self._create_waveform(player.info, CONFIG.max_data_points) self._resize_labels(player.info) # Only update the label if some tag value changed self._update_label(player) def _on_song_started(self, player, song): if player.info: # Trigger a re-computation of the waveform self._create_waveform(player.info, CONFIG.max_data_points) self._resize_labels(player.info) self._waveform_scale.set_placeholder(True) self._update(player, True) def _on_song_ended(self, player, song, ended): self._update(player) def _update(self, player, full_redraw=False): self._update_label(player) self._update_waveform(player, full_redraw) def _update_label(self, player): if player.info: if self._hovering: # Show the position pointed by the mouse position = self._waveform_scale.get_mouse_position() else: # Show the position of the player (converted in seconds) position = player.get_position() / 1000.0 length = player.info("~#length") remaining = length - position self._elapsed_label.set_time(position) self._remaining_label.set_time(remaining) self._elapsed_label.set_disabled(not player.seekable) self._remaining_label.set_disabled(not player.seekable) self.set_sensitive(player.seekable) else: self._remaining_label.set_disabled(True) self._elapsed_label.set_disabled(True) self.set_sensitive(False) def _update_waveform(self, player, full_redraw=False): if player.info: # Position in ms, length in seconds position = player.get_position() / 1000.0 length = player.info("~#length") if length != 0: self._waveform_scale.set_position(position / length) else: print_d("Length reported as zero for %s" % player.info) self._waveform_scale.set_position(0) if position == 0 or full_redraw: self._waveform_scale.queue_draw() else: (x, y, w, h) = self._waveform_scale.compute_redraw_area() self._waveform_scale.queue_draw_area(x, y, w, h) else: self._waveform_scale.set_placeholder(True) self._waveform_scale.queue_draw() def _on_mouse_hover(self, _, event): def clamp(a, x, b): '''Return x if a <= x <= b, else the a or b nearest to x.''' return min(max(x, a), b) width = self._waveform_scale.get_allocation().width self._waveform_scale.set_mouse_x_position(clamp(0, event.x, width)) if self._hovering: (x, y, w, h) = self._waveform_scale.compute_hover_redraw_area() self._waveform_scale.queue_draw_area(x, y, w, h) else: self._waveform_scale.queue_draw() self._update_label(self._player) self._hovering = True def _on_mouse_leave(self, _, event): self._waveform_scale.set_mouse_x_position(-1) self._waveform_scale.queue_draw() self._hovering = False self._update_label(self._player) def _resize_labels(self, song): """Resize the labels to make sure there is enough space to display the length of the songs. This prevents the waveform from changing size when the position changes from 9:59 to 10:00 for example.""" length = util.format_time_display(song("~#length")) # Get the width needed to display the length of the song (the text # displayed in the labels will always be shorter than that) layout = self._remaining_label.get_layout() layout.set_text(length, -1) width, height = layout.get_pixel_size() # Set it as the minimum width of the labels to prevent them from # changing width self._remaining_label.set_size_request(width, -1) self._elapsed_label.set_size_request(width, -1)
class WaveformSeekBar(Gtk.Box): """A widget containing labels and the seekbar.""" def __init__(self, player, library): super().__init__() self._player = player self._rms_vals = [] self._hovering = False self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() self._waveform_scale = WaveformScale(player) self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(self._waveform_scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self.set_time_label_visibility(CONFIG.show_time_labels) self._waveform_scale.connect('size-allocate', self._update_redraw_interval) self._waveform_scale.connect('motion-notify-event', self._on_mouse_hover) self._waveform_scale.connect('leave-notify-event', self._on_mouse_leave) self._label_tracker = TimeTracker(player) self._label_tracker.connect('tick', self._on_tick_label, player) self._redraw_tracker = TimeTracker(player) self._redraw_tracker.connect('tick', self._on_tick_waveform, player) connect_destroy(player, 'seek', self._on_player_seek) connect_destroy(player, 'song-started', self._on_song_started) connect_destroy(player, 'song-ended', self._on_song_ended) connect_destroy(player, 'notify::seekable', self._on_seekable_changed) connect_destroy(library, 'changed', self._on_song_changed, player) self.connect('destroy', self._on_destroy) self._update(player) if player.info: self._create_waveform(player.info, CONFIG.max_data_points) def set_time_label_visibility(self, is_visible): self._time_labels_visible = is_visible if is_visible: self._elapsed_label.show() self._remaining_label.show() else: self._elapsed_label.hide() self._remaining_label.hide() def _create_waveform(self, song, points): # Close any existing pipeline to avoid leaks self._clean_pipeline() if not song.is_file: return command_template = """ uridecodebin name=uridec ! audioconvert ! level name=audiolevel interval={} post-messages=true ! fakesink sync=false""" interval = int(song("~#length") * 1E9 / points) if not interval: return print_d("Computing data for each %.3f seconds" % (interval / 1E9)) command = command_template.format(interval) pipeline = Gst.parse_launch(command) pipeline.get_by_name("uridec").set_property("uri", uri2gsturi(song("~uri"))) bus = pipeline.get_bus() self._bus_id = bus.connect("message", self._on_bus_message, points) bus.add_signal_watch() pipeline.set_state(Gst.State.PLAYING) self._pipeline = pipeline self._new_rms_vals = [] def _on_bus_message(self, bus, message, points): force_stop = False if message.type == Gst.MessageType.ERROR: error, debug = message.parse_error() print_d("Error received from element {name}: {error}".format( name=message.src.get_name(), error=error)) print_d("Debugging information: {}".format(debug)) elif message.type == Gst.MessageType.ELEMENT: structure = message.get_structure() if structure.get_name() == "level": rms_db = structure.get_value("rms") if rms_db: # Calculate average of all channels (usually 2) rms_db_avg = sum(rms_db) / len(rms_db) # Normalize dB value to value between 0 and 1 rms = pow(10, (rms_db_avg / 20)) self._new_rms_vals.append(rms) if len(self._new_rms_vals) >= points: # The audio might be much longer than we anticipated # and we would get way too many events due to the too # short interval set. force_stop = True else: print_w("Got unexpected message of type {}".format( message.type)) if message.type == Gst.MessageType.EOS or force_stop: self._clean_pipeline() # Update the waveform with the new data self._rms_vals = self._new_rms_vals self._waveform_scale.reset(self._rms_vals) self._update_redraw_interval() # Clear temporary reference to the waveform data del self._new_rms_vals def _clean_pipeline(self): if hasattr(self, "_pipeline") and self._pipeline: self._pipeline.set_state(Gst.State.NULL) if self._bus_id: bus = self._pipeline.get_bus() bus.remove_signal_watch() bus.disconnect(self._bus_id) self._bus_id = None if self._pipeline: self._pipeline = None def _update_redraw_interval(self, *args): if self._player.info and self.is_visible(): # Must be recomputed when size is changed interval = self._waveform_scale.compute_redraw_interval() self._redraw_tracker.set_interval(interval) def _on_destroy(self, *args): self._clean_pipeline() self._label_tracker.destroy() self._redraw_tracker.destroy() def _on_tick_label(self, tracker, player): self._update_label(player) def _on_tick_waveform(self, tracker, player): self._update_waveform(player) def _on_seekable_changed(self, player, *args): self._update_label(player) def _on_player_seek(self, player, song, ms): self._update(player) def _on_song_changed(self, library, songs, player): if not player.info: return # Check that the currently playing song has changed if player.info in songs: # Trigger a re-computation of the waveform self._create_waveform(player.info, CONFIG.max_data_points) self._resize_labels(player.info) # Only update the label if some tag value changed self._update_label(player) def _on_song_started(self, player, song): if player.info: # Trigger a re-computation of the waveform self._create_waveform(player.info, CONFIG.max_data_points) self._resize_labels(player.info) self._rms_vals.clear() self._update(player, True) def _on_song_ended(self, player, song, ended): self._update(player) def _update(self, player, full_redraw=False): self._update_label(player) self._update_waveform(player, full_redraw) def _update_label(self, player): if not self._time_labels_visible: self.set_sensitive(player.info is not None and player.seekable) return if player.info: if self._hovering: # Show the position pointed by the mouse position = self._waveform_scale.get_mouse_position() else: # Show the position of the player (converted in seconds) position = player.get_position() / 1000.0 length = player.info("~#length") remaining = length - position self._elapsed_label.set_time(position) self._remaining_label.set_time(remaining) self._elapsed_label.set_disabled(not player.seekable) self._remaining_label.set_disabled(not player.seekable) self.set_sensitive(player.seekable) else: self._remaining_label.set_disabled(True) self._elapsed_label.set_disabled(True) self.set_sensitive(False) def _update_waveform(self, player, full_redraw=False): if player.info: # Position in ms, length in seconds position = player.get_position() / 1000.0 length = player.info("~#length") if length != 0: self._waveform_scale.set_position(position / length) else: print_d("Length reported as zero for %s" % player.info) self._waveform_scale.set_position(0) if position == 0 or full_redraw: self._waveform_scale.queue_draw() else: (x, y, w, h) = self._waveform_scale.compute_redraw_area() self._waveform_scale.queue_draw_area(x, y, w, h) else: self._rms_vals.clear() self._waveform_scale.queue_draw() def _on_mouse_hover(self, _, event): def clamp(a, x, b): """Return x if a <= x <= b, else the a or b nearest to x.""" return min(max(x, a), b) width = self._waveform_scale.get_allocation().width self._waveform_scale.set_mouse_x_position(clamp(0, event.x, width)) if self._hovering: (x, y, w, h) = self._waveform_scale.compute_hover_redraw_area() self._waveform_scale.queue_draw_area(x, y, w, h) else: self._waveform_scale.queue_draw() self._update_label(self._player) self._hovering = True def _on_mouse_leave(self, _, event): self._waveform_scale.set_mouse_x_position(-1) self._waveform_scale.queue_draw() self._hovering = False self._update_label(self._player) def _resize_labels(self, song): """Resize the labels to make sure there is enough space to display the length of the songs. This prevents the waveform from changing size when the position changes from 9:59 to 10:00 for example.""" length = util.format_time_display(song("~#length")) # Get the width needed to display the length of the song (the text # displayed in the labels will always be shorter than that) layout = self._remaining_label.get_layout() layout.set_text(length, -1) width, height = layout.get_pixel_size() # Set it as the minimum width of the labels to prevent them from # changing width self._remaining_label.set_size_request(width, -1) self._elapsed_label.set_size_request(width, -1)
class WaveformSeekBar(Gtk.Box): """A widget containing labels and the seekbar.""" def __init__(self, player, library): super(WaveformSeekBar, self).__init__() self._player = player self._rms_vals = [] self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() self._waveform_scale = WaveformScale(player) self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(self._waveform_scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self._waveform_scale.connect('size-allocate', self._update_redraw_interval) self._label_tracker = TimeTracker(player) self._label_tracker.connect('tick', self._on_tick_label, player) self._redraw_tracker = TimeTracker(player) self._redraw_tracker.connect('tick', self._on_tick_waveform, player) connect_destroy(player, 'seek', self._on_player_seek) connect_destroy(player, 'song-started', self._on_song_started) connect_destroy(player, 'song-ended', self._on_song_ended) connect_destroy(player, 'notify::seekable', self._on_seekable_changed) connect_destroy(library, 'changed', self._on_song_changed, player) self.connect('destroy', self._on_destroy) self._update(player) if player.info: self._create_waveform(player.info, CONFIG.max_data_points) def _create_waveform(self, song, points): # Close any existing pipelines to avoid warnings if hasattr(self, "_pipeline") and self._pipeline: self._pipeline.set_state(Gst.State.NULL) self._clean_pipeline() command_template = """ filesrc name=fs ! decodebin ! audioconvert ! level name=audiolevel interval={} post-messages=true ! fakesink sync=false""" interval = int(song("~#length") * 1E9 / points) print_d("Computing data for each %.3f seconds" % (interval / 1E9)) command = command_template.format(interval) pipeline = Gst.parse_launch(command) pipeline.get_by_name("fs").set_property("location", song("~filename")) bus = pipeline.get_bus() self._bus_id = bus.connect("message", self._on_bus_message) bus.add_signal_watch() pipeline.set_state(Gst.State.PLAYING) self._pipeline = pipeline self._new_rms_vals = [] def _on_bus_message(self, bus, message): if message.type == Gst.MessageType.ERROR: error, debug = message.parse_error() print_d("Error received from element {name}: {error}".format( name=message.src.get_name(), error=error)) print_d("Debugging information: {}".format(debug)) elif message.type == Gst.MessageType.ELEMENT: structure = message.get_structure() if structure.get_name() == "level": rms_db = structure.get_value("rms") # Calculate average of all channels (usually 2) rms_db_avg = sum(rms_db) / len(rms_db) # Normalize dB value to value between 0 and 1 rms = pow(10, (rms_db_avg / 20)) self._new_rms_vals.append(rms) else: print_w("Got unexpected message of type {}".format( message.type)) elif message.type == Gst.MessageType.EOS: self._pipeline.set_state(Gst.State.NULL) self._clean_pipeline() # Only update the waveform if it has changed if self._player.info and self._rms_vals != self._new_rms_vals: self._rms_vals = self._new_rms_vals self._waveform_scale.reset(self._rms_vals) self._waveform_scale.set_placeholder(False) self._update_redraw_interval() else: del self._new_rms_vals def _clean_pipeline(self): if self._bus_id: bus = self._pipeline.get_bus() bus.remove_signal_watch() bus.disconnect(self._bus_id) self._bus_id = None if self._pipeline: self._pipeline = None def _update_redraw_interval(self, *args): if self._player.info: # Must be recomputed when size is changed interval = self._waveform_scale.compute_redraw_interval() self._redraw_tracker.set_interval(interval) def _on_destroy(self, *args): self._label_tracker.destroy() self._redraw_tracker.destroy() def _on_tick_label(self, tracker, player): self._update_label(player) def _on_tick_waveform(self, tracker, player): self._update_waveform(player) def _on_seekable_changed(self, player, *args): self._update_label(player) def _on_player_seek(self, player, song, ms): self._update(player) def _on_song_changed(self, library, songs, player): # Check that the currently playing song has changed if player.info and player.info in songs: # Trigger a re-computation of the waveform self._create_waveform(player.info, CONFIG.max_data_points) # Only update the label if some tag value changed self._update_label(player) def _on_song_started(self, player, song): if player.info: # Trigger a re-computation of the waveform self._create_waveform(player.info, CONFIG.max_data_points) self._waveform_scale.set_placeholder(True) self._update(player, True) def _on_song_ended(self, player, song, ended): self._update(player) def _update(self, player, full_redraw=False): self._update_label(player) self._update_waveform(player, full_redraw) def _update_label(self, player): if player.info: # Position in ms, length in seconds position = player.get_position() / 1000.0 length = player.info("~#length") remaining = length - position self._elapsed_label.set_time(position) self._remaining_label.set_time(remaining) self._elapsed_label.set_disabled(not player.seekable) self._remaining_label.set_disabled(not player.seekable) self.set_sensitive(player.seekable) else: self._remaining_label.set_disabled(True) self._elapsed_label.set_disabled(True) self.set_sensitive(False) def _update_waveform(self, player, full_redraw=False): if player.info: # Position in ms, length in seconds position = player.get_position() / 1000.0 length = player.info("~#length") if length != 0: self._waveform_scale.set_position(position / length) else: print_d("Length reported as zero for %s" % player.info) self._waveform_scale.set_position(0) if position == 0 or full_redraw: self._waveform_scale.queue_draw() else: (x, y, w, h) = self._waveform_scale.compute_redraw_area() self._waveform_scale.queue_draw_area(x, y, w, h) else: self._waveform_scale.set_placeholder(True) self._waveform_scale.queue_draw()
class WaveformSeekBar(Gtk.Box): """A widget containing labels and the seekbar.""" def __init__(self, player, library): super(WaveformSeekBar, self).__init__() self._player = player self._rms_vals = [] self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() self._waveform_scale = WaveformScale(player) self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(self._waveform_scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self._waveform_scale.connect('size-allocate', self._update_redraw_interval) self._label_tracker = TimeTracker(player) self._label_tracker.connect('tick', self._on_tick_label, player) self._redraw_tracker = TimeTracker(player) self._redraw_tracker.connect('tick', self._on_tick_waveform, player) connect_destroy(player, 'seek', self._on_player_seek) connect_destroy(player, 'song-started', self._on_song_started) connect_destroy(player, 'song-ended', self._on_song_ended) connect_destroy(player, 'notify::seekable', self._on_seekable_changed) connect_destroy(library, 'changed', self._on_song_changed, player) self.connect('destroy', self._on_destroy) self._update(player) if player.info: self._create_waveform(player.info, CONFIG.max_data_points) def _create_waveform(self, song, points): # Close any existing pipeline to avoid leaks self._clean_pipeline() command_template = """ filesrc name=fs ! decodebin ! audioconvert ! level name=audiolevel interval={} post-messages=true ! fakesink sync=false""" interval = int(song("~#length") * 1E9 / points) print_d("Computing data for each %.3f seconds" % (interval / 1E9)) command = command_template.format(interval) pipeline = Gst.parse_launch(command) pipeline.get_by_name("fs").set_property("location", song("~filename")) bus = pipeline.get_bus() self._bus_id = bus.connect("message", self._on_bus_message) bus.add_signal_watch() pipeline.set_state(Gst.State.PLAYING) self._pipeline = pipeline self._new_rms_vals = [] def _on_bus_message(self, bus, message): if message.type == Gst.MessageType.ERROR: error, debug = message.parse_error() print_d("Error received from element {name}: {error}".format( name=message.src.get_name(), error=error)) print_d("Debugging information: {}".format(debug)) elif message.type == Gst.MessageType.ELEMENT: structure = message.get_structure() if structure.get_name() == "level": rms_db = structure.get_value("rms") # Calculate average of all channels (usually 2) rms_db_avg = sum(rms_db) / len(rms_db) # Normalize dB value to value between 0 and 1 rms = pow(10, (rms_db_avg / 20)) self._new_rms_vals.append(rms) else: print_w("Got unexpected message of type {}" .format(message.type)) elif message.type == Gst.MessageType.EOS: self._clean_pipeline() # Update the waveform with the new data self._rms_vals = self._new_rms_vals self._waveform_scale.reset(self._rms_vals) self._waveform_scale.set_placeholder(False) self._update_redraw_interval() # Clear temporary reference to the waveform data del self._new_rms_vals def _clean_pipeline(self): if hasattr(self, "_pipeline") and self._pipeline: self._pipeline.set_state(Gst.State.NULL) if self._bus_id: bus = self._pipeline.get_bus() bus.remove_signal_watch() bus.disconnect(self._bus_id) self._bus_id = None if self._pipeline: self._pipeline = None def _update_redraw_interval(self, *args): if self._player.info and self.is_visible(): # Must be recomputed when size is changed interval = self._waveform_scale.compute_redraw_interval() self._redraw_tracker.set_interval(interval) def _on_destroy(self, *args): self._clean_pipeline() self._label_tracker.destroy() self._redraw_tracker.destroy() def _on_tick_label(self, tracker, player): self._update_label(player) def _on_tick_waveform(self, tracker, player): self._update_waveform(player) def _on_seekable_changed(self, player, *args): self._update_label(player) def _on_player_seek(self, player, song, ms): self._update(player) def _on_song_changed(self, library, songs, player): if not player.info: return if not player.info.is_file: print_d("%s is not a local file, skipping waveform calculation." % player.info("~filename")) return # Check that the currently playing song has changed if player.info in songs: # Trigger a re-computation of the waveform self._create_waveform(player.info, CONFIG.max_data_points) # Only update the label if some tag value changed self._update_label(player) def _on_song_started(self, player, song): if player.info and player.info.is_file: # Trigger a re-computation of the waveform self._create_waveform(player.info, CONFIG.max_data_points) self._waveform_scale.set_placeholder(True) self._update(player, True) def _on_song_ended(self, player, song, ended): self._update(player) def _update(self, player, full_redraw=False): self._update_label(player) self._update_waveform(player, full_redraw) def _update_label(self, player): if player.info: # Position in ms, length in seconds position = player.get_position() / 1000.0 length = player.info("~#length") remaining = length - position self._elapsed_label.set_time(position) self._remaining_label.set_time(remaining) self._elapsed_label.set_disabled(not player.seekable) self._remaining_label.set_disabled(not player.seekable) self.set_sensitive(player.seekable) else: self._remaining_label.set_disabled(True) self._elapsed_label.set_disabled(True) self.set_sensitive(False) def _update_waveform(self, player, full_redraw=False): if player.info: # Position in ms, length in seconds position = player.get_position() / 1000.0 length = player.info("~#length") if length != 0: self._waveform_scale.set_position(position / length) else: print_d("Length reported as zero for %s" % player.info) self._waveform_scale.set_position(0) if position == 0 or full_redraw: self._waveform_scale.queue_draw() else: (x, y, w, h) = self._waveform_scale.compute_redraw_area() self._waveform_scale.queue_draw_area(x, y, w, h) else: self._waveform_scale.set_placeholder(True) self._waveform_scale.queue_draw()
class SeekBar(Gtk.Box): def __init__(self, player, library): super(SeekBar, self).__init__() self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) scale.set_adjustment(Gtk.Adjustment.new(0, 0, 0, 3, 15, 0)) scale.set_draw_value(False) self._scale = scale self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self._id = self._scale.connect("value-changed", self._on_user_changed, player) self._scale.connect("value-changed", self._on_scale_value_changed, player) self._tracker = TimeTracker(player) self._tracker.connect("tick", self._on_tick, player) connect_destroy(player, "seek", self._on_player_seek) connect_destroy(player, "song-started", self._on_song_started) connect_destroy(player, "notify::seekable", self._on_seekable_changed) connect_destroy(library, "changed", self._on_song_changed, player) self.connect("destroy", self._on_destroy) with self._inhibit(): self._update(player) self._tracker.tick() def _on_destroy(self, *args): self._tracker.destroy() @contextlib.contextmanager def _inhibit(self): with GObject.signal_handler_block(self._scale, self._id): yield def _on_user_changed(self, scale, player): if player.seekable: player.seek(scale.get_value() * 1000) def _on_scale_value_changed(self, scale, player): self._update(player) def _on_tick(self, tracker, player): position = player.get_position() // 1000 with self._inhibit(): self._scale.set_value(position) def _on_seekable_changed(self, player, *args): with self._inhibit(): self._update(player) def _on_song_changed(self, library, songs, player): if player.info in songs: with self._inhibit(): self._update(player) def _on_player_seek(self, player, song, ms): with self._inhibit(): self._scale.set_value(ms // 1000) self._update(player) def _on_song_started(self, player, song): with self._inhibit(): self._scale.set_value(0) self._update(player) def _update(self, player): if player.info: self._scale.set_range(0, player.info("~#length")) else: self._scale.set_range(0, 1) if not player.seekable: self._scale.set_value(0) value = self._scale.get_value() max_ = self._scale.get_adjustment().get_upper() remaining = value - max_ self._elapsed_label.set_time(value) self._remaining_label.set_time(remaining) self._remaining_label.set_disabled(not player.seekable) self._elapsed_label.set_disabled(not player.seekable) self.set_sensitive(player.seekable)
def enabled(self): self._seekpoint_A, self._seekpoint_B = self._get_seekpoints() self._tracker = TimeTracker(app.player) self._tracker.connect('tick', self._on_tick)
class SeekPointsPlugin(EventPlugin, PluginConfigMixin): """The plugin class.""" PLUGIN_ID = "Seekpoints" PLUGIN_NAME = _("Seekpoint Bookmarks") PLUGIN_ICON = Icons.GO_JUMP PLUGIN_CONFIG_SECTION = __name__ PLUGIN_DESC = _( "Store Seekpoints A and/or B for tracks. " "Skip to time A and stop after time B when track is played.\n" "Note that changing the names of the points below does not " "update the actual bookmark names, it only changes which " "bookmark names the plugin looks for when deciding whether to seek.") CFG_SEEKPOINT_A_TEXT = "A" CFG_SEEKPOINT_B_TEXT = "B" DEFAULT_A_TEXT = "A" DEFAULT_B_TEXT = "B" def enabled(self): self._seekpoint_A, self._seekpoint_B = self._get_seekpoints() self._tracker = TimeTracker(app.player) self._tracker.connect('tick', self._on_tick) def disabled(self): self._tracker.destroy() def plugin_on_song_started(self, song): """Seeks to point A if it exists, and also fetches the bookmarks of the current track that matches the seekpoint names set in config. """ self._seekpoint_A, self._seekpoint_B = self._get_seekpoints() if not self._seekpoint_A: return self._seek(self._seekpoint_A) def _on_tick(self, tracker): """Checks whether the current position is past point B if it exists, and if it is -- seek to the end of the track. """ if not self._seekpoint_B: return time = app.player.get_position() // 1000 if self._seekpoint_B <= time: self._seek(app.player.info("~#length")) def _get_seekpoints(self): """Reads seekpoint-names from config, which are compared to the bookmark-names of the current track to get timestamps (if any). """ if not app.player.song: return None, None marks = [] if has_bookmark(app.player.song): marks = app.player.song.bookmarks seekpoint_A = None seekpoint_B = None seekpoint_A_name = self.config_get(self.CFG_SEEKPOINT_A_TEXT, self.DEFAULT_A_TEXT) seekpoint_B_name = self.config_get(self.CFG_SEEKPOINT_B_TEXT, self.DEFAULT_B_TEXT) for time, mark in marks: if mark == seekpoint_A_name: seekpoint_A = time elif mark == seekpoint_B_name: seekpoint_B = time # if seekpoints are not properly ordered (or identical), the track # will likely endlessly seek when looping tracks, so discard B # (maybe raise an exception for the plugin list?). if (seekpoint_A is not None) and (seekpoint_B is not None): if seekpoint_A >= seekpoint_B: return seekpoint_A, None return seekpoint_A, seekpoint_B def _seek(self, seconds): app.player.seek(seconds * 1000) def PluginPreferences(self, parent): vb = Gtk.VBox(spacing=12) # Bookmark name to use for point A hb = Gtk.HBox(spacing=6) entry = UndoEntry() entry.set_text( self.config_get(self.CFG_SEEKPOINT_A_TEXT, self.DEFAULT_A_TEXT)) entry.connect('changed', self.config_entry_changed, self.CFG_SEEKPOINT_A_TEXT) lbl = Gtk.Label(label=_("Bookmark name for point A")) entry.set_tooltip_markup( _("Bookmark name to check for when " "a track is started, and if found the player seeks to that " "timestamp")) lbl.set_mnemonic_widget(entry) hb.pack_start(lbl, False, True, 0) hb.pack_start(entry, True, True, 0) vb.pack_start(hb, True, True, 0) # Bookmark name to use for point B hb = Gtk.HBox(spacing=6) entry = UndoEntry() entry.set_text( self.config_get(self.CFG_SEEKPOINT_B_TEXT, self.DEFAULT_B_TEXT)) entry.connect('changed', self.config_entry_changed, self.CFG_SEEKPOINT_B_TEXT) lbl = Gtk.Label(label=_("Bookmark name for point B")) entry.set_tooltip_markup( _("Bookmark name to use each tick during " "play of a track if it exist. If the current position exceeds " "the timestamp, seek to the end of the track.")) lbl.set_mnemonic_widget(entry) hb.pack_start(lbl, False, True, 0) hb.pack_start(entry, True, True, 0) vb.pack_start(hb, True, True, 0) return vb
class SeekBar(Gtk.Box): def __init__(self, player, library): super(SeekBar, self).__init__() self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) scale.set_adjustment(Gtk.Adjustment.new(0, 0, 0, 3, -15, 0)) scale.set_draw_value(False) self._scale = scale self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self._id = self._scale.connect( 'value-changed', self._on_user_changed, player) self._scale.connect( 'value-changed', self._on_scale_value_changed, player) self._tracker = TimeTracker(player) self._tracker.connect('tick', self._on_tick, player) connect_destroy(player, 'seek', self._on_player_seek) connect_destroy(player, 'song-started', self._on_song_started) connect_destroy(player, "notify::seekable", self._on_seekable_changed) connect_destroy( library, "changed", self._on_song_changed, player) self.connect("destroy", self._on_destroy) with self._inhibit(): self._update(player) self._tracker.tick() def _on_destroy(self, *args): self._tracker.destroy() @contextlib.contextmanager def _inhibit(self): with GObject.signal_handler_block(self._scale, self._id): yield def _on_user_changed(self, scale, player): if player.seekable: player.seek(scale.get_value() * 1000) def _on_scale_value_changed(self, scale, player): self._update(player) def _on_tick(self, tracker, player): position = player.get_position() // 1000 with self._inhibit(): self._scale.set_value(position) def _on_seekable_changed(self, player, *args): with self._inhibit(): self._update(player) def _on_song_changed(self, library, songs, player): if player.info in songs: with self._inhibit(): self._update(player) def _on_player_seek(self, player, song, ms): with self._inhibit(): self._scale.set_value(ms // 1000) self._update(player) def _on_song_started(self, player, song): with self._inhibit(): self._scale.set_value(0) self._update(player) def _update(self, player): if player.info: self._scale.set_range(0, player.info("~#length")) else: self._scale.set_range(0, 1) if not player.seekable: self._scale.set_value(0) value = self._scale.get_value() max_ = self._scale.get_adjustment().get_upper() remaining = value - max_ self._elapsed_label.set_time(value) self._remaining_label.set_time(remaining) self._remaining_label.set_disabled(not player.seekable) self._elapsed_label.set_disabled(not player.seekable) self.set_sensitive(player.seekable)
class WaveformSeekBar(Gtk.Box): """A widget containing labels and the seekbar.""" def __init__(self, player, library): super(WaveformSeekBar, self).__init__() self._player = player self._rms_vals = [] self._elapsed_label = TimeLabel() self._remaining_label = TimeLabel() self._waveform_scale = WaveformScale() self.pack_start(Align(self._elapsed_label, border=6), False, True, 0) self.pack_start(self._waveform_scale, True, True, 0) self.pack_start(Align(self._remaining_label, border=6), False, True, 0) for child in self.get_children(): child.show_all() self._tracker = TimeTracker(player) self._tracker.connect('tick', self._on_tick, player) connect_destroy(player, 'seek', self._on_player_seek) connect_destroy(player, 'song-started', self._on_song_started) connect_destroy(player, 'song-ended', self._on_song_ended) connect_destroy(player, 'notify::seekable', self._on_seekable_changed) connect_destroy(library, 'changed', self._on_song_changed, player) self.connect('destroy', self._on_destroy) self._update(player) self._tracker.tick() if player.info: self._create_waveform(player.info, CONFIG.data_size) def _create_waveform(self, song, points): # Close any existing pipelines to avoid warnings if hasattr(self, "_pipeline") and self._pipeline: self._pipeline.set_state(Gst.State.NULL) command_template = """ filesrc name=fs ! decodebin ! audioconvert ! level name=audiolevel interval={} post-messages=true ! fakesink sync=false""" interval = int(song("~#length") * 1E9 / points) print_d("Computing data for each %.3f seconds" % (interval / 1E9)) command = command_template.format(interval) pipeline = Gst.parse_launch(command) pipeline.get_by_name("fs").set_property("location", song("~filename")) bus = pipeline.get_bus() self._bus_id = bus.connect("message", self._on_bus_message) bus.add_signal_watch() pipeline.set_state(Gst.State.PLAYING) self._pipeline = pipeline self._rms_vals = [] def _on_bus_message(self, bus, message): if message.type == Gst.MessageType.ERROR: error, debug = message.parse_error() print_d("Error received from element {name}: {error}".format( name=message.src.get_name(), error=error)) print_d("Debugging information: {}".format(debug)) elif message.type == Gst.MessageType.ELEMENT: structure = message.get_structure() if structure.get_name() == "level": rms_db = structure.get_value("rms") # Calculate average of all channels (usually 2) rms_db_avg = sum(rms_db) / len(rms_db) # Normalize dB value to value between 0 and 1 rms = pow(10, (rms_db_avg / 20)) self._rms_vals.append(rms) else: print_w("Got unexpected message of type {}" .format(message.type)) elif message.type == Gst.MessageType.EOS: self._pipeline.set_state(Gst.State.NULL) if self._player.info: self._waveform_scale.reset(self._rms_vals, self._player) self._waveform_scale.set_placeholder(False) def _on_destroy(self, *args): self._tracker.destroy() def _on_tick(self, tracker, player): self._update(player) def _on_seekable_changed(self, player, *args): self._update(player) def _on_player_seek(self, player, song, ms): self._update(player) def _on_song_changed(self, library, songs, player): if player.info: self._create_waveform(player.info, CONFIG.data_size) self._waveform_scale.set_placeholder(True) self._update(player) def _on_song_started(self, player, song): self._update(player) def _on_song_ended(self, player, song, ended): self._update(player) def _update(self, player): if player.info: # Position in ms, length in seconds position = player.get_position() / 1000.0 length = player.info("~#length") remaining = length - position if length != 0: self._waveform_scale.set_position(position / length) else: print_d("Length reported as zero for %s" % player.info) self._waveform_scale.set_position(0) self._elapsed_label.set_time(position) self._remaining_label.set_time(remaining) self._remaining_label.set_disabled(not player.seekable) self._elapsed_label.set_disabled(not player.seekable) self.set_sensitive(player.seekable) else: self._waveform_scale.set_placeholder(True) self._remaining_label.set_disabled(True) self._elapsed_label.set_disabled(True) self.set_sensitive(player.seekable) self._waveform_scale.queue_draw()
class SeekPointsPlugin(EventPlugin, PluginConfigMixin): """The plugin class.""" PLUGIN_ID = "Seekpoints" PLUGIN_NAME = _("Seekpoint Bookmarks") PLUGIN_ICON = Icons.GO_JUMP PLUGIN_CONFIG_SECTION = __name__ PLUGIN_DESC = _( "Store Seekpoints A and/or B for tracks. " "Skip to time A and stop after time B when track is played.\n" "Note that changing the names of the points below does not " "update the actual bookmark names, it only changes which " "bookmark names the plugin looks for when deciding whether to seek.") CFG_SEEKPOINT_A_TEXT = "A" CFG_SEEKPOINT_B_TEXT = "B" DEFAULT_A_TEXT = "A" DEFAULT_B_TEXT = "B" def enabled(self): self._seekpoint_A, self._seekpoint_B = self._get_seekpoints() self._tracker = TimeTracker(app.player) self._tracker.connect('tick', self._on_tick) def disabled(self): self._tracker.destroy() def plugin_on_song_started(self, song): """Seeks to point A if it exists, and also fetches the bookmarks of the current track that matches the seekpoint names set in config. """ self._seekpoint_A, self._seekpoint_B = self._get_seekpoints() if not self._seekpoint_A: return self._seek(self._seekpoint_A) def _on_tick(self, tracker): """Checks whether the current position is past point B if it exists, and if it is -- seek to the end of the track. """ if not self._seekpoint_B: return time = app.player.get_position() // 1000 if self._seekpoint_B <= time: self._seek(app.player.info("~#length")) def _get_seekpoints(self): """Reads seekpoint-names from config, which are compared to the bookmark-names of the current track to get timestamps (if any). """ if not app.player.song: return None, None marks = [] if has_bookmark(app.player.song): marks = app.player.song.bookmarks seekpoint_A = None seekpoint_B = None seekpoint_A_name = self.config_get(self.CFG_SEEKPOINT_A_TEXT, self.DEFAULT_A_TEXT) seekpoint_B_name = self.config_get(self.CFG_SEEKPOINT_B_TEXT, self.DEFAULT_B_TEXT) for time, mark in marks: if mark == seekpoint_A_name: seekpoint_A = time elif mark == seekpoint_B_name: seekpoint_B = time # if seekpoints are not properly ordered (or identical), the track # will likely endlessly seek when looping tracks, so discard B # (maybe raise an exception for the plugin list?). if (seekpoint_A is not None) and (seekpoint_B is not None): if seekpoint_A >= seekpoint_B: return seekpoint_A, None return seekpoint_A, seekpoint_B def _seek(self, seconds): app.player.seek(seconds * 1000) def PluginPreferences(self, parent): vb = Gtk.VBox(spacing=12) # Bookmark name to use for point A hb = Gtk.HBox(spacing=6) entry = UndoEntry() entry.set_text(self.config_get(self.CFG_SEEKPOINT_A_TEXT, self.DEFAULT_A_TEXT)) entry.connect('changed', self.config_entry_changed, self.CFG_SEEKPOINT_A_TEXT) lbl = Gtk.Label(label=_("Bookmark name for point A")) entry.set_tooltip_markup(_("Bookmark name to check for when " "a track is started, and if found the player seeks to that " "timestamp")) lbl.set_mnemonic_widget(entry) hb.pack_start(lbl, False, True, 0) hb.pack_start(entry, True, True, 0) vb.pack_start(hb, True, True, 0) # Bookmark name to use for point B hb = Gtk.HBox(spacing=6) entry = UndoEntry() entry.set_text(self.config_get(self.CFG_SEEKPOINT_B_TEXT, self.DEFAULT_B_TEXT)) entry.connect('changed', self.config_entry_changed, self.CFG_SEEKPOINT_B_TEXT) lbl = Gtk.Label(label=_("Bookmark name for point B")) entry.set_tooltip_markup(_("Bookmark name to use each tick during " "play of a track if it exist. If the current position exceeds " "the timestamp, seek to the end of the track.")) lbl.set_mnemonic_widget(entry) hb.pack_start(lbl, False, True, 0) hb.pack_start(entry, True, True, 0) vb.pack_start(hb, True, True, 0) return vb
class MatrixOrbitalLCD(EventPlugin): PLUGIN_ID = "MatrixOrbitalLCD" PLUGIN_NAME = "Matrix Orbital LCD" PLUGIN_DESC = _("Print info to a Matrix Orbital MX2/MX3 LCD.") PLUGIN_ICON = Icons.UTILITIES_TERMINAL PLUGIN_VERSION = "1.0" def align_text2bytes(self, text, h_align=Alignment.CENTER, v_align=Alignment.TOP): if (h_align == Alignment.LEFT): index = 1 elif (h_align == Alignment.RIGHT): index = max(CONFIG.lcd_width - len(text) + 1, 1) elif (h_align == Alignment.CENTER): index = floor((int(CONFIG.lcd_width) - len(text)) / 2) + 1 alignment = b"\xFEG" + bytes(chr(index), "utf8") if v_align == Alignment.TOP: alignment += b"\x01" elif v_align == Alignment.BOTTOM: alignment += b"\x02" return alignment + bytes(text, "utf8") def reset_lcd(self): # Empty LCD screen (X) and send cursor home (H). self.write_bytes(b"\xFEX\xFEH") def write_bytes(self, text): self._dev.write(text) def write_header_with_text(self, text=""): self.reset_lcd() self._write_header() self.write_bytes( self.align_text2bytes(text, Alignment.CENTER, Alignment.BOTTOM)) def _write_header(self, header="Quod Libet"): self.write_bytes(self.align_text2bytes(header)) def PluginPreferences(self, parent): def _path_changed(entry): CONFIG.lcd_dev = entry.get_text() def _width_changed(entry): try: CONFIG.lcd_width = int(entry.get_text()) except: CONFIG.lcd_width = 20 def _interval_changed(entry): try: CONFIG.lcd_interval = int(entry.get_text()) except: CONFIG.lcd_interval = 150 label = Gtk.Label(label=_("LCD serial device path:")) hbox = Gtk.HBox() hbox.pack_start(label, False, True, 6) entry = Gtk.Entry() entry.set_text(CONFIG.lcd_dev) entry.connect("changed", _path_changed) hbox.pack_start(entry, True, True, 6) vbox = Gtk.VBox() vbox.pack_start(hbox, False, True, 6) label = Gtk.Label(label=_("LCD device width (characters):")) hbox = Gtk.HBox() hbox.pack_start(label, False, True, 6) entry = Gtk.Entry() entry.set_text(CONFIG.lcd_width) entry.connect("changed", _width_changed) hbox.pack_start(entry, True, True, 6) vbox.pack_start(hbox, True, True, 0) label = Gtk.Label(label=_("LCD update interval (ms):")) hbox = Gtk.HBox() hbox.pack_start(label, False, True, 6) entry = Gtk.Entry() entry.set_text(CONFIG.lcd_interval) entry.connect("changed", _interval_changed) hbox.pack_start(entry, True, True, 6) vbox.pack_start(hbox, True, True, 6) return vbox def _failed_initialization(self): if not hasattr(self, "_dev"): print_e("Matrix Orbital LCD plugin not initialized correctly.") return True return False def enabled(self): if system() != "Linux": print_e(self.PLUGIN_NAME + " plugin requires a Linux environment.") return try: self._dev = open(CONFIG.lcd_dev, 'wb', buffering=0) except: print_e("Matrix Orbital LCD device not found at " + CONFIG.lcd_dev) return self.reset_lcd() # Ready horizontal bars. self.write_bytes(b"\xFEh") # Turn backlight on. self.write_bytes(b"\xFEB\x00") self._write_header() self._npld = NowPlayingLCDData(self) self._tracker = TimeTracker(app.player) self._tracker.connect('tick', self._npld.on_tracker_tick) self._tracker.set_interval(int(CONFIG.lcd_interval)) def disabled(self): if self._failed_initialization(): return self._tracker.destroy() self.reset_lcd() # Turn backlight off. self.write_bytes(b"\xFEF") def plugin_on_seek(self, song, msec): if self._failed_initialization(): return self.reset_lcd() self._npld.prevent_update(1) self._write_header(_("Seeking...")) percent_played = int(round(msec / float(song.get("~#length", 0)) / 10)) # Draw a horizontal bar graph starting at column 1 on row 2 # to the right (0), with a length of 0-100 pixels. self.write_bytes(b"\xFE\x7C\x01\x02\x00" + bytes(chr(percent_played), "utf8")) def plugin_on_song_started(self, song): if self._failed_initialization() or song is None: return self._npld.reset() self._npld.set_basic_info(song("artist"), song("title")) self._npld.set_disc_info(song("album"), song.get("discnumber"), song.get("tracknumber")) self.reset_lcd() def plugin_on_song_ended(self, song, stopped): if self._failed_initialization(): return self._npld.reset() self.write_header_with_text(_("* not playing *")) def plugin_on_paused(self): if self._failed_initialization(): return # No need to set pause for trackers manually. self.write_header_with_text(_("* paused *")) def plugin_on_unpaused(self): if self._failed_initialization(): return self._npld.set_forced_update()