Chordal: a Scala DSL for music theory
Another post about my previous DSL experiments. This time, the target was a different hobby of mine: music. Chordal is another DSL experiment in Scala to represent music elements; working with Notes to create Chords, and then using those same Chords to build Progressions.
I thought "Chordal" was an original name until I found plenty of repos with the same one. Oh well.
Some of the challenges I faced came from building the AST. Music theory is a huge world with a lot of inherent structure and composition rules baked in. The atomic unit could be considered the pitch, which gains meaning once paired or grouped: two or three of them become a note, then a triad. Add one more and you've got a tetrachord. Keep stacking and you're into extended chords, which open up a lot of contexts that are genuinely hard to represent cleanly.
Then there's the diatonic vs. chromatic scale problem. The chromatic scale is useful for movement/transposition (going from one note to another by some number of steps), but that convenience comes with a catch: correct representation. Enter enharmonics, the fact that a note or chord can be spelled in different ways and still be "the same" pitch, but not the same note in context. Db and C# are a good example: same pitch, different note depending on what's around it.
What a rant. There are probably some errors, both in theory and in code, but I can live with that for now. It was a ride.
Features
- Working enharmonics.
- Extension methods to work with strings as notes.
- Useful rendering of notes.
- Harmony
- Chord creation
- Triads.
- Tetrachords (subject to model changes, partial support).
- Add chords.
- Power chords.
- Suspended chords.
- Partial extension support.
- Progressions
- Generic list-like implementation.
- Quick default methods for building common progressions.
Usage
DSL
package org.aranadedoros.chordal
import chords.*
import dsl.*
import dsl.Notes.{A, D, E, G}
import extensions.Ninth
import notes.{dim, major, note, pow, Note}
import scala.language.postfixOps
// work in progress, subject to change
val C = Note("C")
val chord1 =
chord {
root(C)
quality(MajorTriad)
withExtensions(Ninth)
}
println(chord1)
val chord2 =
chord {
root("D".note)
sus(Sus4)
}
println(chord2)
val chord3 =
chord {
root(E)
}
println(chord3)
val dMajor: Triad =
D major
println(dMajor)
val eMajorWithBassOnG: SlashChord =
E.major / G
println(eMajorWithBassOnG)
val g5: PowerChord =
G pow
println(g5.name)
val adim7 =
A.dim withSeventh MinorSeventh
println(adim7)
Output
C9
Dsus4
E5
(D,F#,A)
(E,G#,B)/G
G5
(A,C,Eb,G)
Overall, this project turned out really satisfying. It scratched my DSL itch for a good long while. I don't think I'll be doing another one anytime soon.
Top comments (0)