[pitivi] This commit adds a waveform audio previewer.



commit caccf6908bea02b69b8678fcf1fc5041bcb20035
Author: Simon Corsin <simoncorsin gmail com>
Date:   Wed Jun 19 01:54:52 2013 +0200

    This commit adds a waveform audio previewer.
    
        + previewers: Adds an audio previewer.
        + elements: Add this previewer as a child of audio elements.
        + Adds a new folder "coptimizations"
        + Adds a new C extension, renderer, which renders the
          samples on a cairo surface.
        + Makefile.am:
        + /* Fallthrough */
        + configure.ac:
        + autogen.sh:
        + Updates to compile the renderer.

 autogen.sh                        |    4 +
 configure.ac                      |   15 ++++-
 pitivi/Makefile.am                |    1 +
 pitivi/coptimizations/Makefile.am |    9 ++
 pitivi/coptimizations/renderer.c  |   95 +++++++++++++++++++++++
 pitivi/timeline/elements.py       |    6 +-
 pitivi/timeline/previewers.py     |  152 +++++++++++++++++++++++++++++++++++++
 7 files changed, 279 insertions(+), 3 deletions(-)
---
diff --git a/autogen.sh b/autogen.sh
index 0aeb081..61a8c98 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -33,6 +33,8 @@ version_check "automake" "$AUTOMAKE automake automake-1.7 automake-1.6 automake-
               "ftp://ftp.gnu.org/pub/gnu/automake/"; 1 6 || DIE=1
 version_check "pkg-config" "" \
               "http://www.freedesktop.org/software/pkgconfig"; 0 8 0 || DIE=1
+version_check "libtoolize" "$LIBTOOLIZE libtoolize glibtoolize" \
+              "ftp://ftp.gnu.org/pub/gnu/libtool/"; 2 2 6 || DIE=1
 
 die_check $DIE
 
@@ -66,6 +68,8 @@ echo "+ checking for GNOME Doc Utils"
 tool_run "gnome-doc-prepare" "--automake" \
     "echo Install gnome-doc-utils if gnome-doc-prepare is missing."
 
+# This is needed to create ltmain.sh for our C bits.
+tool_run "$libtoolize" "--copy --force"
 tool_run "$aclocal" "-I common/m4 $ACLOCAL_FLAGS"
 tool_run "$autoconf"
 tool_run "$automake" "-a -c"
diff --git a/configure.ac b/configure.ac
index 7f60445..7c5a472 100644
--- a/configure.ac
+++ b/configure.ac
@@ -9,6 +9,8 @@ AC_INIT(PiTiVi, 0.15.2,
     https://bugzilla.gnome.org/browse.cgi?product=pitivi,
     pitivi)
 
+LT_INIT()
+
 dnl initialize automake
 AM_INIT_AUTOMAKE
 
@@ -42,6 +44,14 @@ AC_MSG_NOTICE(Using localstatedir $LOCALSTATEDIR)
 dnl check for python
 AS_PATH_PYTHON(2.5)
 
+dnl python checks (you can change the required python version bellow)
+AM_PATH_PYTHON(2.7.0)
+PY_PREFIX=`$PYTHON -c 'import sys ; print sys.prefix'`
+PYTHON_LIBS="-lpython$PYTHON_VERSION"
+PYTHON_CFLAGS="-I$PY_PREFIX/include/python$PYTHON_VERSION"
+AC_SUBST([PYTHON_LIBS])
+AC_SUBST([PYTHON_CFLAGS])
+
 dnl ALL_LINGUAS="fr"
 GETTEXT_PACKAGE="pitivi"
 AC_SUBST([GETTEXT_PACKAGE])
@@ -49,7 +59,7 @@ AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE], "$GETTEXT_PACKAGE", [Gettext package])
 AM_GLIB_GNU_GETTEXT
 IT_PROG_INTLTOOL([0.35.0])
 
-CONFIGURED_PYTHONPATH=$PYTHONPATH
+CONFIGURED_PYTHONPATH=$PYTHONPATH:pitivi/coptimizations/.libs
 AC_SUBST(CONFIGURED_PYTHONPATH)
 
 CONFIGURED_LD_LIBRARY_PATH=$LD_LIBRARY_PATH
@@ -60,6 +70,8 @@ AC_SUBST(CONFIGURED_GST_PLUGIN_PATH)
 
 AC_CONFIG_FILES([bin/pitivi], [chmod +x bin/pitivi])
 
+PKG_CHECK_MODULES([cairo], [cairo])
+
 GNOME_DOC_INIT([0.18.0])
 
 dnl output stuff
@@ -75,6 +87,7 @@ pitivi/dialogs/Makefile
 pitivi/undo/Makefile
 pitivi/utils/Makefile
 pitivi/timeline/Makefile
+pitivi/coptimizations/Makefile
 po/Makefile.in
 tests/Makefile
 data/Makefile
diff --git a/pitivi/Makefile.am b/pitivi/Makefile.am
index 19a8a26..9759e5d 100644
--- a/pitivi/Makefile.am
+++ b/pitivi/Makefile.am
@@ -1,4 +1,5 @@
 SUBDIRS = \
+       coptimizations \
        dialogs \
        utils \
        timeline \
diff --git a/pitivi/coptimizations/Makefile.am b/pitivi/coptimizations/Makefile.am
new file mode 100644
index 0000000..3301e1f
--- /dev/null
+++ b/pitivi/coptimizations/Makefile.am
@@ -0,0 +1,9 @@
+pyexecdir=$(PWD)
+
+pyexec_LTLIBRARIES = renderer.la
+
+renderer_la_SOURCES = renderer.c
+AM_CFLAGS = $(cairo_CFLAGS)
+LIBS = $(cairo_LIBS)
+renderer_la_CFLAGS = $(PYTHON_CFLAGS) $(AM_CFLAGS)
+renderer_la_LDFLAGS = -module -avoid-version -export-symbols-regex initrenderer $(LIBS)
diff --git a/pitivi/coptimizations/renderer.c b/pitivi/coptimizations/renderer.c
new file mode 100644
index 0000000..79f9ffe
--- /dev/null
+++ b/pitivi/coptimizations/renderer.c
@@ -0,0 +1,95 @@
+#include <Python.h>
+#include <stdio.h>
+#include <cairo.h>
+#include </usr/include/pycairo/pycairo.h>
+
+static Pycairo_CAPI_t *Pycairo_CAPI;
+
+/*
+ * This function must be called with a range of samples, and a desired
+ * width and height.
+ * It will average samples if needed.
+ */
+static PyObject* py_fill_surface(PyObject* self, PyObject* args)
+{
+  PyObject *samples;
+  PyObject *sampleObj;
+  int length, i;
+  double sample;
+  cairo_surface_t *surface;
+  cairo_t *ctx;
+  int width, height;
+  float pixelsPerSample;
+  float currentPixel;
+  int samplesInAccum;
+  float x = 0.;
+  float lastX = 0.;
+  double accum;
+  double lastAccum = 0.;
+
+  if (!PyArg_ParseTuple(args, "O!ii", &PyList_Type, &samples, &width, &height))
+    return NULL;
+
+  length = PyList_Size(samples);
+
+  surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+
+  ctx = cairo_create(surface);
+
+  cairo_set_source_rgb(ctx, 0.2, 0.6, 0.0);
+  cairo_set_line_width(ctx, 0.5);
+  cairo_move_to(ctx, 0, height);
+
+  pixelsPerSample = width / (float) length;
+  currentPixel = 0.;
+  samplesInAccum = 0;
+  accum = 0.;
+
+  for (i = 0; i < length; i++)
+    {
+      /* Guaranteed to return something */
+      sampleObj = PyList_GetItem(samples, i);
+      sample = PyFloat_AsDouble(sampleObj);
+
+      /* If the object was not a float or convertible to float */
+      if (PyErr_Occurred())
+       {
+         cairo_surface_finish(surface);
+         Py_DECREF(samples);
+         return NULL;
+       }
+
+      currentPixel += pixelsPerSample;
+      samplesInAccum += 1;
+      accum += sample;
+      if (currentPixel > 1.0)
+       {
+         accum /= samplesInAccum;
+         cairo_line_to(ctx, x, height - accum);
+         lastAccum = accum;
+         accum = 0;
+         currentPixel -= 1.0;
+         samplesInAccum = 0;
+         lastX = x;
+       }
+      x += pixelsPerSample;
+    }
+
+  Py_DECREF(samples);
+  cairo_line_to(ctx, width, height);
+  cairo_close_path(ctx);
+  cairo_fill_preserve(ctx);
+
+  return PycairoSurface_FromSurface(surface, NULL);
+}
+
+static PyMethodDef renderer_methods[] = {
+  {"fill_surface", py_fill_surface, METH_VARARGS},
+  {NULL, NULL}
+};
+
+void initrenderer()
+{
+  Pycairo_IMPORT;
+  (void) Py_InitModule("renderer", renderer_methods);
+}
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index 75fb7a5..568d8b7 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -34,7 +34,7 @@ import cairo
 
 from gi.repository import Clutter, Gtk, GtkClutter, Cogl, GES, Gdk, Gst, GstController, GLib
 from pitivi.utils.timeline import Zoomable, EditingContext, Selection, SELECT, UNSELECT, SELECT_ADD, Selected
-from previewers import VideoPreviewer, BORDER_WIDTH
+from previewers import AudioPreviewer, VideoPreviewer, BORDER_WIDTH
 
 import pitivi.configure as configure
 from pitivi.utils.ui import EXPANDED_SIZE, SPACING, KEYFRAME_SIZE, CONTROL_WIDTH
@@ -52,7 +52,9 @@ def get_preview_for_object(bElement, timeline):
         # FIXME: RandomAccessAudioPreviewer doesn't work yet
         # previewers[key] = RandomAccessAudioPreviewer(instance, uri)
         # TODO: return waveform previewer
-        return Clutter.Actor()
+        previewer = AudioPreviewer(bElement, timeline)
+        previewer.startLevelsDiscovery(bElement.get_parent().get_uri())
+        return previewer
     elif track_type == GES.TrackType.VIDEO:
         if bElement.get_parent().is_image():
             # TODO: return still image previewer
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
index e4c67b5..585dc78 100644
--- a/pitivi/timeline/previewers.py
+++ b/pitivi/timeline/previewers.py
@@ -20,21 +20,30 @@
 # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
+import numpy
 import hashlib
 import os
 import sqlite3
 import sys
+import cairo
 import xdg.BaseDirectory as xdg_dirs
 from random import randrange
+from datetime import datetime, timedelta
 
 from gi.repository import Clutter, Gst, GLib, GdkPixbuf, Cogl
 from pitivi.utils.loggable import Loggable
 from pitivi.utils.timeline import Zoomable
 from pitivi.utils.ui import EXPANDED_SIZE, SPACING
 from pitivi.utils.misc import path_from_uri, quote_uri
+from pitivi.utils.ui import EXPANDED_SIZE, SPACING, CONTROL_WIDTH
+
+from renderer import *
+
+INTERVAL = 500000  # For the waveform update interval.
 
 BORDER_WIDTH = 3  # For the timeline elements
 
+MARGIN = 500  # For the waveforms, ensures we always have a little extra surface when scrolling while 
playing.
 
 """
 Convention throughout this file:
@@ -463,3 +472,146 @@ class ThumbnailCache(Loggable):
         self.debug('Saving thumbnail cache file to disk for "%s"' % self._filename)
         self._db.commit()
         self.log("Saved thumbnail cache file: %s" % self._filehash)
+
+
+class AudioPreviewer(Clutter.Actor, Zoomable):
+    """
+    Audio previewer based on the results from the "level" gstreamer element.
+    """
+    def __init__(self, bElement, timeline):
+        Clutter.Actor.__init__(self)
+        Zoomable.__init__(self)
+        self.discovered = False
+        self.bElement = bElement
+        self.timeline = timeline
+
+        self.actors = []
+
+        self.set_content_scaling_filters(Clutter.ScalingFilter.NEAREST, Clutter.ScalingFilter.NEAREST)
+        self.canvas = Clutter.Canvas()
+        self.set_content(self.canvas)
+        self.width = 0
+        self.lastUpdate = datetime.now()
+
+        self.interval = timedelta(microseconds=INTERVAL)
+
+        self.current_geometry = (-1, -1)
+
+        self.surface = None
+        self.timeline.connect("scrolled", self._scrolledCb)
+        self.canvas.connect("draw", self._drawContentCb)
+        self.canvas.invalidate()
+
+        self._callback_id = 0
+
+    def startLevelsDiscovery(self, uri):
+        self.peaks = None
+        self.pipeline = Gst.parse_launch("uridecodebin uri=" + uri + " ! audioconvert ! level 
interval=10000000 post-messages=true ! fakesink")
+        bus = self.pipeline.get_bus()
+        bus.add_signal_watch()
+        bus.connect("message", self._messageCb)
+        self.pipeline.set_state(Gst.State.PLAYING)
+
+    def set_size(self, width, height):
+        if self.discovered:
+            self._maybeUpdate()
+
+    def updateOffset(self):
+        print self.timeline.get_scroll_point().x
+
+    def zoomChanged(self):
+        self._maybeUpdate()
+
+    def _maybeUpdate(self):
+        if self.discovered:
+            if datetime.now() - self.lastUpdate > self.interval:
+                self.lastUpdate = datetime.now()
+                self._compute_geometry()
+            else:
+                if self._callback_id:
+                    GLib.source_remove(self._callback_id)
+                self._callback_id = GLib.timeout_add(500, self._compute_geometry)
+
+    def _compute_geometry(self):
+        start = self.timeline.get_scroll_point().x - self.nsToPixel(self.bElement.props.start)
+        start = max(0, start)
+        end = min(self.timeline.get_scroll_point().x + self.timeline._container.get_allocation().width - 
CONTROL_WIDTH + MARGIN,
+                  self.nsToPixel(self.bElement.props.duration))
+
+        pixelWidth = self.nsToPixel(self.bElement.props.duration)
+
+        self.start = int(start / pixelWidth * self.nbSamples)
+        self.end = int(end / pixelWidth * self.nbSamples)
+
+        self.width = int(end - start)
+
+        if self.width < 0:  # We've been called at a moment where size was updated but not scroll_point.
+            return
+
+        self.canvas.set_size(self.width, 65)
+
+        Clutter.Actor.set_size(self, self.width, EXPANDED_SIZE)
+        self.set_position(start, self.props.y)
+        self.canvas.invalidate()
+
+    def _messageCb(self, bus, message):
+        s = message.get_structure()
+        p = None
+        if s:
+            p = s.get_value("rms")
+        if p:
+            if self.peaks is None:
+                if len(p) > 1:
+                    self.peaks = [[], []]
+                else:
+                    self.peaks = [[]]
+            if p[0] < 0:  # FIXME bug in level, this should not be necessary.
+                p[0] = 10 ** (p[0] / 20) * 100
+                self.peaks[0].append(p[0])
+            else:
+                self.peaks[0].append(self.peaks[0][-1])
+
+            if len(p) > 1:
+                if p[1] < 0:
+                    p[1] = 10 ** (p[1] / 20) * 100
+                    self.peaks[1].append(p[1])
+                else:
+                    self.peaks[1].append(self.peaks[1][-1])
+
+        if message.type == Gst.MessageType.EOS:
+            # Let's go mono.
+            if (len(self.peaks) > 1):
+                samples = (numpy.array(self.peaks[0]) + numpy.array(self.peaks[1])) / 2
+            else:
+                samples = numpy.array(self.peaks[0])
+
+            self.samples = samples.tolist()
+            self.nbSamples = len(self.samples)
+
+            self.discovered = True
+            self.start = 0
+            self.end = self.nbSamples
+            self._compute_geometry()
+            self.pipeline.set_state(Gst.State.NULL)
+
+        elif message.type == Gst.MessageType.ERROR:
+            # Something went wrong TODO : recover
+            self.pipeline.set_state(Gst.State.NULL)
+
+    def _drawContentCb(self, canvas, cr, surf_w, surf_h):
+        cr.set_operator(cairo.OPERATOR_CLEAR)
+        cr.paint()
+        if not self.discovered:
+            return
+
+        if self.surface:
+            self.surface.finish()
+
+        self.surface = fill_surface(self.samples[self.start:self.end], int(self.width), int(EXPANDED_SIZE))
+
+        cr.set_operator(cairo.OPERATOR_OVER)
+        cr.set_source_surface(self.surface, 0, 0)
+        cr.paint()
+
+    def _scrolledCb(self, unused):
+        self._maybeUpdate()


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