From c0c98560f9f3d020a4b5c941a3f229d27cae5fd2 Mon Sep 17 00:00:00 2001 From: yomguy <> Date: Thu, 21 Aug 2008 14:51:19 +0000 Subject: [PATCH] * Add analysis component * Add analysis results in templates * Add 2 large views of audiolab plots --- telemeta/analysis/__init__.py | 5 + telemeta/analysis/api.py | 29 +++ telemeta/analysis/core.py | 182 ++++++++++++++++++ telemeta/analysis/length.py | 35 ++++ telemeta/analysis/max_level.py | 37 ++++ telemeta/analysis/mean_level.py | 37 ++++ telemeta/htdocs/css/telemeta.css | 11 ++ telemeta/templates/mediaitem_detail.html | 19 ++ .../scikits.audiolab.egg-info/SOURCES.txt | 45 +++++ .../audiolab/scikits/audiolab/pysndfile.pyc | Bin 30913 -> 32481 bytes telemeta/visualization/__init__.py | 6 +- telemeta/visualization/octave_core.py | 3 +- telemeta/visualization/spectrogram3.py | 2 +- telemeta/visualization/waveform3.py | 2 +- telemeta/web/base.py | 15 +- 15 files changed, 421 insertions(+), 7 deletions(-) create mode 100644 telemeta/analysis/__init__.py create mode 100644 telemeta/analysis/api.py create mode 100644 telemeta/analysis/core.py create mode 100644 telemeta/analysis/length.py create mode 100644 telemeta/analysis/max_level.py create mode 100644 telemeta/analysis/mean_level.py diff --git a/telemeta/analysis/__init__.py b/telemeta/analysis/__init__.py new file mode 100644 index 00000000..cc434015 --- /dev/null +++ b/telemeta/analysis/__init__.py @@ -0,0 +1,5 @@ +from telemeta.analysis.api import * +from telemeta.analysis.core import * +from telemeta.analysis.max_level import * +from telemeta.analysis.mean_level import * +from telemeta.analysis.length import * diff --git a/telemeta/analysis/api.py b/telemeta/analysis/api.py new file mode 100644 index 00000000..b13107c0 --- /dev/null +++ b/telemeta/analysis/api.py @@ -0,0 +1,29 @@ +# Copyright (C) 2008 Parisson SARL +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://svn.parisson.org/telemeta/TelemetaLicense. +# +# Author: Guillaume Pellerin + +from telemeta.core import * + +class IMediaItemAnalyzer(Interface): + """Media item analyzer driver interface""" + + def get_id(): + """Return a short id alphanumeric, lower-case string.""" + + def get_name(): + """Return the analysis name, such as "Mean Level", "Max level", + "Total length, etc.. + """ + + def get_unit(): + """Return the unit of the data such as "dB", "seconds", etc... + """ + + def render(media_item, options=None): + """Return a list containing data results of the process""" + diff --git a/telemeta/analysis/core.py b/telemeta/analysis/core.py new file mode 100644 index 00000000..7b8dca08 --- /dev/null +++ b/telemeta/analysis/core.py @@ -0,0 +1,182 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Authors: +# Bram de Jong +# Guillaume Pellerin + +from django.conf import settings +from telemeta.core import * +import optparse, math, sys +import numpy +import scikits.audiolab as audiolab + + +class AudioProcessor(Component): + + def __init__(self): + self.fft_size = 2048 + self.window_function = numpy.ones + self.window = self.window_function(self.fft_size) + self.spectrum_range = None + self.lower = 100 + self.higher = 22050 + self.lower_log = math.log10(self.lower) + self.higher_log = math.log10(self.higher) + self.clip = lambda val, low, high: min(high, max(low, val)) + + def pre_process(self, media_item): + wav_file = settings.MEDIA_ROOT + '/' + media_item.file + self.audio_file = audiolab.sndfile(wav_file, 'read') + self.frames = self.audio_file.get_nframes() + self.samplerate = self.audio_file.get_samplerate() + self.channels = self.audio_file.get_channels() + + def post_process(self, audio_file): + pass + + def get_mono_samples(self): + samples = self.audio_file.read_frames(self.frames) + # convert to mono by selecting left channel only + if self.channels > 1: + samples = samples[:,0] + return samples + + def read(self, start, size, resize_if_less=False): + """ read size samples starting at start, if resize_if_less is True and less than size + samples are read, resize the array to size and fill with zeros """ + + # number of zeros to add to start and end of the buffer + add_to_start = 0 + add_to_end = 0 + + if start < 0: + # the first FFT window starts centered around zero + if size + start <= 0: + return numpy.zeros(size) if resize_if_less else numpy.array([]) + else: + self.audio_file.seek(0) + + add_to_start = -start # remember: start is negative! + to_read = size + start + + if to_read > self.frames: + add_to_end = to_read - self.frames + to_read = self.frames + else: + self.audio_file.seek(start) + + to_read = size + if start + to_read >= self.frames: + to_read = self.frames - start + add_to_end = size - to_read + + try: + samples = self.audio_file.read_frames(to_read) + except IOError: + # this can happen for wave files with broken headers... + return numpy.zeros(size) if resize_if_less else numpy.zeros(2) + + # convert to mono by selecting left channel only + if self.channels > 1: + samples = samples[:,0] + + if resize_if_less and (add_to_start > 0 or add_to_end > 0): + if add_to_start > 0: + samples = numpy.concatenate((numpy.zeros(add_to_start), samples), axis=1) + + if add_to_end > 0: + samples = numpy.resize(samples, size) + samples[size - add_to_end:] = 0 + + return samples + + + def spectral_centroid(self, seek_point, spec_range=120.0): + """ starting at seek_point read fft_size samples, and calculate the spectral centroid """ + + samples = self.read(seek_point - self.fft_size/2, self.fft_size, True) + + samples *= self.window + fft = numpy.fft.fft(samples) + spectrum = numpy.abs(fft[:fft.shape[0] / 2 + 1]) / float(self.fft_size) # normalized abs(FFT) between 0 and 1 + length = numpy.float64(spectrum.shape[0]) + + # scale the db spectrum from [- spec_range db ... 0 db] > [0..1] + db_spectrum = ((20*(numpy.log10(spectrum + 1e-30))).clip(-spec_range, 0.0) + spec_range)/spec_range + + energy = spectrum.sum() + spectral_centroid = 0 + + if energy > 1e-20: + # calculate the spectral centroid + + if self.spectrum_range == None: + self.spectrum_range = numpy.arange(length) + + spectral_centroid = (spectrum * self.spectrum_range).sum() / (energy * (length - 1)) * self.samplerate * 0.5 + + # clip > log10 > scale between 0 and 1 + spectral_centroid = (math.log10(self.clip(spectral_centroid, self.lower, self.higher)) - self.lower_log) / (self.higher_log - self.lower_log) + + return (spectral_centroid, db_spectrum) + + + def peaks(self, start_seek, end_seek): + """ read all samples between start_seek and end_seek, then find the minimum and maximum peak + in that range. Returns that pair in the order they were found. So if min was found first, + it returns (min, max) else the other way around. """ + + # larger blocksizes are faster but take more mem... + # Aha, Watson, a clue, a tradeof! + block_size = 4096 + + max_index = -1 + max_value = -1 + min_index = -1 + min_value = 1 + + if end_seek > self.frames: + end_seek = self.frames + + if block_size > end_seek - start_seek: + block_size = end_seek - start_seek + + if block_size <= 1: + samples = self.read(start_seek, 1) + return samples[0], samples[0] + elif block_size == 2: + samples = self.read(start_seek, True) + return samples[0], samples[1] + + for i in range(start_seek, end_seek, block_size): + samples = self.read(i, block_size) + + local_max_index = numpy.argmax(samples) + local_max_value = samples[local_max_index] + + if local_max_value > max_value: + max_value = local_max_value + max_index = local_max_index + + local_min_index = numpy.argmin(samples) + local_min_value = samples[local_min_index] + + if local_min_value < min_value: + min_value = local_min_value + min_index = local_min_index + + return (min_value, max_value) if min_index < max_index else (max_value, min_value) + + + \ No newline at end of file diff --git a/telemeta/analysis/length.py b/telemeta/analysis/length.py new file mode 100644 index 00000000..3c6cf7a2 --- /dev/null +++ b/telemeta/analysis/length.py @@ -0,0 +1,35 @@ +# Copyright (C) 2008 Parisson SARL +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://svn.parisson.org/telemeta/TelemetaLicense. +# +# Author: Guillaume Pellerin + +from telemeta.analysis.core import * +from telemeta.analysis.api import IMediaItemAnalyzer +import numpy + +class LengthAnalyzer(AudioProcessor): + """Media item analyzer driver interface""" + + implements(IMediaItemAnalyzer) + + def __init__(self): + self.fft_size = 2048 + self.window_function = numpy.hanning + self.window = self.window_function(self.fft_size) + + def get_id(self): + return "length" + + def get_name(self): + return "Length" + + def get_unit(self): + return "s" + + def render(self, media_item, options=None): + self.pre_process(media_item) + return numpy.round(numpy.divide(self.frames, self.samplerate),2) diff --git a/telemeta/analysis/max_level.py b/telemeta/analysis/max_level.py new file mode 100644 index 00000000..6aaa48be --- /dev/null +++ b/telemeta/analysis/max_level.py @@ -0,0 +1,37 @@ +# Copyright (C) 2008 Parisson SARL +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://svn.parisson.org/telemeta/TelemetaLicense. +# +# Author: Guillaume Pellerin + +from telemeta.analysis.core import * +from telemeta.analysis.api import IMediaItemAnalyzer +import numpy + +class MaxLevelAnalyzer(AudioProcessor): + """Media item analyzer driver interface""" + + implements(IMediaItemAnalyzer) + + def __init__(self): + self.fft_size = 2048 + self.window_function = numpy.hanning + self.window = self.window_function(self.fft_size) + + def get_id(self): + return "max_level" + + def get_name(self): + return "Maximum level" + + def get_unit(self): + return "dB" + + def render(self, media_item, options=None): + self.pre_process(media_item) + samples = self.get_mono_samples() + print str(numpy.max(samples)) + return numpy.round(20*numpy.log10(numpy.max(samples)),2) \ No newline at end of file diff --git a/telemeta/analysis/mean_level.py b/telemeta/analysis/mean_level.py new file mode 100644 index 00000000..1efe14e5 --- /dev/null +++ b/telemeta/analysis/mean_level.py @@ -0,0 +1,37 @@ +# Copyright (C) 2008 Parisson SARL +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://svn.parisson.org/telemeta/TelemetaLicense. +# +# Author: Guillaume Pellerin + +from telemeta.analysis.core import * +from telemeta.analysis.api import IMediaItemAnalyzer +import numpy + +class MeanLevelAnalyser(AudioProcessor): + """Media item analyzer driver interface""" + + implements(IMediaItemAnalyzer) + + def __init__(self): + self.fft_size = 2048 + self.window_function = numpy.hanning + self.window = self.window_function(self.fft_size) + + def get_id(self): + return "mean_level" + + def get_name(self): + return "Mean level" + + def get_unit(self): + return "dB" + + def render(self, media_item, options=None): + self.pre_process(media_item) + samples = self.get_mono_samples() + size = numpy.size(samples) + return numpy.round(20*numpy.log10(numpy.mean(numpy.sqrt(numpy.square(samples)))),2) diff --git a/telemeta/htdocs/css/telemeta.css b/telemeta/htdocs/css/telemeta.css index 3dbfab5e..1fe5a561 100644 --- a/telemeta/htdocs/css/telemeta.css +++ b/telemeta/htdocs/css/telemeta.css @@ -135,6 +135,17 @@ h3 { font-size: 1em; } +.analyser { + background-color: #fff; + color: #555; + border: 1px solid #adadad; + width: 301px; + padding: 2px; + margin: 5px 0 0; + overflow: auto; + font-size: 1em; +} + /* Geographic navigator */ ul.continents, ul.continents ul { list-style: none; margin: 0; padding: 0;} ul.continents { margin: 1em 0; } diff --git a/telemeta/templates/mediaitem_detail.html b/telemeta/templates/mediaitem_detail.html index a662d3a2..5d4f8a4a 100644 --- a/telemeta/templates/mediaitem_detail.html +++ b/telemeta/templates/mediaitem_detail.html @@ -53,6 +53,25 @@ {{ format.name }} {% endfor %}

+
+

Analysis:

+
+ + {% for analyser in analysers %} + + + + + + {% endfor %} +
+ {{ analyser.name }} + = + {{ analyser.value }} + + {{ analyser.unit }} +
+
{% endif %}
diff --git a/telemeta/util/audiolab/scikits.audiolab.egg-info/SOURCES.txt b/telemeta/util/audiolab/scikits.audiolab.egg-info/SOURCES.txt index a155f472..c314c46d 100644 --- a/telemeta/util/audiolab/scikits.audiolab.egg-info/SOURCES.txt +++ b/telemeta/util/audiolab/scikits.audiolab.egg-info/SOURCES.txt @@ -1,13 +1,23 @@ COPYING.txt +Changelog FLAC_SUPPORT.txt +INSTALL.txt MANIFEST.in +Makefile README.txt +TODO +generate.sh generate_const.py +generate_const.pyc header_parser.py +header_parser.pyc setup.cfg setup.py site.cfg.win32 +site.cfg_noflac +tester.py scikits/__init__.py +scikits/__init__.pyc scikits.audiolab.egg-info/PKG-INFO scikits.audiolab.egg-info/SOURCES.txt scikits.audiolab.egg-info/dependency_links.txt @@ -16,11 +26,46 @@ scikits.audiolab.egg-info/requires.txt scikits.audiolab.egg-info/top_level.txt scikits.audiolab.egg-info/zip-safe scikits/audiolab/__init__.py +scikits/audiolab/__init__.pyc scikits/audiolab/info.py +scikits/audiolab/info.pyc scikits/audiolab/matapi.py +scikits/audiolab/matapi.pyc scikits/audiolab/pyaudioio.py scikits/audiolab/pysndfile.py scikits/audiolab/pysndfile.py.in +scikits/audiolab/pysndfile.pyc +scikits/audiolab/docs/Makefile +scikits/audiolab/docs/audiolab1.png +scikits/audiolab/docs/base.tex +scikits/audiolab/docs/index.txt +scikits/audiolab/docs/user.tex +scikits/audiolab/docs/examples/format1.py +scikits/audiolab/docs/examples/format2.py +scikits/audiolab/docs/examples/matlab1.py +scikits/audiolab/docs/examples/quick1.py +scikits/audiolab/docs/examples/usage1.py +scikits/audiolab/docs/examples/usage2.py +scikits/audiolab/docs/examples/write1.py +scikits/audiolab/misc/Makefile +scikits/audiolab/misc/Sconstruct +scikits/audiolab/misc/badflac.c +scikits/audiolab/misc/badflac.flac +scikits/audiolab/misc/winfdopen.c +scikits/audiolab/soundio/SConstruct +scikits/audiolab/soundio/_alsa.pyx +scikits/audiolab/soundio/alsa.py +scikits/audiolab/soundio/alsa_ctypes.py +scikits/audiolab/soundio/setup.py +scikits/audiolab/soundio/simple.c +scikits/audiolab/soundio/simple2.c +scikits/audiolab/test_data/original.aif +scikits/audiolab/test_data/test.aiff +scikits/audiolab/test_data/test.au +scikits/audiolab/test_data/test.flac +scikits/audiolab/test_data/test.raw +scikits/audiolab/test_data/test.sdif +scikits/audiolab/test_data/test.wav scikits/audiolab/tests/__init__.py scikits/audiolab/tests/test_matapi.py scikits/audiolab/tests/test_pysndfile.py diff --git a/telemeta/util/audiolab/scikits/audiolab/pysndfile.pyc b/telemeta/util/audiolab/scikits/audiolab/pysndfile.pyc index a162db43f18e5179c5a1b11c47e215815d665864..f160cf6200fdc7b84b181f0dc2f0e3bb6ec605d7 100644 GIT binary patch delta 2938 zcmbW3ze_?<7=|_VCQ@s)WEPSa3U1v@>DLehMQKZ^S6Ylbsc~=DNClzADF_nnPy|5| z21O7NhoGUsrdkSeZ;0>@sEDShV{_BHo#o;A_|Ey>@7%r_U%rh3dvCU_JX{L*?Q+0i zFi3G-Ba)_Tx)dcDDMeJGk<{Wphjc2T(p*GjR(r+>v5H8qJC;y1;E1B=hXb}2ItgfH zQr9Wgr-(@HK7qD6!>IbqS;W-*iqhQ+kd{|%bIn62x9+>*Aj*ctp+<=16`xBND4KU$ z>0w_Q#r^#oC~A^-QM4+TQM3{>Q#Npf;(2!oMPq||5XzsG*Aqn1I}beh%U;=xYO5pT zAj&0{38G?%S9~kI;g~60VU1x20h0W5M_G8XF}Oe wRmC17FZp2g2Gr7`4sWl?B=(QvBT!3Xp9G$7n`_5tpBP!UfcN8K<&3@I7rzrNkpKVy delta 1370 zcmaF(m+|07Mz+nLc)5Pe+{ku9R>+irfk8hbKQ~oBH$OLDKP9znvSX^oW(~PAMr?A7 zu^N+q$S32H+NzL(OR88Ar!E7fR2;fCzgJp;)g3@LPM+t_Hu<|sBrdJ1R3~E93i85K z^=NF`K&EnwIR?bdzeY*L_Lxo#1LORC)xXUw`;8L{BfcFE2Wht%d= zdkG$Fa-c+e-}@Y{7~jn9XNy%1l8`EK8Ce*hfJ@uv*MXB*vFQVb$>!_fE;#h*Os9o36V8WeqD(O4rM6cMg5UD$Lo#%{$ftg%@j?mrtAxy_ELtT>bNf}F=# zv`hv$;BIaN4yn!kc|Odz$$lt|#aA=z>Sc)@uJTJvs4uOJrX&Kf`3`#%g WXHD-QRoZ$4Zj#{>YQ+Ot#u diff --git a/telemeta/visualization/__init__.py b/telemeta/visualization/__init__.py index b39eaa55..06d036b3 100644 --- a/telemeta/visualization/__init__.py +++ b/telemeta/visualization/__init__.py @@ -1,7 +1,9 @@ from telemeta.visualization.api import * -from telemeta.visualization.waveform import * +#from telemeta.visualization.waveform import * #from telemeta.visualization.waveform2 import * from telemeta.visualization.waveform3 import * #from telemeta.visualization.spectrogram import * from telemeta.visualization.spectrogram2 import * -from telemeta.visualization.spectrogram3 import * \ No newline at end of file +from telemeta.visualization.spectrogram3 import * +from telemeta.visualization.waveform4 import * +from telemeta.visualization.spectrogram4 import * \ No newline at end of file diff --git a/telemeta/visualization/octave_core.py b/telemeta/visualization/octave_core.py index cb8f4475..8b262b82 100644 --- a/telemeta/visualization/octave_core.py +++ b/telemeta/visualization/octave_core.py @@ -1,7 +1,6 @@ from telemeta.core import * -from telemeta.export import * -from telemeta.visualization.api import IMediaItemVisualizer +#from telemeta.visualization.api import IMediaItemVisualizer from django.conf import settings from tempfile import NamedTemporaryFile import os diff --git a/telemeta/visualization/spectrogram3.py b/telemeta/visualization/spectrogram3.py index 0930f502..455cf998 100644 --- a/telemeta/visualization/spectrogram3.py +++ b/telemeta/visualization/spectrogram3.py @@ -30,7 +30,7 @@ class SpectrogramVisualizer3(Component): wav_file = settings.MEDIA_ROOT + '/' + media_item.file pngFile_w = NamedTemporaryFile(suffix='.png') pngFile_s = NamedTemporaryFile(suffix='.png') - image_width = 1800 + image_width = 305 image_height = 150 fft_size = 2048 args = (wav_file, pngFile_w.name, pngFile_s.name, image_width, image_height, fft_size) diff --git a/telemeta/visualization/waveform3.py b/telemeta/visualization/waveform3.py index 4d2e806c..e908dfd4 100644 --- a/telemeta/visualization/waveform3.py +++ b/telemeta/visualization/waveform3.py @@ -30,7 +30,7 @@ class WaveFormVisualizer(Component): wav_file = settings.MEDIA_ROOT + '/' + media_item.file pngFile_w = NamedTemporaryFile(suffix='.png') pngFile_s = NamedTemporaryFile(suffix='.png') - image_width = 300 + image_width = 305 image_height = 152 fft_size = 2048 args = (wav_file, pngFile_w.name, pngFile_s.name, image_width, image_height, fft_size) diff --git a/telemeta/web/base.py b/telemeta/web/base.py index 9f332ef1..47c29a24 100644 --- a/telemeta/web/base.py +++ b/telemeta/web/base.py @@ -25,12 +25,14 @@ from telemeta.models import MediaCollection from telemeta.core import Component, ExtensionPoint from telemeta.export import * from telemeta.visualization import * +from telemeta.analysis import * class WebView(Component): """Provide web UI methods""" exporters = ExtensionPoint(IExporter) visualizers = ExtensionPoint(IMediaItemVisualizer) + analyzers = ExtensionPoint(IMediaItemAnalyzer) def index(self, request): """Render the homepage""" @@ -42,9 +44,11 @@ class WebView(Component): def item_detail(self, request, item_id, template='mediaitem_detail.html'): """Show the details of a given item""" item = MediaItem.objects.get(pk=item_id) + formats = [] for exporter in self.exporters: formats.append({'name': exporter.get_format(), 'extension': exporter.get_file_extension()}) + visualizers = [] for visualizer in self.visualizers: visualizers.append({'name':visualizer.get_name(), 'id': @@ -54,9 +58,18 @@ class WebView(Component): else: visualizer_id = 'waveform3' + analyzers = [] + for analyzer in self.analyzers: + value = analyzer.render(item) + analyzers.append({'name':analyzer.get_name(), + 'id':analyzer.get_id(), + 'unit':analyzer.get_unit(), + 'value':str(value)}) + return render_to_response(template, {'item': item, 'export_formats': formats, - 'visualizers': visualizers, 'visualizer_id': visualizer_id}) + 'visualizers': visualizers, 'visualizer_id': visualizer_id, + 'analysers': analyzers}) def item_visualize(self, request, item_id, visualizer_id): for visualizer in self.visualizers: -- 2.39.5