DEV Community

Cover image for Camelot Wheel for Developers: Harmonic Mixing Without the Music Theory PhD
Freqblog
Freqblog

Posted on • Originally published at freqblog.com

Camelot Wheel for Developers: Harmonic Mixing Without the Music Theory PhD

Here's the bargain music theory hands developers: take the circle of fifths — a 700-year-old chord-relationship diagram — rotate it slightly, replace the keys with numbers, and you get a tool that lets a 16-year-old DJ build a harmonic set without knowing what "fifth" means.

That tool is the Camelot wheel. It's the foundation Mixed In Key, Rekordbox, Serato, and Engine DJ all build on. And implementing the rules in your own software takes about 50 lines of code.

What the wheel does

Every musical key has a relationship to every other key. Some pairs sound great together (relative major/minor). Some clash horribly (a minor third apart). Music theory expresses these relationships in terms like "subdominant" and "tritone substitution" — useful if you're writing a fugue, useless if you're trying to mix two house tracks at 2am.

The Camelot wheel collapses all this into a clock face: 12 numbered positions, each with an A (minor) and B (major) variant. Two tracks mix harmonically if they share a position, are adjacent on the wheel, or are on the same number with different letters.

Camelot Key name Camelot Key name
1A A♭ minor 1B B major
2A E♭ minor 2B F# major
3A B♭ minor 3B D♭ major
4A F minor 4B A♭ major
5A C minor 5B E♭ major
6A G minor 6B B♭ major
7A D minor 7B F major
8A A minor 8B C major
9A E minor 9B G major
10A B minor 10B D major
11A F# minor 11B A major
12A C# minor 12B E major

The four rules

Rule 1: Same key (perfect mix)

Track at 8A mixes with another track at 8A. Boring but harmonically perfect. Useful for back-to-back tracks where the mix focuses on rhythm rather than chord progression.

Rule 2: Relative key (mood swap)

8A mixes with 8B. Same number, opposite letter. Same notes, different mood — a minor track and its relative major share all seven scale degrees, so they're harmonically identical from a pitch-class perspective. Mixing 8A → 8B is the classic "go from melancholic to hopeful" move.

Rule 3: Adjacent (energy lift / drop)

8A mixes with 7A or 9A. Same letter, ±1 number. The fifth-relationship gives you a smooth modulation that feels like the energy is climbing or relaxing without ever clashing.

Wraparound matters: the wheel is a circle. 12A → 1A is one step, not eleven. 1A → 12A likewise.

Rule 4 (advanced): Energy boost / drop

8A → 3A is a +7 jump — a bigger key change that landing-DJs use to lift energy mid-set. 8A → 1A is -7. These aren't strictly harmonic but every commercial DJ tool exposes them as "energy boost / drop" because they're a staple of festival mixing.

The 50-line implementation

Here's a complete Python implementation. Drop it into your project, no dependencies.

def camelot_compatible(camelot: str, *, extended: bool = False) -> list[tuple[str, str]]:
    """Return [(camelot, relation), ...] for every key that mixes with `camelot`.
    `extended=True` adds the +7/-7 energy-boost variants."""
    import re
    m = re.fullmatch(r'(1[0-2]|[1-9])([AB])', camelot.upper())
    if not m:
        raise ValueError(f"invalid Camelot value: {camelot!r}")
    n = int(m.group(1))
    letter = m.group(2)
    other_letter = "B" if letter == "A" else "A"

    def wrap(x: int) -> int:
        return ((x - 1) % 12) + 1

    pairs = [
        (f"{n}{letter}",        "same"),
        (f"{n}{other_letter}",  "relative"),
        (f"{wrap(n + 1)}{letter}", "adjacent_up"),
        (f"{wrap(n - 1)}{letter}", "adjacent_down"),
    ]
    if extended:
        pairs.append((f"{wrap(n + 7)}{letter}", "energy_boost"))
        pairs.append((f"{wrap(n - 7)}{letter}", "energy_drop"))
    return pairs


# Example
>>> camelot_compatible("8A")
[('8A', 'same'), ('8B', 'relative'), ('9A', 'adjacent_up'), ('7A', 'adjacent_down')]

>>> camelot_compatible("12A", extended=True)
[('12A', 'same'), ('12B', 'relative'), ('1A', 'adjacent_up'),
 ('11A', 'adjacent_down'), ('7A', 'energy_boost'), ('5A', 'energy_drop')]
Enter fullscreen mode Exit fullscreen mode

If you'd rather skip writing this yourself, our API exposes the same logic as GET /key/<camelot>/compatible?extended=trueno auth, no quota, pure-logic helper.

Distance: how do you score a transition?

Once you have compatibility, you need a distance metric for ranking transitions. Here's the rubric most DJ tools converge on:

  1. 0 hops — same Camelot exactly
  2. 1 hop — relative (same number, opposite letter), or adjacent (±1, same letter)
  3. 2-3 hops — "stretchy" but workable; many crowd-friendly mixes live here
  4. 4+ hops — clash territory, only attempt with a long mix or filter
def camelot_distance(a: str, b: str) -> int:
    """Distance in 'wheel hops' between two Camelot values."""
    import re
    ma = re.fullmatch(r'(1[0-2]|[1-9])([AB])', a.upper())
    mb = re.fullmatch(r'(1[0-2]|[1-9])([AB])', b.upper())
    if not ma or not mb:
        return 99    # treat as far
    an, al = int(ma.group(1)), ma.group(2)
    bn, bl = int(mb.group(1)), mb.group(2)
    nd = min(abs(an - bn), 12 - abs(an - bn))    # circular distance
    if nd == 0 and al != bl:
        return 1                                 # relative
    if al == bl:
        return nd
    return nd + 1                                # both differ
Enter fullscreen mode Exit fullscreen mode

Watch out: the wraparound. 1A and 12A are one hop apart, not eleven. The min(diff, 12 - diff) trick handles it.

Building a harmonic playlist

Now you have compatibility and distance. The simplest playlist algorithm:

  1. Pick a seed track
  2. For each subsequent slot: filter the candidate pool to tracks within hop-distance ≤ K of the previous track's key
  3. Among those, pick by a secondary criterion (BPM continuity, similarity, popularity)
  4. Mark used, repeat
def harmonic_walk(seed_key: str, candidates: list[Track],
                  max_hops: int = 2, n: int = 20) -> list[Track]:
    playlist = []
    current_key = seed_key
    used = set()
    while len(playlist) < n:
        nexts = sorted(
            (t for t in candidates
             if t.id not in used
             and camelot_distance(current_key, t.camelot) <= max_hops),
            key=lambda t: (camelot_distance(current_key, t.camelot), -t.popularity),
        )
        if not nexts:
            break
        pick = nexts[0]
        playlist.append(pick)
        used.add(pick.id)
        current_key = pick.camelot
    return playlist
Enter fullscreen mode Exit fullscreen mode

This is also exposed as a single API call — GET /radio?seed_track_id=...&n=20&max_key_distance=2 — if you'd rather not run the candidate pool yourself.

BPM continuity matters too

Harmonic compatibility on its own isn't enough. Two tracks in 8A at 95 BPM and 175 BPM can't be mixed together without a cue-point trick or a halftime drop. A real harmonic-mixing matcher needs both:

  • Camelot distance ≤ K (harmonic constraint)
  • BPM difference ≤ ±N BPM (rhythmic constraint)

For house and techno, ±5 BPM is forgiving (you can pitch ±3% on a player). For trance and DnB, you can be looser (±10 BPM). For hip-hop and R&B you usually want exact match because the swing pattern doesn't sound right pitched.

Open Key vs Camelot

Open Key is an alternative notation that some platforms (Mixed In Key, Serato) support alongside Camelot. The mapping is straightforward:

Camelot Open Key Camelot Open Key
8A 1m 8B 1d
9A 2m 9B 2d
10A 3m 10B 3d
... ... ... ...

m = minor, d = major (dominant). Same circle, just rotated by 7 positions and re-labeled. The compatibility rules are identical — if your code uses Camelot, support both notations as input via a small lookup table and you're done.

Edge cases

  • Atonal / modal tracks — ambient, drone, and a lot of jazz don't have a clear key centre. Detection algorithms return low confidence; treat as "skip from harmonic constraints" in your matcher.
  • Key changes mid-track — some tracks modulate. Most detectors return the dominant key by duration; if your input source supports it, prefer the chorus/drop key for a DJ mix because that's what people will be hearing during a transition.
  • Half-time / double-time confusion — not a key issue but related: a track perceived at 170 BPM might be detected at 85. More on that here.

Try FreqBlog — free tier, no card: https://freqblog.com/

Further reading


Originally published at freqblog.com.

Top comments (0)