Lovewerk

Stochastic Melody Generation in Unity

 

For Ludum Dare 47, I wanted to try out some ideas for creating a generative music system. My objective was to create a small, modular set of rules which could be combined to create interesting musical results. There could be any number of musical rules, but here are a few I had in mind:

 

  • Number of consecutive repeated notes to allow
  • Desired contour of upcoming melodic notes
  • Which pitches are allowed, and which of those should be preferred
  • Which note durations are allowed
  • Which intervals between notes are preferred

Unfortunately, due to time constraints, I was not able to implement all of those, so I focused on developing a system for stochastically choosing rhythm and pitch. By creating collections of acceptable pitches and note durations, I could assign a weight to each element and then use probability to select a pitch and duration for each note to be played. For prototyping, my teammates set up a soundbank using the audio middleware FMOD, which allowed me to generate a note, and then send the desired pitch to FMOD.

 

To begin, I set up a Metronome which sends regular ticks according to the desired tempo (named bpm for “beats per minute” in the script). When the MelodyGenerator is enabled, it generates a note and plays it on the next received metronome tick. Each note also contains a duration, and this duration defines how many ticks the MelodyGenerator will wait before playing another note. I should point out for clarity that my use of the term bpm for the tempo is misleading. Traditionally, one can subdivide the ticks of a metronome to play faster rhythms than the pulse of the metronome. However, since MelodyGenerator is not keeping track of time itself, but rather the ticks it receives, these ticks should be thought of as the shortest possible note duration. This could be partially solved by introducing a time signature, where certain ticks would be designated as downbeats (and therefore non-downbeats would be considered subdivisions), but since it is mainly a convenience to do so, it seemed unneccessary to add this in the prototyping phase.

 

Getting back on track, pitches and durations were calculated based on MelodicProbabilties. As a ScriptableObject, it can be created as data and then referenced by MelodyGenerator. The logic is the same for pitches and durations, so I will only explain for pitches. To set up a pitch collection, the user assigns values to List<WeightedInt> pitches. Every time an element in MelodicProbabilities is changed, Unity triggers the OnValidate() callback. In OnValidate() the method CalculateCumulativeWeights is called, which returns the total weight of all pitches in the list, then populates and sorts the private List pitchesCumulative. The difference between pitches and pitchesCumulative is that the weight of each element in pitchesCumulative is the sum of all previous element weights in pitches and its own (so list [(a, 2), (b,1)] would become [(a, 2), b(3)], etc.). With these values stored, MelodicProbabilities is ready to handle MelodyGenerator’s requests for new notes.

 

When MelodicProbabilities’ GenerateNote() method is called, it uses the SelectWeightedRandom() method to choose the next value. By using the total weight of all pitches and the list pitchesCumulative (sorted by weight) which we stored earlier, we can perform a binary search on pitchesCumulative (the weights, not the values), where the searched index is a random value between 0 and totalWeight. Since the weight of each element in the sorted list pitchesCumulative will be greater than the previous one (unless it had a weight of zero), we can think of the weight difference between element i and element i-1 as element i’s “slice of the pie” where the pie is the totalWeight. If there is no weight value equal to the random value we searched for, we can take the bitwise complement of the value returned by binarySearch to obtain the index of the next element larger than the searched value.

 

Here is a demo of weighted pitch and rhythm generation. The pitch set used is a pentatonic scale, which was chosen arbitrarily, but a more interesting pitch collection could always be used instead. That said, even with just these two simple rules governing preferred pitches and durations, a surprising amount of variation can be achieved (particularly with the durations from 0:40 onwards).