From 1b28770a5379a3109047af6c9f513d9142485354 Mon Sep 17 00:00:00 2001 From: yomguy Date: Thu, 14 Oct 2010 11:49:09 +0000 Subject: [PATCH] add a new light grapher, add dependencies for other media types --- INSTALL | 2 +- timeside/grapher/__init__.py | 1 + timeside/grapher/core.py | 121 ++++++++++++++++++++++++ timeside/grapher/waveform_awdio.py | 70 ++++++++++++++ timeside/tools/waveform_batch_awdio.py | 124 +++++++++++++++++++++++++ 5 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 timeside/grapher/waveform_awdio.py create mode 100644 timeside/tools/waveform_batch_awdio.py diff --git a/INSTALL b/INSTALL index e3af4ad..f0bb996 100644 --- 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 diff --git a/timeside/grapher/__init__.py b/timeside/grapher/__init__.py index 35f0db5..421390c 100644 --- a/timeside/grapher/__init__.py +++ b/timeside/grapher/__init__.py @@ -4,3 +4,4 @@ from core import * from waveform import * from spectrogram import * from waveform_joydiv import * +from waveform_awdio import * diff --git a/timeside/grapher/core.py b/timeside/grapher/core.py index f4aad49..48f46d8 100644 --- a/timeside/grapher/core.py +++ b/timeside/grapher/core.py @@ -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 index 0000000..668c192 --- /dev/null +++ b/timeside/grapher/waveform_awdio.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2010 Guillaume Pellerin +# Copyright (c) 2010 Olivier Guilyardi + +# 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 . + + +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 index 0000000..6cc97f9 --- /dev/null +++ b/timeside/tools/waveform_batch_awdio.py @@ -0,0 +1,124 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2009-2010 Guillaume Pellerin + +# 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 . + +# Author: Guillaume Pellerin + +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() -- 2.39.5