From Random Signal to Arrangemements

Generating Chord Progressions from Random Signal

Few articles with original writing appeared recently with their own interpretation of Gecho, not always entirely consistent with the truth.

In a fraction of them I noticed one misconception being perpetuated: reportedly, Gecho can "compose music out of chaos", i.e., invent melodies and chord progressions that sound nice. But wait, this sounds like a problem for A.I.! It can't really fit into a single-chip synthesizer. Or can it?

There are known rules about what sounds good toghether and what does not. Machine learning or genetic algorithms come to mind - why not try to apply few simple principles and see where it goes.

In machine learning, there are many approaches, but in one of them you basically have two functions (as in programming lanugage). One combines stuff in random ways, while the other knows certain rules, about how does a good outcome look like. In our case, this means "what sounds good to humans" - something, computer usually does not have a clue about. Which chord should come next after the current chord? Which notes sound nice with it? Perhaps, these aren't simply notes that happen to exist in that chord, right?

First function catches random information from environment, uses it to shuffle around the fragments of information (in our case notes) and then sends output to the second function, which either accepts or throws the combination away and asks for another. This needs to happen a lot, because in the complete chaos, good combinations may show up rarely.

Imagine this process as two little robots. One can read text from paper tape, full of gibberish. He rolls it in front of his eyes and when sees something that ressembles a chord, he writes it down to sheet of paper. After the sheet is full, he hands it to his friend, who knows something about music. The other robot has listened to many songs and knows what sounds fine and what doesn't. He has a red pen and quickly crosses out the awful bits, leaving something that us, humans, might enjoy listening to.

I've implemented the first robot. There are more possibilities about how to define the second one - we'll get to it later - meanwhile his action is substituted with patterns that define simple repeating of chords, adding some structure.

The Code

How it works:

  • Random stream of data is parsed to see if anything appears that looks like a chord, in the simplest notation that Gecho uses internally (where one character represents a 3-note chord).
  • 4 chords are collected in each round, this group forms a "fragment" of the song. The random signal is parsed until 8 of these fragments are stored in memory.
  • In order to add some structure, there are few patterns defined for repeating fragments, for example: {2,2,1,1,2,1,3,1,4,4,5,5,4,5,2,3} or {1,1,2,2,3,3,2,2,4,4,5,5,6,6,5,2} and one is picked up randomly. Otherwise, it would sound too chaotic of a completely new chord appeared every 2 seconds.
  • The chords are expanded from this simple notation to cover all 16 channels, in usual way.

This allows us to have a channel, where the song is *completely* invented from the noise.

Gecho's Internal encoding of chord progressions is a human-readable string format, e.g. "c3d#3e3", but it has some disadvantages. Certain notes are represented by more than one character, as a sharp (#) symbol is used. In order to make manipulations easier, I've added representation where "black" keys are represented by characters 'j' to 'n'. Basic c-minor chord (without octave information) becomes simply "cke". Similariy, whole chords are encoded by the key - "k" means d# and 'K' stands for D#.

Our first robot needs to know whether something in the random stream of data can be used as a chord. The function is simple, it operates on one character at a time:

int looks_like_a_chord(char text)
{
if( (text >= 'a' && text <= 'g') ||
(text >= 'A' && text <= 'G') ||
(text >= 'j' && text <= 'n') ||
(text >= 'J' && text <= 'N') )
{
return 1;
}
return 0;
}

In order to make the whole "composition" listenable, few patterns can be defined to help arranging the groups of chords (4 chrods per group) into a larger sequence:

const int possible_fragment_order[][16] = {
{1,1,2,2,3,3,2,2,4,4,5,5,6,6,5,2},
{2,2,1,1,2,1,3,1,4,4,5,5,4,5,2,3}
};

The main function to put it all together is here:

int get_music_from_noise(char **progression_buffer)
{
char *temp_chord_progression;
int temp_chord_progression_ptr = 0;
temp_chord_progression = (char*)malloc(5000); //allocate lot of temp space
 
int noise_data_n = 128;
char *noise_data;
noise_data = (char*)malloc(noise_data_n + 16);
 
char found_sequence[4];
int sequence_length;
 
#define NOTES_PER_FRAGMENT 4
#define FRAGMENTS_REQUIRED 8
 
char fragments[FRAGMENTS_REQUIRED][NOTES_PER_FRAGMENT];
 
for(int f=0;f < FRAGMENTS_REQUIRED;f++)
{
sequence_length = 0;
while(sequence_length < NOTES_PER_FRAGMENT)
{
int noise_data_ptr = 0;
while(noise_data_ptr < noise_data_n)
{
noise_data_ptr += fill_with_random_value(noise_data + noise_data_ptr);
}
 
//check if any sequence of chords appeared
for(int i=0; i<noise_data_n && sequence_length<NOTES_PER_FRAGMENT; i++)
{
if(looks_like_a_chord(noise_data[i]))
{
found_sequence[sequence_length] = noise_data[i];
sequence_length++;
}
}
}
strncpy(fragments[f],found_sequence,NOTES_PER_FRAGMENT);
}
 
new_random_value(); //get new value to random_value global variable
 
//pick one of the possible fragment-order sequences randomly
int fragment_order_n = random_value %
(sizeof(possible_fragment_order) / sizeof(possible_fragment_order[0]));
 
//find out what is the number of fragments listed in a sequence
int total_fragments_needed = sizeof(possible_fragment_order[0])
/ sizeof(typeof(possible_fragment_order[0][0]));
 
int sequenced_chords_ptr = 0;
 
//allocate array for chords in one-char notation
char *sequenced_chords = (char*)malloc(total_fragments_needed * NOTES_PER_FRAGMENT + 2);
 
for(int n_fragment=0; n_fragment < total_fragments_needed; n_fragment++)
{
const char *selected_fragment =
fragments[possible_fragment_order[fragment_order_n][n_fragment] - 1];
 
strncpy(sequenced_chords+sequenced_chords_ptr, selected_fragment, NOTES_PER_FRAGMENT);
sequenced_chords_ptr += NOTES_PER_FRAGMENT;
}
 
for(int i=0;i<sequenced_chords_ptr;i++)
{
temp_chord_progression_ptr += get_chord_expansion(
sequenced_chords+i, temp_chord_progression+temp_chord_progression_ptr);
temp_chord_progression[temp_chord_progression_ptr] = ',';
temp_chord_progression_ptr++;
}
//delete the last comma, terminate the string
temp_chord_progression[--temp_chord_progression_ptr] = 0;
 
shuffle_octaves(temp_chord_progression);
 
//allocate memory and copy over the chord progression to progression_buffer
progression_buffer[0] = (char*)malloc(temp_chord_progression_ptr + 1);
strncpy(progression_buffer[0], temp_chord_progression, temp_chord_progression_ptr);
free(temp_chord_progression);
free(noise_data);
free(sequenced_chords);
 
return temp_chord_progression_ptr;
}

Curious how it sounds?

Let's see how are our two robots doing. First one knows his tricks quite well, second one is so far relying on a table of rules, and does not do too much thinking. Still, he's a big help: you may actually perceive this "compilation of chord progression fragments" as a music because your brain is good at recognizing patterns on more levels.

 

Next steps:

How to make both robots smarter?

  • Apply rules to filter only fragments that sound fine (these rules are known, majority of popular music consists of limited amount of chord progressions - but we will apply wider range here to get more interesting results).
  • In similar way, derive melody that plays nicely with background progression.
  • Add "learning" mode, where you press a button to confirm you like the particular bit (or other button to disqualify it from appearing in future). This way you can start with a longer sequence filtered out of chaos, and in few iterations narrow it down to a nicely listenable song.