Etudes

Hannah Kim
7 min readJan 20, 2024

--

whiskers_of_whisdom.ck

music_is.ck

Creating these poems has been an intriguing journey.

For the first poem, I began by generating a stream-of-consciousness piece about a cat. I built a dictionary of cat-related words and periodically instructed the model to find similar words from that list, then printed out the results in a “cat-like path”. When I shared it with friends, they noted that it was quite characteristic of me, given my strong affinity for cats (especially since I am publishing a game called “Kittyopolis” with a few of my friends).

For my second poem, I wanted to delve into the concept of music and what it means to me. However, during the development of this poem, I encountered some frustrations with the model’s inability to consistently generate logical-sounding sentences and tunes. In the end, I decided to embrace the model’s limitations; I instead made a poem about what music signifies to the AI itself (meta). One cool aspect of this poem I’d like to highlight is the intentional design of the bass line accompanying the line “Music is in me.” The bass line is deep and tangible as if the music is literally moving within you.

I have never made “computer poems”, much less “computer music”, in my life. A few friends mentioned that my music sounds like something they would make in GarageBand in middle school; little do they know, I take that wholeheartedly as a compliment.

Throughout this process, I realized how cool it is that technology has democratized creativity and allowed us to explore artistic expressions that were once confined to traditional mediums. While my compositions may have a touch of simplicity reminiscent of middle school experimentation, I appreciate the authenticity of the comparison. It’s a reminder that art doesn’t always have to be complex or polished; sometimes, it’s the raw, unfiltered expressions that entertain.

What defines a poem, and what is the nature of music when it emerges from the algorithms and code of a machine? I think that even through computational models, words can be arranged, sometimes in ways that defy conventional grammar, to create pieces that evoke emotions, imagery, and contemplation. Similarly, music, harmonies, rhythms, and melodies produced by a computer may lack the human touch but can still evoke emotions, trigger memories, and inspire movement. It raises questions about what makes music truly “musical.” Is it the composer’s intention, the emotional resonance it elicits, or the sheer arrangement of sounds?

I mean, when did art ever stay confined by its current mediums? Art is radical, and I believe in the enduring power of creativity.

//-------------------------------------------------------
// whiskers-of-whisdom

// Generating a cat's stream of unconsciouness.
// A word2vec poem with sonification based on word vectors.
//
// author: Hannah Kim (hkim24@stanford.edu)
//-------------------------------------------------------

// Load sound file; default is "cat-meow.wav" in the same directory
me.dir() + "data/cat-meow.wav" => string filename;
// if there is argument, use it as the filename
if( me.args() ) me.arg(0) => filename;


// Default starting word for the poem
"meow" => string STARTING_WORD;
// Check arguments; if provided, set new starting word
if(me.args() > 0) me.arg(0) => STARTING_WORD;


// Instantiate Word2Vec model for word vector analysis
Word2Vec model;
// Load a pre-trained model from a specified file
me.dir() + "glove-wiki-gigaword-50.txt" => string filepath;
if(!model.load(filepath)) {
<<< "Cannot load model:", filepath >>>;
me.exit();
}

// Poem generation conditions
50 => int WORDS; // Number of words to generate
3 => int K_NEAREST; // Number of nearest words to retrieve for each word

// Timing settings
500::ms => dur T_WORD; // Duration per word
false => int shouldScaleTimeToWordLength; // Flag to scale time to word length

// Initialize variables for current word and its vector
STARTING_WORD => string word;
float vec[model.dim()];
string words[K_NEAREST]; // Array to hold nearest words
dur addTime; // Additional time for scaling to word length

// Additional cat-related words
["kitty", "purr", "catnip", "snuggle", "claw", "pounce", "scratch", "stretch", "milk"] @=> string catWords[];

// Indentation settings for visual representation of poem
10 => int currentIndent;
2 => int indentChange;
20 => int maxIndent;
0 => int minIndent;
1 => int indentIncreasing;
0.5 => float directionChangeProbability;



// Sound buffer for playing the sound file
SndBuf buf => dac;
// Load the sound file into the buffer
filename => buf.read;
// Range settings for word vector dimensions (for sound mapping)
float mins[0], maxs[0];
model.minMax(mins, maxs);


// Modify the sound buffer playback rate based on word vector
fun void modifyPlaybackRate(SndBuf buf, float vec[], float mins[], float maxs[]) {
Math.remap(vec[0], mins[0], maxs[0], 0.01, 2.3) => float playbackRate;
playbackRate => buf.rate;
}


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


// here's where everything runs!
for (int w; w < WORDS; w++)
{
// Print the word, with indentation
for (int i; i < currentIndent; i++) {
chout <= " ";
}
chout <= word <= " "; chout.flush();
// If scaling time with word length
if (shouldScaleTimeToWordLength)
word.length() * T_WORD => addTime;

// Get word vector (for sonification)
model.getVector(word, vec);
// Modify sound based on word vector
modifyPlaybackRate(buf, vec, mins, maxs);
// Play the sound
1 => buf.pos;
T_WORD + addTime => now;

// Occasionally switch to cat-specific words
if (Math.random2(0, 10) < 5) {
catWords[Math.random2(0, catWords.size() - 1)] => word;
} else {
// Get similar words
model.getSimilar(word, words.size(), words);
// Choose one at random
words[Math.random2(1, words.size() - 1)] => word;
}

// Randomly decide whether to change direction
if (Math.random2f(0.0, 1.0) < directionChangeProbability) {
if (indentIncreasing) {
0 => indentIncreasing;
} else {
1 => indentIncreasing;
}
}
// Adjust indentation based on current direction
if (indentIncreasing) {
indentChange + currentIndent => currentIndent;
if (currentIndent > maxIndent) {
maxIndent=> currentIndent;
false => indentIncreasing;
}
} else {
currentIndent - indentChange => currentIndent;
if (currentIndent < minIndent) {
minIndent => currentIndent;
true => indentIncreasing;
}
}

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

// Print at the end
chout <= IO.newline(); chout.flush();
chout <= "\"Whisker Whisdom\"" <= IO.newline();
chout <= "-- insights of a house cat" <= IO.newline();
chout.flush();
//-------------------------------------------------------
// music_is

// Chuck tells me what music is to them.
//
// author: Hannah Kim (hkim24@stanford.edu)
//-------------------------------------------------------

// Instantiate Word2Vec model and load pre-trained model
Word2Vec model;
me.dir() + "glove-wiki-gigaword-50-tsne-2.txt" => string filepath;
if(!model.load(filepath)) {
<<< "Cannot load model:", filepath >>>;
me.exit();
}

["is my", "is the", "is a"] @=> string startingWords[];
["whisper", "pulse", "voice", "color", "story", "symphony"] @=> string firstPhrases[];
["of the", "in the", "between the", "of my", "inside the", "along the", "to my"] @=> string connectingWords[];
["dreams", "sea", "stars", "life", "melody", "emotion", "shadow", "heart", "rhythm", "wind", "memory"] @=> string secondPhrases[];


fun void kick(){

SndBuf buf => Gain g => dac;
// read in the file
me.dir() + "data/kick.wav" => buf.read;
// set the gain
.5 => g.gain;

while (true){
// set the play position to beginning
0 => buf.pos;
// randomize gain a bit
Math.random2f(.8,.9) => buf.gain;
0.5::second => now;
}

}


fun void bass() {
// connect patch
SinOsc s => LPF filter => dac; // LPF is a low-pass filter
filter.freq(200.0);
.6=> s.gain;

// scale (in semitones)
[ 0, 3, 5 ] @=> int scale[];

// infinite time loop
while( true ) {
// get note class
scale[ Math.random2(0,2) ] => float freq;
// get the final freq
Std.mtof( 21.0 + 12 + freq) => s.freq;
// advance time
.125::second => now;
}

}

fun void melody() {
SinOsc s => dac;
.4=> s.gain;
[ 0, 3, 5, 7, 8] @=> int scale[];
while( true ) {
// get note class
scale[ Math.random2(0,4) ] => float freq;
// get the final freq
Std.mtof( 21.0 + 36 + freq) => s.freq;
if( Math.randomf() > .3 ) .125::second => now;
else .25::second => now;
}
}

fun void hihat(){
SndBuf buf => Gain g => dac;
me.dir() + "data/hi-hat.wav" => buf.read;
.5 => g.gain;
0.25::second=>now;
while( true )
{
0 => buf.pos;
Math.random2f(.8,.9) => buf.gain;
0.5::second => now;
}
}


fun void snare(){

SndBuf buf => Gain g => dac;
me.dir() + "data/snare.wav" => buf.read;
.5 => g.gain;
0.75::second=>now;
while( true )
{
0 => buf.pos;
Math.random2f(.01,.05) => buf.gain;
if( Math.randomf() > .3 ) .125::second => now;
else .25::second => now;
}

}

fun void clap(){
SndBuf buf => Gain g => dac;
me.dir() + "data/clap.wav" => buf.read;
.1 => g.gain;
1::second=>now;
while( true )
{
0 => buf.pos;
Math.random2f(.8,.9) => buf.gain;

while(true) {
repeat(3){
0 => buf.pos;
1::second => now;
}
0 => buf.pos;
.375::second => now;
.9 => buf.gain;
0 => buf.pos;
0.625::second => now;
}
}
}

fun string formSentence() {
string sentence;
startingWords[Std.rand2(0, startingWords.size()-1)] => string startingWord;
connectingWords[Std.rand2(0, connectingWords.size()-1)] => string connectingWord;
// Choose a first phrase
string firstPhrase;
if (Math.random2f(0.0, 1.0) < 0.2) {
firstPhrases[Std.rand2(0, firstPhrases.size()-1)] => firstPhrase;
} else {
string similarWords[20];
model.getSimilar(firstPhrases[Std.rand2(0, firstPhrases.size()-1)], 20, similarWords);
similarWords[Std.rand2(0, 19)] => firstPhrase;
}

// Choose a second phrase
string secondPhrase;
if (Math.random2f(0.0, 1.0) < 0.2) {
secondPhrases[Std.rand2(0, secondPhrases.size()-1)] => secondPhrase;
} else {

string similarWords[20];
model.getSimilar(secondPhrases[Std.rand2(0, secondPhrases.size()-1)], 20, similarWords);
similarWords[Std.rand2(0, 19)] => secondPhrase;
}

// Form the sentence
startingWord + " " + firstPhrase + " " + connectingWord + " " + secondPhrase => sentence;
return sentence;
}


fun void print_line(string line){
chout <= IO.newline(); chout.flush();
chout <= line;
}

fun void print_poem(int seconds){
chout <= IO.newline(); chout.flush();
chout<=" "+formSentence();
seconds::ms=>now;

}

0.5::second=>now;
print_line("Music \n"); spork~kick(); 2::second=>now;
print_line("Music \n"); 2::second=>now;
print_line("Music \n"); 2::second=>now;
print_line(" is\n"); 2::second=>now;
print_line(" my life.\n"); spork~hihat(); 2::second=>now;
print_line(" my world.\n"); 2::second=>now;
print_line(" in me.\n"); spork ~ bass(); 4::second=>now;

chout<="Music \n";
repeat(15){
print_poem(250);
}
chout <= IO.newline();
chout <= IO.newline();

chout<="Chuck, that doesn't even make sense.";
chout <= IO.newline(); chout.flush(); 1::second=>now;

spork~clap();
chout <= IO.newline(); chout.flush();
chout<="No. Music \n";
repeat(30){
print_poem(125);
}
spork~melody();
while(true){
print_poem(125);
}

chout <= IO.newline(); chout.flush();

--

--