# 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
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"""
import re
__all__ = ['Processor', 'MetaProcessor', 'implements', 'abstract',
- 'interfacedoc', 'processors', 'get_processor']
+ 'interfacedoc', 'processors', 'get_processor', 'ProcessPipe']
_processors = {}
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."""
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)
+
from scikits import audiolab
import numpy
-class AudiolabDecoder(Processor):
+class FileDecoder(Processor):
"""A simple audiolab-based example decoder"""
implements(IDecoder)
@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):
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
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
# 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
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)
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():
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
+++ /dev/null
-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
-
--- /dev/null
+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
+
--- /dev/null
+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()