#summary This is a little tutorial on how to create your own object for sound processing.

= Introduction =

For this example, we will be creating a Vocoder effect. Beginning with a very simple class, we will gradually add features to create a more sophisticated class. The resulting class will act almost like a real pyo object.

First of all, let's create a file called "vocoder_lib.py" where we will put the classes to import in the main script.

Files "vocoder_lib.py" and "vocoder_main.py" can be found in the examples folder from the pyo's sources.

= Simple Vocoder =

There is one thing to remember to avoid weird behaviour from our class: *All pyo objects must live over time in order to be able to compute audio samples*. It's a good habit to always create pyo objects with the prefix "self" to avoid them being destroyed at the end of the __init__ method.

Start by importing all modules you will need in your class (at least pyo):

{{{
import math
from pyo import *
}}}

And here is the class !SimpleVocoder:

{{{
class SimpleVocoder:
    def __init__(self, in1, in2, num=32, base=50, spread=1.5, q=5):
        self._in1 = in1
        self._in2 = in2
        self._num = num
        self._base = base
        self._spread = spread
        self._q = q
        self._freqs = Sig([self._base * math.pow(i+1, self._spread) for i in range(self._num)])
        self._clipped_freqs = Clip(self._freqs, 20, 20000)
    
        self._src = Biquadx(self._in1, freq=self._clipped_freqs, q=self._q, type=2, stages=4)
        self._envelope = Follower(self._src, freq=5, mul=self._q*30)
        self._exc = Biquadx(self._in2, freq=self._clipped_freqs, q=self._q, type=2, stages=4, mul=self._envelope).out()
}}}

As you can see, it's a very simple class, but ready to be used! 

Let's review what happened here. First, we keep references to pyo objects given at input for later use. Then, we compute the filter's frequencies and pass the list to a simple Sig() object. We then use that Sig() object to convert floats to audio signals to be able to Clip() the filter's frequencies (filters are unstable beyond Nyquist's frequency). Once this is done, we can create the vocoder effect by applying an envelope follower on each band of the first signal, and using these envelopes as the amplitudes of the filters on the second signal. 

Now, we can use this class in our main script (remember to place the main script in the same folder as the vocoder_lib.py file):

{{{
from pyo import *
from vocoder_lib import SimpleVocoder                 
s = Server(sr=44100, nchnls=2, buffersize=1024, duplex=0).boot()
a = SfPlayer(SNDS_PATH + "/transparent.aif", loop=True, mul=3).play()
b = Noise()
voc = SimpleVocoder(in1=a, in2=b, num=32, base=50, spread=1.2, q=5)   
s.gui(locals())
}}}

It works, but it's useless because it's very hard to change anything in the processing.

Let's add some methods to control the Vocoder:

{{{
    def setBase(self, x):
        self._base = x
        self._freqs.value = [self._base * math.pow(i+1, self._spread) for i in range(self._num)]

    def setSpread(self, x):
        self._spread = x
        self._freqs.value = [self._base * math.pow(i+1, self._spread) for i in range(self._num)]

    def setQ(self, x):
        self._q = x
        self._envelope.mul = self._q * 30
        self._src.q = self._exc.q = self._q
}}}

Now, while it's playing, we can give calls like this to the interpreter:

{{{
voc.setBase(60)
voc.setSpread(1.5)
voc.setQ(10)
}}}

We can also use attributes to reduce the live typing by adding these lines to our class:

{{{
    @property
    def base(self): return self._base
    @base.setter
    def base(self, x): self.setBase(x)

    @property
    def spread(self): return self._spread
    @spread.setter
    def spread(self, x): self.setSpread(x)

    @property
    def q(self): return self._q
    @q.setter
    def q(self, x): self.setQ(x)
}}}

Don't forget to give "object" as the parent class to !SimpleVocoder:

{{{
class SimpleVocoder(object):
}}} 

Using attributes, we can then replace our calls to the interpreter with these ones:

{{{
voc.base = 60
voc.spread = 1.5
voc.q = 10
}}}

That's it! We now have a vocoder ready to be used in our programs.

= Vocoder (with more pyo functionality) = 

The !SimpleVocoder is useful if it's the last element in a processing chain. We can't use it like a normal pyo object. For example, if we want to pass it's resulting sound to a reverb object, we need to modify the class itself to add a reverb unit. It would be very useful to be able to pass it to other objects like any objects in the library. There is a few steps we need to take in order to create a class with all of pyo's functionality. That's what we shall do now.

*Things to consider*:

  * The parent class must be !PyoObject
  * When a !PyoObject receives another !PyoObject, it looks for a list of objects called "`self._base_objs`"
  * Adding "mul" and "add" arguments (they change objects in `self._base_objs`)
  * All !PyoObject support "list expansion"
  * All !PyoObject with sound in input support crossfading between old and new sources
  * We will probably want to override the .play(), .out() and .stop() methods
  * There is an attribute for every function modifying a parameter
  * The `__dir__` method should return a list of the available attributes as strings
  * We can define a .ctrl() method to popup a GUI to control parameters

== Declaring the class ==

We will create a new class called Vocoder with !PyoObject as it's parent class. Another good habit is to put a `__doc__` string at the beginning of our classes. Doing so will allow other user to retrieve documentation for the object with the standard python help() function.

{{{
class Vocoder(PyoObject):
    """
    Vocoder effect.

    A vocoder is an analysis/synthesis system. In the encoder, the input is passed 
    through a multi-band filter, each band is passed through an envelope follower, 
    and the control signals from the envelope followers are communicated to the 
    decoder. The decoder applies these (amplitude) control signals to corresponding 
    filters in the (re)synthesizer.
    
    
    Parent class: PyoObject

    Parameters:

    in1 : PyoObject
        Audio source generating the spectral envelope.
    in2 : PyoObject
        Audio source exciting the bank of filters.
    base : float or PyoObject, optional
        Base frequency used to compute filter notch frequencies. 
        Defaults to 50.
    spread : float or PyoObject, optional
        Spreading of the filter notch frequencies. Defaults to 1.5.
    q : float or PyoObject, optional
        Q (inverse of the bandwidth) of the filters. Defaults to 5.
    num : int, optional
        Number of bands (filter notches) of the vocoder. Available only
        at initialization. Defaults to 20.
        
    Methods:

    setIn1(x) : Replace the `in1` attribute.
    setIn2(x) : Replace the `in2` attribute.
    setBase(x) : Replace the `base` attribute.
    setSpread(x) : Replace the `spread` attribute.
    setQ(x) : Replace the `q` attribute.
    
    Attributes:

    in1 : PyoObject. Audio source generating the spectral envelope.
    in2 : PyoObject. Audio source exciting the bank of filters.
    base : float or PyoObject, Base frequency.
    spread : float or PyoObject, Spreading of the filter notch frequencies.
    q : float or PyoObject, Q of the filters.

    See also: BandSplit, Phaser

    Examples:

    >>> s = Server().boot()
    >>> s.start()
    >>> a = SfPlayer(SNDS_PATH + "/transparent.aif", loop=True)
    >>> b = Noise()
    >>> lfo = Sine(freq=.05, mul=50, add=100)
    >>> voc = Vocoder(in1=a, in2=b, num=20, base=lfo, spread=[1.2,1.22]).out()

    """
}}}

== The `__init__` method ==

This is the place where we have to take care of some pyo's generic behaviours. The most important thing to remember is when a !PyoObject receives another !PyoObject in input, it looks for an attributes called `self._base_objs`, which is a list of object's base classes (The Sine object uses internally an object called Sine_base) considered as the audio output signal of the object. The getBaseObjects() method returns the list of base classes from a !PyoObject. We will called this method on the object generating the output signal of our process. 

We also need to add two arguments to the definition of the object: "mul" and "add". The attributes "`self._mul`" and "`self._add`" are handled by the parent class and automatically applied on the objects in "`self._base_objs`".`

Finally, we have to consider the "list expansion" feature, allowing lists in argument to make an instance of our object able to manage multiple audio streams. Two functions help us to accomplish this:

  * convertArgsToLists(`*`args) : Returns arguments converted to lists and the maximum list size.
  * wrap(list,i) : Returns value at position "i" in "list" with wrap around len(list).
  
Here is the code:

{{{
    def __init__(self, in1, in2, base=50, spread=1.5, q=5, num=20, mul=1, add=0):
        # keep references of all raw arguments
        self._in1 = in1
        self._in2 = in2
        self._base = base
        self._spread = spread
        self._q = q
        self._num = num
        self._mul = mul
        self._add = add

        # list of filter's notch frequencies
        self._partials = [i+1 for i in range(self._num)]

        # Using InputFader for sound input allows crossfades when changing sources
        self._in1_fader = InputFader(in1)
        self._in2_fader = InputFader(in2)

        # Convert all arguments to lists for "list expansion"
        # convertArgsToLists function returns variables in argument as lists + maximum list size
        in1_fader, in2_fader, base, spread, q, mul, add, lmax = convertArgsToLists(self._in1_fader, self._in2_fader, base, spread, q, mul, add)
    
        # Init some lists to keep track of created objects
        self._pows = []
        self._bases = []
        self._freqs = []
        self._srcs = []
        self._amps = []
        self._excs = []
        self._outs = []
    
        # self._base_objs is the audio output seen by the outside world!
        # .play(), .out(), .stop() and .mix() methods act on this list
        # "mul" and "add" attributes are also applied on this list's objects 
        self._base_objs = []
    
        # Each cycle of the loop creates a mono stream of sound
        for i in range(lmax):
            self._pows.append(Pow(self._partials, wrap(spread,i)))
            self._bases.append(Sig(wrap(base,i)))
            self._freqs.append(Clip(self._pows[-1] * self._bases[-1], 20, 20000))
            self._srcs.append(Biquadx(wrap(in1_fader,i), freq=self._freqs[-1], q=wrap(q,i), type=2, stages=2))
            self._amps.append(Follower(self._srcs[-1], freq=5, mul=wrap(q,i)*30))
            self._excs.append(Biquadx(wrap(in2_fader,i), freq=self._freqs[-1], q=wrap(q,i), type=2, stages=2, mul=self._amps[-1]))
            # Here we mix in mono all sub streams created by "num" bands of vocoder
            self._outs.append(Mix(input=self._excs[-1], voices=1, mul=wrap(mul,i), add=wrap(add,i)))
            # getBaseObjects() method returns the list of Object_Base, needed in the self._base_objs list
            self._base_objs.extend(self._outs[-1].getBaseObjects())
}}}  

== set methods and attributes ==

Now, we will add methods and attributes for all controllable parameters. This should be noted that we use the setInput() method of the !InputFader object to change an input source (setIn1() and setIn2()). This object implements the crossfade between the old and the new sources with a crossfade duration argument:

{{{
    def setIn1(self, x, fadetime=0.05):
        """
        Replace the `in1` attribute.

        Parameters:

        x : PyoObject
            New signal to process.
        fadetime : float, optional
            Crossfade time between old and new input. Defaults to 0.05.

        """
        self._in1 = x
        self._in1_fader.setInput(x, fadetime)

    def setIn2(self, x, fadetime=0.05):
        """
        Replace the `in2` attribute.

        Parameters:

        x : PyoObject
            New signal to process.
        fadetime : float, optional
            Crossfade time between old and new input. Defaults to 0.05.

        """
        self._in2 = x
        self._in2_fader.setInput(x, fadetime)
    
    def setBase(self, x):
        """
        Replace the `base` attribute.

        Parameters:

        x : float or PyoObject
            New `base` attribute.

        """
        self._base = x
        x, lmax = convertArgsToLists(x)
        [obj.setValue(wrap(x,i)) for i, obj in enumerate(self._bases)]

    def setSpread(self, x):
        """
        Replace the `spread` attribute.

        Parameters:

        x : float or PyoObject
            New `spread` attribute.

        """
        self._spread = x
        x, lmax = convertArgsToLists(x)
        [obj.setExponent(wrap(x,i)) for i, obj in enumerate(self._pows)]

    def setQ(self, x):
        """
        Replace the `q` attribute.

        Parameters:

        x : float or PyoObject
            New `q` attribute.

        """
        self._q = x
        x, lmax = convertArgsToLists(x)
        [obj.setMul(wrap(x,i)*30) for i, obj in enumerate(self._amps)]
        [obj.setQ(wrap(x,i)) for i, obj in enumerate(self._srcs)]
        [obj.setQ(wrap(x,i)) for i, obj in enumerate(self._excs)]

    @property
    def in1(self): return self._in1
    @in1.setter
    def in1(self, x): self.setIn1(x)

    @property
    def in2(self): return self._in2
    @in2.setter
    def in2(self, x): self.setIn2(x)

    @property
    def base(self): return self._base
    @base.setter
    def base(self, x): self.setBase(x)

    @property
    def spread(self): return self._spread
    @spread.setter
    def spread(self, x): self.setSpread(x)

    @property
    def q(self): return self._q
    @q.setter
    def q(self, x): self.setQ(x)
}}}

== The `__dir__` method ==

We will override the `__dir__` to return a list of all controllable attributes for our object. User can retrieve this value by calling dir(obj).

{{{
    def __dir__(self):
        return ["in1", "in2", "base", "spread", "q", "mul", "add"]
}}}

== The ctrl method ==

The ctrl() method of a !PyoObject is used to popup a GUI to control the parameters of the object. The initialization of sliders is done with a list of !SLMap objects where we can set the range of the slider, the type of scaling, the name of the attribute affected to the slider and the init value. We will define a default "map_list" that will be used if the user doesn't provide one.

{{{
    def ctrl(self, map_list=None, title=None, wxnoserver=False):
        # Define the object's default map_list used if None is passed to PyoObject
        # map_list is a list of SLMap objects defined for each available attribute
        # in the controller window
        self._map_list = [SLMap(20., 250., "lin", "base", self._base),
                          SLMap(0.5, 2., "lin", "spread", self._spread),
                          SLMap(1., 50., "log", "q", self._q),
                          SLMapMul(self._mul)]
        PyoObject.ctrl(self, map_list, title, wxnoserver)
}}} 

Finally, we might want to override .play(), .stop() and .out() methods to be sure all our internal !PyoObjects are consequently managed instead of only objects in `self._base_obj`, as it is in current objects. See the definition of these methods in the !PyoObject man page to understand the meaning of arguments.

{{{
def play(self, dur=0, delay=0):
    dur, delay, lmax = convertArgsToLists(dur, delay)
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._pows)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._bases)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._freqs)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._srcs)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._amps)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._excs)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._outs)]
    self._base_objs = [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._base_objs)]
    return self

def stop(self):
    [obj.stop() for obj in self._pows]
    [obj.stop() for obj in self._bases]
    [obj.stop() for obj in self._freqs]
    [obj.stop() for obj in self._srcs]
    [obj.stop() for obj in self._amps]
    [obj.stop() for obj in self._excs]
    [obj.stop() for obj in self._outs]
    [obj.stop() for obj in self._base_objs]
    return self

def out(self, chnl=0, inc=1, dur=0, delay=0):
    dur, delay, lmax = convertArgsToLists(dur, delay)
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._pows)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._bases)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._freqs)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._srcs)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._amps)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._excs)]
    [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._outs)]
    if type(chnl) == ListType:
        self._base_objs = [obj.out(wrap(chnl,i), wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._base_objs)]
    else:
        if chnl < 0:    
            self._base_objs = [obj.out(i*inc, wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(random.sample(self._base_objs, len(self._base_objs)))]
        else:
            self._base_objs = [obj.out(chnl+i*inc, wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._base_objs)]
    return self
}}}

== Overriding the .play(), .stop() and .out() methods ==

Here we are, we've been created a truly object for sound processing! Of course, there is a little overhead in CPU with object written in pure python, the next step is to write it directly in C. A tutorial on how to write a pyo object in C will follow.
