ChucK/Dev/UGen Tutorial

From CSWiki
Jump to: navigation, search

UGen Tutorial

This short tutorial will show you some steps to create a simple UGen for ChucK. We wish to create a UGen called "Clip" which will clip the output in the range -1.0 and 1.0. In other words, if the signal exceeds 1.0 in either direction we want to set the output to 1.0. (In reality you should use Dyno for this purpose, since it supports softer dynamic compression instead of simple hard clipping, so it will generally sound better.)

So, first we must create a C++ file and add it to the ChucK project. This tutorial assumes you have downloaded the ChucK source code and are able to compile it. Your new UGen will be added right into the main executable.

0. Create a test program

We need something that will simply output a few samples, so we can check if it is working. Start with something that simply instantiates the Clip object and writes out 3 samples of output. You can run this with the --silent option since you won't be using the audio output immediately.

test_clip.ck

Step s => Gain g => Clip c => blackhole;

0.75 => g.gain;
0 => int i;
while (i < 3)
{
  i => s.next;
  1::samp => now;
  <<<c.last()>>>;
  i++;
}

This should fail with:

[test.ck]:line(1): undefined type 'Clip'...
[test.ck]:line(1): ... in declaration ...

1. Create ugen_clip.h and ugen_clip.cpp

ugen_clip.h

#ifndef __UGEN_CLIP_H__
#define __UGEN_CLIP_H__

#include "chuck_dl.h"


#endif // __UGEN_CLIP_H__

ugen_clip.cpp

#include "ugen_clip.h"
#include "chuck_type.h"
#include "chuck_ugen.h"
#include "chuck_vm.h"
#include "chuck_globals.h"

2. Add ugen_clip.o to makefile.alsa, and add a compile line for it

ugen_clip.o: ugen_clip.h ugen_clip.cpp
$(CXX) $(FLAGS) ugen_clip.cpp

Also add ugen_clip.o to the list of dependencies, in the long list of files with OBJS= in front of it.

Of course, if you're not using Linux, you should edit the appropriate Make file, such as makefile.win32 on Windows, or makefile.osx on OS X.

At this point, hit 'make' to make sure it compiles successfully.

 $ make

3. Add a "query" function

This function will be called when ChucK first starts up, and it will be used to register the Clip class and its members. The next step will show how this function gets called. For now it does nothing, but we will fill it in soon.

ugen_clip.cpp

DLL_QUERY clip_query( Chuck_DL_Query * QUERY )
{
    Chuck_Env * env = Chuck_Env::instance();
    Chuck_DL_Func * func = NULL;

    return TRUE;
}

4. Add a call to load_module() in chuck_compile.cpp:load_internal_modules()

chuck_compile.cpp

EM_log( CK_LOG_SEVERE, "class 'clip'..." );
if( !load_module( env, clip_query, "clip", "global" ) ) goto error;

Also make sure to add your header to chuck_compile.cpp:

chuck_compile.cpp

#include "ugen_clip.h"

And add a prototype for clip_query() to ugen_clip.h:

ugen_clip.h

// query
DLL_QUERY clip_query( Chuck_DL_Query * query );

5. Add a tick() function.

ugen_clip.h

CK_DLL_TICK( clip_tick );

ugen_clip.cpp

CK_DLL_TICK( clip_tick )
{
    *out = in > 1.0f ? 1.0f : (in < -1.0f ? -1.0f : in);
    return TRUE;
}

6. Create the class in the query function

ugen_clip.cpp

DLL_QUERY clip_query( Chuck_DL_Query * QUERY )
{
    Chuck_Env * env = Chuck_Env::instance();
    Chuck_DL_Func * func = NULL;

    if( !type_engine_import_ugen_begin( env, "Clip", "UGen", env->global(), 
                                        NULL, NULL, clip_tick, NULL ) )
        return FALSE;

    // end import
    if( !type_engine_import_class_end( env ) )
        return FALSE;

    return TRUE;
}

At this point we can run the test program. You should see,

0.00000
0.75000
1.00000

Clip is working! However we've hard coded our limits at 1.0 and -1.0. Let's start with adding a parameter to Clip called "max" which will replace 1.0. Then we'll add "min" which will replace -1.0. This will make Clip more general-purpose.

7. Add a constructor and destructor

These functions are called when an instance of Clip is created and destroyed, respectively. The only thing we need to do is reserve some space for a variable. We can reserve space for an "int", which in ChucK can be used as a pointer to a SAMPLE, that is, a floating point value.

ugen_clip.h

CK_DLL_CTOR( clip_ctor );
CK_DLL_DTOR( clip_dtor );

ugen_clip.cpp

static t_CKUINT clip_offset_data = 0;

CK_DLL_CTOR( clip_ctor )
{
    // return data to be used later
    OBJ_MEMBER_UINT(SELF, clip_offset_data) = (t_CKUINT)new SAMPLE(1.0f);
}

CK_DLL_DTOR( clip_dtor )
{
    delete (SAMPLE*) OBJ_MEMBER_UINT(SELF, clip_offset_data);
    OBJ_MEMBER_UINT(SELF, clip_offset_data) = 0;
}

.. and in the clip_query() function:

    // add member variable
    clip_offset_data = type_engine_import_mvar( env, "int", "@clip_data", FALSE );
    if( clip_offset_data == CK_INVALID_OFFSET ) goto error;

... (make sure type_engine_import_class_end() is called before this function exits, even in the error condition! see the "goto" in the final code at the end of this document.)

... now add the contructor and destructor to the ugen:

    if( !type_engine_import_ugen_begin( env, "Clip", "UGen", env->global(), 
                                        clip_ctor, clip_dtor, clip_tick, NULL ) )
        return FALSE;

8. Add a function to receive the control variable

All variables can be read or written, however we need to define functions to allow this to happen. This means that we can potentially do other things when variables are read or written, but in this case we just want to set the 'max' value.

ugen_clip.h

CK_DLL_CTRL( clip_ctrl_max );

ugen_clip.cpp

CK_DLL_CTRL( clip_ctrl_max )
{
    SAMPLE * d = (SAMPLE *)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    *d = (SAMPLE)GET_CK_FLOAT(ARGS);
    RETURN->v_float = (t_CKFLOAT)(*d);
}

... and in the clip_query() function:

    // add ctrl: max
    func = make_new_mfun( "float", "max", clip_ctrl_max );
    func->add_arg( "float", "max" );
    if( !type_engine_import_mfun( env, func ) ) goto error;

    // end import
    ...

9. To be able to retrieve the value, we need a "cget" for max

ugen_clip.h

CK_DLL_CGET( clip_cget_max );

ugen_clip.cpp

CK_DLL_CGET( clip_cget_max )
{
    SAMPLE * d = (SAMPLE *)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    RETURN->v_float = (t_CKFLOAT)(*d);
}

... in the clip_query() function:

    // add cget: max
    func = make_new_mfun( "float", "max", clip_cget_max );
    if( !type_engine_import_mfun( env, func ) ) goto error;

10. To access the 'max' variable in the tick() function:

ugen_clip.cpp

CK_DLL_TICK( clip_tick )
{
    SAMPLE *max = (SAMPLE *)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    *out = in > *max ? *max : (in < -1.0f ? -1.0f : in);
    return TRUE;
}

Now try setting 'max' in the test program:

test_clip.ck

Step s => Clip c => Gain g => blackhole;

0.9 => c.max;
0.75 => g.gain;
0 => int i;
while (i < 3)
{
  i => s.next;
  1::samp => now;
  <<<c.last()>>>;
  i++;
}

The output should be:

0.000000 :(float)
0.750000 :(float)
0.900000 :(float)

11. Create a data structure

The SAMPLE type holds only a single value. We want to add another value, 'min'. We only reserved one pointer, so to do so, we can create a dedicated struct and point to that instead of to a SAMPLE:

ugen_clip.cpp

struct Clip_Data
{
    SAMPLE max;
    SAMPLE min;
    Clip_Data( ) { max = 1.0f; min = -1.0f; }
};

12. Change all uses of SAMPLE for Clip_Data

ugen_clip.cpp

CK_DLL_CTOR( clip_ctor )
{
    // return data to be used later
    OBJ_MEMBER_UINT(SELF, clip_offset_data) = (t_CKUINT)new Clip_Data;
}

CK_DLL_DTOR( clip_dtor )
{
    delete (Clip_Data*) OBJ_MEMBER_UINT(SELF, clip_offset_data);
    OBJ_MEMBER_UINT(SELF, clip_offset_data) = 0;
}

CK_DLL_TICK( clip_tick )
{
    Clip_Data *d = (Clip_Data *)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    *out = in > d->max ? d->max : (in < d->min ? d->min : in);
    return TRUE;
}

CK_DLL_CTRL( clip_ctrl_max )
{
    Clip_Data *d = (Clip_Data*)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    d->max = (SAMPLE)GET_CK_FLOAT(ARGS);
    RETURN->v_float = (t_CKFLOAT)(d->max);
}

CK_DLL_CGET( clip_cget_max )
{
    Clip_Data *d = (Clip_Data*)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    RETURN->v_float = (t_CKFLOAT)(d->max);
}

13. Add CGET and CTRL functions for 'min'

ugen_clip.h

CK_DLL_CTRL( clip_ctrl_min );
CK_DLL_CGET( clip_cget_min );

ugen_clip.cpp

CK_DLL_CTRL( clip_ctrl_min )
{
    Clip_Data *d = (Clip_Data*)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    d->min = (SAMPLE)GET_CK_FLOAT(ARGS);
    RETURN->v_float = (t_CKFLOAT)(d->min);
}

CK_DLL_CGET( clip_cget_min )
{
    Clip_Data *d = (Clip_Data*)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    RETURN->v_float = (t_CKFLOAT)(d->min);
}

... and in the clip_query() function:

    // add ctrl: min
    func = make_new_mfun( "float", "min", clip_ctrl_min );
    func->add_arg( "float", "min" );
    if( !type_engine_import_mfun( env, func ) ) goto error;

    // add cget: min
    func = make_new_mfun( "float", "min", clip_cget_min );
    if( !type_engine_import_mfun( env, func ) ) goto error;

Now 'min' should work in the test program too!

test_clip.ck

Step s => Gain g => Clip c => blackhole;

0.9 => c.max;
-0.9 => c.min;
-0.75 => g.gain;
0 => int i;
while (i < 3)
{
  i => s.next;
  1::samp => now;
  <<<c.last()>>>;
  i++;
}

The output should be:

0.000000 :(float)
-0.750000 :(float)
-0.900000 :(float)

14. The final files

Here are the final ugen_clip.h and ugen_clip.cpp files for your perusal.

ugen_clip.h

#ifndef __UGEN_CLIP_H__
#define __UGEN_CLIP_H__

#include "chuck_dl.h"

// query
DLL_QUERY clip_query( Chuck_DL_Query * query );

// clip
CK_DLL_CTOR( clip_ctor );
CK_DLL_DTOR( clip_dtor );
CK_DLL_TICK( clip_tick );
CK_DLL_CTRL( clip_ctrl_max );
CK_DLL_CGET( clip_cget_max );
CK_DLL_CTRL( clip_ctrl_min );
CK_DLL_CGET( clip_cget_min );

#endif // __UGEN_CLIP_H__


ugen_clip.cpp

#include "ugen_clip.h"
#include "chuck_type.h"
#include "chuck_ugen.h"
#include "chuck_vm.h"
#include "chuck_globals.h"

static t_CKUINT clip_offset_data = 0;

DLL_QUERY clip_query( Chuck_DL_Query * QUERY )
{
    Chuck_Env * env = Chuck_Env::instance();
    Chuck_DL_Func * func = NULL;

    //---------------------------------------------------------------------
    // init as base class: clip
    //---------------------------------------------------------------------
    if( !type_engine_import_ugen_begin( env, "Clip", "UGen", env->global(),
                                        clip_ctor, clip_dtor, clip_tick, NULL ) )
        return FALSE;

    // add member variable
    clip_offset_data = type_engine_import_mvar( env, "int", "@clip_data", FALSE );
    if( clip_offset_data == CK_INVALID_OFFSET ) goto error;

    // add ctrl: max
    func = make_new_mfun( "float", "max", clip_ctrl_max );
    func->add_arg( "float", "max" );
    if( !type_engine_import_mfun( env, func ) ) goto error;

    // add cget: max
    func = make_new_mfun( "float", "max", clip_cget_max );
    if( !type_engine_import_mfun( env, func ) ) goto error;

    // add ctrl: min
    func = make_new_mfun( "float", "min", clip_ctrl_min );
    func->add_arg( "float", "min" );
    if( !type_engine_import_mfun( env, func ) ) goto error;

    // add cget: min
    func = make_new_mfun( "float", "min", clip_cget_min );
    if( !type_engine_import_mfun( env, func ) ) goto error;

    // end import
    if( !type_engine_import_class_end( env ) )
        return FALSE;

    return TRUE;

error:
    // end import
    if( !type_engine_import_class_end( env ) )
        return FALSE;

    return FALSE;
}

struct Clip_Data
{
    SAMPLE max;
    SAMPLE min;
    Clip_Data( ) { max = 1.0f; min = -1.0f; }
};

CK_DLL_CTOR( clip_ctor )
{
    // return data to be used later
    OBJ_MEMBER_UINT(SELF, clip_offset_data) = (t_CKUINT)new Clip_Data;
}

CK_DLL_DTOR( clip_dtor )
{
    delete (Clip_Data*) OBJ_MEMBER_UINT(SELF, clip_offset_data);
    OBJ_MEMBER_UINT(SELF, clip_offset_data) = 0;
}

CK_DLL_TICK( clip_tick )
{
    Clip_Data *d = (Clip_Data *)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    *out = in > d->max ? d->max : (in < d->min ? d->min : in);
    return TRUE;
}

CK_DLL_CTRL( clip_ctrl_max )
{
    Clip_Data *d = (Clip_Data*)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    d->max = (SAMPLE)GET_CK_FLOAT(ARGS);
    RETURN->v_float = (t_CKFLOAT)(d->max);
}

CK_DLL_CGET( clip_cget_max )
{
    Clip_Data *d = (Clip_Data*)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    RETURN->v_float = (t_CKFLOAT)(d->max);
}

CK_DLL_CTRL( clip_ctrl_min )
{
    Clip_Data *d = (Clip_Data*)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    d->min = (SAMPLE)GET_CK_FLOAT(ARGS);
    RETURN->v_float = (t_CKFLOAT)(d->min);
}

CK_DLL_CGET( clip_cget_min )
{
    Clip_Data *d = (Clip_Data*)OBJ_MEMBER_UINT(SELF, clip_offset_data);
    RETURN->v_float = (t_CKFLOAT)(d->min);
}