One of my recent projects was to create software that would automatically generate music based on a predefined set of rules. The degree of randomness I've planned to introduce would let me create different melodies every time, while the set of rules I was planning to create would ensure that it still would sound nice. You can access the complete source code here. Below we'll dive more deeply into the details regarding what it does.
Domain description
The rules I was planning to expand upon are the concept of functional harmony. This concept is based on the idea that each chord has its own function. The functionality of a given chord is based on where the chord “wants” to go next because a harmonic progression has two dimensions: the chord’s pitches and how they are interacting (interval hierarchy); and its function in the overall harmonic context. So, functional harmony goes through the cycle of creating and releasing tension and as a result, we have stable and unstable moments that vary in different degrees of intensity.
The three most important functions are:
- The Tonic; can either be or feel very stable, and generally is the final chord of a piece of music or a section
- The Sub-Dominant; prepares the harmonic cadence and introduces some degree of instability
- The Dominant; the most unstable chord that wants to resolve to another chord
Encoding the domain
Now let's encode this knowledge into code. I've chosen F# for this task since its type system is quite handy for expressing all sorts of domains.
Let's start from the basics and describe what chords we have in our palette.
type ChordQuality =
| Major
| Minor
There are many more chord qualities, but this is just enough for our needs.
Now, let's describe the knowledge we've obtained from the previous paragraph.
type HarmonyItem =
| Tonic
| SubDominant
| Dominant
The transitions between them will look as below
type HarmonyTransition =
| Dublicate
| IncreaseTension
| MaximizeTension
| DecreaseTension
| Resolve
Now let's see how the transitions are applied
let applyCommand command chord =
match command with
| Dublicate -> dublicate chord
| IncreaseTension -> increaseTension chord
| DecreaseTension -> decreaseTension chord
| MaximizeTension -> maximizeTension chord
| Resolve -> resolve chord
let dublicate harmonyItem =
harmonyItem
let increaseTension harmonyItem =
match harmonyItem with
| Tonic -> SubDominant
| SubDominant -> Dominant
| Dominant -> Dominant
let decreaseTension harmonyItem =
match harmonyItem with
| Tonic -> Tonic
| SubDominant -> Tonic
| Dominant -> SubDominant
let maximizeTension harmonyItem =
Dominant
let resolve harmonyItem =
Tonic
With that said let's have a look at what hides behind each item in our functional progression. So basically each chord will have a quality and it's offset in notes from the root note.
type HarmonyItemValue = {
value: int
chordQuality: ChordQuality
}
let getHarmonyItemValue item =
match item with
| Tonic -> { value = 0; chordQuality = Major }
| SubDominant -> { value = 5; chordQuality = Major }
| Dominant -> { value = 7; chordQuality = Major }
Given this, we can create an array of pitches from each harmony item.
type Pitch = {
midiNote: int
duration: float
}
let createChordFromRootNote rootNote item =
let itemValue = getHarmonyItemValue item
match (itemValue.value, itemValue.chordQuality) with
| (value, Major) -> [|
{
midiNote = rootNote + value
duration = 1.0
};
{
midiNote = rootNote + value + 4
duration = 0.125
};
{
midiNote = rootNote + value + 7
duration = 1.0
}|]
| (value, Minor) -> [|
{
midiNote = rootNote + value
duration = 1.0
};
{
midiNote = rootNote + value + 4
duration = 0.125
};
{
midiNote = rootNote + value + 7
duration = 1.0
}|]
Generating the progression
So to create different progressions each time we need to add some randomness to the process. To achieve that we'll have some degree associated with the probability of each transition. Let's say we're in our tonic chord and we have the probability of 0.1 that we'll stay there for the next chord, while the probabilities of increasing tension are equal among themselves and are total to 0.45 each. In such a case let's assign a threshold for each transition. Say Tonic will be 0.1, SubDominant will be 0.55 which is the tonic threshold of 0.1 + tonic probability and Dominant will be 1.0 which is the probability of a full group of events. In such a case once we generate a random number between 0.0 and 1.0 we can select the minimal item that has a threshold greater than the given random number.
Here's how it looks in the code.
type HarmonyTransitionProbability = {
transition: HarmonyTransition
coinThreshold: float
}
let regenerateHarmonyTransitionProbability currentHarmonyItem =
match currentHarmonyItem with
| Tonic ->
[|
{ transition = Dublicate; coinThreshold = 0.1 };
{ transition = IncreaseTension; coinThreshold = 0.55 };
{ transition = MaximizeTension; coinThreshold = 1.0 };
|]
| SubDominant ->
[|
{ transition = Dublicate; coinThreshold = 0.1 };
{ transition = IncreaseTension; coinThreshold = 0.55 };
{ transition = Resolve; coinThreshold = 1.0 };
|]
| Dominant ->
[|
{ transition = Dublicate; coinThreshold = 0.1 };
{ transition = Resolve; coinThreshold = 0.9 };
{ transition = DecreaseTension; coinThreshold = 1.0 };
|]
let rnd = Random()
let generateNextChord currentChord coin =
let probabilityMap = regenerateHarmonyTransitionProbability currentChord
let command = (Array.filter (fun x -> coin <= x.coinThreshold) probabilityMap).[0].transition
applyCommand command currentChord
let generateProgression (initialChord: HarmonyItem) (length: int) : HarmonyItem array =
let rec generate (currentChord: HarmonyItem) (remaining: int) (progression: HarmonyItem list) =
if remaining = 0 then
List.toArray (List.rev progression)
else
let coin = rnd.NextDouble()
Console.WriteLine(coin)
let nextChord = generateNextChord currentChord coin
generate nextChord (remaining - 1) (nextChord :: progression)
generate initialChord (length - 1) [initialChord]
Domain evolution
So far we have covered only some basic concepts. But even some more mainstream progressions such as Axis of Awesome 4 chord wamp operate on the concept of substitutes. Substitutes are the duplicates of harmonic functions we already know but are not as distinct as their counterpart. So let's introduce them in our domain as well.
To me, this was the most enjoyable part of expressing my domain in F# since I had to remember to add it in two places: harmony items and transitions between them.
type HarmonyItem =
| Tonic
| TonicSubstitute1
| TonicSubstitute2
| SubDominant
| Dominant
type HarmonyTransition =
| Dublicate
| IncreaseTension
| MaximizeTension
| DecreaseTension
| DecreaseTensionToFisrtSubstitute
| DecreaseTensionToSecondSubstitute
| Resolve
| ResolveToFirstSubstitute
| ResolveToSecondSubstitute
At this point, in any place where I apply pattern matching, the compiler issues me a warning about incomplete pattern matching. So I just add missing cases until the compiler is satisfied and voila: a new version of the domain is complete. To some extent, this reminds me of lean on the compiler technique from the all-time classic "Working effectively with legacy code".
Generating sound
At this point, we can produce the array of pitches that are midi notes. To create sound from these notes I've used a specialized programming language called SuperCollider. I won't dive much into details here, but you may have a look at the code if you're interested. Beware, there are quite a lot of branches there and all of them contain some interesting code.
Conclusion
I've been a proponent of F# for quite a long time. So instead of expanding once again on the power of its type system, I'll just leave here a link to one of my favorite tracks that was created with the code in this article.
Top comments (0)