From: Olivier Guilyardi Date: Fri, 27 Nov 2009 19:51:36 +0000 (+0000) Subject: core: add processor buffersize (default 65536), required to be a power of 2 and ... X-Git-Tag: 0.3.2~216 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=a2a69f388ebece93b8c226f3987828f87c5ce066;p=timeside.git core: add processor buffersize (default 65536), required to be a power of 2 and >= 4096 for proper support of fft-based analyzers and similar refactor api: - improve documentation - remove IEncoder.finish(), same as process() with nframes < buffersize - remove format(), description() and such from IDecoder, a decoder can handle multiple formats so this information is dynamic - add duration(), samplerate(), channels() etc... to IDecoder, obsoleting all corresponding analyzers - add IGrapher.process() - require IGrapher.render() to return a PIL image instead of streaming binary data, and move the width/height arguments into the constructor - add IAnalyzer.process() - add IValueAnalyzer and let dc, maxlevel and meanlevel analyzers implement it - move result() into IValueAnalyzer. This method may not be adequate for other types of analyzers --- diff --git a/analyze/dc.py b/analyze/dc.py index 43cfdb5..ac31e6f 100644 --- a/analyze/dc.py +++ b/analyze/dc.py @@ -20,13 +20,13 @@ # Author: Guillaume Pellerin from timeside.analyze.core import * -from timeside.api import IAnalyzer +from timeside.api import IValueAnalyzer import numpy class MeanDCShiftAnalyser(AudioProcessor): """Media item analyzer driver interface""" - implements(IAnalyzer) + implements(IValueAnalyzer) @staticmethod def id(): diff --git a/analyze/max_level.py b/analyze/max_level.py index fa173fb..c40ae4d 100644 --- a/analyze/max_level.py +++ b/analyze/max_level.py @@ -20,13 +20,13 @@ # Author: Guillaume Pellerin from timeside.analyze.core import * -from timeside.api import IAnalyzer +from timeside.api import IValueAnalyzer import numpy class MaxLevelAnalyzer(AudioProcessor): """Media item analyzer driver interface""" - implements(IAnalyzer) + implements(IValueAnalyzer) @staticmethod def id(): diff --git a/analyze/mean_level.py b/analyze/mean_level.py index 7f18f37..0ab38ff 100644 --- a/analyze/mean_level.py +++ b/analyze/mean_level.py @@ -20,13 +20,13 @@ # Author: Guillaume Pellerin from timeside.analyze.core import * -from timeside.api import IAnalyzer +from timeside.api import IValueAnalyzer import numpy class MeanLevelAnalyser(AudioProcessor): """Media item analyzer driver interface""" - implements(IAnalyzer) + implements(IValueAnalyzer) @staticmethod def id(): diff --git a/analyze/vamp/core.py b/analyze/vamp/core.py index fb0a3f4..6248508 100644 --- a/analyze/vamp/core.py +++ b/analyze/vamp/core.py @@ -21,7 +21,6 @@ from timeside.core import * from tempfile import NamedTemporaryFile -from timeside.api import IAnalyzer from timeside.exceptions import SubProcessError import os import random diff --git a/api.py b/api.py index 344871e..7674679 100644 --- a/api.py +++ b/api.py @@ -35,7 +35,8 @@ class IProcessor(Interface): and be passed as a GET parameter. Thus it should be as short as possible.""" class IEncoder(IProcessor): - """Encoder driver interface""" + """Encoder driver interface. Each encoder is expected to support a specific + format.""" def __init__(self, output, nchannels, samplerate): """The constructor must always accept the output, nchannels and samplerate @@ -79,83 +80,105 @@ class IEncoder(IProcessor): mode.""" def process(self, frames): - """Encode the frames passed as a numpy array, where columns are channels. + """Encode buffersize frames passed as a numpy array, where columns are channels. + A number of frames less than buffersize means that the end of the data + has been reached, and that the encoder should close the output file, stream, + etc... + In streaming mode the callback passed to the constructor is called whenever a block of encoded data is ready.""" - def finish(self): - """Flush the encoded data and close the output file/stream. Calling this method - may cause the streaming callback to be called if in streaming mode.""" - - class IDecoder(IProcessor): - """Decoder driver interface""" + """Decoder driver interface. Decoders are different of encoders in that + a given driver may support several input formats, hence this interface doesn't + export any static method, all informations are dynamic.""" - @staticmethod - def format(): - """Return the decode/encoding format as a short string - Example: "MP3", "OGG", "AVI", ... - """ - - @staticmethod - def description(): - """Return a string describing what this decode format provides, is good - for, etc... The description is meant to help the end user decide what - format is good for him/her - """ + def __init__(self, filename): + """Create a new decoder for filename. Implementations of this interface + may accept optionnal arguments after filename.""" - @staticmethod - def file_extension(): - """Return the filename extension corresponding to this decode format""" + def channels(): + """Return the number of channels""" - @staticmethod - def mime_type(): - """Return the mime type corresponding to this decode format""" + def samplerate(): + """Return the samplerate""" - def process(self, source, options=None): - """Perform the decoding process and stream the result through a generator + def duration(): + """Return the duration in seconds""" - source is the audio/video source file absolute path. + def format(): + """Return a user-friendly file format string""" + + def encoding(): + """Return a user-friendly encoding string""" - It is highly recommended that decode drivers implement some sort of - cache instead of re-encoding each time process() is called. + def resolution(): + """Return the sample depth""" - It should be possible to make subsequent calls to process() with - different items, using the same driver instance. - """ + def process(self): + """Return a generator over the decoded data, as numpy arrays, where columns are + channels, each array containing buffersize frames or less if the end of file + has been reached.""" class IGrapher(IProcessor): """Media item visualizer driver interface""" + def __init__(self, width, height): + """Create a new grapher. Implementations of this interface + may accept optionnal arguments. width and height are generally + in pixels but could be something else for eg. svg rendering, etc..""" + @staticmethod def name(): """Return the graph name, such as "Waveform", "Spectral view", - etc.. - """ + etc.. """ def set_colors(self, background=None, scheme=None): """Set the colors used for image generation. background is a RGB tuple, and scheme a a predefined color theme name""" pass - def render(self, media_item, width=None, height=None, options=None): - """Generator that streams the graph output as a PNG image""" + def process(self, frames): + """Process a block of buffersize frames passed as a numpy array, where + columns are channels. Passing less than buffersize frames means that + the end of data has been reached""" + + def render(self): + """Return a PIL Image object visually representing all of the data passed + by repeatedly calling process()""" class IAnalyzer(IProcessor): - """Media item analyzer driver interface""" + """Media item analyzer driver interface. This interface is abstract, it doesn't + describe a particular type of analyzer but is rather meant to group analyzers. + In particular, the way the result is returned may greatly vary from sub-interface + to sub-interface. For example the IValueAnalyzer returns a final single numeric + result at the end of the whole analysis. But some other analyzers may return + numpy arrays, and this, either at the end of the analysis, or from process() + for each block of data (as in Vamp).""" + + def __init__(self): + """Create a new analyzer. Implementations of this interface + may accept optionnal arguments.""" @staticmethod def name(): """Return the analyzer name, such as "Mean Level", "Max level", - "Total length, etc.. - """ + "Total length, etc.. """ @staticmethod def unit(): - """Return the unit of the data such as "dB", "seconds", etc... - """ + """Return the unit of the data such as "dB", "seconds", etc... """ + + def process(self, frames): + """Process a block of buffersize frames passed as a numpy array, where + columns are channels. Passing less than buffersize frames means that + the end of data has been reached""" + +class IValueAnalyzer(IAnalyzer): + """Interface for analyzers which return a single numeric value from result()""" - def render(self, media, options=None): - """Return the result data of the process""" + def result(): + """Return the final result of the analysis performed over the data passed by + repeatedly calling process()""" diff --git a/core.py b/core.py index f2c87b7..c7a26c9 100644 --- a/core.py +++ b/core.py @@ -52,6 +52,32 @@ class Processor(Component): """Base component class of all processors""" __metaclass__ = MetaProcessor + DEFAULT_BUFFERSIZE = 0x10000 + MIN_BUFFERSIZE = 0x1000 + + __buffersize = DEFAULT_BUFFERSIZE + + def buffersize(self): + """Get the current buffer size""" + return __buffersize + + def set_buffersize(self, value): + """Set the buffer size used by this processor. The buffersize must be a + power of 2 and greater than or equal to MIN_BUFFERSIZE or an exception will + be raised.""" + if value < self.MIN_BUFFERSIZE: + raise Error("Invalid buffer size: %d. Must be greater than or equal to %d", + value, MIN_BUFFERSIZE); + v = value + while v > 1: + if v & 1: + raise Error("Invalid buffer size: %d. Must be a power of two", + value, MIN_BUFFERSIZE); + v >>= 1 + + self.__buffersize = value + + def processors(interface=IProcessor, recurse=True): """Returns the processors implementing a given interface and, if recurse, any of the descendants of this interface.""" diff --git a/graph/spectrogram_audiolab.py b/graph/spectrogram_audiolab.py index 709d8d3..59c9c1d 100644 --- a/graph/spectrogram_audiolab.py +++ b/graph/spectrogram_audiolab.py @@ -34,7 +34,6 @@ class SpectrogramGrapherAudiolab(Processor): @staticmethod def id(): - #FIXME: too long + underscore discouraged return "spectrogram" def name(self): diff --git a/graph/waveform_audiolab.py b/graph/waveform_audiolab.py index b11888c..513ec97 100644 --- a/graph/waveform_audiolab.py +++ b/graph/waveform_audiolab.py @@ -34,7 +34,6 @@ class WaveFormGrapherAudiolab(Processor): @staticmethod def id(): - #FIXME: too long + underscore discouraged return "waveform" def name(self):