Files
maple/maple-py/re/REFERENCE_COMPARISON.md
T

471 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<id>.jpg`, plus optional `/PIONEER/*SETTING.DAT`.
maple writes `PIONEER/rekordbox/export.pdb` (`cli.py:156`) and
`PIONEER/USBANLZ/<bucket>/<id>/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.