[pitivi] timeline: Fix audio waveform offset
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] timeline: Fix audio waveform offset
- Date: Mon, 4 Oct 2021 21:23:37 +0000 (UTC)
commit fb894c19abc986ad1832e2a92b38b57991687b61
Author: Alexandru Băluț <alexandru balut gmail com>
Date: Sun Sep 19 23:03:15 2021 +0200
timeline: Fix audio waveform offset
Fix a rounding error when calculating the pixel where the start of the
rendered sample is on the timeline (`offset_px`).
Fixes #2574
pitivi/timeline/elements.py | 26 ++++++++---------
pitivi/timeline/previewers.py | 47 ++++++++++++++++++-------------
pitivi/timeline/timeline.py | 1 +
pitivi/utils/timeline.py | 4 ++-
tests/common.py | 11 ++++++--
tests/test_medialibrary.py | 2 +-
tests/test_previewers.py | 65 ++++++++++++++++++++++++++++++++++++++++++-
7 files changed, 117 insertions(+), 39 deletions(-)
---
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index 289902d8f..57f873dbd 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -666,9 +666,9 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
self.props.vexpand = True
- self.__previewer = self._get_previewer()
- if self.__previewer:
- self.add(self.__previewer)
+ self.previewer = self._get_previewer()
+ if self.previewer:
+ self.add(self.previewer)
self.__background = self._get_background()
if self.__background:
@@ -690,12 +690,12 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
def update_previewer(self):
"""Refreshes the previewer widget."""
- if self.__previewer:
- self.__previewer.refresh()
+ if self.previewer:
+ self.previewer.refresh()
def release(self):
- if self.__previewer:
- self.__previewer.release()
+ if self.previewer:
+ self.previewer.release()
if self.markers:
self._ges_elem.markers_manager.set_markers_box(None)
@@ -807,8 +807,8 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
def do_draw(self, cr):
self.propagate_draw(self.__background, cr)
- if self.__previewer:
- self.propagate_draw(self.__previewer, cr)
+ if self.previewer:
+ self.propagate_draw(self.previewer, cr)
if self.keyframe_curve and self.keyframe_curve.is_drawable():
project = self.timeline.app.project_manager.current_project
@@ -824,8 +824,8 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
selected and len(self.timeline.selection) == 1:
self.__create_keyframe_curve()
- if self.__previewer:
- self.__previewer.set_selected(selected)
+ if self.previewer:
+ self.previewer.set_selected(selected)
self.update_sizes_and_positions()
@@ -837,8 +837,8 @@ class TimelineElement(Gtk.Layout, Zoomable, Loggable):
if self.__background:
self.__background.set_size_request(width, height)
- if self.__previewer:
- self.__previewer.set_size_request(width, height)
+ if self.previewer:
+ self.previewer.set_size_request(width, height)
if self.markers:
self.markers.set_size_request(width, markers_height)
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
index 24a56f7a7..82fbe213f 100644
--- a/pitivi/timeline/previewers.py
+++ b/pitivi/timeline/previewers.py
@@ -1164,9 +1164,9 @@ class AudioPreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
self.surface = None
# The zoom level when self.surface has been created.
self._surface_zoom_level = 0
- # The samples range used when self.surface has been created.
- self._surface_start_ns = 0
- self._surface_end_ns = 0
+ # The pixels range self.surface corresponds to.
+ self._surface_start_px = 0
+ self._surface_end_px = 0
# The playback rate from last time the surface was updated.
self._rate = 1.0
@@ -1278,47 +1278,54 @@ class AudioPreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
# Nothing to draw.
return
- # The area we have to refresh is determined by the start and end
- # calculated in the context of the asset duration.
- rect = Gdk.cairo_get_clip_rectangle(context)[1]
- clip = self.ges_elem.get_parent()
+ # The area we have to refresh is this rect inside the clip.
+ # For example rect.x is > 0 when the start of the clip is out of view.
+ # rect.width = how many pixels of the clip are in view horizontally.
+ res, rect = Gdk.cairo_get_clip_rectangle(context)
+ assert res
+
start = self.ges_elem.props.start
inpoint = self.ges_elem.props.in_point
duration = self.ges_elem.props.duration
- max_duration = self.ges_elem.get_asset().get_filesource_asset().get_duration()
- start_ns = min(max(0, self.pixel_to_ns(rect.x) + inpoint), max_duration)
- end_ns = min(max(0, self.pixel_to_ns(rect.x + rect.width) + inpoint), max_duration)
# Get the overall rate of the clip in the current area the clip is used
+ clip = self.ges_elem.get_parent()
internal_end = clip.get_internal_time_from_timeline_time(self.ges_elem, start + duration)
internal_duration = internal_end - inpoint
rate = internal_duration / duration
+ inpoint_px = self.ns_to_pixel(start) - self.ns_to_pixel(start - inpoint / rate)
+ max_duration_px = self.ns_to_pixel(clip.maxduration)
+
+ start_px = min(max(0, inpoint_px + rect.x), max_duration_px)
+ end_px = min(max(0, inpoint_px + rect.x + rect.width), max_duration_px)
+
zoom = self.get_current_zoom_level()
height = self.get_allocation().height - 2 * CLIP_BORDER_WIDTH
if not self.surface or \
height != self.surface.get_height() or \
zoom != self._surface_zoom_level or \
- start_ns < self._surface_start_ns or \
+ start_px < self._surface_start_px or \
rate != self._rate or \
- end_ns > self._surface_end_ns:
+ end_px > self._surface_end_px:
+ # Generate a new surface since the previously generated one, if any,
+ # cannot be reused.
if self.surface:
self.surface.finish()
self.surface = None
self._surface_zoom_level = zoom
# The generated waveform is for an extended range if possible,
# so if the user scrolls we don't rebuild the waveform every time.
- extra = self.pixel_to_ns(WAVEFORM_SURFACE_EXTRA_PX)
- self._surface_start_ns = max(0, start_ns - extra)
- self._surface_end_ns = min(end_ns + extra, max_duration)
+ self._surface_start_px = max(0, start_px - WAVEFORM_SURFACE_EXTRA_PX)
self._rate = rate
+ self._surface_end_px = min(end_px + WAVEFORM_SURFACE_EXTRA_PX, max_duration_px)
sample_duration = SAMPLE_DURATION / rate
- range_start = min(max(0, int(self._surface_start_ns / sample_duration)), len(self.samples))
- range_end = min(max(0, int(self._surface_end_ns / sample_duration)), len(self.samples))
+ range_start = min(max(0, int(self.pixel_to_ns(self._surface_start_px) / sample_duration)),
len(self.samples))
+ range_end = min(max(0, int(self.pixel_to_ns(self._surface_end_px) / sample_duration)),
len(self.samples))
samples = self.samples[range_start:range_end]
- surface_width = self.ns_to_pixel(self._surface_end_ns - self._surface_start_ns)
+ surface_width = self._surface_end_px - self._surface_start_px
self.surface = renderer.fill_surface(samples, surface_width, height)
# Paint the surface, ignoring the clipped rect.
@@ -1327,8 +1334,8 @@ class AudioPreviewer(Gtk.Layout, Previewer, Zoomable, Loggable):
# the surface in context, if the entire asset would be drawn.
# 2. - inpoint, because we're drawing a clip, not the entire asset.
context.set_operator(cairo.OPERATOR_OVER)
- offset = self.ns_to_pixel(self._surface_start_ns - inpoint)
- context.set_source_surface(self.surface, offset, CLIP_BORDER_WIDTH)
+ offset_px = self._surface_start_px - inpoint_px
+ context.set_source_surface(self.surface, offset_px, CLIP_BORDER_WIDTH)
context.paint()
def _emit_done_on_idle(self):
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 5dcfca382..111977038 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -1555,6 +1555,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
duration = self.timeline.mod_duration(original_duration)
if duration != original_duration:
obj.set_duration(duration)
+ obj.props.max_duration = min(obj.props.max_duration, duration)
elif isinstance(obj, GES.Asset):
ges_clip = self.timeline.add_clip_to_layer(layer, obj, clip_position)
duration = ges_clip.props.duration
diff --git a/pitivi/utils/timeline.py b/pitivi/utils/timeline.py
index f87980658..36486d1c8 100644
--- a/pitivi/utils/timeline.py
+++ b/pitivi/utils/timeline.py
@@ -257,7 +257,7 @@ class EditingContext(GObject.Object, Loggable):
app (Pitivi): The app.
"""
- def __init__(self, focus, timeline, mode, edge, app, log_actions):
+ def __init__(self, focus, timeline, mode, edge, app, log_actions=None):
GObject.Object.__init__(self)
Loggable.__init__(self)
if isinstance(focus, GES.TrackElement):
@@ -456,6 +456,7 @@ class Zoomable:
# DIE YOU CUNTMUNCH CLOCK_TIME_NONE UBER STUPIDITY OF CRACK BINDINGS !!
if duration == Gst.CLOCK_TIME_NONE:
return 0
+
return int((float(duration) / Gst.SECOND) * cls.zoomratio)
@classmethod
@@ -465,6 +466,7 @@ class Zoomable:
# DIE YOU CUNTMUNCH CLOCK_TIME_NONE UBER STUPIDITY OF CRACK BINDINGS !!
if duration == Gst.CLOCK_TIME_NONE:
return 0
+
return (float(duration) / Gst.SECOND) * cls.zoomratio
def zoom_changed(self):
diff --git a/tests/common.py b/tests/common.py
index f6168ea69..d074fc703 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -282,13 +282,12 @@ def setup_timeline(func):
self.layer = self.timeline.append_layer()
self.action_log = self.app.action_log
- project = self.app.project_manager.current_project
self.timeline_container = TimelineContainer(self.app, editor_state=self.app.gui.editor.editor_state)
- self.timeline_container.set_project(project)
+ self.timeline_container.set_project(self.project)
self.app.gui.editor.timeline_ui = self.timeline_container
timeline = self.timeline_container.timeline
- timeline.app.project_manager.current_project = project
+ self.assertEqual(timeline.app.project_manager.current_project, self.project)
timeline.get_parent = mock.MagicMock(return_value=self.timeline_container)
func(self)
@@ -541,6 +540,12 @@ class TestCase(unittest.TestCase, Loggable):
return element
return None
+ def get_clip_element(self, ges_clip, element_class=GES.VideoSource):
+ for element in ges_clip.get_children(False):
+ if isinstance(element, element_class):
+ return element
+ return None
+
@staticmethod
def commit_cb(action_log, stack, stacks):
stacks.append(stack)
diff --git a/tests/test_medialibrary.py b/tests/test_medialibrary.py
index cd4d4b11a..c65852215 100644
--- a/tests/test_medialibrary.py
+++ b/tests/test_medialibrary.py
@@ -110,7 +110,7 @@ class BaseTestMediaLibrary(common.TestCase):
"notify::fraction", self._progress_bar_cb)
self._create_assets(samples)
- self.mainloop.run(timeout_seconds=20)
+ self.mainloop.run(timeout_seconds=25)
self.assertFalse(self.medialibrary._progressbar.props.visible)
def check_add_proxy(self, asset, scaled=False, w=160, h=120,
diff --git a/tests/test_previewers.py b/tests/test_previewers.py
index 2f0dfcf35..bdf320ade 100644
--- a/tests/test_previewers.py
+++ b/tests/test_previewers.py
@@ -15,12 +15,13 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, see <http://www.gnu.org/licenses/>.
"""Tests for the timeline.previewers module."""
-# pylint: disable=protected-access
+# pylint: disable=protected-access,unused-argument
import os
import tempfile
from unittest import mock
import numpy
+from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import GES
from gi.repository import Gst
@@ -31,6 +32,8 @@ from pitivi.timeline.previewers import Previewer
from pitivi.timeline.previewers import THUMB_HEIGHT
from pitivi.timeline.previewers import THUMB_PERIOD
from pitivi.timeline.previewers import ThumbnailCache
+from pitivi.utils.timeline import EditingContext
+from pitivi.utils.timeline import Zoomable
from tests import common
from tests.test_medialibrary import BaseTestMediaLibrary
@@ -96,6 +99,66 @@ class TestAudioPreviewer(BaseTestMediaLibrary):
self.assertEqual(samples, SIMPSON_WAVFORM_VALUES)
+ @common.setup_timeline
+ def test_offset(self):
+ # Set the videorate of tears_of_steel.webm.
+ self.project.videorate = Gst.Fraction(24, 1)
+ Zoomable.set_zoom_level(78)
+
+ timeline = self.timeline_container.timeline
+
+ start_frame = 43
+ start = self.timeline.get_frame_time(start_frame)
+ self.assertEqual(start, 1791666667)
+
+ clips = []
+ starts = []
+ asset = GES.UriClipAsset.request_sync(common.get_sample_uri("tears_of_steel.webm"))
+ for delta_frame in range(10):
+ ges_layer = timeline.ges_timeline.append_layer()
+ ges_clip = timeline.add_clip_to_layer(ges_layer, asset, start)
+
+ editing_context = EditingContext(ges_clip, self.timeline, GES.EditMode.EDIT_TRIM,
GES.Edge.EDGE_START, self.app)
+ new_start = self.timeline.get_frame_time(start_frame + delta_frame)
+ editing_context.edit_to(new_start, ges_layer)
+ editing_context.finish()
+
+ clips.append(ges_clip)
+ starts.append(new_start)
+
+ # Check the clips.
+ expected_starts = [1791666667, 1833333334, 1875000000, 1916666667, 1958333334, 2000000000,
2041666667, 2083333334, 2125000000, 2166666667]
+ self.assertListEqual(starts, expected_starts,
+ "the start values calculated in the test are off")
+ self.assertListEqual([c.start for c in clips], expected_starts,
+ "the start of the clips are wrong")
+ expected_inpoints = [0, 41666667, 83333333, 125000000, 166666667, 208333333, 250000000, 291666667,
333333333, 375000000]
+ self.assertListEqual([c.inpoint for c in clips], expected_inpoints)
+
+ # Check the audio previewers.
+ audio_previewers = list(self.get_clip_element(c, GES.AudioSource).ui.previewer
+ for c in clips)
+
+ offsets = []
+
+ def set_source_surface(surface, offset_x, offset_y):
+ offsets.append(offset_x)
+
+ samples = list(range(199))
+ for previewer in audio_previewers:
+ previewer.samples = samples
+ with mock.patch.object(Gdk, "cairo_get_clip_rectangle") as cairo_get_clip_rectangle:
+ cairo_get_clip_rectangle.return_value = (True, mock.Mock(x=0, width=10000))
+ from pitivi.timeline import previewers
+ with mock.patch.object(previewers.renderer, "fill_surface") as fill_surface:
+ context = mock.Mock()
+ context.set_source_surface = set_source_surface
+ previewer.do_draw(context)
+ fill_surface.assert_called_once_with(samples, 949, -1)
+
+ expected_offsets = [0, -20, -40, -59, -79, -99, -119, -138, -158, -178]
+ self.assertListEqual(offsets, expected_offsets)
+
class TestPreviewer(common.TestCase):
"""Tests for the `Previewer` class."""
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]