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(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._on_size_change) 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) self._redraw_tracker = TimeTracker(player) self._redraw_tracker.connect('tick', self._on_tick_waveform) 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) self.connect('destroy', self._on_destroy) def _create_waveform(self, song): if not song.is_file or not self._waveform_scale.get_realized(): return # Close any existing pipeline to avoid leaks self._clean_pipeline() command_template = """ uridecodebin name=uridec ! audioconvert ! level name=audiolevel interval={} post-messages=true ! fakesink sync=false""" interval = int(song("~#length") * 1E9 / self._waveform_scale.width) 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 _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_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 _on_size_change(self, widget, allocation): self._update() if self._player.info: self._create_waveform(self._player.info) def _on_destroy(self, *args): self._clean_pipeline() self._label_tracker.destroy() self._redraw_tracker.destroy() def _on_tick_label(self, tracker): if self.is_visible(): self._update_label() def _on_tick_waveform(self, tracker): if self.is_visible(): self._update_waveform() def _on_seekable_changed(self, player, *args): self._update_label() def _on_player_seek(self, player, song, ms): self._update() def _on_song_changed(self, library, songs): if not self._player.info: return # Check that the currently playing song has changed if self._player.info in songs: # Trigger a re-computation of the waveform self._create_waveform(self._player.info) self._resize_labels(self._player.info) # Only update the label if some tag value changed self._update_label() def _on_song_started(self, player, song): if self._player.info: # Trigger a re-computation of the waveform self._create_waveform(self._player.info) self._resize_labels(self._player.info) self._waveform_scale.set_placeholder(True) self._update(True) def _on_song_ended(self, player, song, ended): self._update() def _update(self, full_redraw=False): self._update_label() self._update_waveform(full_redraw) def _update_label(self): if self._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 self._player (converted in seconds) position = self._player.get_position() / 1000.0 length = self._player.info("~#length") remaining = length - position self._elapsed_label.set_time(position) self._remaining_label.set_time(remaining) self._elapsed_label.set_disabled(not self._player.seekable) self._remaining_label.set_disabled(not self._player.seekable) self.set_sensitive(self._player.seekable) else: self._remaining_label.set_disabled(True) self._elapsed_label.set_disabled(True) self.set_sensitive(False) def _update_waveform(self, full_redraw=False): if self._player.info: # Position in ms, length in seconds position = self._player.get_position() / 1000.0 length = self._player.info("~#length") if length != 0: self._waveform_scale.set_position(position / length) else: print_d("Length reported as zero for %s" % self._player.info) self._waveform_scale.set_position(0) if (position == 0 or full_redraw or self._waveform_scale.is_placeholder()): # if the placeholder flag is set this means that the source is # most probably not a local file and has thus no waveform # In that case we want a 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._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() 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) def update_colors(self): self._waveform_scale.update_colors()
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 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 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()