5 a signal-external: pan~

Signal classes are normal Pd-classes, that offer additional methods for signals.

All methods and concepts that can be realized with normal object classes can therefore be realized with signal classes too.

Per agreement, the symbolic names of signal classes end with a tilde ~.

The class “pan~” shall demonstrate, how signal classes are written.

A signal on the left inlet is mixed with a signal on the second inlet. The mixing-factor between 0 and 1 is defined via a t_float-message on a third inlet.

5.1 variables of a signal class

Since a signal-class is only an extended normal class, there are no principal differences between the data spaces.

typedef struct _pan_tilde {  
  t_object x_obj;  
 
  t_sample f_pan;  
  t_float  f;  
 
  t_inlet *x_in2;  
  t_inlet *x_in3;  
 
  t_outlet*x_out;  
 
} t_pan_tilde;

Only one variable f_pan for the mixing-factor of the panning-function is needed.

The other variable f is needed whenever a signal-inlet is needed too. If no signal but only a float-message is present at a signal-inlet, this variable is used to automatically convert the float to signal.

Finally, we have the members x_in2, x_in3 and x_out, which are needed to store handles to the various extra inlets (resp. outlets) of the object.

5.2 signal-classes

void pan_tilde_setup(void) {  
  pan_tilde_class = class_new(gensym("pan~"),  
        (t_newmethod)pan_tilde_new,  
        (t_method)pan_tilde_free,  
        sizeof(t_pan_tilde),  
        CLASS_DEFAULT,  
        A_DEFFLOAT, 0);  
 
  class_addmethod(pan_tilde_class,  
        (t_method)pan_tilde_dsp, gensym("dsp"), 0);  
  CLASS_MAINSIGNALIN(pan_tilde_class, t_pan_tilde, f);  
}

Something has changed with the class_new function: the third argument specifies a “free-method” (aka destructor), which is called whenever an instance of the object is to be deleted (just like the “new-method” is called whenever an instance is to be created). In the prior examples this was set to 0 (meaning: we don’t care), but in this example we have to clean up some ressources when we don’t need them any more.

More interestingly, a method for signal-processing has to be provided by each signal class.

Whenever Pd’s audio engine is started, a message with the selector “dsp” is sent to each object. Each class that has a method for the “dsp”-message is recognised as signal class.

Signal classes that want to provide signal-inlets have to declare this via the CLASS_MAINSIGNALIN-macro. This enables signals at the first (default) inlet. If more than one signal-inlet is needed, they have to be created explicitly in the constructor-method.

Inlets that are declared as signal-inlets cannot provide methods for t_float-messages any longer.

The first argument of the macro is a pointer to the signal class. The second argument is the type of the class’s data space.

The last argument is a dummy-variable out of the data space that is needed to replace non-existing signal at the signal-inlet(s) with t_float-messages.

5.3 construction of signal-inlets and -outlets

void *pan_tilde_new(t_floatarg f)  
{  
  t_pan_tilde *x = (t_pan_tilde *)pd_new(pan_tilde_class);  
 
  x->f_pan = f;  
 
  x->x_in2 = inlet_new(&x->x_obj, &x->x_obj.ob_pd, &s_signal, &s_signal);  
  x->x_in3 = floatinlet_new (&x->x_obj, &x->f_pan);  
 
  x->x_out = outlet_new(&x->x_obj, &s_signal);  
 
  return (void *)x;  
}

Additional signal-inlets are added like other inlets with the routine inlet_new. The last two arguments are references to the symbolic selector “signal” in the lookup-table.

Signal-outlets are also created like normal (message-)outlets, by setting the outlet-selector to “signal”.

The newly created inlets/outlets are “user-allocated” data. Pd will keep track of all the ressources it automatically creates (like the default inlet), and will eventually free these ressources once they are no longer needed. However, if we request an “extra” ressource (like the additional inlets/outlets in this example; or - more commonly - memory that is allocated via malloc or similar), we have to make sure ourselves, that these ressources are freed when no longer needed. If we fail to do so, we will invariably create a dreaded memory leak.

Therefore, we store the “handles” to the newly created inlets/outlets as returned by the ..._new routines for later use.

5.4 DSP-methods

Whenever Pd’s audio engine is turned on, all signal-objects declare their perform-routines that are to be added to the DSP-tree.

The “dsp”-method has two arguments, the pointer to the class-data space, and a pointer to an array of signals.

The signals are arranged in the array in such way, that they are ordered in a clockwise way in the graphical representation of the object.4

void pan_tilde_dsp(t_pan_tilde *x, t_signal **sp)  
{  
  dsp_add(pan_tilde_perform, 5, x,  
          sp[0]->s_vec, sp[1]->s_vec, sp[2]->s_vec, sp[0]->s_n);  
}

dsp_add adds a perform-routine (as declared in the first argument) to the DSP-tree.

The second argument is the number of the following pointers to diverse variables. Which pointers to which variables are passed is not limited.

Here, sp[0] is the first in-signal, sp[1] represents the second in-signal and sp[3] points to the out-signal.

The structure t_signal contains a pointer to the its signal-vector ().s_vec (an array of samples of type t_sample), and the length of this signal-vector ().s_n.

Since all signal vectors of a patch (not including it’s sub-patches) are of the same length, it is sufficient to get the length of one of these vectors.

5.5 perform-routine

The perform-routine is the DSP-heart of each signal class.

A pointer to an integer-array is passed to it. This array contains the pointers, that were passed via dsp_add, which must be casted back to their real type.

The perform-routine has to return a pointer to integer, that points to the address behind the stored pointers of the routine. This means, that the return argument equals the argument of the perform-routine plus the number of pointer variables (as declared as the second argument of dsp_add) plus one.

t_int *pan_tilde_perform(t_int *w)  
{  
  t_pan_tilde *x = (t_pan_tilde *)(w[1]);  
  t_sample  *in1 =    (t_sample *)(w[2]);  
  t_sample  *in2 =    (t_sample *)(w[3]);  
  t_sample  *out =    (t_sample *)(w[4]);  
  int          n =           (int)(w[5]);  
 
  t_sample f_pan = (x->f_pan<0)?0.0:(x->f_pan>1)?1.0:x->f_pan;  
 
  while (n--) *out++ = (*in1++)*(1-f_pan)+(*in2++)*f_pan;  
 
  return (w+6);  
}

Each sample of the signal vectors is read and manipulated in the while-loop.

Optimisation of the DSP-tree tries to avoid unnecessary copy-operations. Therefore it is possible, that in- and out-signal are located at the same address in the memory. In this case, the programmer has to be careful not to write into the out-signal before having read the in-signal to avoid overwriting data that is not yet saved.

5.6 destructor

void pan_tilde_free(t_pan_tilde *x)  
{  
  inlet_free(x->x_in2);  
  inlet_free(x->x_in3);  
  outlet_free(x->x_out);  
}

The object has some dynamically allocated ressources, namely the additional inlets and outlets we created in the constructor.

Since Pd doesn’t track dynamically allocated ressources for us, we have to free them manually in the “free-method” (aka: destructor). We do so by calling inlet_free (resp. outlet_free) on the handles to our additional iolets.

Note that we do not need to free the default first outlet. As it is created automatically by Pd, it is also freed automatically.

5.7 the code: pan~

#include "m_pd.h"  
 
static t_class *pan_tilde_class;  
 
typedef struct _pan_tilde {  
  t_object  x_obj;  
  t_sample f_pan;  
  t_sample f;  
 
  t_inlet *x_in2;  
  t_inlet *x_in3;  
  t_outlet*x_out;  
} t_pan_tilde;  
 
t_int *pan_tilde_perform(t_int *w)  
{  
  t_pan_tilde *x = (t_pan_tilde *)(w[1]);  
  t_sample  *in1 =    (t_sample *)(w[2]);  
  t_sample  *in2 =    (t_sample *)(w[3]);  
  t_sample  *out =    (t_sample *)(w[4]);  
  int          n =           (int)(w[5]);  
  t_sample f_pan = (x->f_pan<0)?0.0:(x->f_pan>1)?1.0:x->f_pan;  
 
  while (n--) *out++ = (*in1++)*(1-f_pan)+(*in2++)*f_pan;  
 
  return (w+6);  
}  
 
void pan_tilde_dsp(t_pan_tilde *x, t_signal **sp)  
{  
  dsp_add(pan_tilde_perform, 5, x,  
          sp[0]->s_vec, sp[1]->s_vec, sp[2]->s_vec, sp[0]->s_n);  
}  
 
void pan_tilde_free(t_pan_tilde *x)  
{  
  inlet_free(x->x_in2);  
  inlet_free(x->x_in3);  
  outlet_free(x->x_out);  
}  
 
void *pan_tilde_new(t_floatarg f)  
{  
  t_pan_tilde *x = (t_pan_tilde *)pd_new(pan_tilde_class);  
 
  x->f_pan = f;  
 
  x->x_in2=inlet_new(&x->x_obj, &x->x_obj.ob_pd, &s_signal, &s_signal);  
  x->x_in3=floatinlet_new (&x->x_obj, &x->f_pan);  
  x->x_out=outlet_new(&x->x_obj, &s_signal);  
 
  return (void *)x;  
}  
 
void pan_tilde_setup(void) {  
  pan_tilde_class = class_new(gensym("pan~"),  
        (t_newmethod)pan_tilde_new,  
        0, sizeof(t_pan_tilde),  
        CLASS_DEFAULT,  
        A_DEFFLOAT, 0);  
 
  class_addmethod(pan_tilde_class,  
        (t_method)pan_tilde_dsp, gensym("dsp"), 0);  
  CLASS_MAINSIGNALIN(pan_tilde_class, t_pan_tilde, f);  
}

4If both left and right in- and out-signals exist, this means: First is the leftmost in-signal followed by the right in-signals; after the right out-signals, finally there comes the leftmost out-signal.