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 rule I was planning to expand upon is 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: is 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
}|]
Join the conversation! Your thoughts help the community grow.