def test_time_label(self): l = TimeLabel() l.set_time(42) time_text = l.get_text() l.set_disabled(True) disabled_text = l.get_text() self.assertNotEqual(time_text, disabled_text) l.set_disabled(False) self.assertEqual(l.get_text(), time_text)
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() 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 location="{}" ! decodebin ! 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)) filename = song("~filename").replace('"', '\\"') command = command_template.format(filename, interval) pipeline = Gst.parse_launch(command) 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 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)
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 SeekBar(Gtk.Box): def __init__(self, player, library): Gtk.Box.__init__(self) self.elapsed_label = TimeLabel() self.elapsed_label.set_name('elapsed-label') self.remaining_label = TimeLabel() self.remaining_label.set_name('remaining-label') self.remaining_label.set_width_chars(6) self.scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL) self.scale.set_name('seek-bar') self.scale.set_adjustment(Gtk.Adjustment.new(0, 0, 0, 30, 5, 0)) self.scale.set_digits(3) self.scale.set_show_fill_level(False) self.scale.set_draw_value(False) self._timer_modes = ['both', 'elapsed', 'remaining'] self.c_timer_mode = __name__ + '_timer_mode' self.__timer_mode = 'both' self.__pressed_lmb = False self.__source_id = None self.elapsed_button = Gtk.Button() self.box = Gtk.Box(spacing=3) self.elapsed_button.add(self.box) self.elapsed_button.set_relief(Gtk.ReliefStyle.NONE) self.pack_start(self.scale, True, True, 0) try: self.__timer_mode = config.get('plugins', self.c_timer_mode) except NoOptionError: config.set('plugins', self.c_timer_mode, 'both') else: if self.__timer_mode not in self._timer_modes: self.__timer_mode = 'both' self._set_timer_mode(self.__timer_mode) self.elapsed_label.show() self.remaining_label.show() self.box.show() self.elapsed_button.show() self.scale.show() self.elapsed_button.connect('clicked', self._on_timer_clicked) self.scale.connect('button-release-event', self._on_button_release, player) self.__id_button_press_event = self.scale.connect( 'button-press-event', self._on_button_press, player) self.__id_change_value = self.scale.connect( 'change-value', self._on_scale_value_change_request, player) self.__id_value_changed = self.scale.connect( 'value-changed', self._on_scale_value_changed, player) GObject.signal_handler_block(self.scale, self.__id_value_changed) self._tracker = SynchronizedTimeTracker(player) self._tracker.connect('tick', self._on_tick, player) connect_destroy(player, 'seek', self._on_seek) connect_destroy(player, 'song-started', self._on_song_start) connect_destroy(player, "notify::seekable", self._update) self.connect("destroy", self._on_destroy) self._update(player) def _set_timer_mode(self, mode='both'): widgets = find_widgets(self, TimeLabel) for elem in widgets: name = elem.get_name() if name == 'elapsed-label' or name == 'remaining-label': self.remove(elem) widgets = find_widgets(self.box, TimeLabel) for elem in widgets: name = elem.get_name() if name == 'elapsed-label' or name == 'remaining-label': self.box.remove(elem) if mode == 'both': self.box.pack_start(self.elapsed_label, True, True, 0) self.pack_start(self.remaining_label, False, True, 0) elif mode == 'remaining': self.box.pack_start(self.remaining_label, True, True, 0) elif mode == 'elapsed': self.box.pack_start(self.elapsed_label, True, True, 0) self.__timer_mode = mode config.set('plugins', self.c_timer_mode, self.__timer_mode) def _on_destroy(self, *args): self._tracker.destroy() def _on_timer_clicked(self, button): i = self._timer_modes.index(self.__timer_mode) self._set_timer_mode(self._timer_modes[i-1]) def _on_seek(self, player, song, ms): self._update_labels(player, ms) self._update_scale(player, ms) def _on_button_press(self, scale, event, player): GObject.signal_handler_block(self.scale, self.__id_button_press_event) self.__pressed_lmb = True GObject.signal_handler_unblock(self.scale, self.__id_value_changed) GObject.signal_handler_block(self.scale, self.__id_change_value) self.__pressed_lmb_scale_value = scale.get_value() def _on_button_release(self, scale, event, player): GObject.signal_handler_block(self.scale, self.__id_value_changed) value = scale.get_value() if player.seekable and self.__pressed_lmb_scale_value != value: player.seek(value * 1000) GObject.signal_handler_unblock(self.scale, self.__id_change_value) self.__pressed_lmb = False GObject.signal_handler_unblock( self.scale, self.__id_button_press_event) def _on_scale_value_changed(self, scale, player): elapsed = scale.get_value() remaining = elapsed - player.info("~#length") self.elapsed_label.set_time(elapsed) self.remaining_label.set_time(remaining) def _on_scale_value_change_request(self, scale, scroll, value, player): self.__pressed_lmb = True if self.__source_id is not None: GLib.source_remove(self.__source_id) self.__source_id = GLib.timeout_add( 200, self.__scroll_timeout, value, player) def __scroll_timeout(self, value, player): if player.seekable: player.seek(value * 1000) self.__pressed_lmb = False self.__source_id = None def _on_tick(self, tracker, player): if not self.__pressed_lmb: self._update_labels(player) self._update_scale(player) def _update_labels(self, player, ms=None): if ms is not None: elapsed = ms // 1000 else: elapsed = player.get_position() // 1000 remaining = elapsed - player.info("~#length") self.elapsed_label.set_time(elapsed) self.remaining_label.set_time(remaining) def _update_scale(self, player, ms=None): if ms is not None: pval = ms / 1000 else: pval = player.get_position() / 1000 sval = self.scale.get_value() if (abs(pval - sval) > 0.001): self.scale.set_value(pval) def _on_song_start(self, player, *args): self._update(player, song_start=True) def _update(self, player, *args, song_start=False): if player.info: self.scale.set_range(0, player.info("~#length")) else: self.scale.set_range(0, 1) if player.seekable: self.elapsed_label.set_disabled(False) self.remaining_label.set_disabled(False) self.set_sensitive(True) if song_start: self._update_labels(player, 0) self._update_scale(player, 0) else: self._update_labels(player) self._update_scale(player) else: self.scale.set_value(0) self.elapsed_label.set_disabled(True) self.remaining_label.set_disabled(True) self.set_sensitive(False)