Home Pricing Docs Compare SDK Blog Changelog Status Get API Key →
DJ Tech · Music Theory

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

April 2026 · 7 min read · FreqBlog

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.

CamelotKey nameCamelotKey name
1AA♭ minor1BB major
2AE♭ minor2BF# major
3AB♭ minor3BD♭ major
4AF minor4BA♭ major
5AC minor5BE♭ major
6AG minor6BB♭ major
7AD minor7BF major
8AA minor8BC major
9AE minor9BG major
10AB minor10BD major
11AF# minor11BA major
12AC# minor12BE 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')]

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

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

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:

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:

CamelotOpen KeyCamelotOpen Key
8A1m8B1d
9A2m9B2d
10A3m10B3d
............

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

Skip the implementation work

FreqBlog Music API exposes Camelot, Open Key, key name, key confidence, plus a free /key/<camelot>/compatible helper (no auth, no quota); metadata lookups from £0.17 per 1,000 requests.

View API →

Further reading