From 9189c8e4c3bf013fe9f10efebea8193ed92689f7 Mon Sep 17 00:00:00 2001 From: yomguy Date: Thu, 1 Apr 2010 21:04:31 +0000 Subject: [PATCH] change processor module title, remove obsolete processors, cleanup --- __init__.py | 6 +- analyze/__init__.py | 13 -- analyze/channels.py | 48 ----- analyze/core.py | 200 ------------------- analyze/duration.py | 45 ----- analyze/encoding.py | 43 ---- analyze/resolution.py | 52 ----- analyze/samplerate.py | 43 ---- analyze/vamp/__init__.py | 3 - analyzer/__init__.py | 6 + analyze/format.py => analyzer/core.py | 25 +-- {analyze => analyzer}/dc.py | 33 ++- analyze/max_level.py => analyzer/duration.py | 37 ++-- analyzer/max_level.py | 63 ++++++ {analyze => analyzer}/mean_level.py | 36 +++- analyzer/vamp/__init__.py | 3 + {analyze => analyzer}/vamp/core.py | 10 +- decode/__init__.py | 7 - decode/flac.py | 69 ------- decode/mp3.py | 73 ------- decode/ogg.py | 68 ------- decode/wav.py | 69 ------- decoder/__init__.py | 3 + {decode => decoder}/core.py | 1 - encode/__init__.py | 7 - encoder/__init__.py | 7 + {encode => encoder}/core.py | 2 - {encode => encoder}/flac.py | 6 +- {encode => encoder}/mp3.py | 4 +- {encode => encoder}/ogg.py | 2 +- {encode => encoder}/wav.py | 2 +- grapher/core.py | 30 ++- tests/test.py | 39 ++-- 33 files changed, 210 insertions(+), 845 deletions(-) delete mode 100644 analyze/__init__.py delete mode 100644 analyze/channels.py delete mode 100644 analyze/core.py delete mode 100644 analyze/duration.py delete mode 100644 analyze/encoding.py delete mode 100644 analyze/resolution.py delete mode 100644 analyze/samplerate.py delete mode 100644 analyze/vamp/__init__.py create mode 100644 analyzer/__init__.py rename analyze/format.py => analyzer/core.py (60%) rename {analyze => analyzer}/dc.py (61%) rename analyze/max_level.py => analyzer/duration.py (58%) create mode 100644 analyzer/max_level.py rename {analyze => analyzer}/mean_level.py (58%) create mode 100644 analyzer/vamp/__init__.py rename {analyze => analyzer}/vamp/core.py (98%) delete mode 100644 decode/__init__.py delete mode 100644 decode/flac.py delete mode 100644 decode/mp3.py delete mode 100644 decode/ogg.py delete mode 100644 decode/wav.py create mode 100644 decoder/__init__.py rename {decode => decoder}/core.py (98%) delete mode 100644 encode/__init__.py create mode 100644 encoder/__init__.py rename {encode => encoder}/core.py (98%) rename {encode => encoder}/flac.py (98%) rename {encode => encoder}/mp3.py (99%) rename {encode => encoder}/ogg.py (99%) rename {encode => encoder}/wav.py (98%) diff --git a/__init__.py b/__init__.py index f021203..81ad004 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ from core import * from metadata import Metadata -import decode -import encode -import analyze +import decoder +import encoder +import analyzer import grapher diff --git a/analyze/__init__.py b/analyze/__init__.py deleted file mode 100644 index 307016e..0000000 --- a/analyze/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from timeside.analyze.core import * -from timeside.analyze.channels import * -from timeside.analyze.format import * -from timeside.analyze.encoding import * -from timeside.analyze.resolution import * -from timeside.analyze.samplerate import * -from timeside.analyze.duration import * -from timeside.analyze.max_level import * -from timeside.analyze.mean_level import * -from timeside.analyze.dc import * - - diff --git a/analyze/channels.py b/analyze/channels.py deleted file mode 100644 index 6ad202d..0000000 --- a/analyze/channels.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 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 - -from timeside.analyze.core import * -from timeside.api import IAnalyzer -import numpy - -class ChannelAnalyser(AudioProcessor): - """Media item analyzer driver interface""" - - implements(IAnalyzer) - - @staticmethod - def id(): - return "nchannels" - - def name(self): - return "Channels" - - def unit(self): - return "" - - def render(self, media_item, options=None): - self.pre_process(media_item) - if self.channels == 1: - return 'mono' - if self.channels == 2: - return 'stereo' - else: - return self.channels diff --git a/analyze/core.py b/analyze/core.py deleted file mode 100644 index c2371d2..0000000 --- a/analyze/core.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 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 . - -# Authors: -# Bram de Jong -# Guillaume Pellerin - -from timeside.core import * -import optparse, math, sys -import numpy -import scikits.audiolab as audiolab - -# FIXME: AudioProcessor: wrong name, should be Analyzer or AnalyzerCore -class AudioProcessor(Processor): - - 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 = media_item - 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() - self.format = self.audio_file.get_file_format() - self.encoding = self.audio_file.get_encoding() - - def get_samples(self): - samples = self.audio_file.read_frames(self.frames) - return samples - - def get_mono_samples(self): - # convert to mono by selecting left channel only - samples = self.get_samples() - if self.channels > 1: - return samples[:,0] - else: - 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: - if resize_if_less: - return numpy.zeros(size) - else: - return 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... - if resize_if_less: - return numpy.zeros(size) - else: - return 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 - - if min_index < max_index: - return (min_value, max_value) - else: - return (max_value, min_value) - - - diff --git a/analyze/duration.py b/analyze/duration.py deleted file mode 100644 index 64546d0..0000000 --- a/analyze/duration.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 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 - -from timeside.analyze.core import * -from timeside.api import IAnalyzer -import numpy -import datetime - -class DurationAnalyzer(AudioProcessor): - """Media item analyzer driver interface""" - - implements(IAnalyzer) - - @staticmethod - def id(): - return "duration" - - def name(self): - return "Duration" - - def unit(self): - return "h:m:s" - - def render(self, media_item, options=None): - self.pre_process(media_item) - media_time = numpy.round(float(self.frames)/float(self.samplerate),0) - return datetime.timedelta(0,media_time) diff --git a/analyze/encoding.py b/analyze/encoding.py deleted file mode 100644 index 0448f47..0000000 --- a/analyze/encoding.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 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 - -from timeside.analyze.core import * -from timeside.api import IAnalyzer -import numpy - -class EncodingAnalyser(AudioProcessor): - """Media item analyzer driver interface""" - - implements(IAnalyzer) - - @staticmethod - def id(): - return "encoding" - - def name(self): - return "Encoding format" - - def unit(self): - return "" - - def render(self, media_item, options=None): - self.pre_process(media_item) - return self.encoding diff --git a/analyze/resolution.py b/analyze/resolution.py deleted file mode 100644 index 6040fec..0000000 --- a/analyze/resolution.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 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 - -from timeside.analyze.core import * -from timeside.api import IAnalyzer -import numpy - -class ResolutionAnalyser(AudioProcessor): - """Media item analyzer driver interface""" - - implements(IAnalyzer) - - @staticmethod - def id(): - return "resolution" - - def name(self): - return "Resolution" - - def unit(self): - return "bits" - - def render(self, media_item, options=None): - self.pre_process(media_item) - if '8' in self.encoding: - return 8 - if '16' in self.encoding: - return 16 - if '24' in self.encoding: - return 24 - if '32' in self.encoding: - return 32 - else: - return '' diff --git a/analyze/samplerate.py b/analyze/samplerate.py deleted file mode 100644 index d235ebd..0000000 --- a/analyze/samplerate.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 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 - -from timeside.analyze.core import * -from timeside.api import IAnalyzer -import numpy - -class SampleRateAnalyzer(AudioProcessor): - """Media item analyzer driver interface""" - - implements(IAnalyzer) - - @staticmethod - def id(): - return "samplerate" - - def name(self): - return "Samplerate" - - def unit(self): - return "Hz" - - def render(self, media_item, options=None): - self.pre_process(media_item) - return self.samplerate diff --git a/analyze/vamp/__init__.py b/analyze/vamp/__init__.py deleted file mode 100644 index d889d34..0000000 --- a/analyze/vamp/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -from timeside.analyze.vamp.core import * diff --git a/analyzer/__init__.py b/analyzer/__init__.py new file mode 100644 index 0000000..f0af279 --- /dev/null +++ b/analyzer/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from timeside.analyzer.core import * +from timeside.analyzer.duration import * +from timeside.analyzer.max_level import * +from timeside.analyzer.mean_level import * +from timeside.analyzer.dc import * diff --git a/analyze/format.py b/analyzer/core.py similarity index 60% rename from analyze/format.py rename to analyzer/core.py index 2c7fdf3..17cbfa1 100644 --- a/analyze/format.py +++ b/analyzer/core.py @@ -17,27 +17,10 @@ # You should have received a copy of the GNU General Public License # along with TimeSide. If not, see . -# Author: Guillaume Pellerin +# Authors: +# Guillaume Pellerin -from timeside.analyze.core import * -from timeside.api import IAnalyzer +from timeside.core import * +from timeside.grapher.core import * import numpy -class FormatAnalyser(AudioProcessor): - """Media item analyzer driver interface""" - - implements(IAnalyzer) - - @staticmethod - def id(): - return "format" - - def name(self): - return "File format" - - def unit(self): - return "" - - def render(self, media_item, options=None): - self.pre_process(media_item) - return self.format diff --git a/analyze/dc.py b/analyzer/dc.py similarity index 61% rename from analyze/dc.py rename to analyzer/dc.py index ac31e6f..e92cfd5 100644 --- a/analyze/dc.py +++ b/analyzer/dc.py @@ -19,26 +19,41 @@ # Author: Guillaume Pellerin -from timeside.analyze.core import * +from timeside.analyzer.core import * from timeside.api import IValueAnalyzer import numpy -class MeanDCShiftAnalyser(AudioProcessor): - """Media item analyzer driver interface""" +class MeanDCShift(Processor): implements(IValueAnalyzer) + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(MeanDCShift, self).setup(channels, samplerate, nframes) + self.value = 0 + @staticmethod + @interfacedoc def id(): return "dc" - def name(self): + @staticmethod + @interfacedoc + def name(): return "Mean DC shift" - def unit(self): + @staticmethod + @interfacedoc + def unit(): return "%" - def render(self, media_item, options=None): - self.pre_process(media_item) - samples = self.get_mono_samples() - return numpy.round(100*numpy.mean(samples),4) + def __str__(self): + return "%s %s" % (str(self.value), unit()) + + def process(self, frames, eod=False): + self.value = numpy.round(100*numpy.mean(samples),4) + return frames, eod + + def result(self): + return self.value + diff --git a/analyze/max_level.py b/analyzer/duration.py similarity index 58% rename from analyze/max_level.py rename to analyzer/duration.py index af6c4a0..456a527 100644 --- a/analyze/max_level.py +++ b/analyzer/duration.py @@ -19,27 +19,40 @@ # Author: Guillaume Pellerin -from timeside.analyze.core import * +from timeside.analyzer.core import * from timeside.api import IValueAnalyzer -import numpy +import datetime -class MaxLevelAnalyzer(AudioProcessor): - """Media item analyzer driver interface""" +class Duration(Processor): implements(IValueAnalyzer) + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(Duration, self).setup(channels, samplerate, nframes) + self.value = 0 + @staticmethod + @interfacedoc def id(): - return "max_level" + return "duration" @staticmethod + @interfacedoc def name(): - return "Maximum peak level" + return "Duration" + + @staticmethod + @interfacedoc + def unit(): + return "h:m:s" + + def __str__(self): + return "%s %s" % (str(self.value), unit()) + + def process(self, frames, eod=False): + return frames, eod - def unit(self): - return "dB" + def result(self): + return datetime.timedelta(0,numpy.round(self.nframes / float(self.samplerate), 0)) - def render(self, media_item, options=None): - self.pre_process(media_item) - samples = self.get_samples() - return numpy.round(20*numpy.log10(numpy.max(samples)),2) diff --git a/analyzer/max_level.py b/analyzer/max_level.py new file mode 100644 index 0000000..93ff3ec --- /dev/null +++ b/analyzer/max_level.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin +# Copyright (c) 2009 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 . + +# Author: Guillaume Pellerin + +from timeside.analyzer.core import * +from timeside.api import IValueAnalyzer +import numpy + + +class MaxLevel(Processor): + implements(IValueAnalyzer) + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(MaxLevel, self).setup(channels, samplerate, nframes) + self.value = -140 + + @staticmethod + @interfacedoc + def id(): + return "maxlevel" + + @staticmethod + @interfacedoc + def name(): + return "Max level" + + @staticmethod + @interfacedoc + def unit(): + return "dB" + + def __str__(self): + return "%s %s" % (str(self.value), unit()) + + def process(self, frames, eod=False): + max = numpy.round(20*numpy.log10(frames.max()), 2) + if max > self.value: + self.value = max + + return frames, eod + + def result(self): + return self.value + diff --git a/analyze/mean_level.py b/analyzer/mean_level.py similarity index 58% rename from analyze/mean_level.py rename to analyzer/mean_level.py index 0ab38ff..8b68324 100644 --- a/analyze/mean_level.py +++ b/analyzer/mean_level.py @@ -19,26 +19,44 @@ # Author: Guillaume Pellerin -from timeside.analyze.core import * +from timeside.analyzer.core import * from timeside.api import IValueAnalyzer import numpy -class MeanLevelAnalyser(AudioProcessor): - """Media item analyzer driver interface""" +class MeanLevel(Processor): implements(IValueAnalyzer) + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(MeanLevel, self).setup(channels, samplerate, nframes) + self.value = -140 + @staticmethod + @interfacedoc def id(): return "meanlevel" - def name(self): + @staticmethod + @interfacedoc + def name(): return "Mean RMS level" - def unit(self): + @staticmethod + @interfacedoc + def unit(): return "dB" - def render(self, media_item, options=None): - self.pre_process(media_item) - samples = self.get_mono_samples() - return numpy.round(20*numpy.log10(numpy.mean(numpy.sqrt(numpy.square(samples)))),2) + def __str__(self): + return "%s %s" % (str(self.value), unit()) + + def process(self, frames, eod=False): + max = numpy.round(20*numpy.log10(numpy.mean(numpy.sqrt(numpy.square(frames.max())))), 2) + if max > self.value: + self.value = max + + return frames, eod + + def result(self): + return self.value + diff --git a/analyzer/vamp/__init__.py b/analyzer/vamp/__init__.py new file mode 100644 index 0000000..1ded83d --- /dev/null +++ b/analyzer/vamp/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from timeside.analyzer.vamp.core import * diff --git a/analyze/vamp/core.py b/analyzer/vamp/core.py similarity index 98% rename from analyze/vamp/core.py rename to analyzer/vamp/core.py index 6248508..49ae9a0 100644 --- a/analyze/vamp/core.py +++ b/analyzer/vamp/core.py @@ -36,7 +36,7 @@ class VampCoreAnalyzer: # needs vamp-examples package self.host = 'vamp-simple-host' self.buffer_size = 0xFFFF - + def id(self): return "vamp_plugins" @@ -69,7 +69,7 @@ class VampCoreAnalyzer: def get_wav_path(self, media_item): return settings.MEDIA_ROOT + '/' + media_item.file #return media_item - + def render(self, plugin, media_item): self.wavFile = self.get_wav_path(media_item) args = ' -s ' + ':'.join(plugin) + ' ' + str(self.wavFile) @@ -88,7 +88,7 @@ class VampCoreAnalyzer: def core_process(self, command, buffer_size): """Encode and stream audio data through a generator""" - + __chunk = 0 try: @@ -100,7 +100,7 @@ class VampCoreAnalyzer: close_fds = True) except: raise SubProcessError('Command failure:', command, proc) - + # Core processing while True: __chunk = proc.stdout.read(buffer_size) @@ -110,5 +110,5 @@ class VampCoreAnalyzer: if len(__chunk) == 0: break yield __chunk - + diff --git a/decode/__init__.py b/decode/__init__.py deleted file mode 100644 index d04cfce..0000000 --- a/decode/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- - -from timeside.decode.core import * -from timeside.decode.ogg import * -from timeside.decode.flac import * -from timeside.decode.wav import * -from timeside.decode.mp3 import * diff --git a/decode/flac.py b/decode/flac.py deleted file mode 100644 index 5289713..0000000 --- a/decode/flac.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 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 - -import os -import string -import subprocess - -from timeside.decode.core import * -from timeside.api import IDecoder -from mutagen.flac import FLAC -from tempfile import NamedTemporaryFile - -class FlacDecoder(DecoderCore): - """Defines methods to decode from FLAC""" - - implements(IDecoder) - - @staticmethod - def id(): - return "flacdec" - - def format(self): - return 'FLAC' - - def file_extension(self): - return 'flac' - - def mime_type(self): - return 'audio/x-flac' - - def description(self): - return """ - Free Lossless Audio Codec (FLAC) is a file format for lossless audio - data compression. During compression, FLAC does not lose quality from - the audio stream, as lossy compression formats such as MP3, AAC, and - Vorbis do. Josh Coalson is the primary author of FLAC. - (source Wikipedia) - """ - - def get_file_info(self): - try: - file1, file2 = os.popen4('metaflac --list "'+self.dest+'"') - info = [] - for line in file2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('DecoderError: metaflac is not installed or ' + \ - 'file does not exist.') - diff --git a/decode/mp3.py b/decode/mp3.py deleted file mode 100644 index f4f1973..0000000 --- a/decode/mp3.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007 Parisson SARL -# Copyright (c) 2006-2007 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 - -import os -import string -import subprocess - -from timeside.decode.core import * -from timeside.api import IDecoder - - -class Mp3Decoder(DecoderCore): - """Defines methods to decode from MP3""" - - implements(IDecoder) - - def __init__(self): - self.command = 'sox -t mp3 "%s" -q -b 16 -r 44100 -t wav -c2 - ' - - @staticmethod - def id(): - return "mp3dec" - - def format(self): - return 'MP3' - - def file_extension(self): - return 'mp3' - - def mime_type(self): - return 'audio/mpeg' - - def description(self): - return """ - MPEG-1 Audio Layer 3, more commonly referred to as MP3, is a patented - digital audio encoding format using a form of lossy data compression. - It is a common audio format for consumer audio storage, as well as a - de facto standard of digital audio compression for the transfer and - playback of music on digital audio players. MP3 is an audio-specific - format that was designed by the Moving Picture Experts Group as part - of its MPEG-1 standard. (source Wikipedia) - """ - - def get_file_info(self): - try: - file_out1, file_out2 = os.popen4('mp3info "'+self.dest+'"') - info = [] - for line in file_out2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('DecoderError: file does not exist.') - diff --git a/decode/ogg.py b/decode/ogg.py deleted file mode 100644 index ffd6903..0000000 --- a/decode/ogg.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 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 - -import os -import string -import subprocess - -from timeside.decode.core import * -from timeside.api import IDecoder -from mutagen.oggvorbis import OggVorbis - -class OggDecoder(DecoderCore): - """Defines methods to decode from OGG Vorbis""" - - implements(IDecoder) - - @staticmethod - def id(): - return "oggdec" - - def format(self): - return 'OggVorbis' - - def file_extension(self): - return 'ogg' - - def mime_type(self): - return 'application/ogg' - - def description(self): - return """ - Vorbis is a free software / open source project headed by the Xiph.Org - Foundation (formerly Xiphophorus company). The project produces an audio - format specification and software implementation (codec) for lossy audio - compression. Vorbis is most commonly used in conjunction with the Ogg - container format and it is therefore often referred to as Ogg Vorbis. - (source Wikipedia) - """ - - def get_file_info(self): - try: - file_out1, file_out2 = os.popen4('ogginfo "'+self.dest+'"') - info = [] - for line in file_out2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('DecoderError: file does not exist.') - diff --git a/decode/wav.py b/decode/wav.py deleted file mode 100644 index 08bca88..0000000 --- a/decode/wav.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 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 - -import os -import string -import subprocess - -from timeside.decode.core import * -from timeside.api import IDecoder - -class WavDecoder(DecoderCore): - """Defines methods to decode from WAV""" - - implements(IDecoder) - - @staticmethod - def id(): - return "wavdec" - - def format(self): - return 'WAV' - - def file_extension(self): - return 'wav' - - def mime_type(self): - return 'audio/x-wav' - - def description(self): - return """ - WAV (or WAVE), short for Waveform audio format, also known as Audio for - Windows, is a Microsoft and IBM audio file format standard for storing - an audio bitstream on PCs. It is an application of the RIFF bitstream - format method for storing data in “chunks”, and thus is also close to - the 8SVX and the AIFF format used on Amiga and Macintosh computers, - respectively. It is the main format used on Windows systems for raw and - typically uncompressed audio. The usual bitstream encoding is the Pulse - Code Modulation (PCM) format. - """ - - def get_file_info(self): - try: - file1, file2 = os.popen4('wavinfo "'+self.dest+'"') - info = [] - for line in file2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('DecoderError: wavinfo id not installed or file does not exist.') - diff --git a/decoder/__init__.py b/decoder/__init__.py new file mode 100644 index 0000000..9f37f75 --- /dev/null +++ b/decoder/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from timeside.decoder.core import * diff --git a/decode/core.py b/decoder/core.py similarity index 98% rename from decode/core.py rename to decoder/core.py index b3c5b15..85d818e 100644 --- a/decode/core.py +++ b/decoder/core.py @@ -20,7 +20,6 @@ # Author: Guillaume Pellerin -from timeside.decode import * from timeside.core import * import subprocess diff --git a/encode/__init__.py b/encode/__init__.py deleted file mode 100644 index a3dd62f..0000000 --- a/encode/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- - -from timeside.encode.core import * -from timeside.encode.ogg import * -from timeside.encode.wav import * -from timeside.encode.mp3 import * -from timeside.encode.flac import * diff --git a/encoder/__init__.py b/encoder/__init__.py new file mode 100644 index 0000000..2818f2d --- /dev/null +++ b/encoder/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from timeside.encoder.core import * +from timeside.encoder.ogg import * +from timeside.encoder.wav import * +from timeside.encoder.mp3 import * +from timeside.encoder.flac import * diff --git a/encode/core.py b/encoder/core.py similarity index 98% rename from encode/core.py rename to encoder/core.py index f7284b6..3053165 100644 --- a/encode/core.py +++ b/encoder/core.py @@ -20,8 +20,6 @@ # Author: Guillaume Pellerin - -from timeside.encode import * from timeside.core import * import subprocess diff --git a/encode/flac.py b/encoder/flac.py similarity index 98% rename from encode/flac.py rename to encoder/flac.py index 256c7a7..0753441 100644 --- a/encode/flac.py +++ b/encoder/flac.py @@ -23,7 +23,7 @@ import os import string import subprocess -from timeside.encode.core import * +from timeside.encoder.core import * from timeside.api import IEncoder from tempfile import NamedTemporaryFile @@ -41,7 +41,7 @@ class FlacEncoder(EncoderCore): @staticmethod def id(): return "flacenc" - + def format(self): return 'FLAC' @@ -121,7 +121,7 @@ class FlacEncoder(EncoderCore): command = 'flac %s - -o %s ' % (args, temp_file.name) stream = self.core_process(command, source) - + for __chunk in stream: #temp_file.write(__chunk) #temp_file.flush() diff --git a/encode/mp3.py b/encoder/mp3.py similarity index 99% rename from encode/mp3.py rename to encoder/mp3.py index a29d369..70f6dfb 100644 --- a/encode/mp3.py +++ b/encoder/mp3.py @@ -24,7 +24,7 @@ import os import string import subprocess -from timeside.encode.core import * +from timeside.encoder.core import * from timeside.api import IEncoder @@ -55,7 +55,7 @@ class Mp3Encoder(EncoderCore): @staticmethod def id(): return "mp3enc" - + def format(self): return 'MP3' diff --git a/encode/ogg.py b/encoder/ogg.py similarity index 99% rename from encode/ogg.py rename to encoder/ogg.py index c0855f9..566c529 100644 --- a/encode/ogg.py +++ b/encoder/ogg.py @@ -23,7 +23,7 @@ import os import string import subprocess -from timeside.encode.core import * +from timeside.encoder.core import * from timeside.api import IEncoder class OggVorbisEncoder(EncoderCore): diff --git a/encode/wav.py b/encoder/wav.py similarity index 98% rename from encode/wav.py rename to encoder/wav.py index 1903050..4a7a90c 100644 --- a/encode/wav.py +++ b/encoder/wav.py @@ -22,7 +22,7 @@ import os import string -from timeside.encode.core import * +from timeside.encoder.core import * from timeside.api import IEncoder class WavEncoder(EncoderCore): diff --git a/grapher/core.py b/grapher/core.py index f90d71e..0c3ff70 100644 --- a/grapher/core.py +++ b/grapher/core.py @@ -48,7 +48,8 @@ color_schemes = { } -class SpectralCentroid(object): +class Spectrum(object): + """ FFT based frequency analysis of audio frames.""" def __init__(self, fft_size, nframes, samplerate, lower, higher, window_function=numpy.ones): self.fft_size = fft_size @@ -64,6 +65,8 @@ class SpectralCentroid(object): self.spectrum_adapter = FixedSizeInputAdapter(self.fft_size, 1, pad=True) def process(self, frames, eod, spec_range=120.0): + """ Returns a tuple containing the spectral centroid and the spectrum (dB scales) of the input audio frames. + An adapter is used to fix the buffer length and then provide fixed FFT window sizes.""" for buffer, end in self.spectrum_adapter.process(frames, True): samples = buffer[:,0].copy() @@ -92,7 +95,7 @@ class SpectralCentroid(object): def interpolate_colors(colors, flat=False, num_colors=256): - """ given a list of colors, create a larger list of colors interpolating + """ Given a list of colors, create a larger list of colors interpolating the first one. If flatten is True a list of numers will be returned. If False, a list of (r,g,b) tuples. num_colors is the number of colors wanted in the final list """ @@ -122,9 +125,11 @@ def interpolate_colors(colors, flat=False, num_colors=256): class WaveformImage(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, fft_size, bg_color=None, color_scheme=None, filename=None): - self.image_width = image_width self.image_height = image_height self.nframes = nframes @@ -148,7 +153,7 @@ class WaveformImage(object): self.lower = 500 self.higher = 16000 - self.spectral_centroid = SpectralCentroid(self.fft_size, self.nframes, self.samplerate, self.lower, self.higher, numpy.hanning) + self.spectrum = Spectrum(self.fft_size, self.nframes, self.samplerate, self.lower, self.higher, numpy.hanning) self.image = Image.new("RGB", (self.image_width, self.image_height), self.bg_color) self.pixel = self.image.load() @@ -158,9 +163,9 @@ class WaveformImage(object): self.pixel_cursor = 0 def peaks(self, samples): - """ 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. """ + """ 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] @@ -232,7 +237,7 @@ class WaveformImage(object): buffer = frames[:,0].copy() buffer.shape = (len(frames),1) - (spectral_centroid, db_spectrum) = self.spectral_centroid.process(buffer, True) + (spectral_centroid, db_spectrum) = self.spectrum.process(buffer, True) for samples, end in self.pixels_adapter.process(buffer, eod): if self.pixel_cursor < self.image_width: peaks = self.peaks(samples) @@ -240,6 +245,7 @@ class WaveformImage(object): self.pixel_cursor += 1 def save(self): + """ Apply last 2D transforms and write all pixels to the file. """ a = 25 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])) @@ -247,6 +253,8 @@ class WaveformImage(object): 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.""" def __init__(self, image_width, image_height, nframes, samplerate, fft_size, bg_color=None, color_scheme='default', filename=None): self.image_width = image_width @@ -268,7 +276,7 @@ class SpectrogramImage(object): self.lower = 20 self.higher = 16000 - self.spectral_centroid = SpectralCentroid(self.fft_size, self.nframes, self.samplerate, self.lower, self.higher, numpy.hanning) + self.spectrum = Spectrum(self.fft_size, self.nframes, self.samplerate, self.lower, self.higher, numpy.hanning) # generate the lookup which translates y-coordinate to fft-bin self.y_to_bin = [] @@ -309,11 +317,12 @@ class SpectrogramImage(object): # FIXME : breaks spectrum linearity for samples, end in self.pixels_adapter.process(buffer, eod): if self.pixel_cursor < self.image_width: - (spectral_centroid, db_spectrum) = self.spectral_centroid.process(samples, False) + (spectral_centroid, db_spectrum) = self.spectrum.process(samples, False) self.draw_spectrum(self.pixel_cursor, db_spectrum) self.pixel_cursor += 1 def save(self): + """ Apply last 2D transforms and write all pixels to the file. """ self.image.putdata(self.pixels) self.image.transpose(Image.ROTATE_90).save(self.filename) @@ -322,6 +331,7 @@ class Noise(object): """A class that mimics audiolab.sndfile but generates noise instead of reading a wave file. Additionally it can be told to have a "broken" header and thus crashing in the middle of the file. Also useful for testing ultra-short files of 20 samples.""" + def __init__(self, num_frames, has_broken_header=False): self.seekpoint = 0 self.num_frames = num_frames diff --git a/tests/test.py b/tests/test.py index 6e762ed..c2b007b 100755 --- a/tests/test.py +++ b/tests/test.py @@ -12,13 +12,7 @@ class TestAnalyzers: def list(self): analyzers = [] - for analyzer_class in self.analyzers: - # FIXME: should access the name, id and unit member statically - # there should be no need to instantiate analyzer_class - # eg: access directly analyzer_class.name(), etc... - # - # This remark is true at many places in this file - analyzer = analyzer_class() + for analyzer in self.analyzers: analyzers.append({'name':analyzer.name(), 'id':analyzer.id(), 'unit':analyzer.unit(), @@ -27,8 +21,7 @@ class TestAnalyzers: def run(self, media): print '\n=== Analyzer testing ===\n' - for analyzer_class in self.analyzers: - analyzer = analyzer_class() + for analyzer in self.analyzers: id = analyzer.id() value = analyzer.render(media) print id + ' = ' + str(value) + ' ' + analyzer.unit() @@ -39,8 +32,7 @@ class TestDecoders: def list(self): decoders_list = [] - for decoder_class in self.decoders: - decoder = decoder_class() + for decoder in self.decoders: decoders_list.append({'format': decoder.format(), 'mime_type': decoder.mime_type(), 'file_extension': decoder.file_extension(), @@ -48,8 +40,7 @@ class TestDecoders: print decoders_list def get_decoder(self, mime_type): - for decoder_class in self.decoders: - decoder = decoder_class() + for decoder in self.decoders: if decoder.mime_type() == mime_type: return decoder @@ -76,24 +67,21 @@ class TestEncoders: def list(self): encoders = [] - for encoder_class in self.encoders: - encoder = encoder_class() + for encoder in self.encoders: encoders.append({'format': encoder.format(), 'mime_type': encoder.mime_type(), }) print encoders def get_encoder(self, mime_type): - for encoder_class in self.encoders: - encoder = encoder_class() + for encoder in self.encoders: if encoder.mime_type() == mime_type: return encoder def run(self, source, metadata): print '\n=== Encoder testing ===\n' - for encoder_class in self.encoders: + for encoder in self.encoders: print '==================================' - encoder = encoder_class() mime = mimetype(source) format = encoder.format() decoders = TestDecoders() @@ -115,8 +103,7 @@ class TestGraphers: def list(self): graphers = [] - for grapher_class in self.graphers: - grapher = grapher_class() + for grapher in self.graphers: graphers.append({'id':grapher.id(), 'name':grapher.name(), }) @@ -124,8 +111,7 @@ class TestGraphers: def run(self, media): print '\n=== Grapher testing ===\n' - for grapher_class in self.graphers: - grapher = grapher_class() + for grapher in self.graphers: id = grapher.id() image = grapher.render(media) file_path = 'results/'+id+'.png' @@ -135,6 +121,7 @@ class TestGraphers: print 'Image exported to :' + file_path file.close() + def mimetype(path): if hasattr(magic, "Magic"): if not hasattr(mimetype, "magic"): @@ -155,13 +142,13 @@ if __name__ == '__main__': a = TestAnalyzers() d = TestDecoders() e = TestEncoders() - #g = TestGraphers() + g = TestGraphers() a.list() d.list() e.list() - #g.list() + g.list() a.run(sample) - #g.run(sample) + g.run(sample) e.run(sample, metadata) d.export('samples/') -- 2.39.5