[pitivi] viewer: Add per-channel VU meter



commit 7e8ab53426b916476024968959d21c86b84d5620
Author: aabyington4 <aabyington4 gmail com>
Date:   Tue May 12 19:24:37 2020 -0500

    viewer: Add per-channel VU meter
    
    Many video editing softwares include a VU meter/peak meter that displays
    the volume of audio as it is being played.
    
    Currently Pitivi does not have a VU meter, so it lacks this convenient
    method of being able to see the decibels of an audio clip change during
    playback.
    
    To fix this, two new VU meters have been added to the right of viewer,
    one for left audio and one for right audio.
    
    Fixes #595

 docs/QA_Scenarios.md        |  18 +++++
 pitivi/project.py           |   7 +-
 pitivi/utils/pipeline.py    |   2 +
 pitivi/viewer/peak_meter.py | 189 ++++++++++++++++++++++++++++++++++++++++++++
 pitivi/viewer/viewer.py     |  78 ++++++++++++++++--
 tests/test_peak_meter.py    | 119 ++++++++++++++++++++++++++++
 6 files changed, 403 insertions(+), 10 deletions(-)
---
diff --git a/docs/QA_Scenarios.md b/docs/QA_Scenarios.md
index 547298d07..4e14d86b1 100644
--- a/docs/QA_Scenarios.md
+++ b/docs/QA_Scenarios.md
@@ -329,6 +329,24 @@ on GitLab, indicating:
 -   test keyboard shortcuts
     -   try to locate a specific frame using only the keyboard
 
+## Test Peak Meter
+
+1. Create and open a new Pitivi project
+2. Import 'mp3_sample.mp3' from the 'tests/samples' directory into the media library
+3. Insert a 'mp3_sample.mp3' clip from the media library into the timeline
+    -   The timeline should only contain the 'mp3_sample.mp3' clip and nothing else
+4. Position the seeker at the beginning of the clip on the timeline
+5. Press the 'play' button on the viewer controls
+    -   The height of the two peak meters should start changing during playback of the clip but should stop 
once the clip ends
+6. Detach the viewer by pressing the 'detach viewer' button on the viewer controls
+    -   The peak meters should appear on the right side of the viewer in the external viewer window that 
pops up
+7. Close the external viewer window
+    -   The peak meters should appear again in their original location on the right side of the viewer
+8. Drag the corner on the viewer container to resize the viewer container to the minimum size
+    -   The peak meters should now be sized smaller in response to the smaller viewer container
+9. Go to project settings and set the number of audio channels to '8 (7.1)'
+    -   There should now be eight peak bars in total displayed next to the viewer
+
 ## Test Preferences
 
 # User provided Scenarios
diff --git a/pitivi/project.py b/pitivi/project.py
index bf65dd02f..770132044 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -1061,11 +1061,12 @@ class Project(Loggable, GES.Project):
             self.set_modification_state(True)
 
     @property
-    def audiochannels(self):
-        return self.audio_profile.get_restriction()[0]["channels"]
+    def audiochannels(self) -> int:
+        # The map does not always contain "channels".
+        return self.audio_profile.get_restriction()[0]["channels"] or 0
 
     @audiochannels.setter
-    def audiochannels(self, value):
+    def audiochannels(self, value: int):
         if self._set_audio_restriction("channels", int(value)):
             self.set_modification_state(True)
 
diff --git a/pitivi/utils/pipeline.py b/pitivi/utils/pipeline.py
index 9cc476498..74223f229 100644
--- a/pitivi/utils/pipeline.py
+++ b/pitivi/utils/pipeline.py
@@ -550,6 +550,8 @@ class Pipeline(GES.Pipeline, SimplePipeline):
         self._commit_wanted = False
         self._prevent_commits = 0
 
+        self.props.audio_sink = Gst.parse_bin_from_description("level ! audioconvert ! audioresample ! 
autoaudiosink", True)
+
         if "watchdog" in os.environ.get("PITIVI_UNSTABLE_FEATURES", ''):
             watchdog = Gst.ElementFactory.make("watchdog", None)
             if watchdog:
diff --git a/pitivi/viewer/peak_meter.py b/pitivi/viewer/peak_meter.py
new file mode 100644
index 000000000..b0433c1f2
--- /dev/null
+++ b/pitivi/viewer/peak_meter.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2020, Michael Westburg <michael westberg huskers unl edu>
+# Copyright (c) 2020, Matt Lowe <mattlowe13 huskers unl edu>
+# Copyright (c) 2020, Aaron Byington <aabyington4 gmail com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# 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/>.
+import cairo
+from gi.repository import Gtk
+
+from pitivi.utils.ui import gtk_style_context_get_color
+from pitivi.utils.ui import NORMAL_FONT
+from pitivi.utils.ui import set_cairo_color
+from pitivi.utils.ui import SPACING
+
+# The width for the peak meter
+PEAK_METER_WIDTH = 8
+# The maximum height for the peak meter
+PEAK_METER_MAX_HEIGHT = 200
+# The minimum height for the peak meter
+PEAK_METER_MIN_HEIGHT = 80
+# The number of cells on the peak meter bar
+CELL_COUNT = 20
+# The minimum peak value represented by the peak meter
+MIN_PEAK = -60
+# The font size for the scale
+FONT_SIZE = 13
+# The number of values shown on the scale
+SCALE_COUNT = 5
+
+
+class PeakMeterWidget(Gtk.DrawingArea):
+    """Base class for peak meter components."""
+
+    def __init__(self):
+        Gtk.DrawingArea.__init__(self)
+        self.pixel_buffer = None
+
+        style_context = self.get_style_context()
+        style_context.add_class("background")
+        self.connect("size-allocate", self.__size_allocate_cb)
+
+    def __size_allocate_cb(self, unused_event, allocation):
+        if self.pixel_buffer is not None:
+            self.pixel_buffer.finish()
+            self.pixel_buffer = None
+
+        self.pixel_buffer = cairo.ImageSurface(cairo.FORMAT_ARGB32, allocation.width, allocation.height)
+
+    def draw_background(self, context, width, height):
+        style_context = self.get_style_context()
+        Gtk.render_background(style_context, context, 0, 0, width, height)
+
+
+class PeakMeter(PeakMeterWidget):
+    """A meter that shows peak values."""
+
+    def __init__(self):
+        PeakMeterWidget.__init__(self)
+        self.peak = MIN_PEAK
+        self.background_gradient = None
+        self.peak_gradient = None
+        width = PEAK_METER_WIDTH
+        self.set_property("width_request", width)
+
+        style_context = self.get_style_context()
+        style_context.add_class("frame")
+
+        self.connect("size-allocate", self.__size_allocate_cb)
+
+    def do_draw(self, context):
+        if self.pixel_buffer is None:
+            return
+
+        width = self.get_allocated_width()
+        height = self.get_allocated_height()
+        pixel_buffer = self.pixel_buffer
+
+        drawing_context = cairo.Context(pixel_buffer)
+        self.draw_background(drawing_context, width, height)
+        self.__draw_bar(drawing_context, width, height)
+        self.__draw_cells(drawing_context, width, height)
+        self.__draw_frame(drawing_context, width, height)
+        pixel_buffer.flush()
+
+        context.set_source_surface(pixel_buffer, 0.0, 0.0)
+        context.paint()
+
+    def __size_allocate_cb(self, unused_event, allocation):
+        self.__set_gradients(allocation.width, allocation.height)
+
+    def __draw_bar(self, context, width, height):
+        peak_height = self.__normalize_peak(height)
+
+        context.set_source(self.background_gradient)
+        context.rectangle(0, 0, width, height)
+        context.fill()
+
+        context.set_source(self.peak_gradient)
+        context.rectangle(0, height - peak_height + 0, width, peak_height)
+        context.fill()
+
+    def __draw_cells(self, context, width, height):
+        context.set_source_rgba(0.0, 0.0, 0.0, 0.5)
+        context.set_line_width(2.0)
+
+        cell_size = height / CELL_COUNT
+        for i in range(1, CELL_COUNT):
+            context.move_to(0, cell_size * i)
+            context.line_to(width, cell_size * i)
+
+        context.stroke()
+
+    def __draw_frame(self, context, width, height):
+        style_context = self.get_style_context()
+        Gtk.render_frame(style_context, context, 0, 0, width, height)
+
+    def __set_gradients(self, width, height):
+        self.background_gradient = cairo.LinearGradient(0, 0, width, height)
+        self.background_gradient.add_color_stop_rgb(1.0, 0.0, 0.3, 0.0)
+        self.background_gradient.add_color_stop_rgb(0.7, 0.3, 0.3, 0.0)
+        self.background_gradient.add_color_stop_rgb(0.0, 0.3, 0.0, 0.0)
+
+        self.peak_gradient = cairo.LinearGradient(0, 0, width, height)
+        self.peak_gradient.add_color_stop_rgb(1.0, 0.0, 1.0, 0.0)
+        self.peak_gradient.add_color_stop_rgb(0.7, 1.0, 1.0, 0.0)
+        self.peak_gradient.add_color_stop_rgb(0.0, 1.0, 0.0, 0.0)
+
+    def __normalize_peak(self, height):
+        return height / (-MIN_PEAK) * (max(self.peak, MIN_PEAK) - MIN_PEAK)
+
+    def update_peakmeter(self, peak):
+        self.peak = peak
+        self.queue_draw()
+
+
+class PeakMeterScale(PeakMeterWidget):
+    """A scale for the peak meter."""
+
+    def __init__(self):
+        PeakMeterWidget.__init__(self)
+        width = FONT_SIZE * 2
+        self.set_property("width_request", width)
+
+    def do_draw(self, context):
+        if self.pixel_buffer is None:
+            return
+
+        width = self.get_allocated_width()
+        height = self.get_allocated_height()
+        pixel_buffer = self.pixel_buffer
+
+        drawing_context = cairo.Context(pixel_buffer)
+        self.draw_background(drawing_context, width, height)
+        self.__draw_scale(drawing_context)
+        pixel_buffer.flush()
+
+        context.set_source_surface(pixel_buffer, 0.0, 0.0)
+        context.paint()
+
+    def __draw_scale(self, context):
+        bar_height = self.get_bar_height()
+        section_height = bar_height / (SCALE_COUNT - 1)
+
+        style_context = self.get_style_context()
+        color = gtk_style_context_get_color(style_context, Gtk.StateFlags.NORMAL)
+
+        set_cairo_color(context, color)
+        context.set_font_size(FONT_SIZE)
+        context.set_font_face(NORMAL_FONT)
+        text_extent = context.text_extents("0")
+
+        for i in range(SCALE_COUNT):
+            context.move_to(0, section_height * i + SPACING + text_extent.height / 2)
+            context.show_text(str((MIN_PEAK // (SCALE_COUNT - 1)) * i))
+
+    def get_bar_height(self):
+        return self.get_allocated_height() - SPACING * 2
diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py
index bdd3aa2a3..fdf6a7129 100644
--- a/pitivi/viewer/viewer.py
+++ b/pitivi/viewer/viewer.py
@@ -32,6 +32,10 @@ from pitivi.utils.ui import SPACING
 from pitivi.utils.widgets import TimeWidget
 from pitivi.viewer.guidelines import GuidelinesPopover
 from pitivi.viewer.overlay_stack import OverlayStack
+from pitivi.viewer.peak_meter import PEAK_METER_MAX_HEIGHT
+from pitivi.viewer.peak_meter import PEAK_METER_MIN_HEIGHT
+from pitivi.viewer.peak_meter import PeakMeter
+from pitivi.viewer.peak_meter import PeakMeterScale
 
 
 GlobalSettings.add_config_section("viewer")
@@ -62,7 +66,7 @@ GlobalSettings.add_config_option("pointColor", section="viewer",
 
 
 class ViewerContainer(Gtk.Box, Loggable):
-    """Wiget holding a viewer and the controls.
+    """Widget holding a viewer, the controls, and a peak meter.
 
     Attributes:
         pipeline (SimplePipeline): The displayed pipeline.
@@ -112,12 +116,29 @@ class ViewerContainer(Gtk.Box, Loggable):
     def _project_video_size_changed_cb(self, project):
         """Handles Project metadata changes."""
         self._reset_viewer_aspect_ratio(project)
+        self.__update_peak_meters(project)
 
     def _reset_viewer_aspect_ratio(self, project):
         """Resets the viewer aspect ratio."""
         self.target.update_aspect_ratio(project)
         self.timecode_entry.set_framerate(project.videorate)
 
+    def __update_peak_meters(self, project):
+        for peak_meter in self.peak_meters:
+            self.peak_meter_box.remove(peak_meter)
+
+        for i in range(project.audiochannels):
+            if i > len(self.peak_meters) - 1:
+                new_peak_meter = PeakMeter()
+                new_peak_meter.set_property("valign", Gtk.Align.FILL)
+                new_peak_meter.set_property("halign", Gtk.Align.CENTER)
+                new_peak_meter.set_margin_bottom(SPACING)
+                new_peak_meter.set_margin_top(SPACING)
+                self.peak_meters.append(new_peak_meter)
+            self.peak_meter_box.pack_start(self.peak_meters[i], False, False, 0)
+
+        self.peak_meter_box.show_all()
+
     def set_project(self, project):
         """Sets the displayed project.
 
@@ -135,8 +156,10 @@ class ViewerContainer(Gtk.Box, Loggable):
         project.pipeline.connect("state-change", self._pipeline_state_changed_cb)
         project.pipeline.connect("position", self._position_cb)
         project.pipeline.connect("duration-changed", self._duration_changed_cb)
+        project.pipeline.get_bus().connect("message::element", self._bus_level_message_cb)
         self.project = project
 
+        self.__update_peak_meters(project)
         self.__create_new_viewer()
         self._set_ui_active()
 
@@ -156,14 +179,15 @@ class ViewerContainer(Gtk.Box, Loggable):
                                           self.guidelines_popover.overlay)
         self.target = ViewerWidget(self.overlay_stack)
         self._reset_viewer_aspect_ratio(self.project)
+        self.viewer_row_box.pack_start(self.target, expand=True, fill=True, padding=0)
 
         if self.docked:
-            self.pack_start(self.target, expand=True, fill=True, padding=0)
+            self.pack_start(self.viewer_row_box, expand=True, fill=True, padding=0)
         else:
-            self.external_vbox.pack_start(self.target, expand=True, fill=False, padding=0)
-            self.external_vbox.child_set(self.target, fill=True)
+            self.external_vbox.pack_start(self.viewer_row_box, expand=True, fill=False, padding=0)
+            self.external_vbox.child_set(self.viewer_row_box, fill=True)
 
-        self.target.show_all()
+        self.viewer_row_box.show_all()
 
         # Wait for 1s to make sure that the viewer has completely realized
         # and then we can mark the resize status as showable.
@@ -213,6 +237,10 @@ class ViewerContainer(Gtk.Box, Loggable):
             "configure-event", self._external_window_configure_cb)
         self.external_vbox = vbox
 
+        # This holds the viewer and the peak meters box.
+        self.viewer_row_box = Gtk.Box()
+        self.viewer_row_box.set_orientation(Gtk.Orientation.HORIZONTAL)
+
         # Corner marker.
         corner = Gtk.DrawingArea()
         # Number of lines to draw in the corner marker.
@@ -237,6 +265,25 @@ class ViewerContainer(Gtk.Box, Loggable):
         corner.connect("motion-notify-event", self.__corner_motion_notify_cb, hpane, vpane)
         self.pack_end(corner, False, False, 0)
 
+        # Peak Meters
+        self.peak_meter_box = Gtk.Box()
+        self.peak_meter_box.set_margin_right(SPACING)
+        self.peak_meter_box.set_margin_left(SPACING)
+        self.peak_meter_box.set_margin_bottom(SPACING)
+        self.peak_meter_box.set_margin_top(SPACING)
+        self.peak_meter_box.set_property("valign", Gtk.Align.CENTER)
+
+        self.peak_meters = []
+
+        # Peak Meter Scale
+        self.peak_meter_scale = PeakMeterScale()
+        self.peak_meter_scale.set_property("valign", Gtk.Align.FILL)
+        self.peak_meter_scale.set_property("halign", Gtk.Align.CENTER)
+        self.peak_meter_scale.set_margin_left(SPACING)
+        self.peak_meter_box.pack_end(self.peak_meter_scale, False, False, 0)
+        self.viewer_row_box.pack_end(self.peak_meter_box, False, False, 0)
+        self.peak_meter_scale.connect("configure-event", self.__peak_meter_scale_configure_event_cb)
+
         # Buttons/Controls
         bbox = Gtk.Box()
         bbox.set_orientation(Gtk.Orientation.HORIZONTAL)
@@ -358,6 +405,12 @@ class ViewerContainer(Gtk.Box, Loggable):
         overlay = self.overlay_stack.safe_areas_overlay
         overlay.set_visible(not overlay.get_visible())
 
+    def _bus_level_message_cb(self, unused_bus, message):
+        peak_values = message.get_structure().get_value("peak")
+        if peak_values:
+            for count, peak in enumerate(peak_values):
+                self.peak_meters[count].update_peakmeter(peak)
+
     def __corner_draw_cb(self, unused_widget, cr, lines, space, margin):
         cr.set_line_width(1)
 
@@ -396,6 +449,15 @@ class ViewerContainer(Gtk.Box, Loggable):
         hpane.set_position(event.x_root - self.__translation[0])
         vpane.set_position(event.y_root - self.__translation[1])
 
+    def __peak_meter_scale_configure_event_cb(self, unused_widget, unused_event):
+        container_height = self.viewer_row_box.get_allocated_height()
+        margins = self.peak_meter_box.get_allocated_height() - self.peak_meter_scale.get_bar_height() + 
SPACING * 4
+
+        bar_height = max(min(PEAK_METER_MAX_HEIGHT, container_height - margins), PEAK_METER_MIN_HEIGHT)
+        box_height = bar_height + SPACING * 2
+
+        self.peak_meter_box.set_property("height_request", box_height)
+
     def activate_compact_mode(self):
         self.back_button.hide()
         self.forward_button.hide()
@@ -464,7 +526,8 @@ class ViewerContainer(Gtk.Box, Loggable):
             self.overlay_stack.enable_resize_status(False)
             position = self.project.pipeline.get_position()
             self.project.pipeline.set_simple_state(Gst.State.NULL)
-            self.remove(self.target)
+            self.remove(self.viewer_row_box)
+            self.viewer_row_box.remove(self.target)
             self.__create_new_viewer()
         self.buttons_container.set_margin_bottom(SPACING)
         self.external_vbox.pack_end(self.buttons_container, False, False, 0)
@@ -508,7 +571,8 @@ class ViewerContainer(Gtk.Box, Loggable):
             self.overlay_stack.enable_resize_status(False)
             position = self.project.pipeline.get_position()
             self.project.pipeline.set_simple_state(Gst.State.NULL)
-            self.external_vbox.remove(self.target)
+            self.external_vbox.remove(self.viewer_row_box)
+            self.viewer_row_box.remove(self.target)
             self.__create_new_viewer()
 
         self.undock_button.show()
diff --git a/tests/test_peak_meter.py b/tests/test_peak_meter.py
new file mode 100644
index 000000000..e2086b96e
--- /dev/null
+++ b/tests/test_peak_meter.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2020, Michael Westburg <michael westberg huskers unl edu>
+# Copyright (c) 2020, Matt Lowe <mattlowe13 huskers unl edu>
+# Copyright (c) 2020, Aaron Byington <aabyington4 gmail com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# 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 pitivi.peakmeter module."""
+from gi.repository import GLib
+
+from pitivi.project import ProjectManager
+from pitivi.viewer.peak_meter import MIN_PEAK
+from pitivi.viewer.viewer import ViewerContainer
+from tests import common
+
+
+class TestPeakMeter(common.TestCase):
+    """Tests for the peak meter."""
+
+    def test_peak_meter_update(self):
+        """Checks that the peak value updates correctly when audio is played."""
+        app = common.create_pitivi_mock()
+        app.project_manager = ProjectManager(app)
+
+        viewer = ViewerContainer(app)
+        self.assertListEqual(viewer.peak_meters, [])
+
+        uri = common.get_sample_uri("1sec_simpsons_trailer.mp4")
+        xges = r"""<ges version='0.7'>
+  <project properties='properties;' metadatas='metadatas, author=(string)Author, scaled_proxy_width=(int)0, 
scaled_proxy_height=(int)0, render-scale=(double)100, format-version=(string)0.7;'>
+    <encoding-profiles>
+      <encoding-profile name='pitivi-profile' description='Pitivi encoding profile' type='container' 
preset-name='webmmux' format='video/webm' >
+        <stream-profile parent='pitivi-profile' id='0' type='video' presence='0' format='video/x-vp8, 
profile=(string){ 0, 1, 2, 3 }' preset-name='vp8enc' restriction='video/x-raw, width=(int)1080, 
height=(int)1920, framerate=(fraction)30/1, pixel-aspect-ratio=(fraction)1/1' pass='0' variableframerate='0' 
/>
+        <stream-profile parent='pitivi-profile' id='1' type='audio' presence='0' format='audio/x-vorbis, 
rate=(int)[ 1, 200000 ], channels=(int)[ 1, 255 ]' preset-name='vorbisenc' restriction='audio/x-raw, 
rate=(int)48000, channels=(int)2' />
+      </encoding-profile>
+    </encoding-profiles>
+    <ressources>
+      <asset id='%(uri)s' extractable-type-name='GESUriClip' properties='properties, 
supported-formats=(int)6, duration=(guint64)1228000000;' metadatas='metadatas, 
video-codec=(string)&quot;H.264\ /\ AVC&quot;, bitrate=(uint)1370124, 
datetime=(datetime)2007-02-19T05:03:04Z, encoder=(string)Lavf54.6.100, container-format=(string)&quot;ISO\ 
MP4/M4A&quot;, audio-codec=(string)&quot;MPEG-4\ AAC\ audio&quot;, maximum-bitrate=(uint)130625, 
file-size=(guint64)232417;' >
+        <stream-info id='11c5d3bc5140b4cd95fc0c2b7125dff3b9c6db88183a85200821b61365719f91/002' 
extractable-type-name='GESAudioUriSource' properties='properties, track-type=(int)2;' metadatas='metadatas;' 
caps='audio/mpeg, mpegversion=(int)4, framed=(boolean)true, stream-format=(string)raw, level=(string)2, 
base-profile=(string)lc, profile=(string)lc, codec_data=(buffer)1190, rate=(int)48000, channels=(int)2'/>
+        <stream-info id='11c5d3bc5140b4cd95fc0c2b7125dff3b9c6db88183a85200821b61365719f91/001' 
extractable-type-name='GESVideoUriSource' properties='properties, track-type=(int)4;' metadatas='metadatas;' 
caps='video/x-h264, stream-format=(string)avc, alignment=(string)au, level=(string)3.1, profile=(string)high, 
codec_data=(buffer)0164001fffe100176764001facd94050045a1000003e90000bb800f183196001000668ebe3cb22c0, 
width=(int)1280, height=(int)544, framerate=(fraction)24000/1001, pixel-aspect-ratio=(fraction)1/1, 
interlace-mode=(string)progressive, chroma-format=(string)4:2:0, bit-depth-luma=(uint)8, 
bit-depth-chroma=(uint)8, parsed=(boolean)true'/>
+      </asset>
+      <asset id='crossfade' extractable-type-name='GESTransitionClip' properties='properties, 
supported-formats=(int)6;' metadatas='metadatas, 
description=(string)GES_VIDEO_STANDARD_TRANSITION_TYPE_CROSSFADE;' >
+      </asset>
+    </ressources>
+    <timeline properties='properties, auto-transition=(boolean)true, snapping-distance=(guint64)6420600;' 
metadatas='metadatas, markers=(GESMarkerList)&quot;EMPTY&quot;, duration=(guint64)2456000000;'>
+      <track caps='video/x-raw(ANY)' track-type='4' track-id='0' properties='properties, 
message-forward=(boolean)true, restriction-caps=(string)&quot;video/x-raw\,\ width\=\(int\)1080\,\ 
height\=\(int\)1920\,\ framerate\=\(fraction\)30/1\,\ pixel-aspect-ratio\=\(fraction\)1/1&quot;, 
id=(string)87987a781a347bd399437dc56c9c6cd7;' metadatas='metadatas;'/>
+      <track caps='audio/x-raw(ANY)' track-type='2' track-id='1' properties='properties, 
message-forward=(boolean)true, restriction-caps=(string)&quot;audio/x-raw\,\ rate\=\(int\)48000\,\ 
channels\=\(int\)2&quot;, id=(string)866a7d96e4e467e9f07ac713d07551de;' metadatas='metadatas;'/>
+      <layer priority='0' properties='properties, auto-transition=(boolean)true;' metadatas='metadatas, 
volume=(float)1;'>
+        <clip id='0' asset-id='%(uri)s' type-name='GESUriClip' layer-priority='0' track-types='6' start='0' 
duration='1228000000' inpoint='0' rate='0' properties='properties, name=(string)uriclip1;' 
metadatas='metadatas;'>
+          <source track-id='1' properties='properties, track-type=(int)2, 
has-internal-source=(boolean)true;'  children-properties='properties, GstVolume::mute=(boolean)false, 
GstVolume::volume=(double)1;'>
+            <binding type='direct' source_type='interpolation' property='volume' mode='1' track_id='1' 
values =' 0:0.10000000000000001  1228000000:0.10000000000000001 '/>
+          </source>
+          <source track-id='0' properties='properties, track-type=(int)4, 
has-internal-source=(boolean)true;'  children-properties='properties, GstFramePositioner::alpha=(double)1, 
GstDeinterlace::fields=(int)0, GstFramePositioner::height=(int)459, GstDeinterlace::mode=(int)0, 
GstFramePositioner::posx=(int)0, GstFramePositioner::posy=(int)730, GstDeinterlace::tff=(int)0, 
GstVideoDirection::video-direction=(int)8, GstFramePositioner::width=(int)1080;'>
+            <binding type='direct' source_type='interpolation' property='alpha' mode='1' track_id='0' values 
=' 0:1  1228000000:1 '/>
+          </source>
+        </clip>
+        <clip id='1' asset-id='%(uri)s' type-name='GESUriClip' layer-priority='0' track-types='6' 
start='1228000000' duration='1228000000' inpoint='0' rate='0' properties='properties, name=(string)uriclip2;' 
metadatas='metadatas;'>
+          <source track-id='1' properties='properties, track-type=(int)2, 
has-internal-source=(boolean)true;'  children-properties='properties, GstVolume::mute=(boolean)false, 
GstVolume::volume=(double)1;'>
+            <binding type='direct' source_type='interpolation' property='volume' mode='1' track_id='1' 
values =' 0:0.10000000000000001  1228000000:0.10000000000000001 '/>
+          </source>
+          <source track-id='0' properties='properties, track-type=(int)4, 
has-internal-source=(boolean)true;'  children-properties='properties, GstFramePositioner::alpha=(double)1, 
GstDeinterlace::fields=(int)0, GstFramePositioner::height=(int)459, GstDeinterlace::mode=(int)0, 
GstFramePositioner::posx=(int)0, GstFramePositioner::posy=(int)730, GstDeinterlace::tff=(int)0, 
GstVideoDirection::video-direction=(int)8, GstFramePositioner::width=(int)1080;'>
+            <binding type='direct' source_type='interpolation' property='alpha' mode='1' track_id='0' values 
=' 0:1  1228000000:1 '/>
+          </source>
+        </clip>
+      </layer>
+      <groups>
+      </groups>
+    </timeline>
+  </project>
+</ges>""" % {"uri": uri}
+
+        proj_uri = self.create_project_file_from_xges(xges)
+        project = app.project_manager.load_project(proj_uri)
+        # Before the project finished loading it has one audio channel.
+        self.assertEqual(project.audiochannels, 1)
+
+        mainloop = common.create_main_loop()
+
+        def project_loaded_cb(project, timeline):
+            mainloop.quit()
+
+        project.connect_after("loaded", project_loaded_cb)
+        mainloop.run()
+        # After the project finishes loading it has the correct number of
+        # audio channels.
+        self.assertEqual(project.audiochannels, 2)
+
+        self.assertEqual(len(viewer.peak_meters), 0)
+        viewer.set_project(project)
+        # After setting the project we should have two peak meters.
+        self.assertEqual([meter.peak for meter in viewer.peak_meters], [MIN_PEAK, MIN_PEAK])
+
+        # Check that after starting playback we get "peak" values on the bus.
+        def bus_message_cb(bus, message):
+            if message.get_structure().get_value("peak") is not None:
+                mainloop.quit()
+
+        def begin_playback():
+            project.pipeline.play()
+            project.pipeline.get_bus().connect("message::element", bus_message_cb)
+
+        GLib.idle_add(begin_playback)
+        mainloop.run()
+
+        peaks = [meter.peak for meter in viewer.peak_meters]
+        for peak in peaks:
+            self.assertGreaterEqual(0, peak, peaks)
+            self.assertGreaterEqual(peak, MIN_PEAK, peaks)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]