Getting Started with chucklib

Chucklib has grown into a fairly large body of code, like the crucial library, and it might be difficult to know where to begin. The actual entry point is fairly simple, however. If you stick to the basic idea of doing simple things first and growing gradually into more complex usages, it will be a lot easier to learn your way around it.

This tutorial divides into four sections.

1. Prototype-based programming: The ability to change behaviors on the fly with a minimum of disruption is the main point of chucklib. Prototype-based programming assists by providing an alternative to hard coded classes that is capable of object-oriented design without requiring you to recompile the library for every change.

2. Processes: In chucklib, a musical behavior (defined as a Pattern or Routine) exists in a process object along with all the other resources it needs to produce its result. Instead of manually manipulating dozens of objects for one component of a piece, you deal mainly with the process objects and they take care of the resources they need.

3. Stream or function replacement: Moving some components of a musical behavior outside the main Pattern/Routine definition allows you to replace those components while the process is playing, so that you can play with the process interactively.

4. Automatic initialization and clean-up: Most of the time, writing a Pattern or Routine is not enough. You might have to load support objects on the server and create data structures in the client to do the work effectively. Chucklib lets you manage this automatically.

Keep in mind, as you review the examples in each section, that each case starts with a "standard" usage, and adds functionality/convenience through a fairly simple change in the code. The distance between "standard" code and chucklib code does not have to be that great, at least not at first.

1. Prototype-based programming

This tutorial cannot pretend to be a complete introduction to prototype-based programming. In brief, prototype-based programming uses prototypes, instead of classes and instances, to encapsulate object behavior.

Object-oriented programming: Classes are compiled into fixed object code. At runtime, instances of the classes are made, and the instances do the actual work.

Prototype-based programming: Prototypes cover the concepts of both classes and instances. A prototype defines behavior and the data required to execute that behavior. When a new "instance" is needed, we make a copy of the prototype and work with the new copy.

The advantage of prototype-based programming for composition is that you can redefine a prototype any time (whereas changing a class requires recompiling the class library, and thereby destroying all the runtime objects existing up to that point). Since it takes less work to change a prototype, it takes less work to move a prototype toward the desired musical result.

Musical processes in chucklib are implemented as prototypes. Since writing prototypes is a lot like writing classes, this might be scary if you feel that writing classes is intimidating. Don't worry -- the minimum that you need to write a chucklib process is small and straightforward. In this context, prototypes provide the opportunity to grow into highly complex algorithms while keeping the code clear. But you don't need to write complex code to use prototypes!

The Proto class implements prototypes for chucklib -- see its help file for details. It's just like an Environment, with some additional features that simplify prototype usage. The basics work like this:

~myProto = Proto.new({

// use ~environmentVariables inside the Proto

~aVariable = 10;

~anotherVariable = 20;

// store a function in the Environment

~aMethod = {

~aVariable.rand; // the function's return value is the method's result

};

~anotherMethod = {

// you can call this Proto's methods by value-ing the function

rrand(~aMethod.value, ~anotherVariable);

};

});

~myProto.aVariable; // access variables by dot-syntax, like normal objects

~myProto.anotherVariable = 30; // change variables

~myProto.aMethod; // enters the Proto environment and runs the function

~myProto.anotherMethod;

That's it... not too complicated! You can make a new "instance" of a Proto by calling copy on it. If you want a new prototype that inherits from an existing Proto, use clone:

~childProto = ~myProto.clone({

~anotherMethod = { ~anotherVariable / ~aVariable };

});

// no need to restate the variables from ~myProto - they are inherited

~childProto.anotherMethod;

2. Processes

A chucklib process uses a Proto object to hold a behavioral definition (a Pattern or Routine) along with data the Pattern needs to do its work.

Not much is needed to create a process. In fact, all you have to do is write a function to return the desired Pattern, and save that function into the Proto under the name ~asPattern. (For convenience, I will stop saying "Pattern or Routine" and just say "Pattern." This is actually more technically accurate -- the ~asPattern function is expected to return some kind of playable Pattern. But, if you're more accustomed to writing Routines or Tasks, you can use the Prout Pattern, which, for playing purposes, behaves just like a Task.)

Proto({

~asPattern = {

Pbind(\degree, Pn(Pseries(0, 1, 8), inf), \dur, 0.125)

};

});

That's just a generic Proto that happens to conform to the specifications for a chucklib process. It isn't really a process yet. To make it one, you have to chuck the Proto into one of the process classes, BP or PR. After that, you can play, stop and otherwise manipulate the process.

Proto({

~asPattern = {

Pbind(\degree, Pn(Pseries(0, 1, 8), inf), \dur, 0.125)

};

}) => BP(\cmaj);

BP(\cmaj).play(1); // (1) says to start on the next beat

BP(\cmaj).stop(1);

BP(\cmaj).free; // destroy the BP and all its contents

BP stands for "Bound Process"; in a bound process, the behavioral definition (~asPattern) is bound to specific data to achieve a particular result. You might also want to define behavior without insisting on specific data. That's the idea of a "process prototype," or PR. In very general terms, you could say that PR is to BP as class is to instance.

A PR is meant to be relatively stable; it exists to be copied for specific uses, but for no other reason. Consequently PR cannot play, stop or do anything musical. It's just a storage device. When you chuck a PR into a BP, it becomes a playable instance. The importance of that comes in the next two sections.

In the meantime, this is the simplest entry point into chucklib. Write your Pattern, Routine or Task, wrap it in a Proto under the ~asPattern method, and you can immediately benefit from chucklib's scheduling strategies and other organizational devices.

3. Stream or function replacement

The previous example created a process that can do only one thing: play an ascending C major scale in 32nd notes. What if you want use the same basic format of the Pattern, but have it play arbitrary pitches in any rhythm?

Since the goal is to reuse the same Pattern structure, that suggests PR should hold the basic structure. But, since we want to change the content of the Pbind, instead of hardwiring Patterns for the keys, we should use placeholders that will look to Proto variables for the real Patterns to use. In chucklib, the placeholder is BPStream.

Proto({

~asPattern = {

Pbind(\degree, BPStream(\degree), \dur, BPStream(\dur));

};

~degree = Pn(Pseries(0, 1, 8), inf);

~dur = 0.125;

}) => PR(\pitches);

// make the playable instance

PR(\pitches) => BP(\p);

Now, when you play BP(\p), it sounds the same as the hard coded example above. But you can now change the stream references just by assigning new Patterns to the corresponding Proto variables. If you do this while the process is playing, the change takes effect immediately.

BP(\p).play(1);

BP(\p).degree = Pbrown(0, 14, 4, inf).round;

BP(\p).dur = Prand([Pn(0.0625, 4), Pn(0.125, 2), 0.25, 0.5], inf);

BP(\p).stop(1);

BP(\p).free;

Why go through the extra layer of the PR? You could just chuck the Proto directly into a BP. That's fine if you only need one BP. What if you want several BPs that do basically the same thing, but use different child Patterns? Your choices are, rerun the Proto definition for every BP, or run it once and save it in a PR, whereupon you can use just one short line -- PR(\name) => BP(\copy) -- to make as new copies, as many times as you need. So, you can use a complex behavior without having to restate the behavior every time.

If you prefer to use Routines or Tasks instead of Patterns, you can still split code out into Proto methods. Since those "methods" are just functions stored in the environment, you can replace them any time and the Routine will pick up the change next time it calls the method.

SynthDef(\sinGrain, { |freq = 440, amp = 0.1|

var sig = SinOsc.ar(freq, 0, amp) * EnvGen.kr(Env.perc(0.01, 0.1), doneAction: 2);

Out.ar(0, sig ! 2);

}).send(s);

Proto({

~calcPitch = { (rrand(0, 7).degreeToKey(#[0, 2, 4, 5, 7, 9, 11], 12) + 60).midicps };

~calcDur = { #[0.125, 0.25, 0.5].choose };

~asPattern = {

Prout({ // note Prout - NOT Routine or Task - here

loop {

s.makeBundle(0.2, {

Synth(\sinGrain, [freq: ~calcPitch.value]);

});

~calcDur.value.wait;

}

})

};

}) => PR(\routine);

PR(\routine) => BP(\r);

BP(\r).play(1);

BP(\r).calcPitch = { exprand(200, 1200) };

BP(\r).stop(1);

BP(\r).free;

At this point, someone could say, "Why do you need the extra weight of PR and BP, when you can do the same thing with a couple of variables and a freestanding Routine?" That's just because this is a trivially simple Routine. If the Routine spanned several dozen lines and depended on 30 or 40 other functions, it would be a nightmare to keep track of all those objects individually, not to mention the trouble if you wanted to make an independent copy of that process. (How would you be sure variable names would not collide?) Making a chucklib process out of it, as shown here, lets you address the whole object group as a single object... which, in terms of performance, it really is. The implementation involves several objects, but as a musical or compositional unit, it's just that -- one unit.

4. Automatic initialization and clean-up

Suppose you write a Pattern that plays bits of a sound file. If you use it in performance, you have to remember to load the file into a server buffer before playing the Pattern (easily forgotten).

// Here, we use a standard synthdef installed by chucklib.

b = Buffer.read(s, "sounds/a11wlk01.wav");

p = Pbind(

\instrument, \bufGrainPan,

\start, Pwhite(0, 0.7, inf) * b.numFrames,

\time, Pwhite(1, 5, inf) * 0.1,

\delta, Pkey(\time),

\bufnum, b,

\pan, Pwhite(-1.0, 1.0, inf),

\amp, 0.5

).play;

p.stop;

b.free;

Wouldn't it be nice if you could associate the buffer with the Pattern, so that the buffer would load automatically when you need the Pattern and would be released when the Pattern is no longer needed? Chucklib makes it simple. When you chuck a PR into a BP, it calls the ~prep function. Whatever initialization you need should go in this function. Later, when you free the BP, ~freeCleanup can remove anything you created.

(

// hide this code away in a separate file that you load at the start of a piece

Proto({

// we could also use some placeholders here

// for simplicity, not here -- "exercise for the reader"

~asPattern = {

Pbind(

\instrument, \bufGrainPan,

\start, Pwhite(0, 0.7, inf) * ~buf.numFrames,

\time, Pwhite(1, 5, inf) * 0.1,

\delta, Pkey(\time),

\bufnum, ~buf,

\pan, Pwhite(-1.0, 1.0, inf),

\amp, 0.5

);

};

// initialization

// by using ~variables for the soundfile to load,

// you can use this process with any soundfile

~path = "sounds/a11wlk01.wav";

~prep = {

~buf = Buffer.read(s, ~path);

};

~freeCleanup = {

~buf.free;

};

}) => PR(\bufSlice);

)

// then when it's time to play, all you need is this

PR(\bufSlice) => BP(\buf);

BP(\buf).play;

BP(\buf).stop;

BP(\buf).free;

That's all well and good, but you might want it to work on a different sound file. It isn't as obvious as it sounds at first. If you do this:

// Doesn't work - buffer is loaded before you give it the right path

PR(\bufSlice) => BP(\buf); BP(\buf).path = "newpath";

-- it's already too late to change the path, because the buffer loads immediately. Chucklib's answer is to let you add arbitrary parameters to a chuck operation. These parameters go into the process object before initialization, so you can change the path, and then the buffer loads.

PR(\bufSlice).chuck(BP(\buf), nil, parms: (path: "sounds/a11wlk01-44_1.aiff"));

BP(\buf).play;

BP(\buf).stop;

BP(\buf).free;

Initially, you might not use the parameter list much, but the more complicated the processes are, the more useful it becomes. Some of the built-in process prototypes, especially the drum machine processes (\bufPerc, \defPerc and \break), use the parameter list for almost all initialization.

Again, the most important point here is -- use what is approachable at first, and don't worry about the whole picture. Once you get comfortable with the basic techniques, there's plenty of room to grow, but there's no need to get anxious about features you might not need yet.