Building a DJ set is finding a path on a graph (and how this became a Haskell program)
I ended up building a small Haskell program to help me plan DJ sets. This post explains where the idea came from and how the problem can be described simply ...
Overall, I’ve been making music for ~25 years or so now; in terms of performing, I fiddled with Ableton, grooveboxes, and even tried livecoding. But I was never too interested in DJing until a couple of years ago, when I decided to give it a go, with various breaks here and there.
Recently I picked it up again, got myself a new DJ controller, downloaded ~30 of my favourite tunes in my target genre, connected everything, loaded Traktor and started playing.
Long story short, I ended up building a small Haskell program to help me plan DJ sets. This post explains where the idea came from and how the problem can be described simply using graphs. It also gives an overview of how the source code is organized and how its main parts relate to the theoretical concepts discussed here. The full code is available on GitHub.
For those unfamiliar with it, DJing is basically about transitioning smoothly from one song to the next using volume changes, EQ, effects, etc.
But obviously not all songs blend well together, so transitions can be tricky. To make a smooth transition between song A and song B, usually a few things need to happen:
Back in the old days, “real DJs” knew their entire music library by heart, even if it filled several heavy bags of vinyl records. They knew exactly which songs blended well together and which didn’t.
Nowadays, the heavy bags are replaced by USB sticks full of MP3s and we have software tools to help decide which track to play next.
One such tool is what we call harmonic mixing (see e.g. here), which tries to solve the problem of choosing tracks that have compatible musical keys.
How does it work in practice?
The DJ software analyzes each song and assigns it one of 24 musical keys, labelled
with a number from 1 to 12 and a letter (usually d or m, e.g. in Traktor, or A or B in other systems).
Here, for example, the track has been assigned 11d:

So what can we do with this information? That’s where the Camelot wheel comes in handy (see detailed info and interactive example here). Basically, once we know the key of a song, the wheel suggests a set of tonally compatible keys:

For example, from 3A we can go “horizontally” to 2A and 4A or “vertically” to 3B (or stay in 3A obviously).
With our song above (11d in Traktor), the same rules let us go to 12d, 10d, 11m or just stay in 11d.
In general, potentially compatible songs have either:
In our program, this is formalized in Camelot.hs
(we use Traktor’s Open Key notation, which uses d for major and m for minor and numbers the wheel differently, but the idea is the same).
In general, however, as music people often say, music theory is just a guideline; it can definitely help us make informed decisions or demystify certain phenomena (like why certain chord sequences always seem to sound so good), but ultimately our ears are the final judge: “if it sounds good, it is good”.
In fact, while using this system to mix songs from my folder I quickly discovered that…
So I started writing down my discoveries on a notebook page:
Song A --> Song B (OMG really loved this!!)
Song X --> Song Y (Try blending the vocal, sounds great)
Song B --> Song X
...
and referring to it as I was mixing, alongside Traktor’s harmonic mixing suggestions.
Eventually, I was using harmonic mixing as a guide for exploration, while also taking the occasional risk and writing down my discoveries, ending up with something like this:

That’s when I realized that I was essentially just building a graph which could, in theory, be used in some interesting ways not only to decide what to play next, but also to build full playlists and whatnot.
But there was a problem: this was already getting painful to manage. Once I had two pages I had to keep flipping between them; what would happen when I reached 10?
So I decided to build a program to handle all this. During that same period I was also (re)learning Haskell so I thought great, let’s do it in Haskell. If nothing else, it could be a chance to learn something beyond the usual textbook exercises.
Yes, those notebook entries naturally describe a graph of what we will call tested transitions: transitions between songs that the user has personally tried, loved and decided were worth recording for next time.
The graph looks something like this:

Here, track t1 transitions nicely into t2 and t4, which both transition nicely into t3.
t5 is sadly left out (solution: add more music to the library, explore and add tested transitions once discovered!).
So those are our tested transitions, which we explicitly keep track of. But, as we saw, we can also use the Camelot wheel to suggest potentially good tracks to play next.
How can we combine the two approaches?
Here, each segment of the Camelot wheel contains songs (the black dots), and the arrows represent our tested transitions.

In other words, given a set of songs, the Camelot wheel gives us a partition of that set into 24 groups. The harmonic mixing rules then give us implicit transitions which we could use alongside the explicit (or tested) transitions.
In summary, given a song we could follow:
You should now be able to start seeing how we could, in principle, ask interesting questions such as
To summarize all of the above, the data structure we end up with looks something like this:

Here, black dots are songs and solid arrows are tested transitions. Songs are grouped by key into 24 partitions, shown as coloured circles.
Dashed arrows represent transitions suggested by harmonic mixing (only a subset is shown, but hopefully you get the idea).
In this example we can see that from 9A we can get to 8A and 10A as well as 9B and 9A itself.
Tested transitions can also break the harmonic mixing rules, like t4 to t9 here, which is totally fine.
You might have noticed numbers on the transitions. Those numbers represent the cost of each transition.
For example, a tested transition costs zero, because the user has personally tried it and loves it. Transitioning to a song with the same key costs 1, while moving to a compatible (but different) key costs 2. The idea is that a shared key is a safer bet, while a compatible key is a bit more risky. Neither guarantees a great transition, as we’ve seen (remember, our ears are the judge).
Picking a random song in an incompatible key would, in theory, carry a much higher cost. But if the user has tested that transition and loves it, it still costs zero: “tested transition always wins” - our version of “if it sounds good it is good”.
All of this is formalized in Haskell in MusicLibrary.hs and Transitions.hs.
If you’ve followed this far, the rest should be fairly simple.
Given a starting song, we can just follow outgoing edges to figure out which tracks to play next; if there are any tested transitions (cost = 0) then great, we can pick one of those. Otherwise, we follow the rules of harmonic mixing, picking a track with the same (cost = 1) or a compatible key (cost = 2).
We use transition costs to help us pick the “best” transition at any given time.
Building an entire playlist or DJ set is just an extension of this where we repeat the process for a
given number of steps (or until we run out of tracks to play) - see Playlist.hs.
As the user adds new songs to their library and continues using the system, they might eventually discover new transitions (guided by harmonic mixing and a bit of chaos) and add them to the graph, which in turn “unlocks” interesting new combinations and potential playlists.
The project is open source and available here.
It currently has 3 main parts:
The idea was to keep both the CLI and web server minimal - basically all they do is call into App.hs,
get their data and present it to the user.
The music library currently lives in a simple YAML file, handwritten by the user (consistent with the manual curation ethos of the project).
tracks:
- id: first_track
title: First Track
key: "3D"
- id: second_track
title: Second Track
key: "4D"
transitions:
- from: first_track
to: second_track
notes: "Mix after the breakdown" # optional
The YAML file is read and parsed into a MusicLibrary, the main data structure handled by
our program (see Yaml.hs for details about the parsing,
for which we used aeson).
The program currently treats the library as read-only: the user maintains it, the program reads it and does its magic. Adding or removing songs and transitions through the program is left for future work (see below), along with importing libraries from DJ software such as Traktor, Serato or rekordbox.
Throughout the program we handle errors via Either AppError _, where AppError
represents the various things that can go wrong.
This gives the CLI and web app details about what happened, so they can show meaningful error messages.
Ultimately, this all reduces to graph problems; the fact that we have “songs” and “transitions” is just a detail.
At the moment we can ask “give me a 10-track playlist starting from song X”. One cool addition would be “give me a 10-track playlist starting from X, making sure you include Y and Z and end in T”. Same idea, but with constraints. This probably maps to some existing graph algorithm but we haven’t looked into it yet.
At the moment, the algorithm only looks at the next step: given song X, it looks at the candidates and picks the one with the lowest cost (choosing randomly if there’s more than one with that same cost).
This seems to work fine in practice, but isn’t always ideal.
Suppose we’re playing song X and find a tested transition to song Y - great! It sounds amazing, but unfortunately Y turns out to be a dead end, as now we don’t have any compatible songs to choose from…
Instead, we could have chosen another song Z: perhaps, going from X to Z wouldn’t be as amazing as going from X to Y, but once we reached Z, we’d have many more transitions at our disposal, helping us avoid getting stuck and allowing the DJ set to continue smoothly (perhaps even unlocking interesting new areas of the library).
The YAML file is currently maintained by hand. Importing track metadata from Traktor, rekordbox, and similar tools would make larger libraries easier to manage while keeping transition curation manual.
The format is deliberately minimal. BPM is omitted because my sets stay around 138–140 BPM, so I already assume the tracks are tempo-compatible. Artist names are also omitted because I know this small collection well. Both would be useful additions as the library grows, but add little to the current proof of concept.
As mentioned above, the library is currently curated manually by the user. But this can become cumbersome as the library grows. It would be nice to manage songs and transitions without editing the YAML. Also, in a web setting this might have the potential to look very cool (see below).
Yes, this is a graph, and we could build something really cool looking that runs in a web browser, letting users visualize and interact with the graph etc… Pretty cool but out of scope.
The two things are not in competition. Harmonic mixing gives us starting points to experiment and explore. When the user finds something really exciting, they can record it. It’s a personal curation system. Harmonic mixing and the various AI song analysis systems tell us what might sound good; this tells us what sounds freaking amazing because the user has personally tested it.
No. In fact, the idea here is just to build a sort of database of personally curated transitions. There’s already plenty of software that suggests tracks. This is for users to curate their own library of amazing transitions.
I ended up building a small Haskell program to help me plan DJ sets. This post explains where the idea came from and how the problem can be described simply ...
During the last couple of years, I’ve been lucky enough to use both Haskell and Clojure professionally (at different companies) and in both cases it’s been a...
In the last few months I got involved in audio software development with C++ and the JUCE framework, and since at the same time I’ve been writing Haskell in ...
Before looking at the halting problem, it is worth considering why it is interesting in the first place. One of the reasons the halting problem is important ...