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')]
If you'd rather skip writing this yourself, our API exposes the same logic as GET /key/<camelot>/compatible?extended=true — no 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:
- 0 hops — same Camelot exactly
- 1 hop — relative (same number, opposite letter), or adjacent (±1, same letter)
- 2-3 hops — "stretchy" but workable; many crowd-friendly mixes live here
- 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.
1Aand12Aare one hop apart, not eleven. Themin(diff, 12 - diff)trick handles it.
Building a harmonic playlist
Now you have compatibility and distance. The simplest playlist algorithm:
- Pick a seed track
- For each subsequent slot: filter the candidate pool to tracks within hop-distance ≤ K of the previous track's key
- Among those, pick by a secondary criterion (BPM continuity, similarity, popularity)
- 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:
- 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
- Mixed In Key vs Rekordbox vs Serato: Why DJ Platforms Disagree on Key 60% of the Time
- Half-Time vs Double-Time BPM Detection
- DJ tooling on the FreqBlog API
Originally published at freqblog.com.
Top comments (0)