The context before Cisp: Streams library

 

Cisp started life as a set of classes I wrote in ChucK, practically a toolbox that allowed to easily construct waveforms directly using streams (taking Döbereiner's CompScheme as an example). Streams are objects similar to lists, except that values are only calculated when they are requested (lazy evaluation)).

 

Why a transpiler?

 

Reflecting on my coding practice with that library, I evolved to ever shorter programs. The reason for this was, that the more lines of code in my program, the more I had to rely on my memory where I had programmed what. In live coding, this made the likelyhood of a crash rather large.

 

Certain syntax structures required large contexts to work and when deeply nested, the code becomes inert and brittle. At some point I hit a minimum: there were limitations within ChucK syntax that couldn't be worked around with helper functions.

 

Some concrete examples:

 

- Numbers (floats, ints) & function objects producing numbers could not be easily mixed within arrays, even though compositionally/conceptually it didn't matter to me if something was a float, an integer, or a function that returned a float.

 

- ChucK is object-oriented and uses classes for abstractions, functions cannot be treated as values, which means to do functional-like programming I had to be model/simulate using objects. In practice, it meant I had to write a class to create a stream with new behavior. Writing a class requires lots of lines of code, and more importantly, it cannot be easily nested.

 

Cisp requirements:


* I wanted rewriting a Cisp program as local as possible: when a value in a list is adjusted it shouldn't require changes elsewhere.
Functions should be treated the same as numbers: if a function name is written somewhere it should be automatically called to produce a value.

 

Function definition is writte like so:

 

(fun myFun

    (ch 1.0 2.0))


Calling this function "myFun" within another stream definition, by only writing its name:

 

(seq myFun 1.0 3.0)

 

 

* Common expressions should have short names:

defining a function: (fun name definition)

defining a bus: (~ busName definition)

 

* Useful defaults, if arguments are not provided it should provide a useful default:

(list-walk list) will just do a random walk over a list

(list-walk list step) you to provide the step stream

(rv 10) generates between 0 and 10

 

 {keywords: [chuck, stream, coding, syntax, code, function, abstraction, cisp, expression]}

 

Cisp

 

Cisp was thus a reversal in approach. Instead of having a musical idea and implementing it using my toolbox in ChucK, I started from the ideas that I had developed up until that point, defined a syntax that expresses those ideas as efficiently as I could and then build a translator to generate the necessary ChucK code.

 

The overal gain of this approach is small or none: I still had to invest a lot of time in programming the translator. However, while composing within Cisp, the experience of programming and especially rewriting was very different. I think this was because the syntax was very close to my mental model. It also allowed me to interact with the program with a different mindset since it was less based on planning, and more following intuition and rewriting a small part of the program based on the current state of the musical sound that it produces.

 

This allowed for a flow of programming, where I did not know exactly where I was going, but exploring the possibilities in a random-walk kind of way: I would start from a small seed program and then extend it step by step.

 

 

The result is of course a very personal syntax. Since I am the only user, I am not sure how intuitive another programmer would find the naming and structure of programs that result.

 

{keywords: [chuck, stream, coding, syntax, code, tools, translation, programming, rewriting, model, interaction, intuition]}

 

Resources:

 

Cisp on github (installation instructions in readme.md)

 

I am currently working on a new implementation of Cisp in oCaml, as a branch of Döbereiner's "processes" repository.

 

My personal website:

https://www.casperschipper.nl/v2/

 

ChucK programming language

 

Lis.py (a tiny lisp compiler written in Python I used as a starting point)

 

Cisp design is heavily influenced by: Paul Berg's AC Toolbox and Luc Döbereiners "CompScheme"

 

{kind: reference}

Another Cisp program, based on a sinewave signal:

Introduction video:

A Cisp program developing:

 

example of a ChucK program, rewritten on-the-fly:

 

My initial attraction for using the ChucK programming language was a very simple one:

It allows the programmer/composer to construct sound from the time level of the individual audiosample, while still being a relatively high level language (compared to C). It also has an attractive way of on-the-fly programming, which includes "sporking shreds": a ChucK function can be evaluated as a separate thread with its own control rate / passage of time.

 

Other popular audio programming languages (Max, Csound, SuperCollider) often have a distinction between control rate and audio rate parts of their programs, but in ChucK the control rate can be adjusted dynamically for each line of code, and evaluation of any block of code can be synced with the audio rate process.

 

{keywords: [chuck, programmer, composer, sound, sample, language, programming, function, audio]}

Casper Schipper

 

Cisp: a Live-Coding Language for Non-Standard Synthesis Algorithms

Why Lisp?

 

I took Lisp style syntax (fun arg1 arg2 arg3 etc..) as a starting point for a couple of reasons:

 

- Nostalgic: when I studied with Paul Berg, we programmed a lot in his AC-toolbox, which was based on Lisp.

- Having no experience with building parsers, I considered something Lisp-like to be easier to start with. I found a very useful example of a lisp parser by Peter Norvig written in Python, so that became my starting point.

- Lisp syntax works well for nested structures, which I was using a lot, even before the transpiler.

- I think the (parenthesis) are not a problem if the editor highlights them. I like that function calls and list use the same syntax, less change of closing with the wrong character.

- I often write long lists of numbers, the spacebar is just faster than ','.

A longer Cisp program:

(# timy
    (1. 4 7 23 22 1 17 3 4 2 1 2 3 2 3 4 5 55 44 13 17 330 220 512 128 1 4 512 4300 6500))

(procedure casper
    (# timy
        (collect
            (seq
                (latch
                    (ch 1 3 7 15 13 22)
                    (rv 1 4)
                    )
                (latch
                    (ch 20 23 11 1 200)
                    (rv 1 7)
                    )
                (latch
                    (rv 2000 8000)
                    (ch 1 7 20)
                    )
                (latch
                    (ch (hold (rv 1 50) (st 100)) (rv 1 40))
                    (ch 1 3 2 2 2 2 2 4 8)))
            16)))

(schedule
    casper
    (ch 2 4 8))

(fun a
    (line
        (seq 0 (table-cap timy))
        (ch 5. 7 2 0.01)))

(fun sr
    (*
    (t
        (ch 0.001 0.01 0.0001 2)
        (ch 0.001 0.1 0.2 0.6 ))
    (t
        (ch 0.75 1.5 0.5 3 4)
        (ch 1 2 5))))

(fun reader
    (index
        (list
            (line (seq a a) sr)
            (rv a a))
    (t
        (count 2)
        (rv 0 5))))


(clone
(pulse-fb-gen
    (index-lin
        (list
            (seq  -1 1)
            (rv -1 1)
            (seq (fillf 32 -1 1))
            (seq OSC.table1)
            (seq OSC.table2)
            (seq OSC.table3)
            )
        (t (rv 0 6) (ch 1 2 3 4 5 6)))
    (index-lin (list
        (index timy reader)
        (st 10)
        ) (t (count 2) (ch 1 2 4 8 12)))
    
    (index-lin
        (list
            (seq  -1 1)
            (rv -1 1)
            (seq (fillf 32 -1 1))
            (seq OSC.table1)
            (seq OSC.table2)
            (seq OSC.table3)
            )
        (line (rv 0 6) (ch 1 2 3 4 5 6)))
    (t (mtof (+ (index (fillf 32 0 100) reader) (seq 0 7 12 19 24 28 true)) ) (fractRandTimer (ch 0.001 0.1 0.2 0.4 0.8 1.6)))
    (index OSC.table1 reader))
1)


{kind: code, keywords: [_, cisp, sound, synthesis, syntax, function]}

A typical Cisp program's lifecycle:

 

1. A small program is written that generates sound.

2. Static parts of the program are replaced with dynamic ones.

3. At some point the program generates interesting sound.

4. A copy is made of the program and may be futher developed. For example by removing parts, or even replacing them with lower complexity and reprogramming those aspects in another direction.

 

{keywords: [cisp, sound, program, coding, development, rewriting, workflow]}

Cisp syntax:


* using naming and syntax that I found intuitive and as short as possible.
st is static value -
    st 1 -> 1 1 1 1 1 1

seq is a sequential loop
    (seq 11 12 13) -> 11 12 13 11 12 13 ..

rv is a random value

    (rv 1 3) -> 1 3 2 1 1 2 2 1 3 etc..

line are linesegments

    (line (seq 0 1) (st 3)) -> 0 0.33 0.67 1.0 0.67

index indexes an array of streams or values:

    (index ((rv 99 100) (rv 1 2)) (count 2)) -> 99.3 1.34 99.9 1.02 99.23 1.9

write writes into an array

    (write arrName (rv 0 1) (count 10))

 

* abstraction and construction of streams and tables
   - defining a function that returns a stream: (fun [definition])
   - defining a bus to pass streams around in my program: (~ name value)
   - reading from a bus (~ name)
   - generating an array (# tablename values)
   - scheduling some actiona separate thread (schedule stream t)


* certain things that where ambiguous for ChucK (int vs float vs stream), is removed, if a function name is found by the transpiler it is automatically called and the other values are also turned into static stream functions, to satisfy the requirements of ChucK syntax. This allows for mixed values in one list:

(3 0.8 (rv 1 10))

 

Here 3 is an integer, 0.8 a float and (rv 1 10) generates values between 1 and 10. Cisp transpiler will automatically turn everything into streams, so the array fulfills the single type requirement of ChucK when compiled.

* light-weight functions: any concrete value be replaced with a function. Since my functions are all of the same type (stream), type definitions can be removed.

 

{keywords: [cisp, sound, synthesis, syntax, function, writing, chuck]}


 

One of my early chuck programs:


 

// basic FM synthesis using sinosc
////////////////////////////////////////////////////////////////
// modulator to carrier
Impulse i => Delay d => Gain g;
d => Gain feedback => BiQuad f => d;

g => Safe safe;


//g => Pan2 panner => dac;

PanBin panner;
panner.connect( safe );

0.5 => f.a2;
-0.5 => f.a1;
0.6 => f.b1;
-0.5 => f.b0;

2::second => d.max;

0.7 => feedback.gain;


int x, y, choice,t;

0 => x;

float dynarray[];
float timarray[];
int farray[];
float panarray[];
float timemup;

dur entrydelay;
now => time then;
now => time then2;

SeqUnit seqD,seqT,seqF,seqP,seqS;
string state;

[seqD,seqT,seqP,seqF] @=> SeqUnit allSeqs[];

for (int j;j<4;j++)
{
    [5,4,3,2,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,1,2,3,4,5,6] @=> allSeqs[j].patArray;
    "pattern" => allSeqs[j].state;
    4 => allSeqs[j].mutateSize;
    500 => allSeqs[j].minMutateT;
    2000 => allSeqs[j].maxMutateT;
    allSeqs[j].init();
}

while (true)
{
    if ((now - then)>=entrydelay)
    {
        cs.rv(0,3) => choice;
        if (choice == 0)
            cs.addArrayf(cs.fillExpF(4,0.,1.,2),cs.fillExpF(4,0.,-1,2)) @=> dynarray;
        if (choice == 1)
            [-1.,-1,-1,-1,1,1,1,1] @=> dynarray;
        if (choice == 2) // two loud, rest soft
            [0.001,-0.002,0.001,-0.003,1,-1,-0.004,0.002] @=> dynarray;
        if (choice == 3)
            [-0.01,0.001,0.004,-0.2,1,0.0030,0.05,-0.1] @=> dynarray;
        cs.rv(0,3) => choice;
        if (choice == 0) //
            cs.fillChoicef(8,[1.,2,4,8,16,32,64]) @=> timarray;
        if (choice == 1)
            cs.fillExpF(8,1.,160.,3) @=> timarray;
        if (choice == 2)
            cs.fillChoicef(8,[cs.rvf(1,100),cs.rvf(30,40)]) @=> timarray;
        if (choice == 3)
            cs.stepperf(cs.rvf(1,4),cs.choosef([1.,2,5,10]),8) @=> timarray;
        cs.rv(0,3) => choice;
        if (choice == 0)
            cs.stepper(cs.rv(10,30),cs.rv(1,4),8) @=> farray;
        if (choice == 1)
            cs.fill(8,20,120) @=> farray;
        if (choice == 2)
            cs.fill(8,80,126) @=> farray;
        if (choice == 3)
            cs.stepper(30,7,8) @=> farray;
        cs.rv(0,3) => choice;
        if (choice == 0)
            [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,1.0] @=> panarray;
        if (choice == 1)
            cs.fillChoicef(8,[cs.rvf(0,1),cs.rvf(0,1)]) @=> panarray;
        if (choice == 2)
            [0.,.25,.5,.75,0,.25,.5,.75] @=> panarray;
        if (choice == 3)
            cs.stepperf(cs.rvf(1.,2.),0.,8) @=> panarray;
        cs.choosef([.1,.5,1.,2,4,3.5])::second => entrydelay;
        cs.choosef([100.]) => timemup;
        cs.choosef([.7,.94,.5]) => feedback.gain;
        now => then;
        
        cs.choosef([.01,0.001,0.0005]) => float plemming;
        
        // grouprep values
        
        /*
        cs.rv(0,1) => choice;
        
        
        if (choice == 0) {
            for (int j;j<4;j++) {
                [[0,1],[1,1],[2,1],[3,1],[4,1],[5,1],[6,1],[7,1]] @=> allSeqs[j].whichrepw;
                [[100,1],[200,1],[500,1],[100,1],[200,1],[500,1],[100,1],[500,1]] @=> allSeqs[j].nrepw;
            }
        }
        if (choice == 1) {
            for (int j;j<4;j++) {
                [[0,cs.rv(1,10)],[1,cs.rv(1,10)],[2,cs.rv(1,10)],[3,cs.rv(1,10)],[4,cs.rv(1,10)],[5,cs.rv(1,10)],[6,cs.rv(1,10)],[7,cs.rv(1,10)]] @=> allSeqs[j].whichrepw;
                [[50,20],[10,10],[200,5],[500,2],[100,1],[1000,1]] @=> allSeqs[j].nrepw;
            }
        }
        */
        
        cs.rv(0,1) => choice;
        for (int j;j<allSeqs.size();j++) {
            if (choice == 0) {
                [0,1,0,1,0,1,0,1,0,1,2,1,0,1,2,100,0,1,2,1,0,1,2,3,4,3,2,6,1,2,3,4,5,6,5,4,3,2,1] @=> allSeqs[j].patArray;
            }
            if (choice == 1) {
                [0,0,0,0,0,0,1,1,1,1,1,1,600,5,4,1,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,1] @=> allSeqs[j].patArray;
            }
        }
   
    }
    
    timarray[seqT.next()]*timemup*1::samp => now;
    (1.0/Std.mtof(farray[seqF.next()]) )::second => d.delay;
    dynarray[seqD.next()]*0.5=> i.next;
    panarray[seqP.next()] => panner.pan;
}


          
       {kind: code, keywords: [_, cisp, sound, synthesis, syntax]}

There is a very similar set of classes and objects in Supercollider: Patterns (which can produce Streams) on the SClang side and Demand rate ugens on the server side.

 

In Supercollider, patterns are always defined with a length, while in my case, the default is infinite. In Cisp, ending/splicing a stream is the responsibility of the calling Stream.


Instead of having a separate class (Pattern) that produces Streams, Streams in Cisp are created by functions only.


Another difference is that Streams in ChucK can be applied on both audio rate and control rate, since the "barrier" between audio and control rate is much lower.


A limitation compared to supercollider is that Streams in Cisp are limited to the float type, in Supercollider any object can be made into a Stream.

---
meta: true
event: almat2020
kind: essay
date: 200919
author: Casper Schipper
place: Online

keywords: [cisp, transpiler, non standard synthesis, python, chuck, coding, programming, sound, syntax, functioning]
---