100 Languages Speedrun: Episode 64: ChucK

taw

Tomasz Wegrzanowski

Posted on January 22, 2022

100 Languages Speedrun: Episode 64: ChucK

ChucK is a music programming language. As music is all about time, it describes itself as a "Strongly-timed, Concurrent, and On-the-fly Music Programming Language".

Programs described in this episode will make a lot of sounds, but due to technical limitations you won't be able to hear them, unless you download them and run them locally. You can install ChucK with brew install chuck or equivalent, so it shouldn't be too hard.

This is not my first encounter with ChucK, once upon a time I set it up to use DDR dance mat as a musical instrument, but that was a while back, so I don't remember all the details.

Hello, World!

ChucK won't support #!, we need to run scripts with chuck hello.ck. Here's a very simple one:

SinOsc hello => dac;
420.0 => hello.freq;
1::second => now;
Enter fullscreen mode Exit fullscreen mode

As you can see there's a lot of =>s. To quote the docs "The ChucK operator (=>) is a massively overloaded operator that, depending on the types involved, performs various actions", and that pretty much sums it up.

You can already see it perform a lot of different roles:

  • we create sine wave generator hello, then use the chuck operator to connects it to dac (audio output)
  • we chuck number 420.0 to hello.freq to change the sine wave frequency to 420.0 Hz
  • we chuck 1::second to now making the program run for 1 second with current settings

Loops

As you can't really hear the sounds, I'll try to print what's going on to the terminal:

SinOsc sine_wave => dac;

for (0 => int i; i<=12*5; i++) {
  Math.pow(2.0, i/12.0) * 55.0 => float freq;
  freq => sine_wave.freq;
  <<<"Playing frequency", freq>>>;
  0.1::second => now;
}
Enter fullscreen mode Exit fullscreen mode

Here's what happens, 0.1 seconds per iteration:

Playing frequency 55.000000
Playing frequency 58.270470
Playing frequency 61.735413
Playing frequency 65.406391
Playing frequency 69.295658
Playing frequency 73.416192
Playing frequency 77.781746
Playing frequency 82.406889
Playing frequency 87.307058
Playing frequency 92.498606
Playing frequency 97.998859
Playing frequency 103.826174
Playing frequency 110.000000
Playing frequency 116.540940
Playing frequency 123.470825
Playing frequency 130.812783
Playing frequency 138.591315
Playing frequency 146.832384
Playing frequency 155.563492
Playing frequency 164.813778
Playing frequency 174.614116
Playing frequency 184.997211
Playing frequency 195.997718
Playing frequency 207.652349
Playing frequency 220.000000
Playing frequency 233.081881
Playing frequency 246.941651
Playing frequency 261.625565
Playing frequency 277.182631
Playing frequency 293.664768
Playing frequency 311.126984
Playing frequency 329.627557
Playing frequency 349.228231
Playing frequency 369.994423
Playing frequency 391.995436
Playing frequency 415.304698
Playing frequency 440.000000
Playing frequency 466.163762
Playing frequency 493.883301
Playing frequency 523.251131
Playing frequency 554.365262
Playing frequency 587.329536
Playing frequency 622.253967
Playing frequency 659.255114
Playing frequency 698.456463
Playing frequency 739.988845
Playing frequency 783.990872
Playing frequency 830.609395
Playing frequency 880.000000
Playing frequency 932.327523
Playing frequency 987.766603
Playing frequency 1046.502261
Playing frequency 1108.730524
Playing frequency 1174.659072
Playing frequency 1244.507935
Playing frequency 1318.510228
Playing frequency 1396.912926
Playing frequency 1479.977691
Playing frequency 1567.981744
Playing frequency 1661.218790
Playing frequency 1760.000000
Enter fullscreen mode Exit fullscreen mode

You can see it already does multiple things:

  • chucking something into an int or float variable assigns to it
  • <<< ... >>> operator prints stuff
  • ChucK has while loops and C-style for loops, except it doesn't have = assignment, you need to use a => chuck operator, an it goes the other way
  • In Western music, notes are constant multiple of each other, that multiple being 2^(1/12), thus the Math.pow(2.0, i/12.0) * base_freq formula

Instruments

ChucK of course comes with a lot of predefined instruments, based on either physical models, or mathematical formula. Let's try a sitar as it has fairly simple controls:

Sitar sitar => dac;

while(true) {
  Math.random2(60, 80) => float note;
  Std.mtof(note) => sitar.freq;
  Math.random2f(0.5, 0.1) => sitar.noteOn;

  <<<"Playing note", note>>>;
  0.2::second => now;
}
Enter fullscreen mode Exit fullscreen mode
$ chuck instrument.ck
Playing note 64.000000
Playing note 74.000000
Playing note 71.000000
Playing note 75.000000
Playing note 65.000000
Playing note 67.000000
...
Enter fullscreen mode Exit fullscreen mode

Step by step:

  • we create a Sitar and connect it with the output dac. We won't really be doing that, but we can also connect instruments to various filters, mixers, analyzers, dynamically disconnect them, and so on.
  • Math.random2 generates random integer from a given range
  • Math.random2f generates random float from a given range
  • Std.mtof does the note to frequency calculations (the 2^(1/12) one), with note 69 being the 440 Hz. They were so close to greatness, and they blew it.
  • sitar.noteOn plucks a Sitar string with given strength - more complicated instruments have a lot of such parameters

Fibonacci

We could do it the usual way, but let's do a more convoluted way, with shreds (ChucK threads) and events:

class FibEvent extends Event {
  int argument;
  int result;
}

fun void reporter(string name, FibEvent e) {
  while(true) {
    // wait for event
    e => now;
    // report what we got the data
    <<<name, "reporting that fib of", e.argument, "is", e.result>>>;
  }
}

// the event
FibEvent e;

// spawn two listeners
spork ~ reporter("a", e);
spork ~ reporter("b", e);

1 => int a;
1 => int b;
1 => int i;

while(true) {
  0.1::second => now;

  i => e.argument;
  a => e.result;
  e.signal(); // send it to one of the reporters

  a + b => int c;
  b => a;
  c => b;
  i + 1 => i;
}
Enter fullscreen mode Exit fullscreen mode

It reports the usual Fibonacci sequence:

$ chuck fib.ck
a reporting that fib of 1 is 1
b reporting that fib of 2 is 1
a reporting that fib of 3 is 2
b reporting that fib of 4 is 3
a reporting that fib of 5 is 5
b reporting that fib of 6 is 8
a reporting that fib of 7 is 13
b reporting that fib of 8 is 21
a reporting that fib of 9 is 34
b reporting that fib of 10 is 55
a reporting that fib of 11 is 89
b reporting that fib of 12 is 144
a reporting that fib of 13 is 233
b reporting that fib of 14 is 377
a reporting that fib of 15 is 610
b reporting that fib of 16 is 987
a reporting that fib of 17 is 1597
b reporting that fib of 18 is 2584
a reporting that fib of 19 is 4181
b reporting that fib of 20 is 6765
...
Enter fullscreen mode Exit fullscreen mode

Step by step:

  • we can create custom event FibEvent inheriting from base Event, with some extra fields
  • we spawn a few shreds with spork ~ reporter("a", e) - to be honest I don't think renaming everything randomly like ChucK does is a particularly good practice
  • inside a reporter thread, e => now; waits to be signaled by event e - it's another overload of =>
  • in the main event we have infinite loop calculating fibonacci numbers
  • the interesting part is mainly e.signal() which signals one of the listeners that event is ready; there's also e.broadcast() which would signal all listeners

Samples

ChucK obviously can load sample files like .wav. Let's go through the whole list of samples included in the brew package:

[
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/special/geetar.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter8/audio/kick_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter8/audio/hihat_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/snare_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/cowbell_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/kick_04.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/clap_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/hihat_02.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/snare_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/snare_03.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/snare_02.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/cowbell_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/click_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/click_02.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/stereo_fx_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/kick_04.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/stereo_fx_03.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/kick_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/clap_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/hihat_04.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/hihat_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/hihat_02.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter5/audio/snare_03.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter5/audio/stereo_fx_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter5/audio/kick_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/snare_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/snare_03.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/snare_02.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/cowbell_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/click_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/click_02.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/stereo_fx_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/stereo_fx_03.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/kick_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/clap_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/hihat_04.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter4/audio/hihat_01.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/data/hihat.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/data/hihat-open.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/data/kick.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/data/snare.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/data/snare-chili.wav",
  "/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/data/snare-hop.wav"
] @=> string files[];

SndBuf buf => dac;

for(0 => int i; i<files.size(); i++) {
  // read a .wav
  files[i] => buf.read;
  // start at the beginning
  0 => buf.pos;
  <<<i, files[i]>>>;
  // wait however long buf is, to let it finish playing
  buf.length() => now;
}
Enter fullscreen mode Exit fullscreen mode

To assign non-primitive types we need to do @=> instead of =>.

The output is a lot of sounds and their files:

$ chuck samples.ck
0 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/special/geetar.wav
1 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter8/audio/kick_01.wav
2 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter8/audio/hihat_01.wav
3 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/snare_01.wav
4 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/cowbell_01.wav
5 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/kick_04.wav
6 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/clap_01.wav
7 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/chapter9/DrumMachine/audio/hihat_02.wav
8 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/snare_01.wav
9 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/snare_03.wav
10 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/snare_02.wav
11 /usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/cowbell_01.wav
...
Enter fullscreen mode Exit fullscreen mode

FizzBuzz

The classic FizzBuzz:

for (1 => int i; i<=100; i++) {
  if (i % 15 == 0) {
    chout <= "FizzBuzz" <= IO.newline();
  } else if (i % 5 == 0) {
    chout <= "Buzz" <= IO.newline();
  } else if (i % 3 == 0) {
    chout <= "Fizz" <= IO.newline();
  } else {
    chout <= i <= IO.newline();
  }
}
Enter fullscreen mode Exit fullscreen mode

We had to replace <<< >>> with C++ style output. <<< >>> outputs to stderr only, and does some weird formatting, so we can't really use it here.

ChucK renamed stdout and stderr to chout and cherr because it just loves doing such silly renames.

With these changes, we get exactly the FizzBuzz output.

Audio FizzBuzz

Here's ChucK playing FizzBuzz in audio form. Fizz and Buzz correspond to audio samples, numbers to notes on a sitar. It all sounds just as awful as you probably imagine.

Sitar sitar => dac;

SndBuf fizz => dac;
SndBuf buzz => dac;

"/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/book/digital-artists/audio/hihat_04.wav" => fizz.read;
"/usr/local/Cellar/chuck/1.4.1.0/share/chuck/examples/data/snare-chili.wav" => buzz.read;

fun void playFizz() {
  0 => fizz.pos;
  fizz.length() => now;
}

fun void playBuzz() {
  0 => buzz.pos;
  buzz.length() => now;
}

for (1 => int i; i<=100; i++) {
  if (i % 15 == 0) {
    playFizz();
    playBuzz();
  } else if (i % 5 == 0) {
    playBuzz();
  } else if (i % 3 == 0) {
    playFizz();
  } else {
    Std.mtof(20+i) => sitar.freq;
    Math.random2f(0.5, 0.1) => sitar.noteOn;
    0.1::second => now;
  }
}
Enter fullscreen mode Exit fullscreen mode

Should you use ChucK?

ChucK's niche is so far from anything I usually do, that I can't really tell you if it's any good.

It definitely lacks functionality for any non-music-related tasks, like files, strings, etc.

From what I can tell, it's not popular among its intended audience, so I guess the answer is a no. It still might be of some interest as an esoteric language, as it's just so weird.

Code

All code examples for the series will be in this repository.

Code for the ChucK episode is available here.

💖 💪 🙅 🚩
taw
Tomasz Wegrzanowski

Posted on January 22, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related