A composition library for Clojure and Clojurescript by @ctford.
Include it as a dependency in your project.clj, along with Overtone:
[overtone "0.9.1"]
[leipzig "0.10.0"]There is a Leiningen template that creates a simple Leipzig project, ready to run.
Leipzig models music as a sequence of notes, each of which is a map. They are ordered by :time:
[{:time 0
:pitch 67
:duration 2
:part :melody}
{:time 2
:pitch 71
:duration 2
:part :melody}]When using Leipzig, it's helpful to keep this model of notes in mind. Leipzig's functions are convenient ways of manipulating this basic structure, but there's nothing to stop you from building your own.
You can create a melody with the phrase function. Here's a simple melody:
(require '[leipzig.melody :refer [all bpm is phrase tempo then times where with]])
(def melody
; Row, row, row your boat
(phrase [3/3 3/3 2/3 1/3 3/3]
[ 0 0 0 1 2]))The first argument to phrase is a sequence of durations. The second is a sequence of pitches. phrase builds a sequence of notes which we can transform with sequence functions, either from Leipzig or ones from Clojure's core libraries.
To play a melody, first define an arrangement. play-note is a multimethod that dispatches on the :part key of each note, so you can easily define an instrument responsible for playing notes of each part. Then, put the sequence of notes into a particular key and tempo and pass them along to play:
(require '[overtone.live :as overtone]
'[leipzig.live :as live]
'[leipzig.scale :as scale])
(overtone/definst beep [freq 440 dur 1.0]
(-> freq
overtone/saw
(* (overtone/env-gen (overtone/perc 0.05 dur) :action overtone/FREE))))
(defmethod live/play-note :default [{midi :pitch seconds :duration}]
(-> midi overtone/midi->hz (beep seconds)))
(->>
melody
(tempo (bpm 90))
(where :pitch (comp scale/C scale/major))
live/play)There's nothing magic about where. It just applies a function to a particular key of each note, like update-in for sequences.
Let's define two other parts to go with the original melody:
(def reply "The second bar of the melody."
; Gent -ly down the stream
(phrase [2/3 1/3 2/3 1/3 6/3]
[ 2 1 2 3 4]))
(def bass "A bass part to accompany the melody."
(->> (phrase [1 1 2]
[0 -3 0])
(all :part :bass)))
(defmethod live/play-note :bass [{midi :pitch}]
; Halving the frequency drops the note an octave.
(-> midi overtone/midi->hz (/ 2) (beep 0.5)))You can then put multiple series of notes together:
(->>
bass
(then (with bass melody))
(then (with bass melody reply))
(then (times 2 bass))
(tempo (bpm 90))
(where :pitch (comp scale/C scale/major))
live/play)Leipzig features a number of namespaces, each containing functions pertaining to a particular area of composition.
This namespace contains the core functions for creating and manipulating melodies. In particular:
phrasecreates a melody from a sequence of durations and a sequence of pitches.whereapplies a function to a specified key of each note in a melody.
For example:
(->> (phrase [3/3 3/3 2/3 1/3 3/3] [0 0 0 1 2])
(where :time inc))Here are functions to send your melodies to Overtone:
play-noteis a multimethod that dispatches on the:parta note has.playplays the notes.jamloops the notes, reloading the var each time.stopstops all running melodies.
For example:
(defmethod live/play-note :melody [{midi :pitch}]
(some-> midi overtone/midi->hz beep))
(def boring-scale
(->> (phrase (repeat 1) (range 8))
(all :part :melody)
(where :pitch (comp C major))))
(jam (var boring-scale)); Edits to boring-scale will be played each time we go back round the loop.
This namespace contains functions for placing melodies within musical scales. In particular:
majorandminorare functions that place a pitch within a relative scale.A,B,Cetc are functions that take a relative pitch, and place it in a specific absolute key.
For example:
(->> (phrase (repeat 1) (range 8))
(where :pitch (comp C major)))The phrase function accepts chords as well as simple pitches. This namespace provides simple ways
to manipulate them:
triadis the tonic, which can be manipulated to form other chords.rootscales the chord up to the specified root.inversioninverts the chord, leaving the root where it is.
For example, a fourth chord, then the second inversion of the fifth:
(phrase
[4 4]
[(-> triad (root 3))
(-> triad (inversion 2) (root 4))])This namespace translates midi pitches into frequencies. Overtone's midi->hz will usually do
just fine, but if you want to experiment with more exotic temperaments, there are plenty here.
In particular:
equalis equivalent tomidi->hzand translates frequencies into pitches like a piano is tuned.justuses pure ratios, and more closely models how singers interpret intervals into frequencies.
For example:
(->> (phrase (repeat 1) (range 8))
(where :pitch (comp just C major)))In addition to simple pitches, phrase can take maps representing chords or nils:
(require '[leipzig.chord :as chord])
(def chords "Off-beat chords."
(->> (phrase (repeat 1/2)
[nil chord/triad
nil (-> chord/seventh (chord/root 4) (chord/inversion 1) (dissoc :v))
nil chord/triad
nil chord/triad])
(all :part :chords)))The maps generate a note for each value in the map - the keys are used only to enable chord-transforming functions such as root and inversion.
The nils generate notes without pitches, representing rests. This is convenient, because it allows melodies to have a duration extending beyond their last audible note. However, the play-note implementations and where invocations must be prepared to handle this, e.g. by using when and where's variation wherever:
(require '[leipzig.melody :refer [wherever]]
'[leipzig.scale :refer [lower]])
(defmethod live/play-note :chords [{midi :pitch}]
(when midi (-> midi overtone/midi->hz beep)))
(->>
(times 2 chords)
(wherever :pitch, :pitch lower)
(with (->> melody (then reply)))
(tempo (bpm 90))
(where :pitch (comp scale/C scale/major))
live/play)Leipzig supports Clojurescript for all of its namespaces save leipzig.live. The live namespace depends directly on
Overtone, so it cannot be used in the browser. However the rest of Leipzig can be used so long as an alternative synthesis
engine is present like the Web Audio API. Klangmeister is a good example of this.
See Row, row, row your boat or whelmed for examples.
In Leipzig from scratch, I demonstrate how to create a piece from
lein new onwards.
Leipzig came out of a talk I gave called Functional Composition, where I explain basic music theory using Overtone and Clojure.
API documentation, generated by Codox.
Leipzig is designed to play nicely with Clojure's standard sequence functions. Therefore, Leipzig's functions for transforming notes all take the sequence as a final argument so that they can be threaded with the ->> macro:
(->>
(phrase (repeat 1) (cycle [0 2 4]))
(take 24)
(filter #(-> % :time even?)))These sequence functions all exhibit "closure" i.e. their result is the same shape as their input. That allows them to be used and combined very flexibly. where for example, can raise the pitch, set the part or put the notes into a particular tempo:
(->> notes (where :pitch inc))
(->> notes (where :time (bpm 90)))Leipzig aims to be a library rather than a framework or environment. It uses simple Clojure datastructures and strives to be as open as possible. A new timing scheme, tuning or tempo can be mixed with Leipzig's other functions just as easily as the ones that come with the library.
To run the unit tests without having to start Overtone's Supercollider server:
lein midje leipzig.test.*
Leipzig includes MCP (Model Context Protocol) integration for interactive development with Claude Code.
To enable interactive development, start an NREPL server:
lein repl
With Claude Code installed, the MCP NREPL server (configured in .mcp.json) will automatically connect to your running NREPL, enabling:
- Interactive Clojure code evaluation
- Namespace and function exploration
- Real-time code investigation
The MCP server connects to the NREPL port specified in .nrepl-port (created automatically by lein repl).
As pointed out by @clojens, leipzig.live imports overtone.live, which implicitly boots an internal Supercollider server and can cause problems for folks using 64 bit Windows.