#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.