]> git.parisson.com Git - timeside.git/commitdiff
add a new light grapher, add dependencies for other media types
authoryomguy <yomguy@parisson.com>
Thu, 14 Oct 2010 11:49:09 +0000 (11:49 +0000)
committeryomguy <yomguy@parisson.com>
Thu, 14 Oct 2010 11:49:09 +0000 (11:49 +0000)
INSTALL
timeside/grapher/__init__.py
timeside/grapher/core.py
timeside/grapher/waveform_awdio.py [new file with mode: 0644]
timeside/tools/waveform_batch_awdio.py [new file with mode: 0644]

diff --git a/INSTALL b/INSTALL
index e3af4ade96d74ec136032e1011ce3495111bfb46..f0bb9960903946b1993e1fae6c0c43e311a59479 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -35,7 +35,7 @@ Say 'YES' to all questions. Then::
 
     $ apt-get install -t lenny-backports vim subversion python python-setuptools python-xml python-mutagen \
                             python-imaging python-numpy python-gst0.10 gstreamer0.10-plugins-base \
-                            gstreamer0.10-fluendo-mp3 gstreamer0.10-plugins-good
+                            gstreamer0.10-fluendo-mp3 gstreamer0.10-plugins-good gstreamer0.10-plugins-bad
 
 
 2. Install TimeSide
index 35f0db50fa3dead6c5511206eb04fcb1ab57326b..421390c0a5230598d015dd5189ceb8a280892581 100644 (file)
@@ -4,3 +4,4 @@ from core import *
 from waveform import *
 from spectrogram import *
 from waveform_joydiv import *
+from waveform_awdio import *
index f4aad496b5da4e48f138418661f4756b6cd08ae5..48f46d88c5e79b5726b7d3e0a9ad83c62dad3c8d 100644 (file)
@@ -251,6 +251,127 @@ class WaveformImage(object):
         self.image.save(filename)
 
 
+class WaveformImageSimple(object):
+    """ Builds a PIL image representing a waveform of the audio stream.
+    Adds pixels iteratively thanks to the adapter providing fixed size frame buffers.
+    Peaks are colored relative to the spectral centroids of each frame packet. """
+
+    def __init__(self, image_width, image_height, nframes, samplerate, bg_color, color_scheme):
+        self.image_width = image_width
+        self.image_height = image_height
+        self.nframes = nframes
+        self.samplerate = samplerate
+        self.bg_color = bg_color
+        self.color_scheme = color_scheme
+
+        if isinstance(color_scheme, dict):
+            colors = color_scheme['waveform']
+        else:
+            colors = default_color_schemes[color_scheme]['waveform']
+
+        self.line_color = colors[0]
+
+        self.samples_per_pixel = self.nframes / float(self.image_width)
+        self.buffer_size = int(round(self.samples_per_pixel, 0))
+        self.pixels_adapter = FixedSizeInputAdapter(self.buffer_size, 1, pad=False)
+        self.pixels_adapter_nframes = self.pixels_adapter.nframes(self.nframes)
+
+        self.image = Image.new("RGBA", (self.image_width, self.image_height), self.bg_color)
+        self.pixel = self.image.load()
+        self.draw = ImageDraw.Draw(self.image)
+        self.previous_x, self.previous_y = None, None
+        self.frame_cursor = 0
+        self.pixel_cursor = 0
+
+    def peaks(self, samples):
+        """ Find the minimum and maximum peak of the samples.
+        Returns that pair in the order they were found.
+        So if min was found first, it returns (min, max) else the other way around. """
+
+        max_index = numpy.argmax(samples)
+        max_value = samples[max_index]
+
+        min_index = numpy.argmin(samples)
+        min_value = samples[min_index]
+
+        if min_index < max_index:
+            return (min_value, max_value)
+        else:
+            return (max_value, min_value)
+
+    def draw_peaks(self, x, peaks):
+        """ draw 2 peaks at x using the spectral_centroid for color """
+
+        y1 = self.image_height * 0.5 - peaks[0] * (self.image_height - 4) * 0.5
+        y2 = self.image_height * 0.5 - peaks[1] * (self.image_height - 4) * 0.5
+        
+        if self.previous_y and x < self.image_width-2 and self.pixel_cursor % 2:
+            if y1 < y2:
+                self.draw.line((x, 0, x, y1), self.line_color)
+                self.draw.line((x, self.image_height , x, y2), self.line_color)
+            else:
+                self.draw.line((x, 0, x, y2), self.line_color)
+                self.draw.line((x, self.image_height , x, y1), self.line_color)
+        else:
+            self.draw.line((x, 0, x, self.image_height), self.line_color)
+
+        self.previous_x, self.previous_y = x, y1
+
+#        self.draw_anti_aliased_pixels(x, y1, y2, self.line_color)
+
+    def draw_anti_aliased_pixels(self, x, y1, y2, color):
+        """ vertical anti-aliasing at y1 and y2 """
+        
+        y_max = max(y1, y2)
+        y_max_int = int(y_max)
+        alpha = y_max - y_max_int
+
+        if alpha > 0.0 and alpha < 1.0 and y_max_int + 1 < self.image_height:
+            current_pix = self.pixel[int(x), y_max_int + 1]
+
+            r = int((1-alpha)*current_pix[0] + alpha*color[0])
+            g = int((1-alpha)*current_pix[1] + alpha*color[1])
+            b = int((1-alpha)*current_pix[2] + alpha*color[2])
+
+            self.pixel[x, y_max_int + 1] = (r,g,b)
+
+        y_min = min(y1, y2)
+        y_min_int = int(y_min)
+        alpha = 1.0 - (y_min - y_min_int)
+
+        if alpha > 0.0 and alpha < 1.0 and y_min_int - 1 >= 0:
+            current_pix = self.pixel[x, y_min_int - 1]
+
+            r = int((1-alpha)*current_pix[0] + alpha*color[0])
+            g = int((1-alpha)*current_pix[1] + alpha*color[1])
+            b = int((1-alpha)*current_pix[2] + alpha*color[2])
+
+            self.pixel[x, y_min_int - 1] = (r,g,b)
+
+    def process(self, frames, eod):
+        if len(frames) != 1:
+            buffer = frames[:,0].copy()
+            buffer.shape = (len(buffer),1)
+            for samples, end in self.pixels_adapter.process(buffer, eod):
+                if self.pixel_cursor < self.image_width:
+                    self.draw_peaks(self.pixel_cursor, self.peaks(samples))
+                    self.pixel_cursor += 1
+            if self.pixel_cursor == self.image_width-2:
+                self.draw_peaks(self.pixel_cursor, (0, 0))
+                self.pixel_cursor += 1
+            else:
+                pass
+
+    def save(self, filename):
+        """ Apply last 2D transforms and write all pixels to the file. """
+
+        # middle line (0 for none)
+        a = 1
+        for x in range(self.image_width):
+            self.pixel[x, self.image_height/2] = tuple(map(lambda p: p+a, self.pixel[x, self.image_height/2]))
+        self.image.save(filename)
+        
+        
 class SpectrogramImage(object):
     """ Builds a PIL image representing a spectrogram of the audio stream (level vs. frequency vs. time).
     Adds pixels iteratively thanks to the adapter providing fixed size frame buffers."""
diff --git a/timeside/grapher/waveform_awdio.py b/timeside/grapher/waveform_awdio.py
new file mode 100644 (file)
index 0000000..668c192
--- /dev/null
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2007-2010 Guillaume Pellerin <yomguy@parisson.com>
+# Copyright (c) 2010 Olivier Guilyardi <olivier@samalyse.com>
+
+# This file is part of TimeSide.
+
+# TimeSide is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+
+# TimeSide 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 General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with TimeSide.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter
+from timeside.api import IGrapher
+from timeside.grapher.core import *
+
+
+class WaveformAwdio(Processor):
+    implements(IGrapher)
+
+    @interfacedoc
+    def __init__(self, width=572, height=74, bg_color=None, color_scheme='iso'):
+        self.width = width
+        self.height = height
+        self.bg_color = bg_color
+        self.color_scheme = color_scheme
+        self.graph = None
+
+    @staticmethod
+    @interfacedoc
+    def id():
+        return "waveform_awdio"
+
+    @staticmethod
+    @interfacedoc
+    def name():
+        return "Waveform Awdio"
+
+    @interfacedoc
+    def set_colors(self, background, scheme):
+        self.bg_color = background
+        self.color_scheme = scheme
+
+    @interfacedoc
+    def setup(self, channels=None, samplerate=None, nframes=None):
+        super(WaveformAwdio, self).setup(channels, samplerate, nframes)
+        if self.graph:
+            self.graph = None
+        self.graph = WaveformImageSimple(self.width, self.height, self.nframes(), self.samplerate(), 
+                                    bg_color=self.bg_color, color_scheme=self.color_scheme)
+
+    @interfacedoc
+    def process(self, frames, eod=False):
+        self.graph.process(frames, eod)
+        return frames, eod
+
+    @interfacedoc
+    def render(self, output):
+        if output:
+            self.graph.save(output)
+        return self.graph.image
diff --git a/timeside/tools/waveform_batch_awdio.py b/timeside/tools/waveform_batch_awdio.py
new file mode 100644 (file)
index 0000000..6cc97f9
--- /dev/null
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2009-2010 Guillaume Pellerin <yomguy@parisson.com>
+
+# This file is part of TimeSide.
+
+# TimeSide is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+
+# TimeSide 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 General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with TimeSide.  If not, see <http://www.gnu.org/licenses/>.
+
+# Author: Guillaume Pellerin <yomguy@parisson.com>
+
+version = '0.2'
+
+import os
+import sys
+import timeside
+
+class GrapherScheme:
+
+    def __init__(self):
+        self.color = 255
+        self.color_scheme = {
+            'waveform': [ # Four (R,G,B) tuples for three main color channels for the spectral centroid method
+                        (self.color,self.color,self.color)
+#                        (0, 0, 0), (0, 0, 0), (0, 0, 0), (0,0,0)
+                        ],
+            'spectrogram': [
+                        (0, 0, 0), (58/4,68/4,65/4), (80/2,100/2,153/2), (90,180,100), (224,224,44), (255,60,30), (255,255,255)
+                        ]}
+
+        # Width of the image
+        self.width = 572
+
+        # Height of the image
+        self.height = 74
+
+        # Background color
+        self.bg_color = None
+#        self.bg_color = None
+
+        # Force computation. By default, the class doesn't overwrite existing image files.
+        self.force = True
+
+
+class Media2Waveform(object):
+
+    def __init__(self, media_dir, img_dir):
+        self.root_dir = media_dir
+        self.img_dir = img_dir
+        self.scheme = GrapherScheme()
+        self.width = self.scheme.width
+        self.height = self.scheme.height
+        self.bg_color = self.scheme.bg_color
+        self.color_scheme = self.scheme.color_scheme
+        self.force = self.scheme.force
+
+        self.media_list = self.get_media_list()
+        if not os.path.exists(self.img_dir):
+            os.makedirs(self.img_dir)
+        self.path_dict = self.get_path_dict()
+
+    def get_media_list(self):
+        media_list = []
+        for root, dirs, files in os.walk(self.root_dir):
+            if root:
+                for file in files:
+                    ext = file.split('.')[-1]
+                    media_list.append(root+os.sep+file)
+        return media_list
+
+    def get_path_dict(self):
+        path_dict = {}
+        for media in self.media_list:
+            filename = media.split(os.sep)[-1]
+            name, ext = os.path.splitext(filename)
+            path_dict[media] = self.img_dir + os.sep + filename.replace('.',  '_') + '.png'
+        return path_dict
+
+    def process(self):
+        for source, image in self.path_dict.iteritems():
+            if not os.path.exists(image) or self.force:
+                print 'Processing ', source
+                audio = os.path.join(os.path.dirname(__file__), source)
+                decoder  = timeside.decoder.FileDecoder(audio)
+                analyzer = timeside.analyzer.Duration()
+                waveform = timeside.grapher.WaveformAwdio(width=self.width, height=self.height,
+                                            bg_color=self.bg_color, color_scheme=self.color_scheme)
+                (decoder | analyzer | waveform).run()
+                duration = analyzer.result()
+                img_name = os.path.split(image)[1]
+                image = os.path.split(image)[0]+os.sep+os.path.splitext(img_name)[0] + '_' +\
+                        '_'.join([str(self.width),  str(self.height),  str(int(duration))])+os.path.splitext(img_name)[1]
+                waveform.graph.filename = image
+                print 'Rendering ', source, ' to ', waveform.graph.filename, '...'
+                print 'frames per pixel = ', waveform.graph.samples_per_pixel
+                if os.path.exists(image):
+                    os.remove(image)
+                waveform.render(output=image)
+                
+
+if __name__ == '__main__':
+    if len(sys.argv) <= 2:
+        print """
+        Usage : python waveform_batch /path/to/media_dir /path/to/img_dir
+
+        Dependencies : timeside, python, python-numpy, python-gst0.10, gstreamer0.10-plugins-base
+        See http://code.google.com/p/timeside/ for more information.
+        """
+    else:
+        media_dir = sys.argv[-2]
+        img_dir = sys.argv[-1]
+        m = Media2Waveform(media_dir, img_dir)
+        m.process()