Answer Set Programming reveals the elegant logical relationships that underpin music, transforming abstract musical concepts into clear rules where scales, chords, and harmonies naturally emerge.
We might tap our fingers on the desk or hum a tune, but few of us know the beautiful, intricate relationships that underpin it all. Answer Set Programming (ASP) gives us a powerful lens to peek behind music's curtain, letting us express complex musical concepts through elegant logical rules.What makes this journey exciting is how ASP transforms abstract musical concepts into crystal-clear logical relationships. Instead of drowning in complex music theory, we'll build an elegant system that reveals how scales, chords, and harmonies naturally emerge from simple rules. By the end, you'll see music through new eyes — not just as sounds to be played, but as a fascinating puzzle of interconnected patterns waiting to be discovered. Roll up your sleeves — we're about to turn musical intuition into logical beauty!
Basic Notes
The musical alphabet consists of the letters A through G which make up the natural notes:
natural_note(a ; b ; c ; d ; e ; f ; g) .
This sequence of 7 notes is repeated creating the basic unit of an octave (where the first note is repeated). An octave is divided into 12 equal parts called 12 tone equal temperament. The interval between each of these parts is called a half-step. Raising a note by a half-step sharpens it (notated with a "♯") and lowering a note by a half-step flattens it ("♭").
There are 7 natural notes, the octave is divided into 12 equal parts, and there are 21 possible ways to represent notes (7 natural, 7 sharp and 7 flat). That's a whole lot of things that don't add up! (And we didn't include double sharps "𝄪" or double flats "𝄫" yet!) The way that all of this gets ironed out is that many of the notes are enharmonic equivalents — they're "spelled" differently but sound the same. (The why of this is left to the reader to discover!)For convenience, the 21 possible "spellings" are:
To establish relationships between notes, we'll define a mapping of half-step progressions through the chromatic scale. This mapping serves two purposes: it provides an ordering of notes and identifies which notes are enharmonic equivalents (different spellings of the same pitch). The specific ordering shown below reflects standard musical notation and practice.
The interval between the same note is zero. This is the anchor for a recursive relation that defines the number of steps between any two notes (within an octave) both ascending and descending
Every major key has a corresponding relative minor key that shares the same key signature. The relative minor is found by moving down three half-steps (or up six half-steps) from the major key's tonic. For example, A minor is the relative minor of C major, and they share the same key signature (no sharps or flats).This relationship creates natural pairs of scales that are closely related but have different tonal centers and emotional qualities. The ASP code defines this relationship by finding the note that is a major sixth above the minor key (or equivalently, a minor third below the major key).Note: This approach of using intervals to define relationships is particularly elegant because it leverages the existing interval logic rather than hardcoding the relationships. It also naturally handles enharmonic equivalents.
The spelling of a scale includes each letter of the natural notes. For example, the C major scale is C, D, E, F, G, A, B and notC, D, E, E♯, G, A, C♭since the latter does not contain the letters F or B. There is such a unique spelling for each tonic. To make this spelling easy, the following auxiliary predicate is defined and enumerates all of the possible combinations:
The alphabet of the scale starting on E♯ is E, F, G, A, B, C, D:
scale_alphabet(D, L) ← alphabet(e♯, D, L) .
{
scale_alphabet/2
1
e
2
f
3
g
4
a
5
b
6
c
7
d
}
Major Scales
Scales can be computed in a number of ways. The most common is to use the intervals defined for each mode (e.g. major scales have the intervals 2, 2, 1, 2, 2, 2, 1). We are going to use the fact that starting from a perfect fourth above the tonic, one can take 6 successive perfect fifths to determine all notes of a scale. For example for a C major scale, moving up a perfect fourth to F and taking 6 successive perfect fifths results in F, C, G, D, A, E, B which are the 4, 1, 5, 2, 6, 3, 7 degrees (simply +3 mod 7) of the scale, respectively:
%* class scale(Root, Mode, Degree, Index, Note) . *%
scale(R, M, D, I, N) ←
M = major, D = 4, I = 1,
interval(S, perfect_fourth), interval(R, N, S),
note(L, _, N), alphabet(R, D, L) .
scale(R, M, D2, I+1, N2) ←
scale(R, M, D1, I, N1), interval(S, perfect_fifth), interval(N1, N2, S),
note(L, _, N2), alphabet(R, D2, L),
D2 = (D1 + 3)\7 + 1, I < 7 .
Unit Tests
An E♭ major scale is E♭, F, G, A♭, B♭, C, D:
scale(D, N) ← scale(e♭, major, D, _, N) .
{
scale/2
1
e♭
2
f
3
g
4
a♭
5
b♭
6
c
7
d
}
Notice that an A♯ major scale does not yield the expected results since is it a theoretical keywhich includes double sharps which have not been introduced!
scale(D, N) ← scale(a♯, major, D, _, N) .
{
scale/2
1
a♯
2
b♯
4
d♯
5
e♯
}
Double Accidentals
To address this issue with the A♯ major scale we need to introduce double sharp (𝄪) or double flat (𝄫) accidentals to maintain proper note spelling using each letter exactly once. Double sharps raise a note by two half-steps while double flats lower a note by two half-steps. Just as with regular accidentals, we need to define these new note possibilities:
With the new notes defined, we need to update our halfstep/2 relationships to include these double accidentals. The relationships become more complex as each pitch can now potentially be spelled in up to 4 different ways. For example, the note we previously knew as C♯/D♭ can also be written as B𝄪 (double sharp) or E𝄫 (double flat) depending on the musical context. The complete set of enharmonic relationships is:
Note that we're repeating some of the halfstep/2 relationships we defined earlier. ASP will handle these redundant facts appropriately.
Unit Test
An A♯ major scale is A♯, B♯, C𝄪, D♯, E♯, F𝄪, G𝄪
scale(D, N) ← scale(a♯, major, D, _, N) .
{
scale/2
1
a♯
2
b♯
3
c𝄪
4
d♯
5
e♯
6
f𝄪
7
g𝄪
}
Minor Scales
Minor scales can be derived from their relative major scales, taking advantage of the relationship we defined earlier. The natural minor scale (also called the Aeolian mode) uses the same notes as its relative major scale but starts from a different tonic — specifically the 6th degree of the major scale.Rather than define the intervals directly (which would be 2, 1, 2, 2, 1, 2, 2), we can leverage our existing relative_minor/2 relationship and the major scale construction. Since we know that:
the 6th degree of a major scale is the tonic (1st degree) of its relative minor
the 7th degree of a major scale is the 2nd degree of its relative minor
the 1st degree of a major scale is the 3rd degree of its relative minor
and so on…
we can map the degrees using the formula MajorD = ((D + 4)\7) + 1 where D is the desired minor scale degree. This creates a mathematical relationship between the degrees that preserves the proper interval structure while maintaining correct note spelling:
scale(R, minor, D, I, N) ←
relative_minor(MajorRoot, R),
MajorD = ((D + 4)\7) + 1, D = 1..7,
scale(MajorRoot, major, MajorD, I, N),
note(L, _, N), alphabet(R, D, L) .
Unit Tests
A minor (the relative minor of C major) is A, B, C, D, E, F, G
scale(D, N) ← scale(a, minor, D, _, N) .
{
scale/2
1
a
2
b
3
c
4
d
5
e
6
f
7
g
}
E minor (the relative minor of G major) is E, F♯, G, A, B, C, D (where F♯ is the proper spelling)
scale(D, N) ← scale(e, minor, D, _, N) .
{
scale/2
1
e
2
f♯
3
g
4
a
5
b
6
c
7
d
}
Tetrads (Sevenths)
A tetrad(more commonly called a seventh chord) is made up of four notes: the root (or tonic), third, fifth and seventh of the scale. Since we have already defined interval/2 (as well as interval/3) we can define the tetrads in a natural way. For example, a major seventh is made up of the root, a major third, a perfect fifth, and a major seventh.
Following the definition of a tetrad/4, we can define all of the root position chords for each key as a relation between the root, third, fifth and seventh and the mode. A diminished chordhas a minor flattened fifth and an augmented chordis composed of two major thirds:
inversion(R, M, root, R, N1, N2, R ) ← chord(R, M, R, N1, N2, _) .
inversion(R, M, first, N1, N2, R, N1) ← chord(R, M, R, N1, N2, _) .
inversion(R, M, second, N2, R, N1, N2) ← chord(R, M, R, N1, N2, _) .
inversion(R, M, root, R, N1, N2, N3) ← chord(R, M, R, N1, N2, N3) .
inversion(R, M, first, N1, N2, N3, R ) ← chord(R, M, R, N1, N2, N3) .
inversion(R, M, second, N2, N3, R, N1) ← chord(R, M, R, N1, N2, N3) .
inversion(R, M, third, N3, R, N1, N2) ← chord(R, M, R, N1, N2, N3) .
Unit Tests
C major in root position and A minor in first inversion contain the notes C, E, G:
def(R, M, I) : inversion(R, M, I, c, e, g, _) .
{def(a,minor,first)}{def(c,major,root)}
E minor and E diminished in root position, C major and C♯ diminished in first inversion, and A minor and A♯ diminished in second inversion contain the notes E, G:
Roman numeral analysis describes harmonic function within a key. It labels chords with Roman numerals corresponding to scale degrees. For example, in C Major, the chord built on the first degree (C) is labeled "I". The chord built on the fourth degree (F) is "IV". This system allows analysis of harmonic progressions without reference to specific notes (e.g., I-IV-V). Roman numeral analysis is a powerful tool for understanding harmonic relationships independent of specific keys. The numeral indicates the scale degree on which the chord is built, while the case (upper/lower) indicates the chord quality.In major keys, the pattern of chord qualities follows naturally from the major scale intervals:
The relationships between scale degrees, chord qualities, and Roman numerals can be encoded directly in ASP using the degree_quality/4 predicate which captures both the position (degree) and quality (mode) of each chord within a key. When combined with our existing scale definitions, this allows us to analyze any chord's function within any key.
The Roman numeral for the G chord in the key of C major is "V" (major):
numeral(M, N) : chord_numeral(c, major, g, M, N) .
{numeral(major,"V")}
The subdominant (IV) in the key of F major is B♭ major:
chord(C, M) : chord_numeral(f, major, C, M, "IV") .
{chord(b♭,major)}
G as the dominant (V) occurs in the key of C major (as G major):
key(K, KM, M) : chord_numeral(K, KM, g, M, "V") .
{key(c,major,major)}
Voice Leading
So far we've built a vocabulary of what — what notes live in a scale, what makes a chord, what function a chord serves in a key. Now we turn to how. How do we get from one chord to the next? This is the art of voice leading and it's where abstract harmony meets the flesh-and-blood reality of singers and instruments moving through time.Think of a choir singing a hymn. Four voices — soprano, alto, tenor, bass — each singing their own melodic line, yet combining at every moment into chords. The "rules" of voice leading, developed over centuries, are really just observations about what makes those individual lines singable and beautiful while still serving the harmony. They're constraints. And constraints, as we've seen, are exactly what ASP loves.
Voices and Ranges
Traditional four-part harmony uses Soprano, Alto, Tenor, and Bass (SATB). Each voice has a comfortable range — push a soprano too low or a bass too high and things get strained. We express these ranges using MIDI note numbers, where middle C is 60 and each increment is a half-step:
voice(soprano, 60, 81) . % C4 to A5
voice(alto, 55, 74) . % G3 to D5
voice(tenor, 48, 67) . % C3 to G4
voice(bass, 40, 60) . % E2 to C4
voice(V) ← voice(V, _, _) .
Unit Test
The four voices are soprano, alto, tenor, and bass:
Until now, we've dealt with pitch classes — the note C without specifying which C. But voice leading requires actual pitches: C4 (middle C) is different from C5 (an octave higher). A pitch combines a note with an octave to produce a specific MIDI number:
pitch(N, Oct, Midi) ←
note(N), Oct = 0..8,
interval(c, N, Semi), Semi >= 0, Semi < 12,
Midi = 12 + Oct * 12 + Semi .
Unit Tests
Middle C (C4) has MIDI number 60:
midi(M) : pitch(c, 4, M) .
{midi(60)}
The E above middle C (E4) has MIDI number 64:
midi(M) : pitch(e, 4, M) .
{midi(64)}
G♯4 has MIDI number 68:
midi(M) : pitch(g♯, 4, M) .
{midi(68)}
Progressions
A chord in isolation is just a snapshot. Music happens when chords connect into progressions — sequences that create tension, release, and narrative. We model a progression as a series of numbered chord instances:
The chord root for each position in the progression:
root(I, R) : chord_root(I, R) .
{root(4,c)}{root(3,g)}{root(2,f)}{root(1,c)}
Voicing a Chord
A voicing assigns each chord tone to a specific voice at a specific pitch. This is where choice enters: there are many ways to voice a C major chord across four singers. We use ASP's choice rules to generate candidates then constrain them:
% Each voice gets exactly one pitch from the chord tones
1 { voicing(ChordId, V, N, Midi) :
chord_tone(ChordId, N),
pitch(N, _, Midi),
voice(V, Low, High),
Midi >= Low, Midi <= High } 1 ← voice(V), chord_id(ChordId) .
Voice Ordering
The bass must be below the tenor, the tenor below the alto, the alto below the soprano. This isn't arbitrary — it's how the overtone series) works, and violating it creates muddy, unpleasant sounds:
Here's where centuries of musical wisdom crystallize into two simple constraints. Parallel fifths and parallel octaves — two voices moving in the same direction while maintaining an interval of a perfect fifth or octave — are the cardinal sins of voice leading. They destroy the independence of the voices, making two lines sound like one:
Good voice leading minimizes unnecessary motion. When a voice can stay put or move by step, it should. Large leaps draw attention and should be used deliberately, not accidentally. We ask the solver to minimize total movement:
When two successive chords share a note, that note should stay in the same voice — it's the path of least resistance, and it creates a sense of continuity. Moving a common tone to a different voice wastes motion and obscures the harmonic connection:
common_tone(C1, C2, N) ←
chord_tone(C1, N),
chord_tone(C2, N),
next_chord(C1, C2) .
% Soft constraint: prefer keeping common tones in same voice %
:~ common_tone(C1, C2, N),
voicing(C1, V1, N, _), voicing(C2, V2, N, _),
V1 != V2 . [1, C1, C2, N]
Unit Test
C major (C, E, G) and F major (F, A, C) share the note C:
With four voices and three chord tones (in a triad), one note must be doubled. Convention favors doubling the root — it reinforces the chord's identity:
% Prefer root in bass %
:~ voicing(Id, bass, N, _), chord_root(Id, Root), N != Root . [2, Id, bass]
% Prefer doubling the root %
root_count(Id, C) ←
chord_root(Id, Root),
C = #count { V : voicing(Id, V, Root, _) } .
:~ root_count(Id, 1) . [1, Id, root]
The Moment of Truth
So here we are. We've laid down the law — voices stay in their lanes, no parallel fifths, keep the movement smooth, hold onto common tones, double the root. We've translated centuries of hard-won musical wisdom into a handful of logical constraints.And now we step back and let the solver think.This is the strange magic of declarative programming: we never told the computer how to voice a chord. We never wrote a procedure that says "first place the bass, then consider the tenor..." We simply described what good looks like and trusted that the answer would emerge from the intersection of all those constraints.What comes back is not one voicing, but many — every possible way to thread four voices through our I-IV-V-I progression without breaking the rules. Each one is a valid solution, a path through the harmonic landscape that our musical ancestors would recognize as correct.
{
voicing/4
1
alto
g
67
1
bass
g
43
1
soprano
e
76
1
tenor
e
64
2
alto
c
60
2
bass
a
45
2
soprano
a
81
2
tenor
a
57
3
alto
d
62
3
bass
d
50
3
soprano
d
74
3
tenor
b
59
4
alto
e
64
4
bass
g
43
4
soprano
g
79
4
tenor
g
55
}{
voicing/4
1
alto
g
67
1
bass
c
60
1
soprano
c
72
1
tenor
e
64
2
alto
a
69
2
bass
c
48
2
soprano
f
77
2
tenor
c
60
3
alto
b
59
3
bass
b
47
3
soprano
g
79
3
tenor
d
50
4
alto
g
55
4
bass
c
48
4
soprano
g
79
4
tenor
e
52
}{
voicing/4
1
alto
g
67
1
bass
c
48
1
soprano
c
72
1
tenor
g
55
2
alto
a
69
2
bass
f
53
2
soprano
a
81
2
tenor
f
65
3
alto
g
67
3
bass
g
55
3
soprano
b
71
3
tenor
b
59
4
alto
g
67
4
bass
g
55
4
soprano
g
79
4
tenor
c
60
}{
voicing/4
1
alto
g
67
1
bass
e
40
1
soprano
g
79
1
tenor
g
55
2
alto
c
60
2
bass
f
41
2
soprano
a
81
2
tenor
f
53
3
alto
g
67
3
bass
d
50
3
soprano
d
74
3
tenor
b
59
4
alto
g
67
4
bass
g
55
4
soprano
e
76
4
tenor
c
60
}{
voicing/4
1
alto
g
67
1
bass
g
43
1
soprano
g
79
1
tenor
e
52
2
alto
c
60
2
bass
f
41
2
soprano
a
81
2
tenor
f
53
3
alto
g
67
3
bass
g
55
3
soprano
g
79
3
tenor
d
62
4
alto
c
72
4
bass
g
55
4
soprano
g
79
4
tenor
c
60
}{
voicing/4
1
alto
c
72
1
bass
c
60
1
soprano
g
79
1
tenor
e
64
2
alto
a
69
2
bass
c
60
2
soprano
f
77
2
tenor
f
65
3
alto
b
71
3
bass
d
50
3
soprano
d
74
3
tenor
g
55
4
alto
c
72
4
bass
c
48
4
soprano
e
76
4
tenor
c
60
}
Making It Sing
But let's be honest: staring at voicing(1, soprano, g, 79) isn't exactly a transcendent musical experience. We've done something remarkable here — we've taught logic to respect the overtone series, to avoid the sins of parallel motion, to find the path of least resistance through a chord progression. And our reward is... a list of atoms? A wall of predicates?No. That won't do.Music is meant to be seen on a staff and heard in the air. So let's close the loop. Let's take these abstract solutions and render them as notation — four voices on a grand staff, stems up and stems down, exactly as Bach would have written them. And then, because we can, let's press play and listen to what pure logic sounds like when it sings. (Please note that iPhones need to turn off silent mode to play the music below.)
The Unfinished Symphony
There are so many more places that we can take this from chord progressions to voice leading, modal interchange, secondary dominants, and modulation. We could explore polytonality by having multiple key centers simultaneously, or dive into microtonal systems by redefining our interval relationships. We could even venture into non-Western musical systems, defining new rules for ragas, maqams, or gamelan scales.What makes ASP truly remarkable here isn't just its ability to model these complex musical relationships — it's how it reveals the inherent logic that musicians intuitively grasp. Every predicate we write is like discovering a new musical truth, each rule a reflection of how musicians actually think about and create music. Just as a composer doesn't consciously calculate intervals while writing a melody, our ASP system doesn't just mechanically apply rules — it understands the deep patterns that make music work.This is what makes this approach so powerful: it bridges the gap between the mathematical precision of logic programming and the intuitive artistry of musical composition. And we've only scratched the surface of what's possible. The symphony remains unfinished, waiting for you to write the next movement!
Your brain wasn't meant to juggle 47 database constraints at once. Here's how Answer Set Programming turns Firestore's query rules into executable logic.2 January 2025