What does robot love sound like?

Matthew Reed
8 min readJan 21, 2023

--

On this webpage, you will meet two programs, dnb.ck and love.ck. Each of these uses pre-trained word vectors to generate a “poem” and prints it out along with some interesting musical ideas. Is this poetry? Is this even music? These are all good questions. But before we think about it, let’s meet these programs.

First, meet dnb.ck:

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

// calculate 8th note
0.25 => float eighth;
0.01 => float MIN_EIGHTH;
0.2 => float MAX_EIGHTH;
float eighthVec1[model.dim()];
float eighthVec2[model.dim()];
model.getVector("frantic", eighthVec1);
model.getVector("calm", eighthVec2);

1 => float pitch;
0.3 => float MIN_PITCH;
1.5 => float MAX_PITCH;
float pitchVec1[model.dim()];
float pitchVec2[model.dim()];
model.getVector("wet", pitchVec1);
model.getVector("sharp", pitchVec2);

1 => float randDropout;
0 => float MIN_RAND_DROPOUT;
1 => float MAX_RAND_DROPOUT;
float randDropoutVec1[model.dim()];
float randDropoutVec2[model.dim()];
model.getVector("pizza", randDropoutVec1);
model.getVector("hypocrite", randDropoutVec2);


16 => int SEQLENGTH;

8 => int MAXSTEPS;

[1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0] @=> float kickPattern[];
[0.0,1.0,0.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0] @=> float hihatPattern[];
[0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.6,0.0,0.0,1,0.0,0.0,0.0] @=> float clapPattern[];
[1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0] @=> float hihat2Pattern[];
[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,1.0] @=> float snarePattern[];

0 => int step;

// getting kick
SndBuf kick => dac;
me.dir() + "sounds/kick2.wav" => filepath;
kick.read(filepath);
kick.pos(kick.samples());

// getting hat
SndBuf hihat => dac;
me.dir() + "sounds/hat.wav" => filepath;
hihat.read(filepath);
hihat.pos(hihat.samples());

// getting hat2
SndBuf hihat2 => dac;
me.dir() + "sounds/hat2.wav" => filepath;
hihat2.read(filepath);
hihat2.pos(hihat2.samples());

// getting clap
SndBuf clap => dac;
me.dir() + "sounds/clap.wav" => filepath;
clap.read(filepath);
clap.pos(clap.samples());

// getting snare
SndBuf snare => dac;
me.dir() + "sounds/snare.wav" => filepath;
snare.read(filepath);
snare.pos(snare.samples());

// make a ConsoleInput
ConsoleInput in;
// tokenizer
StringTokenizer tok;
// line
string line[0];
string words[];

4 => int k;
string currWords[k];
float currWordVec[model.dim()];
20 => int PADDING;
string padding1;
string padding2;
string padding3;
float startWord[model.dim()];
float endWord[model.dim()];

<<< "", "Type 'help' for help" >>>;

while (true) {
getInput() @=> words;
// for (0 => int i; i < words.size(); 1 +=> i) {
// <<< words[i] >>>;
// }
if (words.size() == 1) {
if (words[0] == "quit") {
break;
} else if (words[0] == "help") {
help();
}
} else if (words.size() == 2) {
if (words[0] == "repeat") {
Std.atoi(words[1]) => MAXSTEPS;
<<<"set num repeats to", MAXSTEPS >>>;
}
} else if (words.size() == 3) {
if (words[0] == "poem") {
poem();
}
}
}

fun void poem () {
<<< "Here is a poem that starts as", words[1], "and ends as", words[2] >>>;
model.getVector(words[1], startWord);
model.getVector(words[2], endWord);
0 => step;
while (step < MAXSTEPS * SEQLENGTH){
getCurrentWords();
calculateParameters();
stepForward();
}
}

fun void stepForward(){
step % SEQLENGTH => int modStep;
if (kickPattern[modStep] $ int && dropOut() == 0) {play(kick, kickPattern[modStep], 0);} else {"" => currWords[0];}
if (clapPattern[modStep] $ int && dropOut() == 0) {play(clap, clapPattern[modStep], 0);} else {"" => currWords[1];}
if (hihatPattern[modStep] $ int && dropOut() == 0) {play(hihat, hihatPattern[modStep] * 0.6, 0.1);} else {"" => currWords[3];}
if (hihat2Pattern[modStep] $ int && dropOut() == 0) {play(hihat2, hihat2Pattern[modStep], 0.1);}
if (snarePattern[modStep] $ int && dropOut() == 0) {play(snare, snarePattern[modStep] * 0.3, 0.1);} else {"" => currWords[2];}

"" => padding1;
for (0 => int i; i < PADDING-currWords[0].length(); 1 +=> i) {
" " +=> padding1;
}
"" => padding2;
for (0 => int i; i < PADDING-currWords[1].length(); 1 +=> i) {
" " +=> padding2;
}
"" => padding3;
for (0 => int i; i < PADDING-currWords[2].length(); 1 +=> i) {
" " +=> padding3;
}
1 +=> step;
<<< "", currWords[0], padding1, currWords[1], padding2, currWords[2], padding3, currWords[3] >>>;
wait();
}

fun void play (SndBuf buf, float gain, float rand){
(Math.random2f(-rand, rand) + 1) * pitch => buf.rate;
gain => buf.gain;
0 => buf.pos;
}

fun void rest () {
wait();
}

fun void wait () {
eighth::second => now;
}

fun void help () {
<<< "","COMMANDS:" >>>;
<<< "","repeat <int>" >>>;
<<< "","poem <word1> <word2>">>>;
<<< "","quit" >>>;
}

fun string[] getInput() {
// prompt
in.prompt( ">" ) => now;

// read
while( in.more() )
{
// clear tokens array
line.clear();
// get the input line as one string; send to tokenizer
tok.set( in.getLine() );
// tokenize line into words separated by white space
while( tok.more() )
{
// put into array (lowercased)
line << tok.next().lower();
}
// if non-empty
if( line.size() )
{
// do something with the input
return line;
}
return getInput();
}
}

fun void calculateParameters() {
interpolateVectors(eighthVec1, eighthVec2, MIN_EIGHTH, MAX_EIGHTH) => eighth;
interpolateVectors(pitchVec1, pitchVec2, MIN_PITCH, MAX_PITCH) => pitch;
interpolateVectors(randDropoutVec1, randDropoutVec2, MIN_RAND_DROPOUT, MAX_RAND_DROPOUT) => randDropout;
}

fun float interpolateVectors(float minVec[], float maxVec[], float min, float max) {
distance(currWordVec, minVec) => float sim1;
distance(currWordVec, maxVec) => float sim2;
sim1 / (sim1 + sim2) => float alpha;
return min * alpha + max * (1 - alpha);
}

fun void getCurrentWords() {
(step $ float) / (MAXSTEPS * SEQLENGTH$ float) => float completion;
for (0 => int i; i < model.dim(); 1 +=> i) {
(startWord[i] * (1-completion)) + (endWord[i] * completion) => currWordVec[i];
}
model.getSimilar(currWordVec, currWords.size(), currWords);
}

fun float cosineSimilarity(float vec1[], float vec2[]) {
0 => float dotProduct;
0 => float sumSq1;
0 => float sumSq2;
for (0 => int i; i < vec1.size(); 1 +=> i) {
Math.pow(vec1[i], 2) +=> sumSq1;
Math.pow(vec2[i], 2) +=> sumSq2;
vec1[i] * vec2[i] +=> dotProduct;
}
return dotProduct / (Math.pow(sumSq1, 0.5) * Math.pow(sumSq2, 0.5));
}

fun float distance(float vec1[], float vec2[]) {
0 => float sumSq;
for (0 => int i; i < vec1.size(); 1 +=> i) {
Math.pow(vec1[i] - vec2[i], 2) +=> sumSq;
}
return Math.pow(sumSq, 0.5);
}

fun int dropOut(){
Math.random2f(0,1) < randDropout => int result;
return result;
}

This program explores how we can convert words into a drum and bass beat in probably the worst way possible. First, this program asks for the user to input two words, let’s say “apple” and “Titanic.” Then the program outputs a poem that explores the words that linearly interpolate between these two words using their word vector space. Variations are introduced into the beat by measuring the current words’ relative closeness to various seemingly random words. For instance, the tempo of the beat is determined by the poem’s trajectory and how close it approaches the words “frantic” and “calm”. If for example “apple” is much calmer than “titanic”, then the poem will start slow and speed up over time. The pitch of the drum samples is determined in part by the words’ distance to “wet” and “sharp”, and the dropout rate of various sequencer beats is determined by the words’ distance to “pizza” and “hypocrite” (very logical obviously).

Next, meet love.ck:

// 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();
// }
[0.0,0.5,1.0,1.5,2.0,2.5,3.0,3.5,4.0,4.5,5.0,5.5,6.0,6.5,7.0,7.5,8.0,8.5,9.0,9.5,10.0,10.5,11.0,11.5,12.0] @=> float quarterToneScale[];
[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0] @=> float halfNoteScale[];
[0.0,2.0,4.0,5.0,7.0,9.0,11.0,12.0] @=> float majorScale[];
[0.0,2.0,4.0,7.0,9.0,12.0] @=> float pentatonicScale[];
[0.0,7.0,12.0] @=> float fifthScale[];
[0.0,12.0] @=> float octaveScale[];
[12.0] @=> float soleScale[];

[quarterToneScale,halfNoteScale,pentatonicScale,fifthScale,octaveScale, soleScale] @=> float scales[][];

Mandolin instrument => NRev rev => dac;
0.1 => rev.mix;
// 4 => instrument.preset;
0.8 => instrument.gain;

1.0 => instrument.noteOn;

Clarinet instrument2 => NRev rev2 => dac;
0.3 => rev2.mix;
// 4 => instrument.preset;
0.0 => instrument2.gain;
Std.mtof(50) => instrument2.freq;

1.0 => instrument2.noteOn;

Clarinet instrument3 => NRev rev3 => dac.left;
0.3 => rev3.mix;
// 4 => instrument.preset;
0.0 => instrument3.gain;
Std.mtof(54) => instrument3.freq;

Clarinet instrument4 => NRev rev4 => dac.right;
0.3 => rev4.mix;
// 4 => instrument.preset;
0.0 => instrument4.gain;
Std.mtof(57) => instrument4.freq;

1.0 => instrument3.noteOn;

[26,10,14,10,-1,
24,14,10,14,-1,
22,18,6,18,-1,
20,22,2,22,-1,
20,46,-1,
20,46,-1,
20,46,-1,
20,46,-1,
22,42,-1,
24,38,-1,
26,34,-1,
28,30,-1,
30,26,-1,
32,22,-1,
34,18,-1,
36,14,-1,
38,10,-1,
40,6,-1,
42,2,-1] @=> int heart[];
0 => int heartIndex;
-1 => int currNum;

["l","o","v","e","i","s","l","o","v","e","l","o","v","e","i","s","d","r","e","a","m","s","l","o",
"v","e","i","s","l","i","f","e","l","o","v","e","i","s","m","i","n","d","l","o","v","e","i","s","l",
"o","v","i","n","g","l","o","v","e","i","s","w","o","n","d","e","r","l","o","v","e","i","s","c","r",
"a","z","y","l","o","v","e","i","s","l","o","v","e","d","l","o","v","e","i","s","h","a","p","p","y",
"l","o","v","e","i","s","w","o","n","d","e","r","f","u","l","l","o","v","e","i","s","p","a","s","s",
"i","o","n","l","o","v","e","i","s","i","m","a","g","i","n","e","l","o","v","e","i","s","s","o","u",
"l","l","o","v","e","i","s","t","r","u","e","l","o","v","e","i","s","m","e","l","o","v","e","i","s",
"r","e","m","e","m","b","e","r","l","o","v","e","i","s","g","o","e","s","l","o","v","e","i","s","f",
"r","i","e","n","d","s","l","o","v","e","i","s","a","l","w","a","y","s","l","o","v","e","i","s","k",
"i","n","d","l","o","v","e","i","s","f","u","n","l","o","v","e","i","s","c","r","y","l","o","v","e",
"i","s","s","o","m","e","t","h","i","n","g","l","o","v","e","i","s","l","i","t","t","l","e","l","o",
"v","e","i","s","i","n","s","p","i","r","a","t","i","o","n","l","o","v","e","i","s","l","u","c","k",
"l","o","v","e","i","s","t","h","i","n","g","l","o","v","e","i","s","k","i","s","s","l","o","v","e",
"i","s","l","i","k","e","l","o","v","e","i","s","f","o","r","g","e","t","l","o","v","e","i","s","t",
"a","l","k","i","n","g","l","o","v","e","i","s","f","o","r","e","v","e","r","l","o","v","e","i","s",
"e","v","e","r","y","o","n","e","l","o","v","e","i","s","s","t","r","a","n","g","e","r","l","o","v",
"e","i","s","g","o","o","d","l","o","v","e","i","s","r","e","a","l","l","y","l","o","v","e","i","s",
"e","v","e","r","y","t","h","i","n","g","l","o","v","e","i","s","l","o","v","e","r","s","l","o","v",
"e","i","s","s","h","e","l","o","v","e","i","s","t","a","k","e","s","l","o","v","e","i","s","m","a",
"y","b","e","l","o","v","e","i","s","s","o","r","t","l","o","v","e","i","s","c","o","u","p","l","e",
"l","o","v","e","i","s","n","o","t","h","i","n","g","l","o","v","e","i","s","m","o","m","l","o","v",
"e","i","s","j","o","y","l","o","v","e","i","s","h","e","r","s","e","l","f","l","o","v","e","i","s",
"d","a","d","l","o","v","e","i","s","s","e","e","i","n","g","l","o","v","e","i","s","c","o","m","e",
"s","l","o","v","e","i","s","k","n","o","w","i","n","g","l","o","v","e","i","s","r","e","a","l","i",
"t","y"] @=> string letters[];
0 => int letterIndex;

newLine();
newLine();
// getLoveSynonyms();
poem();

fun void poem () {
while (drawHeart() == 0) {
continue;
}
}

fun void getLoveSynonyms () {
float loveVector[model.dim()];
model.getVector("love", loveVector);
string synonyms[70];
model.getSimilar(loveVector, synonyms.size(), synonyms);
for (0 => int i; i < synonyms.size(); 1 +=> i) {
<<< synonyms[i] >>>;
}
}

fun int drawHeart() {
if (heartIndex == heart.size()) {
return 1;
}
if (currNum == -1) {
writeSpace(heart[heartIndex]);
0 => currNum;
1 +=> heartIndex;
}
writeLetter();
1 +=> currNum;
if (currNum == heart[heartIndex]) {
-1 => currNum;
1 +=> heartIndex;
if (heart[heartIndex] == -1){
newLine();
1 +=> heartIndex;
}
}
return 0;
}

fun void newLine() {
chout <= IO.newline(); chout.flush();
}

fun void writeSpace(int length) {
for (0 => int j; j < length; 1 +=> j) {
chout <= " ";
}
chout.flush();
}

fun void writeLetter() {
chout <= letters[letterIndex % letters.size()];
1 +=> letterIndex;
chout.flush();
// chooseRandomPitch() => instrument.freq;
if (Math.random2f(0,1) < 0.2) {
chooseRandomPitch() => instrument.freq;
1.0 => instrument.noteOn;
}
(1000 - letterIndex) / 3000.0 => instrument.gain;
letterIndex / 5000.0 => instrument2.gain;
letterIndex / 10000.0 => instrument3.gain;
letterIndex / 5000.0 => instrument4.gain;
// 1.0 => instrument2.noteOn;
wait(50);
}

fun float chooseRandomPitch () {
0 => int scaleIndex;
Math.random2f(0,1) * Math.pow(heartIndex,2) => float num;
if (num > 1500) {
5 => scaleIndex;
} else if (num > 1000) {
4 => scaleIndex;
} else if (num > 400) {
3 => scaleIndex;
} else if (num > 200) {
2 => scaleIndex;
}
scales[scaleIndex] @=> float scale[];
randomElementOfArray(scale) => float note;
return Std.mtof(50 + note);
}

fun float randomElementOfArray (float array[]) {
return array[Math.random2(0,array.size() - 1)];
}

fun void wait (int millis) {
millis::ms => now;
}

This program prints out “loveis_____” a bunch of times in the shape of a heart, where the _____ are the 100ish closest words to love in the vector space. I intended for this program to look and sound like a computer learning about the concept of love. As it absorbs these various definitions of love, the notes that the machine outputs are progressively less microtonal and start to use more and more consonant intervals. A clarinet major chord emerges from the dark as the machine begins to understand that phenomenal experience of human love.

So what have these programs taught me about AI and art? One, that they are really good at making CCRMAcore. Two, I have a lot to learn about this intersection of the two fields. I don’t know what art is, and I don’t really know what AI is either, so how am I supposed to make any sense of the intersection? What I enjoyed in this process was the ability to use AI as a tool for creative expression. At no point did AI “make” any of these poems (as much as I wish I could say it did and that I’m not just a terrible poet), rather, the AI was simply the medium I used for expression. Especially in the love case, I think the poem says something about humanity, love, and the ideas we have about love. But these programs have a long way to go before they are being creative. The way I view love.ck is that it used a lot of complicated math to output a top 100 list of concepts we have about love, along with some aesthetic bits that I added.

I still felt very in control of both of these programs. This is something that gives me comfort. I am terrified of the day that I truly believe that AI is being creative. What role, then, do I play in the creative process? I liked being at the helm of this project. I liked being able to tweak how the AI tools were being used and the various parameters associated with them. I don’t think this kind of use of AI is what we should be scared of. We should be scared of when the AI starts writing poetry without us telling it to.

--

--