iTranslated by AI
Sorting Rekordbox Playlists by Camelot Key + BPM: headroom v2.0 - Developing rbsort
In my previous article, I wrote about the issue of volume differences between tracks when playing from a USB drive on CDJs. Since Rekordbox's Auto Gain only works when connected to a DJ controller, I created a tool called headroom to burn the loudness information directly into the file before exporting to USB.
This article is about rbsort, a sub-command added in v2.0. The nature of the problem is exactly the same as the previous one: useful Rekordbox features that function only within the software are not carried over to CDJs. This time, the issue was "sorting order."
The Problem: Screen-sorted order is not carried over to CDJs
For those who select tracks based on the Camelot Key, there is a need to sort warm-up playlists by key and then by ascending BPM. This means ordering them 1A → 1B → 2A → 2B → ... → 12B, and within each key group, sorting from slower tracks to faster tracks. This is the foundation of harmonic mixing for DJs.
There is a common misunderstanding here: you can perform a single-column sort by Key or BPM on both the Rekordbox app screen and the browse screen of players like the CDJ-3000 or XDJ-1000MK2. You simply select Key or BPM from the "Sort by" menu. The problem lies beyond that. To my knowledge, no device supports compound sorting—that is, sorting by Key first, and then by ascending BPM within each Key group. When you sort by Key, the BPM order within Key groups remains as it was originally in the playlist; when you sort by BPM, the Key order is lost.
Concrete Example: Comparing sorting in 6 tracks
Let's assume the following 6 tracks are in a playlist in order of registration.
Input Playlist (by registration order)
| # | Track | Key | BPM |
|---|---|---|---|
| 1 | A | 1A | 130 |
| 2 | B | 2A | 124 |
| 3 | C | 1A | 124 |
| 4 | D | 2B | 130 |
| 5 | E | 2A | 128 |
| 6 | F | 2B | 124 |
If we apply "Sort by Key," "Sort by BPM," and "rbsort," we get the following results:
Sort by Key (Single column)
| # | Track | Key | BPM |
|---|---|---|---|
| 1 | A | 1A | 130 |
| 2 | C | 1A | 124 |
| 3 | B | 2A | 124 |
| 4 | E | 2A | 128 |
| 5 | D | 2B | 130 |
| 6 | F | 2B | 124 |
The Keys are grouped, but the BPM within each Key group remains in the registration order. Within 1A, it goes 130 → 124, and within 2B, it goes 130 → 124, which is the opposite of the desired harmonic rising flow.
Sort by BPM (Single column)
| # | Track | Key | BPM |
|---|---|---|---|
| 1 | C | 1A | 124 |
| 2 | B | 2A | 124 |
| 3 | F | 2B | 124 |
| 4 | E | 2A | 128 |
| 5 | A | 1A | 130 |
| 6 | D | 2B | 130 |
The BPMs are sorted, but the Keys jump back and forth on the Camelot Wheel (1A → 2A → 2B → 2A → 1A → 2B). Harmonic transition is not maintained.
rbsort (Compound: Key → BPM)
| # | Track | Key | BPM |
|---|---|---|---|
| 1 | C | 1A | 124 |
| 2 | A | 1A | 130 |
| 3 | B | 2A | 124 |
| 4 | E | 2A | 128 |
| 5 | F | 2B | 124 |
| 6 | D | 2B | 130 |
The Key groups follow the Camelot ascending order (1A → 2A → 2B), and within each group, tracks go from lower to higher BPM. This is the desired order for a warm-up playlist.
| Sorting Method | Key Order | BPM within Key Groups |
|---|---|---|
| Sort by Key | Sorted | Registration order (No guarantee) |
| Sort by BPM | Scattered | Sorted (but Key groups disappear) |
| rbsort | Sorted | Sorted in ascending order |
Even if you toggle between "Sort by Key" and "Sort by BPM," you cannot achieve a result that satisfies both. This is the gap between single-column and compound sorting.
Another Pitfall: Separation of Screen Sorting and Playlist Save Order
Even if a compound sort UI existed, there is another trap. When exporting to a USB for a CDJ, the playlist order uses the "internal playlist order"; the temporary sort state displayed on the screen is not carried over. Whether in the Rekordbox app or on the CDJ side, "Sort by" is merely a temporary view toggle, distinct from the order held by the playlist. If you were to create the order manually, you would need to drag tracks one by one to overwrite the "save order" within the playlist.
This problem has the exact same structure as the Auto Gain issue from the previous project. Software features that exist only within Rekordbox do not carry over to the other side of the USB. Isn't there something we can do?
Existing Solutions and Their Limitations
1. Rekordbox / CDJ "Sort by"
| Sortable columns at once | Only 1 column (Rekordbox app / CDJ-3000 / XDJ all) |
| Compound sorting | Not provided |
| Playlist save order | Not changed (view only) |
| USB export | Not reflected |
It is fundamentally insufficient for creating a harmonic mix sequence. It only provides "Sort by Key" and "Sort by BPM" separately, with no way to combine them.
2. Manual Playlist Reordering
Dragging and dropping tracks one by one changes the save order. This is carried over to the CDJ.
However, doing this for a playlist with over 100 tracks is not realistic. If you threw 100 tracks into a playlist in the order you previewed them, dividing them into 24 key groups and sorting each by ascending BPM would take half a day.
3. "Smart Playlist" / "My Tag"
Smart Playlists are dynamic filtering mechanisms based on tags or attributes, but they lack a UI to specify sorting orders. A Smart Playlist's order follows some internal Rekordbox default. My Tag is for categorization and also cannot specify sorting.
4. CSV Export + External Sort + Re-import
This is theoretically possible, but Rekordbox does not provide an official path to reconstruct playlists via CSV import. Using third-party tools carries the risk of losing metadata (cue points, hot cues, beat grids).
The rbsort Approach
rbsort utilizes the collection.xml export feature officially provided by Rekordbox. This feature exports the entire collection as an XML file, which can be re-imported into Rekordbox as an "Imported Library." This XML contains almost all information, including playlist ordering.
The processing flow is as follows:
- Export
collection.xmlfrom Rekordbox. - Use
rbsortto read the XML and sort each playlist by Camelot Key → BPM. - Append the sorted copies to the same XML, grouped under a folder named
Sorted (Key+BPM)/. - Register the file in the Rekordbox "Imported Library" and drag the sorted playlists into your main library.
- Export to USB.
The original playlists remain untouched. Since the sorted copies are placed in a separate folder, you can ignore them if you aren't satisfied, or compare multiple versions.
Rekordbox XML Structure
Before designing, I needed to understand the structure of the Rekordbox XML.
<?xml version="1.0" encoding="UTF-8"?>
<DJ_PLAYLISTS Version="1.0.0">
<PRODUCT Name="rekordbox" Version="..." Company="..."/>
<COLLECTION Entries="N">
<TRACK TrackID="123" Name="..." Tonality="1A" AverageBpm="124.00" ...>
<TEMPO Inizio="..." Bpm="..." Metro="..." Battito="..."/>
<POSITION_MARK Name="..." Type="..." Start="..."/>
</TRACK>
...
</COLLECTION>
<PLAYLISTS>
<NODE Type="0" Name="ROOT" Count="N">
<NODE Type="0" Name="My Folder" Count="2">
<NODE Name="My Playlist" Type="1" KeyType="0" Entries="50">
<TRACK Key="123"/>
<TRACK Key="456"/>
...
</NODE>
</NODE>
</NODE>
</PLAYLISTS>
</DJ_PLAYLISTS>
Key points are summarized below:
| Element | Role |
|---|---|
<COLLECTION> |
Metadata for all tracks. Tonality is the Camelot Key, AverageBpm is the BPM. |
<PLAYLISTS> |
Playlist tree structure. The first <NODE Type="0" Name="ROOT"> is always the top. |
<NODE Type="0"> |
Folder (box for grouping playlists). |
<NODE Type="1" KeyType="0"> |
Actual playlist. <TRACK Key="..."> is a reference to a TrackID. |
KeyType="0" |
TrackID reference playlist. |
KeyType="1" |
File path reference playlist (rbsort does not handle this). |
The Camelot Key is stored in the Tonality attribute. However, it only appears in the alphanumeric 1A..12B format if Preferences > View > Key display format > Alphanumeric is set in Rekordbox. If set to musical theory notations like Am or C#m, rbsort cannot handle them as sort targets.
Implementation: 2-pass Scan + Stream Rewrite
I wrote the XML processing using the quick-xml crate for stream processing. I considered deserializing the entire XML as an object using serde, but since details like attribute order or <![CDATA[...]]> might affect compatibility with the DJ_PLAYLISTS format, I decided to keep the original stream intact except for the parts that need modification.
1st Pass: Scanning
- Scan
<COLLECTION>and store(Camelot Key, BPM)for eachTrackIDin aHashMap. - Extract the list of TrackIDs from
<TRACK Key="...">for the target playlist (specified via--playlistor all playlists).
There was a subtle pitfall here. quick-xml emits Event::Start for tags with children and Event::Empty for self-closing tags. I initially assumed <TRACK> would always be a self-closing tag, but in actual Rekordbox exports, <TRACK> is emitted with Event::Start because it contains nested <TEMPO> or <POSITION_MARK> elements. If I only picked up Event::Empty, the collection map would be empty, causing all tracks to be judged as having "unknown Camelot Key/BPM," resulting in a sorted output identical to the input.
I encountered this just before the release. I added a regression test case with an XML where <TEMPO> is nested under <TRACK>.
Sorting
tracks.sort_by(|a, b| {
cmp_some_first(a.camelot, b.camelot)
.then_with(|| cmp_some_first(a.bpm, b.bpm))
});
cmp_some_first is a custom ordering function where Some values are placed before None. Since the Ord implementation for Option<T> places None first, I had to define a custom comparison function. Tracks with unreadable Camelot Keys are collected at the end, and within each key group, tracks with BPM 0 or unparsed data are collected at the end.
2nd Pass: Rewriting
<PLAYLISTS>
<NODE Type="0" Name="ROOT" Count="N"> ← Increment Count to N+1
<NODE ...>...</NODE> ← Existing playlists remain as is
<NODE ...>...</NODE>
+++ Insert new folder "Sorted (Key+BPM)/" here +++
<NODE Type="0" Name="Sorted (Key+BPM)" Count="K">
<NODE Type="1" KeyType="0" Name="My Playlist" Entries="50">
<TRACK Key="..."/> ← In sorted order
...
</NODE>
</NODE>
</NODE>
</PLAYLISTS>
I add exactly one new <NODE Type="0" Name="Sorted (Key+BPM)"> right before the closing tag of the root </NODE>. Regardless of whether a single or all playlists are processed, the number of top-level folders added is consolidated to one. This follows the same pattern as the original headroom tool, which groups processed files into a single backup/ folder. If folders were scattered at the top level of the collection, the Rekordbox sidebar would become chaotic.
Camelot Key Sort Order
The Camelot Wheel has 24 keys from 1A to 12B, where adjacent keys are "harmonic" compatible. I map this against the Rekordbox Tonality attribute (Alphanumeric notation).
pub fn parse_camelot(s: &str) -> Option<u8> {
let s = s.trim();
if s.len() < 2 || s.len() > 3 {
return None;
}
let (num_part, letter_part) = s.split_at(s.len() - 1);
let num: u8 = num_part.parse().ok()?;
if !(1..=12).contains(&num) {
return None;
}
let letter = match letter_part {
"A" | "a" => 0u8,
"B" | "b" => 1u8,
_ => return None,
};
Some((num - 1) * 2 + letter)
}
This maps to a monotonically increasing sort key: 1A=0, 1B=1, 2A=2, ..., 12B=23. Musical theory notations like Am or C# return None and are moved to the end during sorting.
1A → 1B → 2A → 2B → 3A → 3B → 4A → 4B → 5A → 5B → 6A → 6B
→ 7A → 7B → 8A → 8B → 9A → 9B → 10A → 10B → 11A → 11B → 12A → 12B
→ (Tracks with unspecified Camelot Key, BPM ascending → 0/unparsed at the end)
How to Use
Installation
brew install M-Igashi/tap/headroom # macOS
winget install M-Igashi.headroom # Windows
yay -S headroom-bin # Arch Linux (AUR)
cargo install headroom # Any platform
The rbsort sub-command does not require ffmpeg. It works entirely with XML read/write.
Command
# Sort all playlists at once
headroom rbsort --xml ~/Music/rekordbox/collection.xml
# Sort a single playlist (top-level)
headroom rbsort \
--xml ~/Music/rekordbox/collection.xml \
--playlist "Happy House and Trance"
# Specify a nested playlist
headroom rbsort \
--xml ~/Music/rekordbox/collection.xml \
--playlist "Sets/Friday"
If you omit --output, it writes the result as collection-out.xml in the same directory as the input.
Full Workflow
- Rekordbox: Set Preferences > View > Key display format > Alphanumeric.
- Rekordbox: Export
collection.xmlvia File > Export Collection in xml format. - Run
headroom rbsort --xml collection.xmlin your terminal. - Rekordbox: Specify
collection-out.xmlin Preferences > Advanced > Database > rekordbox xml > Imported Library. - Restart Rekordbox (XML is only reloaded at startup).
- Open the rekordbox xml tree in the left sidebar (separate from the main Collection tree).
- Drag the playlist inside the
Sorted (Key+BPM)/folder into your main Collection > Playlists. - Switch to EXPORT mode, mount your USB/SD, right-click the playlist, and select Export Playlist.
The drag-and-drop operation in step 7 is required by the Rekordbox UI, but after that, you can take it straight to the CDJ. The order is burned into the file (via the playlist's internal order in the XML), so there is no need to re-sort on the CDJ side.
Execution Example
Terminal
$ headroom rbsort --xml ~/Music/rekordbox/collection.xml
✓ Sorted 7 playlists (438 total tracks) into 'Sorted (Key+BPM)/'
→ /Users/me/Music/rekordbox/collection-out.xml
ℹ Import via Rekordbox: Preferences > Advanced > Database > rekordbox xml
ℹ Restart Rekordbox, then open the 'rekordbox xml' tree in the left sidebar
After Importing into Rekordbox

Sorted copies with the same names as the originals appear under the Sorted (Key+BPM)/ folder in the sidebar. Opening Hard Techno confirms that the Key column ascends as 1A → 1B → 2A → 2B → 3A, and BPMs are sorted within each key group. The original playlists like headroom-tracks remain at the bottom of the sidebar. You can export them via right-click to USB immediately, and if the CDJ is in its default display mode, the sorting will be carried over.
Constraints and Notes
| Item | Constraint |
|---|---|
| Camelot notation | Alphanumeric 1A..12B only. Am, C#m are treated as the end. |
| Playlist type |
KeyType="0" (TrackID reference) only. KeyType="1" (file path reference) causes an error on single specification, or is silently skipped in batch mode. |
| ROOT folder | Exactly one folder is added. Sorted (Key+BPM)/ remains unique even when processing multiple playlists. |
| Source data | Original playlists are not touched. collection.xml itself is not modified (output is a separate file). |
| ffmpeg | Not needed. rbsort runs only with XML. |
Summary
Software features that function only within Rekordbox (Auto Gain, Multi-column Sort) are not carried over to CDJs across the USB. The previous headroom took an approach of burning gain into files for the loudness issue. This rbsort takes an approach of burning the sort order into the XML for the sorting issue.
- ✅ Original data untouched: Just adds to a separate folder named
Sorted (Key+BPM)/. - ✅ No ffmpeg needed:
rbsortsub-command works entirely with XML. - ✅ Batch processing: Omit
--playlistto process all playlists at once. - ✅ Official Rekordbox path: Uses the
collection.xmlImported Library feature, so it does not rely on unofficial APIs. - ✅ CDJ compatible: Rewrites the internal playlist order, so it is reflected even after USB export.
Previous work: Eliminating Volume Differences Between Tracks During Rekordbox Export -- Development of headroom for Safe Gain Adjustment
⭐ Your stars are a great encouragement!
Discussion