[pitivi] This commit adds a waveform audio previewer.
- From: Thibault Saunier <tsaunier src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] This commit adds a waveform audio previewer.
- Date: Wed, 17 Jul 2013 00:48:13 +0000 (UTC)
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]