# REFERENCE_COMPARISON.md — pyrekordbox & rekordcrate vs maple What the two mature open-source rekordbox libraries can decode/encode, how that lines up with maple's writer and our RE findings, and the concrete GAPS maple must close for a full from-scratch USB export. - **pyrekordbox** (Python, dylanljones) — https://pyrekordbox.readthedocs.io - **rekordcrate** (Rust, Holzhaus) — https://holzhaus.github.io/rekordcrate/ local source at `/mnt/c/Users/ntr/code/rekordcrate/src` - **maple** — `maple-py/src/maple/*`, this `re/` dir. Scope note (coordination): the per-format **library-capability** comparison and the from-scratch-export gaps are mine (task #12). The master.db SQLCipher key mechanics and opaque Content ID identity are owned by `re` (#10); the exact USBANLZ/artwork on-disk path schemes by `re` (#11). Where those are cited below they are tagged `[see re #10]` / `[see re #11]` and should be treated as `re`'s authority, not re-derived here. --- ## 0. One-screen capability matrix | Format | pyrekordbox | rekordcrate | maple | |---|---|---|---| | **export.pdb** | parse only (via the `pdb` parser, read) | **parse + WRITE** (`BinWrite` on rows/pages, full roundtrip) | **WRITE only** (from-scratch, the whole point) | | **exportExt.pdb** | not supported | not supported | not written | | **ANLZ (.DAT/.EXT/.2EX)** | **parse only** ("creating … is planned") | parse + *partial* write (binrw `BinWrite` derived on waveform/cue structs; not a whole-file builder) | **WRITE** (minimal: PMAI + PPTH only) | | **artwork (.jpg thumbs)** | reads `ImagePath` from db only | n/a (just the pdb `Artwork` row path string) | **WRITE** (extracts embedded art) | | **master.db (db6/7)** | **READ + WRITE** (SQLCipher unlock) | not supported | not used | | **mysetting (\*SETTING.DAT)** | **READ + WRITE** (build from scratch) | parse only (`setting` module) | not written | | **rekordbox XML** | **READ + WRITE** | parse only (`xml` module) | not used | The headline: **no existing library writes a complete from-scratch USB export.** rekordcrate writes `export.pdb` rows but not ANLZ/exportExt and has no metadata source; pyrekordbox writes master.db + mysetting + XML but not the USB `export.pdb`/ANLZ. maple is filling a real gap. The reference libs are most valuable to maple as (a) a **read-back oracle** (parse maple's output and diff) and (b) for pyrekordbox, a **source of the real opaque Content IDs** from master.db. --- ## 1. export.pdb ### What rekordcrate does (the closest reference writer) `pdb/mod.rs` decodes **and writes** every row type maple emits: Track, Artist, Album, Artwork, Color, Genre, Key, Label, PlaylistTreeNode, PlaylistEntry, ColumnEntry, HistoryPlaylist, HistoryEntry (`pub enum Row`, lines ~1064-1103). It has a genuine page serializer: `write_page_contents` (mod.rs:212), `write_options_and_get_row_offset` (mod.rs:463), per-row-type alignment (`align_by`, mod.rs:1121-1133), and a `Track` `BinWrite` impl (mod.rs:934). ### Where rekordcrate CONFIRMS maple's layout (high-value confirmations) - **Track fixed prefix = 0x5e bytes, then a 21×u2 (0x2a) offset block.** rekordcrate asserts `start_of_string_section - base_position == 0x5e` and seeks `0x2a` for 21 string offsets (mod.rs ~986-996). maple's `TRACK_FIXED_PREFIX = 0x5E`, `TRACK_HEADER_LEN = 0x5E + 21*2` (`rows.py:8-10`) and its 21-string list match field-for-field, including the exact string order (isrc, …, analyze_path, analyze_date, comment, title, unknown_string8, filename, file_path). **Independent confirmation of maple's track row.** - **Field order of the 0x5e prefix** is identical between rekordcrate's `Track::write_options` and maple's `serialize_track`: unknown1/index_shift/ bitmask/sample_rate/composer_id/file_size/`unknown2`(=opaque lo @0x14)/ `unknown3`+`unknown4`(=seed @0x18/0x1a)/artwork_id/key_id/orig_artist_id/ label_id/remixer_id/bitrate/track_number/tempo/genre_id/album_id/artist_id/ id/disc/play_count/year/sample_depth/duration/unknown5/color/rating/unknown6/ unknown7. - **DeviceSQL string encoding.** rekordcrate `pdb/string.rs` roundtrips the same three forms maple's `strings.py` writes: short-ASCII (`(len<<1)|1`), long-ASCII (`0x40`), long-UTF16LE (`0x90`). Its tests encode `"I ❤ Rust"` as 0x90 and a long string as 0x40 — exactly maple's selection rule. The empty string is `0x03` in both. **Confirms strings.py.** - **Row-group / page-index mechanics.** rekordcrate's `RowGroup` (mod.rs:451, 2102) uses the same 16-rows-per-group, backward-growing offset index, and presence bitmask maple builds in `pager.py`. `Header` (mod.rs:162) carries the same 13-bit num_row_offsets / 11-bit num_rows packing maple writes (`pager.py:131`). ### Where rekordcrate caught a maple BUG — `next_page` chain termination (the FREEZE) rekordcrate `pdb/mod.rs` documents `Page::next_page` (lines 262-265) precisely: > *"Index of the next page with the same page type. If this page is the last one > of that type, the page index stored in the field will point **past the end of > the file**."* maple's writer originally set the **last** page of each table chain to `next_page = its own page_index` (a self-pointer). That is exactly what rekordcrate says must NOT happen — the last page must point *forward, past the allocated pages*. A self-referencing `next_page` makes a firmware page-walk that terminates on "next_page past EOF" **loop forever on the last page → the deck freezes.** This is the root cause being fixed in task #13: `writer.py` now points every last page at a dedicated free-pool sentinel page just past the overflow region (`free_sentinel`, materialised zero-filled) and sets `next_unused_page` beyond it — which removes the self-loop. (rekordcrate's own `read_pages`, mod.rs:185-196, terminates the walk on `page_index == table.last_page` from the directory, so its *parser* tolerates a self-pointer — which is why maple's read-back oracle never flagged it. The *firmware* uses the chain-walk, so only a real deck exposed the freeze. Lesson: a clean rekordcrate parse is NOT proof the chain is firmware-valid.) > **Verified against a real export — the EXACT rule (not just "past EOF").** > I decoded `data/PIONEER/rekordbox/export.pdb` (847 tracks, 174 pages, > `next_unused=177`). rekordcrate's "points past the end of file" is a > simplification; the real, consistent rule across **all 20 tables** is: > **each table's last/only page sets `next_page == that table's > `empty_candidate`**, where `empty_candidate` (table-dir entry +4) is a > *distinct, nonzero* page index `< next_unused` — the head of that table's > free-page chain. Evidence: every last page's `next_page` equalled its own > dir `empty_candidate` (type0 173→174=ec; type1 4→53=ec; type19 40→41=ec; …), > and those targets (174,53,158,163,54,49,42,46,170,20,22,175,176,165,30,32, > 43,44,45,41) are all `< next_unused`. Strange-only tables already match > (next_page = page_idx+1 = their ec; free-map `data_ptr=0x03ffffff`) — maple's > `build_strange_page` does this correctly. > > So maple's current #13 fix (a **single shared sentinel** for every data > table + `empty_candidate=0` in the directory) removes the freeze but still > **diverges** from real rekordbox, which uses a **distinct per-table > `empty_candidate`** that the directory entry also records (nonzero). If > firmware ever cross-checks `next_page` against the directory's > `empty_candidate`, the shared sentinel + zero ec could still misbehave. > Recommended (reported to `maple`): give each data table its own free page, > set `next_page = that page` AND write that index into the table-dir > `empty_candidate`, keep them all `< next_unused`. This is straightforwardly > reproducible (unlike the *value* of `empty_candidate` on an edited ref, which > is GC state — here we only need a valid distinct free page, which a > from-scratch writer fully controls). ### Where rekordcrate is WRONG and maple is RIGHT — page row-count header (>255 rows) Audited because rekordcrate's `Page` header (mod.rs:277-377) reads the row count as **`num_rows_small: u8` @0x18** plus **`num_rows_large: u16` @0x22** with a `calculate_num_rows` rule (`large if large>small && large!=0x1fff else small`). maple instead writes the **canonical crate-digger .ksy bitfield** at 0x18: 13-bit `num_row_offsets` + 11-bit `num_rows`, then `page_flags` @0x1b (`pager.py:131`). These are two different readings of the same bytes — so I **verified against a real export** (`data/PIONEER`, which has 284-row playlist-entry pages): - Real page 18 bytes @0x18 = `1c 81 23 24`. The 24-bit LE value `0x23811c` decodes (kaitai/maple way) to `num_row_offsets=284, num_rows=284` — **correct**. - maple's packer reproduces `1c 81 23` **exactly** for n=284 (and `a6 c0 14` for 166, `7e c0 0f` for 126 — all matching real pages byte-for-byte). - rekordcrate's reading of the *same* bytes: `num_rows_small = 0x1c = 28` (just the low byte) and `num_rows_large @0x22 = 283` (which is really the `transaction_row_index` edit field), giving `calculate_num_rows = 283` — i.e. rekordcrate would report **283 rows on a 284-row page, and 28 if the transaction field were 0.** Its small/large model only coincidentally agrees when `n <= 255`. - **Conclusion:** maple's header encoding is correct and firmware-faithful for pages up to 2047 rows (the 11-bit max); rekordcrate's `num_rows_small`/ `num_rows_large` is an older/divergent decode that breaks for `n > 255`. This matters because maple *can* emit >255-row pages (playlist_entries fits ~284/ page, colors ~394). No maple change needed — this is a maple-is-right result. ### Where rekordcrate's row alignment diverges (maple matches real rekordbox) rekordcrate aligns each row by its Rust struct's natural `align_of_val` (mod.rs:1114-1136), which would 2-align u16-only row types. maple aligns **every row to 4** (`pager._align4`). Checked real exports: **every row in `data/PIONEER` (3567/3567) and `rb_ref` (101/101) is 4-aligned.** So maple's universal 4-alignment matches rekordbox; rekordcrate's per-type scheme is the divergent one (its own code comments admit "we should survey all the different kind of rows and ensure they're aligned properly", mod.rs:1110). No maple change needed. ### Where maple's RE goes BEYOND rekordcrate (we know more than the reference) These are fields rekordcrate leaves as opaque caller-supplied "unknown"s but maple's RE solved/characterised — important because it means **rekordcrate cannot be used to validate these; maple's FORMAT_SPEC is the authority**: - **`index_shift` (row +0x02).** rekordcrate calls it `index_shift` but writes whatever the caller stored — it does **not** know the value (mod.rs:104-105 "Unknown field, called `index_shift` by flesniak"). maple **derives** it as `slot_index << 5` and patches it at page-build time (`pager.py:142-145`, FORMAT_SPEC.md §1). maple is correct where rekordcrate is agnostic. - **The 8-byte opaque blob (0x14 lo / 0x18+0x1a seed).** rekordcrate: `unknown2: u32`, `unknown3: u16` ("always 19048?"), `unknown4: u16` ("always 30967?") — three blind unknowns it round-trips verbatim (mod.rs:115-119). maple's RE (FORMAT_SPEC.md §2) proved this is one LE u64 = `opaque_track_id_lo | (opaque_seed<<32)`, a per-track Content ID + per-install seed stored in master.db, **non-derivable** from metadata, and that the "19048/30967" constants are NOT universal (they only hold for some old exports). maple emits a non-zero stand-in; the real values live in master.db `[see re #10]`. maple's understanding is strictly deeper than rekordcrate's. - **`bitmask` (0x06).** rekordcrate: opaque "unknown" round-tripped. maple writes a fixed `788224` (0xC0700) — itself not yet RE'd to a formula, but maple at least pins a known-good value. - **Strange/"first" pages.** rekordcrate leaves the strange-page heap **zero** (it round-trips parsed input); maple **constructs** the full DeviceSQL free-space map from scratch byte-for-byte (`pager.build_strange_page`, verified vs `data/acemo_fresh` and the real strange pages in data/PIONEER), including the `f8 ff ff 1f`×1004 map, the `0x03ffffff` no-data sentinel, and `unknown7=1` for tracks/history. `re` confirms maple is **strictly more faithful than rekordcrate on strange pages.** This is from-scratch generation rekordcrate never had to do. - **RowGroup trailing u2.** rekordcrate writes a constant `16` placeholder (`Default::default`, mod.rs:454) and does **not** decode it; `re` confirms genuine rekordbox **never** uses 16 there — it is the "rows touched by the last transaction" bitmask (FORMAT_SPEC §6). maple writes `present_flags` (the single-transaction value), closer to real output; either way it is edit-history state (KNOWN_DIVERGENCE §6), not a rekordcrate-validatable field. ### Where rekordcrate (and our RE) AGREE the field is unreproducible — with a caveat rekordcrate documents `empty_candidate` ("maybe links to a chain of empty pages if the database is ever garbage collected") and `next_unused_page` ("points past end of file") as allocator/GC state — matching KNOWN_DIVERGENCE.md items 11 and 1. The **historical VALUE** on an edited reference is indeed unreproducible. **BUT** my data-page audit (see the FREEZE section above) found `empty_candidate` is **load-bearing structure**, not just GC trivia: in data/PIONEER every table's last page has `next_page == that table's empty_candidate`, a distinct free page `< next_unused`. So a from-scratch writer must still emit a *valid* distinct `empty_candidate` per table (which it fully controls) even though it can't match an edited ref's specific value. **Confirms** these are correctly on maple's divergence list, not bugs. ### GAP — none structural for export.pdb maple writes a complete, players-accepted export.pdb. Remaining export.pdb divergences from a reference file are all **edit-history artifacts** (KNOWN_DIVERGENCE.md) that neither library can reproduce from content either. The one content-level gap is the opaque blob (see §4). --- ## 2. exportExt.pdb — GAP (nobody writes it; maybe nobody needs to) - **pyrekordbox:** no support. - **rekordcrate:** no support (its `pdb` module is export.pdb only). - **maple:** does not emit it. Format facts (Deep Symmetry export analysis; reverse-engineered by Dominik Stolz, crate-digger issue #11): `exportExt.pdb` is a *second, smaller* DeviceSQL file beside `export.pdb` holding only two table types — **Tags (type 03)** and **Tag Tracks (type 04)** — i.e. the "My Tag" categorisation feature and its tag→track associations. It uses the same page/row container format as export.pdb. **Assessment / recommendation:** exportExt.pdb is **optional**. It only carries the My-Tag feature; players that don't find it simply show no custom tags, and tracks/playlists from export.pdb play normally. maple has no tag data to populate anyway. **Do not block a v1 export on this.** If maple ever supports tags, the existing `pager.py`/`PageBuilder` already produces the container format — only two new row serialisers (tag row, tag-track row) and a second `write_pdb`-style call writing the 2-table directory would be needed. Confidence the file is skippable: high (it post-dates the core format and is absent from older exports that still play). --- ## 3. ANLZ (.DAT / .EXT / .2EX) — the biggest real GAP ### What the references decode - **pyrekordbox** (`pyrekordbox.anlz`): full **parser** for PMAI + all section tags — PPTH, PQTZ/PQT2 (beat grids), PCOB/PCO2 (cues/loops), PVBR, PSSI (phrase/song-structure, XOR-masked), PWAV/PWV2 (previews), PWV3/4/5 (detailed + colour waveforms), PWV6/7/PWVC (.2EX). Writing is **"planned … but the full structure has to be understood first"** — i.e. **read-only today.** - **rekordcrate** (`anlz.rs`): parses the same tag set; magic constants present in source: PMAI, PPTH, PQTZ, PVBR, PWAV, PWV2, PCOB, PCO2, PCPT, PCP2, PSSI, PWV3/4/5. It derives `BinWrite` on the waveform-column structs (`WaveformPreviewColumn` etc., anlz.rs:368-428) and has a `write_encrypted` for the XOR-masked PSSI (anlz.rs:902) — so it has *piece-level* write code, but **no whole-file ANLZ builder / CLI to emit a valid .DAT/.EXT from track audio.** ### What maple does `anlz.py` writes a **minimal** `ANLZ0000.DAT`: a 28-byte PMAI header + a single **PPTH** section (u4 len + UTF-16BE path). No beat grid, no waveform, no cues, no `.EXT`, no `.2EX`. Rationale in the module docstring: firmware reads PPTH to confirm which audio file the analysis belongs to, and a track whose `analyze_path` points at *nothing* can be hidden from the library, so PPTH alone makes the track "appear analysed enough to surface." ### CONFIRMATIONS - maple's PMAI/PPTH layout (4-cc + u4 len_header + u4 len_tag; PPTH = u4 len_path + UTF-16**BE**, big-endian throughout) matches the format both references parse. The big-endian-for-ANLZ-vs-little-endian-for-pdb split that `anlz.py` calls out is corroborated by both libraries. ### GAPS maple must close for full faithfulness 1. **No `.EXT` file.** Modern CDJs/XDJs (NXS2+) read `ANLZ0000.EXT` for colour waveforms (PWV4/5), extended cues (PCO2), and phrase data (PSSI). Without it maple tracks show **no colour waveform and no phrase/structure** on newer players. The `analyze_path` in the pdb points at the `.DAT`; the player derives the `.EXT` sibling. maple writes neither beyond the `.DAT` PPTH stub. 2. **No beat grid (PQTZ).** Without a beat grid, beat-sync / quantize / loop features have nothing to lock to; the track plays but degrades the CDJ experience. This is the highest-value ANLZ section to add after PPTH. 3. **No waveform (PWAV/PWV3-5).** Cosmetic but conspicuous — tracks show a flat or absent waveform. pyrekordbox can *parse* these to learn the exact byte layout, and rekordcrate's `BinWrite` waveform-column structs are a concrete reference for the encoding, but **maple must generate the waveform data itself** (decode audio → downsample → colour-band), which neither library provides. 4. **Generation, not just layout, is the hard part.** The byte format is fully documented (both libs + Deep Symmetry); what's missing everywhere is the DSP that *produces* beat positions and waveform columns from PCM. That DSP is maple's to build — the references only validate the container. **Recommendation:** keep the PPTH-only `.DAT` as the working baseline (it is what gets tracks visible), and treat PQTZ beat grid + a `.EXT` with PWV5 colour waveform as the next ANLZ milestones. Use pyrekordbox's parser as the round-trip oracle (write a section, parse it back, compare). Confidence the PPTH-only stub is sufficient for *visibility/playback*: medium-high (matches maple's own deck testing rationale); confidence it's sufficient for a *complete* experience: low — beat grid/waveform are expected by users. --- ## 4. master.db (db6/7) + the opaque Content IDs — GAP maple can close via pyrekordbox ### What pyrekordbox does (this is its core strength) — VALIDATED by re #10 - Reads **and writes** the encrypted `master.db`. The db is **SQLCipher4** encrypted with a **single GLOBAL key, identical on every install** (not machine/license-derived). `re` validated the concrete key (FORMAT_SPEC §2 Round-3): `402fd482c38817c35ffa8ffb8c7d93143b749e7d315df7a81732a1ff43608497`. pyrekordbox 0.4.4 + `sqlcipher3-wheels` installs and wires up correctly, so a **valid** master.db opens and exposes `DjmdContent` `[see re #10]`. - Exposes `DjmdContent` (one row per track) with: **`ID`** (the rekordbox internal Content ID), `FolderPath`, `FileNameL`, **`AnalysisDataPath`** (the `/PIONEER/USBANLZ/...` analysis path), **`ImagePath`** (artwork), and FK ids to Artist/Album/Genre/Label/Key/Composer; plus `djmdCue`, `djmdPlaylist`/ `djmdSongPlaylist`, `djmdHistory`, `djmdMixerParam`, `djmdDevice`, etc. > **Reality check from re #10 — currently BLOCKED on data, not on method:** > - The captured `maple-py/captures/p17328_master.db` is **corrupt/truncated** > (zeroed SQLCipher salt + page 1) — no key can open it. The genuine > `exportLibrary.db` shipped beside export.pdb uses a *different* key scheme > that the global master.db key does not open, and pyrekordbox has no > exportLibrary.db support. So the IDs have **not yet been read** on this RE > machine — the path works only against a **live** > `%APPDATA%/Pioneer/rekordbox6/master.db`. > - `opaque_track_id_lo == DjmdContent.ID & 0xFFFFFFFF` is a **strong inference, > still UNTESTED** (the lo range fits the 9-digit Content-ID band), not a > confirmed equality. > - The per-install **seed** (`0x21b4a7ab`) is **NOT** a DjmdContent column and > its source is **still unidentified** (likely a library/USB UUID or > agentRegistry/local_usn-style marker; needs a readable master.db to find). > - pyrekordbox `add_content` lets *rekordbox* assign the Content ID — there is > **no API to force a specific ID** — and it does **not** drive a USB export > (`get_device` is read-only). So pyrekordbox is a **read source of real IDs**, > not a writer/exporter and not a way to dictate IDs. ### Why this matters for maple (ties to FORMAT_SPEC §2 / KNOWN_DIVERGENCE #9) maple's RE proved the export.pdb track blob at 0x14/0x18 is the rekordbox **Content ID** (per-track, 28-bit band) plus a **per-install seed**, both stored in master.db and **not derivable** from the audio/metadata maple has. maple currently emits a non-zero *stand-in* (`crc32(id|file_path)` lo + `TRACK_OPAQUE_SEED = 0x21b4a7ab`). Therefore: - **GAP (closable in principle, not yet proven): real Content IDs.** If maple runs on a machine with rekordbox installed, it can use **pyrekordbox + the global key to open the LIVE `%APPDATA%/Pioneer/rekordbox6/master.db` and read `DjmdContent.ID`** per track, then write those into the pdb blob instead of the stand-in. `re` validated the *mechanism* (global key, DjmdContent schema) but could NOT confirm `ID == opaque_track_id_lo` here because the only captured master.db is corrupt — so this is a **strong-but-untested** lead, not a closed item. Next step (re #10): point pyrekordbox at a live master.db and confirm the equality for the AceMo tracks. The per-install **seed** `0x21b4a7ab` source is still unidentified, so maple keeps the configurable `TRACK_OPAQUE_SEED` either way. - **Caveats:** (a) works only *with a rekordbox install present and a readable master.db* (not the corrupt capture), and only for tracks rekordbox already knows; (b) pyrekordbox `add_content` cannot **force** a chosen ID (rekordbox assigns it) and does **not** drive a USB export, so it can supply IDs but cannot replace maple's writer. Pure from-scratch (no rekordbox install) stays a documented divergence (KNOWN_DIVERGENCE #9), with maple's non-zero stand-in. ### rekordcrate No master.db support at all — it is a *device-export* (USB) library, not a *library-database* library. So it cannot help with Content IDs. --- ## 5. mysettings (\*SETTING.DAT) — GAP (small, easy) maple doesn't write - **pyrekordbox:** reads **and writes/builds** `DEVSETTING.DAT`, `MYSETTING.DAT` (40B, CDJ play settings), `MYSETTING2.DAT` (40B), and `DJMMYSETTING.DAT` (52B, DJM mixer) — dict-style get/set + save. (`DEVSETTING` payload still partly ununderstood.) - **rekordcrate:** `setting` module **parses** these (read-only). - **maple:** does **not** write any of them. These live in `PIONEER/` on the USB alongside `rekordbox/`. They are **optional preference files** — absent, the player just uses its own defaults; tracks and playlists are unaffected. **Recommendation:** low priority. If desired, pyrekordbox can *build* default ones in a few lines (it's read/write), so maple could shell out to / import pyrekordbox to drop standard `MYSETTING.DAT` + `MYSETTING2.DAT` rather than re-implementing the 40-byte structs. Confidence they're optional for playback: high. --- ## 6. Overall directory layout + path schemes (cross-check) `[see re #11 / FORMAT_SPEC §7]` Both references and Deep Symmetry agree on the USB tree maple targets: `/PIONEER/rekordbox/export.pdb` (+ optional `exportExt.pdb`), `/PIONEER/USBANLZ/<...>/ANLZ0000.{DAT,EXT,2EX}`, `/PIONEER/Artwork/<...>/a.jpg`, plus optional `/PIONEER/*SETTING.DAT`. maple writes `PIONEER/rekordbox/export.pdb` (`cli.py:156`) and `PIONEER/USBANLZ///ANLZ0000.DAT` (`cli.py:92`). `re` resolved both path schemes (#11, FORMAT_SPEC §7): - **Artwork — EXACTLY reproducible, and maple already matches.** Real rekordbox uses `/PIONEER/Artwork/{id//20+1:05d}/a{id}.jpg` (+ a `a{id}_m.jpg` 240px thumb), 20 ids per bucket folder (bucket 1 = ids 1-19). `re` verified 176/176 in data/MAPLE, and maple's `artwork._bucket()` already implements this rule. **Reproducible, confirmed — no gap.** - **USBANLZ — NOT reproducible; maple's own scheme is correct-by-construction.** Real rekordbox uses `/PIONEER/USBANLZ/P{bucket}/{8hex}/ANLZ0000.DAT` where the 8-hex is an **opaque per-track analysis id** (NOT the track id, NOT the row 0x14 lo, NOT any crc of the path — `re` ruled these out) and the P-bucket (0-127) is **not a derivable function** of it. Like the 0x14 blob, it is DB-sourced and non-reproducible. maple synthesises its own `P{(crc32(path)>>20)&0xFFF:03X}/{crc32(path):08X}` and writes the `.DAT` there, so the pdb's `analyze_path` and the on-disk file always agree. **This is fine:** the player only requires every pdb path to resolve to a real on-disk file, not to match rekordbox's id scheme. > **Gotcha for the gaps below (from `re`):** `tools/build_acemo.py` MIRRORS a > reference export's paths verbatim and writes **no** ANLZ/artwork files (it's a > byte-diff fixture builder). The **production** `cli.py build()` is the one that > actually writes each ANLZ/artwork file at the recorded path. Don't conflate > the two when reasoning about what maple emits on disk. --- ## 7. Bottom line — prioritised GAP list for a full from-scratch export 1. **(high, lead — untested) Real opaque Content IDs via pyrekordbox/master.db** — mechanism validated by `re` (global SQLCipher key `402fd482…` + `Rekordbox6Database` reads `DjmdContent`), but `DjmdContent.ID == opaque_track_id_lo` is a **strong INFERENCE, NOT validated** (the only captured master.db is corrupt — zeroed salt — so `re` never confirmed the equality on real data). Would close KNOWN_DIVERGENCE #9 *if* confirmed against a readable **live** `%APPDATA%/Pioneer/rekordbox6/master.db`. `[see re #10 / FORMAT_SPEC §2 Round-3]` 2. **(OPEN — Ghidra trace pending) Per-install `opaque_seed` source.** The seed (`0x21b4a7ab` / `0xd6d3a567`) is **NOT** a `DjmdContent` column and `re` has **not located it** — likely a per-library UUID/agent/usn marker or a computed value. It needs a readable master.db to inspect candidate rows, OR a Ghidra trace of rekordbox.exe (which `re` can now attempt — the Ghidra MCP just came online), so it is **not permanently blocked.** Until resolved, maple keeps the configurable `TRACK_OPAQUE_SEED` stand-in. This is the one genuinely open question touching the opaque blob. `[see re #10 — OPEN; FORMAT_SPEC §2 Round-3]` 3. **(high) ANLZ beat grid (PQTZ) + an `.EXT` colour waveform (PWV5)** — needs DSP maple must build; references validate the byte format only. Biggest functional gap on modern players. 4. **(structural — in progress) Per-table `empty_candidate` / last-page `next_page`** — the FREEZE fix (#13). maple must point each table's last page at a distinct free page recorded in the dir's `empty_candidate`, not a shared sentinel with `empty_candidate=0`. See the FREEZE section in §1. 5. **(low) mysettings** — optional; pyrekordbox can build them. 6. **(low/skip) exportExt.pdb** — optional My-Tag data; only if maple adds tags. 7. **(reference, not a gap) Use rekordcrate as a read-back oracle** — parse maple's `export.pdb` with rekordcrate's CLI to catch container regressions; use pyrekordbox's anlz parser as the oracle for any new ANLZ sections. **Caveat (proven this session):** rekordcrate's parser tolerates things the firmware rejects (the `next_page` self-loop) and is itself wrong on some fields (>255-row count, per-type alignment) — so it is a *regression* oracle, NOT a correctness authority. Validate load-critical fields against REAL export bytes (e.g. data/PIONEER), not against rekordcrate. **Headline confirmation for the team:** rekordcrate's *writer* independently validates maple's export.pdb track-row layout, DeviceSQL string encoding, and page/row-group container down to the 0x5e/0x2a offsets — but it is *agnostic* exactly where maple's RE added knowledge (`index_shift` formula, the opaque-id semantics). So maple's FORMAT_SPEC is the authority for those fields, and the remaining real work is **ANLZ generation** and **pulling the real Content IDs from master.db** — neither of which any existing library does end-to-end.