Etude 1: Poets of Sound and Time

Calvin Laughlin
10 min readJan 20, 2024

--

In this programming etude, we created experimental poetry using the audio programming language ChucK and chAI (ChucK for AI), with the AI being utilized through Word2Vec to create machine-generated words within the poems.

A Well Intentioned (Awkward) Poem

This poem reflects the all-too-real experience of intending to give someone a compliment but just stumbling through it, crashing and burning as you go. You keep trying, but you just can’t quite grab the right word and just look worse and worse. However, at the end, the compliment is saved and (hopefully) the person you intended to give it too isn’t too put off by your awkwardness.

The program takes in a user generated compliment, then uses chAI to generate 50 similar words to that compliment and randomly chooses 7, stammering and tripping while it does it, until settling on the desired word. The user can subject themselves to this torture as many times as they’d like.

//------------------------------------------------------------------------------
// name: poem-awkward.ck
// desc: trying to tell someone they're pretty but you can't speak
//
// version: need chuck version 1.5.0.0 or higher
// sorting: part of ChAI (ChucK for AI)
//
// "you're so... uh..."
// -- someone who doesn't know how to profess emotion
//
// NOTE: need a pre-trained word vector model, e.g., from
// https://chuck.stanford.edu/chai/data/glove/
// glove-wiki-gigaword-50-tsne-2.txt (400000 words x 2 dimensions)
//
// author: Calvin Laughlin
// date: Winter 2024
//------------------------------------------------------------------------------


// AI STUFF --------------------------------------------------------------------

// instantiate
Word2Vec model;
// pre-trained model to load
me.dir() + "glove-wiki-gigaword-50-tsne-2.txt" => string filepath;
// load pre-trained model (see URLs above for download)
if( !model.load( filepath ) )
{
<<< "cannot load model:", filepath >>>;
me.exit();
}

50 => int K_NEAREST;
// Number of failed compliment tries
7 => int NUM_TRIES;
// desired compliment
"beautiful" => string compliment;
// word vector
float vec[model.dim()];
// search results
string words[K_NEAREST];

// stumbling words
["uh", "like", "like...", "so", "you know",
"um", "um...", "i mean", "you're so", "..."] @=> string stumbles[];

// starters
["i mean", "i mean...", "you're like", "you're like...","you're so", "you're so ?",
"I'd say you're", "you're", "you are",
"i'm trying to say", "what i mean is"] @=> string starters[];

// get similar words
model.getSimilar( compliment, words.size(), words);


// SOUND DESIGN ----------------------------------------------------------------

[261.63, 293.66, 311.13, 329.63, 369.99, 392.00, 415.30] @=> float notes[];

// timing
400::ms => dur T_WORD; // duration per word
false => int shouldScaleTimeToWordLength;
T_WORD => dur T_LINE_PAUSE; // little pause after each line
T_WORD * 2 => dur T_STANZA_PAUSE;

// sonify
fun void play(float pitch) {
// play(pitch, Math.random2f(.8,1));
play(pitch, 0.99999);
}

// sonify
fun void play(float pitch, float velocity) {
// feedforward
Noise imp => OneZero lowpass => dac;
// feedback
lowpass => Delay delay => lowpass;

// our radius
velocity => float R;
// our delay order
pitch => float L;
// set delay
L::samp => delay.delay;
// set dissipation factor
Math.pow( R, L ) => delay.gain;
// place zero
-1 => lowpass.zero;

// fire excitation
1 => imp.gain;
// for one delay round trip
L::samp => now;
// cease fire
0 => imp.gain;
}

fun void drum()
{
["data/Bngo_1.wav", "data/Bngo_2.wav", "data/Bngo_3.wav", "data/Bngo_4.wav"] @=> string files[];
files[Math.random2(0,files.size()-1)] => string soundPath;
// Create an audio source
SndBuf buffer => dac;
// Load the WAV file
soundPath => buffer.read;
// Play the sound
buffer => blackhole;
// Wait for the sound to finish
buffer.length() => now;
}


// CONSOLE INPUT -------------------------------------------------------

ConsoleInput in;
StringTokenizer tok;
string line[0];

// line break
chout <= IO.newline(); chout.flush();


while( true )
{
// prompt
in.prompt( "what compliment are you trying to give? => " ) => now;

// read
while( in.more() )
{
// remember the input
in.getLine() => compliment;
// get it
tok.set( compliment );
// clear array
line.clear();
// print tokens
while( tok.more() )
{
// put into array
line << tok.next().lower();
}
// if non-empty
if( line.size() == 1 )
{
// execute
chout <= IO.newline(); chout.flush();
run_poem( compliment );
chout <= IO.newline(); chout.flush();
chout <= IO.newline(); chout.flush();
}
else
{
<<< "c'mon, you can think of something...", "" >>>;
}
}
}


// POEM LOGIC ----------------------------------------------------------

// poem intro
fun void intro()
{
say(" a"); play(notes[0]); wait(400::ms);
say("well"); play(notes[2]); wait(400::ms);
say("intentioned"); play(notes[4]); wait(400::ms);
say("(awkward)"); play(notes[3]); wait(600::ms);
say("poem"); play(notes[6]); wait(1::second);
// next line!
chout <= IO.newline(); chout.flush();
chout <= IO.newline(); chout.flush();
say( "hi!" ); play(notes[0]); wait(800::ms);
say("uh..."); drum();
say("I just wanted to say..."); drum();
say("that..."); drum();
say("you're so..."); drum();

model.getSimilar( compliment, words.size(), words);
words[Math.random2(0,words.size()-1)] => string missedCompliment;
while (missedCompliment == compliment)
{
words[Math.random2(0,words.size()-1)] => missedCompliment;
}

say( missedCompliment + "?"); play(notes[Math.random2(0,notes.size()-1)]); wait(400::ms);
say( "no! I mean..." ); wait(400::ms);

// next line!
chout <= IO.newline(); chout.flush();
chout <= IO.newline(); chout.flush();
// pause at end of line
T_LINE_PAUSE => now;
}

fun void say_line()
{
starters[Math.random2(0,starters.size()-1)] => string starterWord;
say(starterWord); drum();

Math.random2(0, 3) => int NUM_STUMBLES;
for ( int n; n < NUM_STUMBLES; n++)
{
stumbles[Math.random2(0,stumbles.size()-1)] => string filler;
say(filler); drum();
}

// get similar words
model.getSimilar( compliment, words.size(), words);
words[Math.random2(0,words.size()-1)] => string missedCompliment;
while (missedCompliment == compliment)
{
words[Math.random2(0,words.size()-1)] => missedCompliment;
}
say( missedCompliment + "?"); play(notes[Math.random2(0,notes.size()-1)]); wait(400::ms);
say( "no! I mean..." );
// next line!
chout <= IO.newline(); chout.flush();
// pause at end of line
T_LINE_PAUSE => now;
}

// Finish with the good compliment
fun void goodLine(string compliment)
{
[130.81, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88] @=> float notes[];

say("what I'm trying to say is..."); wait(1::second);
say(" you're "); play(notes[6]); wait(1::second);
say(" just "); play(notes[4]); wait(1::second);
say(" so "); play(notes[2]); wait(1::second);
chout <= IO.newline(); chout.flush();
chout <= IO.newline(); chout.flush();
say(" " + compliment); play(notes[0]); wait(3::second);
T_LINE_PAUSE => now;
}

// main terminal logic
fun void run_poem(string compliment)
{
intro();
for ( 1 => int s; s < NUM_TRIES; s++ )
{
say_line();
if (s % 3 == 0 && s != 1)
{
chout <= IO.newline(); chout.flush();
}
}
chout <= IO.newline(); chout.flush();
goodLine(compliment);
chout.flush();
}


// SAY AND WAIT --------------------------------------------------------

// say a word with space after
fun void say( string word )
{
say( word, " " );
}

// say a word
fun void say( string word, string append )
{
// print it
chout <= word <= append; chout.flush();
}

// wait
fun void wait()
{
Math.random2(400, 1600)::ms => T_WORD;
wait( T_WORD );
}

// wait
fun void wait( dur T )
{
// let time pass, let sound...sound
T => now;
}

// new line with timing
fun void endl()
{
endl( T_WORD );
}

// new line with timing
fun void endl( dur T )
{
// new line
chout <= IO.newline(); chout.flush();
// let time pass
T => now;
}

One Day After Another

Sometimes, when you’re in a routine, it’s easy to feel like every day is just a collection of moments, one after another, and you’re just living one day to the next, to the next, until they pass so quickly that you don’t even know what you’re doing day to day anymore and it all becomes a blur (deep).

This program starts with three random actions for each point of the day: morning, afternoon, and evening. As the program continues, the BPM steadily increases 25 per iteration, and we use Word2Vec to choose related words that get more chaotic as it continues. The user has the option to use the keyboard to choose another random action to reset each period of the day if the words get too chaotic, which allows for the illusion of control over their destiny day to day (right CMD: morning, right OPTN: afternoon, right SHIFT: evening).

//------------------------------------------------------------------------------
// name: poem-every-day.ck
// desc: the unstoppable march of time
//
// version: need chuck version 1.5.0.0 or higher
// sorting: part of ChAI (ChucK for AI)
//
//
// NOTE: need a pre-trained word vector model, e.g., from
// https://chuck.stanford.edu/chai/data/glove/
// glove-wiki-gigaword-50.txt (400000 words x 2 dimensions)
//
// author: Calvin Laughlin
// date: Winter 2024
//------------------------------------------------------------------------------

// AI STUFF --------------------------------------------------------------------

// instantiate
Word2Vec model;
// pre-trained model to load
me.dir() + "glove-wiki-gigaword-50.txt" => string filepath;
// load pre-trained model (see URLs above for download)
if( !model.load( filepath ) )
{
<<< "cannot load model:", filepath >>>;
me.exit();
}

100 => int numWords; // Number of words to consider

// Function to get nearest words in Word2Vec space
fun string[] getNearestWords(string word, int numWords) {
string nearestWords[numWords];
if (model.contains(word) && model.getSimilar(word, numWords, nearestWords)) {
return nearestWords;
}
return null;
}

// Function to find the index of a word in the global 'nearestWords' array
fun int findIndex(string word, string nearestWords[]) {
for (0 => int i; i < nearestWords.size(); i++) {
if (nearestWords[i] == word) {
return i; // Return index if word is found
}
}
return -1; // Return -1 if word is not found
}

// SOUND DESIGN ----------------------------------------------------------------

// Initialize BPM
60 => float bpm; // Starting at 60 BPM
60 / bpm => float beatDuration;

// Synthesized Bass Drum
SinOsc bass => ADSR envBass => dac;
0.5::second => envBass.attackTime;
0.5::second => envBass.decayTime; // Short decay for a punchy bass drum
0 => envBass.sustainLevel;
0.1::second => envBass.releaseTime;

// Synthesized Snare Drum
Noise snare => ADSR envSnare => dac;
0.005::second => envSnare.attackTime;
0.1::second => envSnare.decayTime; // Short decay for a snappy snare
0 => envSnare.sustainLevel;
0.1::second => envSnare.releaseTime;

// Function to play the drum beat (default)
fun void playDrum() {
// Bass drum sound
100.0 => bass.freq;
envBass.keyOn();

// Snare drum sound
envSnare.keyOn();
}

// Function to play the drum beat (custom frequency)
fun void playDrum(int frequency) {
// Bass drum sound
frequency => bass.freq;
envBass.keyOn();

// Snare drum sound
envSnare.keyOn();
}

// WORD KEYBOARD CONTROL -------------------------------------------------------

["breakfast", "wake up", "shower", "exercise", "brush teeth"] @=> string morning[];
["lunch", "exercise", "class", "work", "walk"] @=> string afternoon[];
["dinner", "sleep", "work", "homework", "movie"] @=> string evening[];

0 => int currentIndex; // Keeping track of

229 => int SHIFT_RIGHT;
231 => int CMD_RIGHT;
230 => int OPTN_RIGHT;

morning[Math.random2(0, morning.size() - 1)] => string morningAction;
afternoon[Math.random2(0, afternoon.size() - 1)] => string afternoonAction;
evening[Math.random2(0, evening.size() - 1)] => string eveningAction;

getNearestWords(morningAction, numWords) @=> string nearestMorningWords[];
getNearestWords(afternoonAction, numWords) @=> string nearestAfternoonWords[];
getNearestWords(eveningAction, numWords) @=> string nearestEveningWords[];

// Keyboard input listener
fun void keyboardListener() {
Hid hi;
HidMsg msg;

if (!hi.openKeyboard(0)) {
<<< "Keyboard not found" >>>;
me.exit();
}

while (true) {
hi => now;
while (hi.recv(msg)) {
if (msg.isButtonDown()) {
// Print the key code for each key press
// <<< "Key Pressed:", msg.which >>>;
// bpm + 5 => bpm;

// choose new actions

if (msg.which == SHIFT_RIGHT) {
evening[Math.random2(0, evening.size() - 1)] => eveningAction;
}
if (msg.which == OPTN_RIGHT) {
afternoon[Math.random2(0, afternoon.size() - 1)] => afternoonAction;
}
if (msg.which == CMD_RIGHT) {
morning[Math.random2(0, morning.size() - 1)] => morningAction;
}
}
}
}
}

spork ~ keyboardListener();

// Update actions based on input
fun void updateAction(string currentAction, string direction, string nearestWords[]) {
if (nearestWords != null) {
// findIndex(currentAction, nearestWords) => int currentIndex;
// <<< currentIndex >>>;
if (currentIndex != -1) { // Check if current word is found
if (direction == "left" && currentIndex > 0 && currentIndex < numWords) {
currentIndex - 1 => currentIndex;
nearestWords[currentIndex] => currentAction;
} else if (direction == "right" && currentIndex < nearestWords.size() - 1 && currentIndex + 1 < numWords) {
currentIndex + 1 => currentIndex;
nearestWords[currentIndex + 1] => currentAction;
}
}
}
}

// CONSOLE OUTPUT ---------------------------------------------------------------

// say a word with space after
fun void say( string word )
{
say( word, " " );
}

// say a word
fun void say( string word, string append )
{
// print it
chout <= word <= append; chout.flush();
}

// wait
fun void wait()
{
wait( beatDuration::second );
}

// wait
fun void wait( dur T )
{
// let time pass, let sound...sound
T => now;
}

// new line without pause
fun void newline()
{
chout <= IO.newline(); chout.flush();
}


// MESSAGE FUNCTIONS --------------------------------------------------------------

// main running function, steadily increasing
fun void clock_is_ticking()
{
// number of days to represent (without BPM cutoff)
1000 => int DAYS;

for (0 => int dy; dy < DAYS; dy++) {

say(" ...and a new day begins"); playDrum(); beatDuration::second => now; newline();
newline();
say(" " + morningAction); playDrum(); beatDuration::second => now; newline();
say(" " + afternoonAction); playDrum(); beatDuration::second => now; newline();
say(" " + eveningAction); playDrum(); beatDuration::second => now; newline();
newline();
say(" another day ends..."); playDrum(); beatDuration::second => now; newline();

// Increment BPM and recalculate beat duration
bpm + 10 => bpm;
60 / bpm => beatDuration;

if (bpm == 320)
{
break;
}

updateAction(morningAction, "right", nearestMorningWords);
updateAction(afternoonAction, "right", nearestAfternoonWords);
updateAction(eveningAction, "right", nearestEveningWords);
}

}

// last message
fun void final_message()
{
60 => bpm;
60 / bpm => beatDuration;

newline();
say("one"); playDrum(80); beatDuration::second => now;
say("day"); playDrum(100); beatDuration::second => now;
say("after"); playDrum(80); beatDuration::second => now;
say("another"); playDrum(60); beatDuration::second => now;
}


// MAIN ------------------------------------------------------------------------

// we begin...
newline();
clock_is_ticking();
final_message();

Reflection

I began with the “Well Intentioned (Awkward) Poem,” the idea of which came to me after I had a very similar real-life experience. In these situations, I often feel like a “bot” because the words that come out of my mouth do not seem my own, and my own mind takes on a Word2Vec mapping that just desperately calls Math.random2 to find something akin to what I was trying to say. Through making this poem, I kept coming back to one question:

What does it really mean to choose the right word?

Often, we have a conception of what we are “trying” to say, but it is often the things that we accidentally say that have the greatest impact. I have noticed in my own speech that sometimes, it’s better not to overthink it, and allow whatever comes to your mind through Math.random2.

I then moved onto “One Day After Another.” This poem was a bit darker than my awkward love poem, and through its creation I realized that it’s actually quite easy to express existential dread through the terminal. This one also provoked a question:

What is it that makes our days meaningful, and at what moment do we actually see change in ourselves?

I came to this question when I was implementing the “user control” aspect of the program. I put “user control” in quotes because the user only has a sliver of control: they can grab another random action to replace one part of the day, but this action is quickly swept up in the chaos and eventually loses its meaning as well. But perhaps it’s not what is actually happening that gives our days meaning; perhaps it is only how we think about our actions and what we choose to do the next day.

Acknowledgments

Some code generated with the help of ChatGPT. Inspiration and partial bits of code taken from Ge Wang’s poem “I Feel,” keyboard control inspiration taken from Andrew Zhu Aday’s “Jam Poetry” machine, and bongo samples taken from Alex Han’s “Shadows of Tomorrow.”

And a special thank you to Arabella for suggesting that I remove the dashes from the awkward poem.

--

--

Calvin Laughlin

Senior at Stanford University studying artificial intelligence and art history. Interested in the intersection of aesthetics, beauty, love and computers.