]> git.parisson.com Git - timeside.git/commitdiff
- add processing pipes support, using ProcessPipe and operator overloading
authorOlivier Guilyardi <olivier@samalyse.com>
Wed, 2 Dec 2009 14:38:11 +0000 (14:38 +0000)
committerOlivier Guilyardi <olivier@samalyse.com>
Wed, 2 Dec 2009 14:38:11 +0000 (14:38 +0000)
- add tests/api/test_pipe.py and rename tests/api/test.py to test_lolevel.py
- remove all buffersize constraints in core/api
- replace IProcessor.set_input_format() with setup(), which also resets the processor
- suppress IProcessor.input_format()
- replace IProcessor.output_format() with channels() and samplerate()
- remove IGrapher.set_nframes(), this is implementation specific
- add short guitar.wav (sweep samples drives my crazy ;)
- shorten example processor names (GainEffect -> Gain, etc...)

api.py
core.py
tests/api/examples.py
tests/api/test.py [deleted file]
tests/api/test_lolevel.py [new file with mode: 0644]
tests/api/test_pipe.py [new file with mode: 0644]
tests/samples/guitar.wav [new file with mode: 0644]

diff --git a/api.py b/api.py
index 28419c5ba0cfb4de6b0a2540ff9e63054bdea15b..ae9c1b05197c6481633f086227361e962aaa0b68 100644 (file)
--- a/api.py
+++ b/api.py
@@ -34,40 +34,29 @@ class IProcessor(Interface):
         # be raised by MetaProcessor if the id is malformed or not unique amongst
         # registered processors.
 
-    def buffersize(self):
-        """Buffersize this processor operates on, that is; the number of frames 
-        expected/returned by process()."""
-
-    def set_buffersize(self, value):        
-        """Set the buffer size used by this processor."""
-
-    def set_input_format(self, nchannels, samplerate):
-        """Set the format of input data passed to process(). It is required to call
-        this method before calling process(), except for output-only processors."""
-
-    def input_format(self):
-        """Return a tuple of the form (nchannels, samplerate) indicating the
-        format of the data expected by process(), with the same values as the 
-        nchannels and samplerate arguments passed to the constructor."""
-    
-    def output_format(self):
-        """Return a tuple of the form (nchannels, samplerate) indicating the
-        format of the data returned by process(). These may differ from the values
-        passed to the constructor (ie: stereo-to-mono effect, samplerate converter, 
-        etc...)"""
-
-    def process(self, frames=None):
-        """Process buffersize input frames and return buffersize output frames, both 
-        as numpy arrays, where columns are channels. An input/output of less than 
-        buffersize frames (or None) means that the end-of-data has been reached (the
-        caller must ensure that this happens).
+    def setup(self, channels=None, samplerate=None):
+        """Setup this processor to handle incoming data with nchannels at samplerate.
+        Also reset the processor internal state so that it can be reused."""
+
+    def channels(self):
+        """Number of channels in the data returned by process(). May be different from
+        the number of channels passed to setup()"""
+
+    def samplerate(self):
+        """Samplerate of the data returned by process(). May be different from
+        the samplerate passed to setup()"""
+
+    def process(self, frames=None, eod=False):
+        """Process input frames and return a (output_frames, eod) tuple.
+        Both input and output frames are numpy arrays, where columns are 
+        channels, and containing an undetermined number of frames.  eod=True 
+        means that the end-of-data has been reached.
         
         Output-only processors (such as decoders) will raise an exception if the
         frames argument is not None. All processors (even encoders) return data,
         even if that means returning the input unchanged.
         
-        Warning: it is required to call set_input_format() before this method
-        for processors which accept input."""
+        Warning: it is required to call setup() before this method."""
 
 class IEncoder(IProcessor):
     """Encoder driver interface. Each encoder is expected to support a specific
@@ -148,17 +137,16 @@ class IGrapher(IProcessor):
 
     def __init__(self, width, height):
         """Create a new grapher. width and height are generally
-        in pixels but could be something else for eg. svg rendering, etc.."""
-        # implementation: additional optionnal arguments are allowed 
+        in pixels but could be something else for eg. svg rendering, etc.. """
+
+        # implementation: additional optionnal arguments (such as the total number 
+        # of frames, etc...) are allowed 
 
     @staticmethod
     def name():
         """Return the graph name, such as "Waveform", "Spectral view",
         etc..  """
 
-    def set_nframes(self, nframes):
-        """Duration in frames of the input data. Must be called before process()."""
-
     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"""
diff --git a/core.py b/core.py
index 95c1f61fc6dc63fc5eca14de7e739b5f840ec420..57f4405efec2906ae822580b340bdcfd56247ac6 100644 (file)
--- a/core.py
+++ b/core.py
@@ -23,7 +23,7 @@ from timeside.exceptions import Error, ApiError
 import re
 
 __all__ = ['Processor', 'MetaProcessor', 'implements', 'abstract', 
-           'interfacedoc', 'processors', 'get_processor']
+           'interfacedoc', 'processors', 'get_processor', 'ProcessPipe']
 
 _processors = {}
 
@@ -55,48 +55,30 @@ class Processor(Component):
     abstract()
     implements(IProcessor)
 
-    DEFAULT_BUFFERSIZE = 0x10000
-    MIN_BUFFERSIZE = 0x1000
-
-    __buffersize = DEFAULT_BUFFERSIZE
-
-    @interfacedoc
-    def buffersize(self):
-        return self.__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
-
     @interfacedoc
-    def set_input_format(self, nchannels=None, samplerate=None):
-        self.input_channels   = nchannels
+    def setup(self, channels=None, samplerate=None):
+        self.input_channels   = channels
         self.input_samplerate = samplerate
 
     @interfacedoc
-    def input_format(self):
-        return (self.input_channels, self.input_samplerate)
+    def channels(self):
+        # default implementation returns the input channels, but processors may change
+        # this behaviour by overloading this method
+        return self.input_channels
 
     @interfacedoc
-    def output_format(self):        
-        return (self.input_channels, self.input_samplerate)
+    def samplerate(self):
+        # default implementation returns the input samplerate, but processors may change
+        # this behaviour by overloading this method
+        return self.input_samplerate
 
     @interfacedoc
     def process(self, frames):
         return frames
 
+    def __or__(self, item):
+        return ProcessPipe(self, item)
+
 def processors(interface=IProcessor, recurse=True):
     """Returns the processors implementing a given interface and, if recurse,
     any of the descendants of this interface."""
@@ -111,3 +93,36 @@ def get_processor(processor_id):
 
     return _processors[processor_id]
 
+class ProcessPipe(object):
+    """Handle a pipe of processors"""
+
+    def __init__(self, *processors):
+        self.processors = processors
+
+    def __or__(self, processor):
+        p = []
+        p.extend(self.processors)
+        p.append(processor)
+        return ProcessPipe(*p)
+
+    def run(self):
+        """Setup/reset all processors in cascade and stream audio data along
+        the pipe"""
+
+        source = self.processors[0]
+        items  = self.processors[1:]
+
+        # setup/reset processors and configure channels and samplerate throughout the pipe
+        source.setup()
+        last = source
+        for item in items:
+            item.setup(last.channels(), last.samplerate())
+            last = item
+
+        # now stream audio data along the pipe
+        eod = False
+        while not eod:
+            frames, eod = source.process()
+            for item in items:
+                frames, eod = item.process(frames, eod)
+
index efaaedfeb49ab006d8fd5fb1421346cef230f9e1..5502bf62748a237068a2606377a124b1b65b15c2 100644 (file)
@@ -4,7 +4,7 @@ from timeside import Metadata
 from scikits import audiolab
 import numpy
 
-class AudiolabDecoder(Processor):
+class FileDecoder(Processor):
     """A simple audiolab-based example decoder"""
     implements(IDecoder)
 
@@ -15,12 +15,24 @@ class AudiolabDecoder(Processor):
 
     @interfacedoc
     def __init__(self, filename):
-        self.file = audiolab.sndfile(filename, 'read')
+        self.filename = filename
+        self.file     = None
+
+    @interfacedoc
+    def setup(self, channels=None, samplerate=None):
+        Processor.setup(self, channels, samplerate)
+        if self.file:
+            self.file.close();
+        self.file = audiolab.sndfile(self.filename, 'read')
         self.position = 0
 
     @interfacedoc
-    def output_format(self):
-        return (self.file.get_channels(), self.file.get_samplerate())
+    def channels(self):
+        return self.file.get_channels()
+        
+    @interfacedoc    
+    def samplerate(self):        
+        return self.file.get_samplerate()
 
     @interfacedoc
     def duration(self):
@@ -58,11 +70,11 @@ class AudiolabDecoder(Processor):
         return Metadata()
 
     @interfacedoc
-    def process(self, frames=None):
+    def process(self, frames=None, eod=False):
         if frames:
             raise Exception("Decoder doesn't accept input frames")
 
-        buffersize = self.buffersize()
+        buffersize = 0x10000
 
         # Need this because audiolab raises a bogus exception when asked
         # to read passed the end of the file
@@ -74,16 +86,20 @@ class AudiolabDecoder(Processor):
 
         self.position += toread
 
+        eod = False
         if toread < buffersize:
             self.file.close()
+            self.file = None
+            eod = True
 
-        return frames
+        return frames, eod
 
-class MaxLevelAnalyzer(Processor):
+class MaxLevel(Processor):
     implements(IValueAnalyzer)
 
     @interfacedoc
-    def __init__(self):
+    def setup(self, channels=None, samplerate=None):
+        Processor.setup(self, channels, samplerate)
         self.max_value = 0
 
     @staticmethod
@@ -102,17 +118,17 @@ class MaxLevelAnalyzer(Processor):
         # power? amplitude?
         return ""
 
-    def process(self, frames=None):        
+    def process(self, frames, eod=False):        
         max = frames.max()
         if max > self.max_value:
             self.max_value = max
 
-        return frames
+        return frames, eod
 
     def result(self):
         return self.max_value
 
-class GainEffect(Processor):
+class Gain(Processor):
     implements(IEffect)
 
     @interfacedoc
@@ -129,8 +145,8 @@ class GainEffect(Processor):
     def name():
         return "Gain test effect"
    
-    def process(self, frames=None):        
-        return numpy.multiply(frames, self.gain)
+    def process(self, frames, eod=False):        
+        return numpy.multiply(frames, self.gain), eod
 
 class WavEncoder(Processor):
     implements(IEncoder)
@@ -142,6 +158,16 @@ class WavEncoder(Processor):
         else:
             raise Exception("Streaming not supported")
     
+    @interfacedoc
+    def setup(self, channels=None, samplerate=None):
+        Processor.setup(self, channels, samplerate)
+        if self.file:
+            self.file.close();
+
+        info = audiolab.formatinfo("wav", "pcm16")
+        self.file = audiolab.sndfile(self.filename, "write", format=info, channels=channels,
+                                     samplerate=samplerate)
+
     @staticmethod
     @interfacedoc
     def id():
@@ -168,15 +194,10 @@ class WavEncoder(Processor):
         pass
 
     @interfacedoc
-    def process(self, frames):
-        if not self.file:
-            # Can't open the file in constructor because input_channels and input_samplerate
-            # aren't available before set_input_format() has been called
-            info = audiolab.formatinfo("wav", "pcm16")
-            self.file = audiolab.sndfile(self.filename, "write", format=info, channels=self.input_channels,
-                                         samplerate=self.input_samplerate)
+    def process(self, frames, eod=False):
         self.file.write_frames(frames)
-        if len(frames) < self.buffersize():
+        if eod:
             self.file.close()
+            self.file = None
 
-        return frames            
+        return frames, eod
diff --git a/tests/api/test.py b/tests/api/test.py
deleted file mode 100644 (file)
index e629a8a..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-from timeside.tests.api import examples
-import os
-
-source=os.path.dirname(__file__) + "../samples/sweep_source.wav"
-
-Decoder = examples.AudiolabDecoder
-print "Creating decoder with id=%s for: %s" % (Decoder.id(), source)
-decoder = Decoder(source)
-nchannels, samplerate = decoder.output_format()
-print "Stats: duration=%f, nframes=%d, nchannels=%d, samplerate=%d, resolution=%d" % (
-        decoder.duration(), decoder.nframes(), nchannels, samplerate, decoder.resolution())
-
-analyzer = examples.MaxLevelAnalyzer()
-analyzer.set_input_format(nchannels, samplerate)
-
-while True:
-    frames = decoder.process()
-    analyzer.process(frames)
-    if len(frames) < decoder.buffersize():
-        break
-
-max_level = analyzer.result()
-print "Max level: %f" % max_level
-
-destination = "normalized.wav"
-Encoder = examples.WavEncoder
-print "Creating encoder with id=%s for: %s" % (Encoder.id(), destination)
-encoder = Encoder(destination)
-decoder = Decoder(source)
-
-nchannels, samplerate = decoder.output_format()
-encoder.set_input_format(nchannels, samplerate)
-
-gain = 1
-if max_level > 0:
-    gain = 0.9 / max_level
-
-effect = examples.GainEffect(gain)
-
-print "Applying effect id=%s with gain=%f" % (effect.id(), gain)
-
-while True:
-    frames = decoder.process()
-    encoder.process(effect.process(frames))
-    if len(frames) < decoder.buffersize():
-        break
-
diff --git a/tests/api/test_lolevel.py b/tests/api/test_lolevel.py
new file mode 100644 (file)
index 0000000..9c341e3
--- /dev/null
@@ -0,0 +1,49 @@
+from timeside.tests.api import examples
+import os
+
+source=os.path.dirname(__file__) + "../samples/guitar.wav"
+
+Decoder = examples.AudiolabDecoder
+print "Creating decoder with id=%s for: %s" % (Decoder.id(), source)
+decoder    = Decoder(source)
+analyzer = examples.MaxLevelAnalyzer()
+decoder.setup()
+nchannels  = decoder.channels()
+samplerate = decoder.samplerate()
+analyzer.setup(nchannels, samplerate)
+
+print "Stats: duration=%f, nframes=%d, nchannels=%d, samplerate=%d, resolution=%d" % (
+        decoder.duration(), decoder.nframes(), nchannels, samplerate, decoder.resolution())
+
+while True:
+    frames, eod = decoder.process()
+    analyzer.process(frames, eod)
+    if eod:
+        break
+
+max_level = analyzer.result()
+print "Max level: %f" % max_level
+
+destination = "normalized.wav"
+Encoder = examples.WavEncoder
+print "Creating encoder with id=%s for: %s" % (Encoder.id(), destination)
+encoder = Encoder(destination)
+
+gain = 1
+if max_level > 0:
+    gain = 0.9 / max_level
+
+effect = examples.GainEffect(gain)
+
+decoder.setup()
+effect.setup(decoder.channels(), decoder.samplerate())
+encoder.setup(effect.channels(), effect.samplerate())
+
+print "Applying effect id=%s with gain=%f" % (effect.id(), gain)
+
+while True:
+    frames, eod = decoder.process()
+    encoder.process(*effect.process(frames, eod))
+    if eod:
+        break
+
diff --git a/tests/api/test_pipe.py b/tests/api/test_pipe.py
new file mode 100644 (file)
index 0000000..d139840
--- /dev/null
@@ -0,0 +1,24 @@
+from timeside.tests.api import examples
+from timeside.core import *
+
+import os
+source=os.path.dirname(__file__) + "../samples/guitar.wav"
+
+decoder  = examples.FileDecoder(source)
+maxlevel = examples.MaxLevel()
+
+(decoder | maxlevel).run()
+
+gain = 1
+if maxlevel.result() > 0:
+    gain = 0.9 / maxlevel.result()
+
+print "input maxlevel: %f" % maxlevel.result()
+print "gain: %f" % gain
+
+gain     = examples.Gain(gain)
+encoder  = examples.WavEncoder("normalized.wav")
+
+(decoder | gain | maxlevel | encoder).run()
+
+print "output maxlevel: %f" % maxlevel.result()
diff --git a/tests/samples/guitar.wav b/tests/samples/guitar.wav
new file mode 100644 (file)
index 0000000..b5a9e80
Binary files /dev/null and b/tests/samples/guitar.wav differ