]> git.parisson.com Git - telemeta.git/commitdiff
* Add analysis component
authoryomguy <>
Thu, 21 Aug 2008 14:51:19 +0000 (14:51 +0000)
committeryomguy <>
Thu, 21 Aug 2008 14:51:19 +0000 (14:51 +0000)
* Add analysis results in templates
* Add 2 large views of audiolab plots

15 files changed:
telemeta/analysis/__init__.py [new file with mode: 0644]
telemeta/analysis/api.py [new file with mode: 0644]
telemeta/analysis/core.py [new file with mode: 0644]
telemeta/analysis/length.py [new file with mode: 0644]
telemeta/analysis/max_level.py [new file with mode: 0644]
telemeta/analysis/mean_level.py [new file with mode: 0644]
telemeta/htdocs/css/telemeta.css
telemeta/templates/mediaitem_detail.html
telemeta/util/audiolab/scikits.audiolab.egg-info/SOURCES.txt
telemeta/util/audiolab/scikits/audiolab/pysndfile.pyc
telemeta/visualization/__init__.py
telemeta/visualization/octave_core.py
telemeta/visualization/spectrogram3.py
telemeta/visualization/waveform3.py
telemeta/web/base.py

diff --git a/telemeta/analysis/__init__.py b/telemeta/analysis/__init__.py
new file mode 100644 (file)
index 0000000..cc43401
--- /dev/null
@@ -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 (file)
index 0000000..b13107c
--- /dev/null
@@ -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 <yomguy@parisson.com>
+
+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 (file)
index 0000000..7b8dca0
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+#
+# Authors:
+#   Bram de Jong <bram.dejong at domain.com where domain in gmail>
+#   Guillaume Pellerin <yomguy at parisson.com>
+
+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 (file)
index 0000000..3c6cf7a
--- /dev/null
@@ -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 <yomguy@parisson.com>
+
+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 (file)
index 0000000..6aaa48b
--- /dev/null
@@ -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 <yomguy@parisson.com>
+
+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 (file)
index 0000000..1efe14e
--- /dev/null
@@ -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 <yomguy@parisson.com>
+
+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)
index 3dbfab5e8127db4039310408f5ba5a6f453321ab..1fe5a561abf5190acd66ab0d7302ae8fe959effa 100644 (file)
@@ -135,6 +135,17 @@ h3 {
     font-size: 1em;\r
 }\r
 \r
+.analyser {\r
+    background-color: #fff;\r
+    color: #555;\r
+    border: 1px solid #adadad;\r
+    width: 301px;\r
+    padding: 2px;\r
+    margin: 5px 0 0;\r
+    overflow: auto;\r
+    font-size: 1em;\r
+}\r
+\r
 /* Geographic navigator */\r
 ul.continents, ul.continents ul { list-style: none; margin: 0; padding: 0;}\r
 ul.continents { margin: 1em 0; }\r
index a662d3a2756396aac2a0f9d7365980591556e557..5d4f8a4aa8edb923edc353e8fce382a8a5d854ec 100644 (file)
         <a href="{% url telemeta-item-export item.id|urlencode,format.extension %}">{{ format.name }}</a>\r
         {% endfor %}</p>\r
     </div>\r
+    <div class="analyser">\r
+        <p>Analysis:</p>\r
+        <br>\r
+        <table>\r
+        {% for analyser in analysers %}\r
+         <tr>\r
+          <td>\r
+            {{ analyser.name }}\r
+          </td>\r
+          <td> =\r
+            {{ analyser.value }}\r
+          </td>\r
+          <td>\r
+            {{ analyser.unit }}\r
+          </td>\r
+        </tr>\r
+        {% endfor %}\r
+       </table>\r
+    </div>\r
 </div>\r
 {% endif %}\r
     <div id="leftcol">\r
index a155f47229d0888b2e31f518aec95f39b60621ef..c314c46d52aea6795766539c212bc58bf09713fa 100644 (file)
@@ -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
index a162db43f18e5179c5a1b11c47e215815d665864..f160cf6200fdc7b84b181f0dc2f0e3bb6ec605d7 100644 (file)
Binary files a/telemeta/util/audiolab/scikits/audiolab/pysndfile.pyc and b/telemeta/util/audiolab/scikits/audiolab/pysndfile.pyc differ
index b39eaa55976c7f2961d8abca211c343fbf6dd22b..06d036b3558e4e3da9af33504892a70f95cd8fda 100644 (file)
@@ -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
index cb8f44753c2e3e9482d7bda1d9821903de713bd3..8b262b82b2dbc81181a6107a0ce3fc03050e0c22 100644 (file)
@@ -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
index 0930f502e05b65e36019df1668844b613e9dddf2..455cf99823859e5a84f8d2653e116bb6009ddc36 100644 (file)
@@ -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)
index 4d2e806c0802407b6d15a59e4c4bca299265e384..e908dfd42e98ef95b9cfddb8d86c398e5c03d265 100644 (file)
@@ -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)
index 9f332ef11220abdd49ed5613c152b97bd2c66fc1..47c29a24ef1d27cfb589eb061876e97fc4c881d8 100644 (file)
@@ -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: