[WIP] Continue fleshing out blog

This commit is contained in:
Jeremy Dormitzer 2019-05-31 23:36:18 -04:00
parent 99b0e1d6f0
commit a63f41c2ef
2 changed files with 100 additions and 6 deletions

View File

@ -1,6 +1,7 @@
#lang pollen
title{A DSL for Music}
published-date[2018 8 5]
;; TODO add author and date
@ -38,14 +39,98 @@ data Music a =
Many of these type declarations are straightfoward, but a couple bear further discussion. A code{PitchClass} is an link[#:href "https://wiki.haskell.org/Algebraic_data_type"]{algebraic data type} representing all of the pitches: C#, Ab, F, and so on. By pairing a pitch class with an octave, we get a ◊code{Pitch}, which represents a specific note (for instance, middle C would be ◊code{(C, 4)}. A ◊code{Primitive} is a basic music building block, either a note or a rest. Note that it is ◊link[#:href "https://wiki.haskell.org/Polymorphism"]{polymorphic}: this is so that we can define types like ◊code{Note Duration Pitch} but also types like ◊code{Note Duration (Pitch, Loudness)} so that we can attach additional data to a primitive if we need to. A control represents the concept of making a modification to some music by changing the tempo, transposing it, or otherwise changing the output while keeping the underlying notes the same.
The Music type is where things get really interesting. Its an algebraic data type representing the concept of music in general. In fact, its powerful enough to fully represent any piece of music, from Hozier to Bach. A Music value is one of four possible types: a Prim, which is either a note or a rest; a Modify, which takes another Music as an argument and modifies it in some way; the :+: infix constructor, which represents two separate Music values played sequentially; and the :=: infix constructor, which represents two separate Music values played simultaneously.
The code{Music} type is where things get really interesting. Its an algebraic data type representing the concept of music in general. In fact, its powerful enough to fully represent any piece of music, from Hozier to Bach. A code{Music} value is one of four possible types: a code{Prim}, which is either a note or a rest; a code{Modify}, which takes another code{Music} as an argument and modifies it in some way; the code{:+:} link[#:href "https://downloads.haskell.org/~ghc/7.2.1/docs/html/users_guide/data-type-extensions.html"]{infix constructor}, which represents two separate ◊code{Music} values played sequentially; and the ◊code{:=:} infix constructor, which represents two separate ◊code{Music} values played simultaneously.
The Music type has some important properties. First, its polymorphic for the same reason that the Primitive type is. This allows us to attach any type of data we want to music primitives, letting us express any musical concept (volume, gain, you name it).
The code{Music} type has some important properties. First, its polymorphic for the same reason that the code{Primitive} type is. This allows us to attach any type of data we want to music primitives, letting us express any musical concept (volume, gain, you name it).
Second, three out of its four constructors are recursive they take other Music values as arguments. This is the key that makes the data model so powerful. It allows you to model arbitrary configurations of notes, e.g. Note 1/4 (C 4) :=: Note 1/4 (E 4) :=: Note 1/4 (G 4) is a C major triad, and that expression evaulates to a Music value that can itself be passed to Modify, :+:, or :=: to weave it into a larger piece of music.
Second, three out of its four constructors are recursive they take other code{Music} values as arguments. This is the key that makes the data model so powerful. It allows you to model arbitrary configurations of notes, e.g. code{Note 1/4 (C 4) :=: Note 1/4 (E 4) :=: Note 1/4 (G 4)} is a C major triad, and that expression evaulates to a code{Music} value that can itself be passed to code{Modify}, code{:+:}, or code{:=:} to weave it into a larger piece of music.
The result is an extraordinarily concise definition that still manages to encompass all possible pieces of music. Using these data structures, we can describe any song we can imagine.
But as powerful as this data type is, I wouldnt call it a domain-specific language. The static type system makes it inflexible: how would you combine a Note 1/8 ((C 4) 8), representing a note with pitch and loudness, with a Note 1/8 (E 4), representing a note with just a pitch? Sure, you could write a function to convert from one to the other, but at that point youve lost elegance and flexibility.
But as powerful as this data type is, I wouldnt call it a domain-specific language. The static type system makes it inflexible: how would you combine a code{Note 1/8 ((C 4) 8)}, representing a note with pitch and loudness, with a ◊code{Note 1/8 (E 4)}, representing a note with just a pitch? Sure, you could write a function to convert from one to the other, but at that point youve lost elegance and flexibility.
Heres where Clojure comes in.
Heres where Clojure comes in.
section{A DSL for music with Clojure}
What would a domain-specific language for music look like in Clojure? I found inspiration in the HTML templating library link[#:href "https://github.com/weavejester/hiccup"]{Hiccup}. Hiccup represents HTML documents (a graph of complex nested nodes, just like music values) using Clojure vectors, like so:
codeblock[#:lang "clojure"]{
[:div {:class "foo"} [:p "foo"]]
}
The Hiccup vectors are actually a DSL that can describe arbitrary HTML markup. Anything that can be expressed in HTML can be expressed using Hiccup. It straddles the line between data and code the vector is flexible and expressive enough to represent any web page, but can be manipulated using standard Clojure library functions.
If we apply this idea to the data structure from HSoM, we end up with something like this:
codeblock[#:lang "clojure"]{
;; notes and rests are maps
(def eighth-note-c
{:duration 1/8
:pitch [:C 4]})
(def eighth-note-e
{:duration 1/8
:pitch [:E 4]})
(def eighth-note-rest
{:duration 1/8})
;; simultaneous music values
[:= eighth-note-c eighth-note-e]
;; sequential music values
[:+ eighth-note-c eighth-note-rest eighth-note-e]
;; modifying music values
[:modify
{:tempo 120 ;; the control
:transpose 3}
{:duration 1/8 ;; the note
:pitch [C 4]}]
;; :=, :+, and :modify can operate on any music value,
;; including arbitrary nesting
[:modify {:tempo 120}
[:+
[:= {:duration 1/4
:pitch [D 4]}
{:duration 1/4
:pitch [F 4]}]
[:= {:duration 1/4
:pitch [C 4]}
{:duration 1/4
:pitch [E 4]}]]]
}
At first glance, this looks the same as the Haskell data types from HSoM. Both representations represent notes with pitch and duration; both use the code{:modify}, code{:=} and code{:+} operators to compose music; both support recursive composition of any depth.
But the Clojure version is actually more expressive and flexible than the Haskell equivalent. A note can have any metadata we want attached:
codeblock[#:lang "clojure"]{
{:duration 1/4
:pitch [:C 4]
:loudness 6}
}
Our DSL has no problem composing notes with differing metadata:
codeblock[#:lang "clojure"]{
[:+
{:duration 1/4
:pitch [:C 4]
:loudness 6}
{:duration 1/8
:pitch [:Eb 4]
:staccato true}]
}
Furthermore, because Clojure is dynamically typed and supports link[#:href "https://en.wikipedia.org/wiki/Duck_typing"]{duck typing} via map keywords, we can write functions that operate on all notes and music values, even those with unexpected metadata.
Like the Hiccup vectors, our music vectors blur the boundary between a DSL and a data structure. The vectors are expressive enough to represent any musical concept, but can still be passed around and operated on by normal Clojure functions. As an added advantage, the vectors look similar enough to the HSoM data structures that I can easily follow along with the textbook using Clojure and Overtone.
section{What's next}
So I have a way to represent music in Clojure now. Whats next? Haskell School of Music ships with a library called Euterpea that knows how to turn the Music data structure into actual sound. So the next step for me is probably porting something like that to Clojure. Im hoping to offload most of that work to Overtone. After that, Ill explore algorithmic composition using the techniques outlined in HSoM. Stay tuned!
(tags "clojure" "music" "procedural generation")

View File

@ -1,5 +1,5 @@
#lang racket
(require pollen/decode txexpr)
(require pollen/decode txexpr gregor)
(provide (all-defined-out))
(define (root . elements)
@ -40,3 +40,12 @@
(txexpr
'pre empty
(list (txexpr 'code (zip-kws new-kws kw-args) elements)))))))
(define (tags . taglist)
;; TODO make these links to an index page for each tag
(txexpr 'span '((class "tags")) `("Tagged " ,(string-join taglist ", "))))
(define (published-date year month day)
(let ((publish-date (date year month day)))
(txexpr
'span '((class "published-date")) `("Posted on " ,(~t publish-date "MMMM d, y")))))