the downfall has begun
This commit is contained in:
1 parent
f71a2a3512
commit
9ccc3cea95
25 files changed
+4976
-109
No files matched your search
@@ -0,0 +1,609 @@
|
||||
# export.pdb value-construction rules (RE findings)
|
||||
|
||||
This file records the exact rules rekordbox uses to populate `export.pdb`
|
||||
fields that maple currently writes wrong or as zero. Each section is a
|
||||
self-contained spec for the `maple` writer to implement. Evidence is from
|
||||
reverse-engineering `rekordbox.exe` (DeviceSQL "Page Manager", `dsql*` /
|
||||
`edb_dyn_*`) in Ghidra plus byte-level analysis of the reference export
|
||||
`maple-py/data/acemo/PIONEER/rekordbox/export.pdb` (6 tracks, 1 album,
|
||||
1 artist, 1 key, 6 artwork).
|
||||
|
||||
Reference-pdb facts used throughout:
|
||||
- `len_page` = 4096.
|
||||
- Row index grows backward from end of page; each group spans 36 (0x24) bytes:
|
||||
`group_base = len_page - group_index*36`. Within a group, `present_flags`
|
||||
(u2) live at `group_base-4`; the row offset for slot `i` (0..15) is the u2 at
|
||||
`group_base-(6+2*i)`; offsets are relative to the heap start (0x28, i.e. end
|
||||
of the 0x28-byte page header).
|
||||
|
||||
---
|
||||
|
||||
## 1. Track row `index_shift` (offset 0x02, u2) — SOLVED
|
||||
|
||||
**Field:** `TrackRow.index_shift` (also present on artist/album/genre/label/
|
||||
key rows — every row type whose layout begins `subtype:u2, index_shift:u2`).
|
||||
|
||||
**Rule:**
|
||||
|
||||
```
|
||||
index_shift = slot_index * 0x20 # i.e. slot_index << 5
|
||||
```
|
||||
|
||||
where `slot_index` is the row's **0-based position in the page row index**
|
||||
(`group_index*16 + slot_within_group`), NOT its ordinal among present rows and
|
||||
NOT its position in the heap. It resets to 0 at the start of every data page.
|
||||
|
||||
Key consequences for the writer:
|
||||
- The first row index slot (slot 0) gets `index_shift = 0`.
|
||||
- Deleted/absent slots still consume a slot number, so a gap in
|
||||
`present_flags` produces a gap in `index_shift`. (See evidence: the AceMo
|
||||
page has slot 5 deleted, so the row in slot 6 has `index_shift = 0xc0`,
|
||||
skipping `0xa0`.)
|
||||
- `index_shift` is purely a function of the row's slot, so the value is fully
|
||||
reproducible from the page layout maple already controls — there is no
|
||||
external/runtime input. If maple writes each track to slot `i`, set
|
||||
`index_shift = i*0x20`.
|
||||
|
||||
**Evidence:**
|
||||
- Reference AceMo `tracks` page (page 2): present slots are 1,2,3,4,6 (slots 0
|
||||
and 5 are deleted), with `index_shift` = 0x20,0x40,0x60,0x80,0xc0
|
||||
respectively. Overflow page (page 48) has one row in slot 1 →
|
||||
`index_shift = 0x20`. The 0x80→0xc0 jump across the deleted slot 5 proves the
|
||||
value tracks `slot_index`, not present-count.
|
||||
- Cross-checked against `artists` (page 5) and `albums` (page 7) rows: every
|
||||
row satisfies `index_shift == slot_index*0x20` with **0 mismatches** over all
|
||||
present rows in the reference file
|
||||
(`maple-py/re/scripts`-style raw parse, 8 rows checked, see RE notes).
|
||||
- Ghidra: the value is assigned inside the DeviceSQL Page Manager record-insert
|
||||
path, which is reached only through vtable dispatch
|
||||
(`(**(code**)(*obj + 0x4d0))(obj,...)` etc. in `FUN_141b8b4c0` /
|
||||
`FUN_141cac390`), so there is no single statically-anchored
|
||||
`index_shift = slot<<5` instruction to cite; a constant-scan for `shl ,5`
|
||||
paired with the row-index masks (0x1fff/0x7ff) and stride 0x24 produced only
|
||||
CRT/`printf("%a")` false positives (`FUN_14232a86c`). The empirical rule
|
||||
above is exact and zero-mismatch, so confidence is **high** on the formula;
|
||||
confidence on "no hidden runtime input" is high because the value is fully
|
||||
explained by slot position alone.
|
||||
|
||||
**Confidence:** high.
|
||||
|
||||
---
|
||||
|
||||
## 2. Track row 0x14 / 0x18 / 0x1a (opaque id blob) — SOLVED (stored value, not a formula)
|
||||
|
||||
**Fields:** `TrackRow.opaque_track_id_lo` (u4 @0x14), `TrackRow.opaque_seed_lo`
|
||||
(u2 @0x18), `TrackRow.opaque_seed_hi` (u2 @0x1a). Together these are one
|
||||
8-byte little-endian value:
|
||||
|
||||
```
|
||||
blob_u64 = opaque_track_id_lo | (opaque_seed << 32)
|
||||
opaque_seed = opaque_seed_lo | (opaque_seed_hi << 16) # u4 at offset 0x18
|
||||
```
|
||||
|
||||
(@flesniak's schema labels these "Some ID? unknown", "always 19048?",
|
||||
"always 30967?". The "19048"/"30967" = 0x4A68/0x78F7 are only correct for some
|
||||
older exports; they are NOT universal — see below.)
|
||||
|
||||
**What they are (proven empirically across 5 GENUINE rekordbox exports):**
|
||||
|
||||
> NOTE (2026-06-09 correction): a prior draft included a 5th row
|
||||
> "usb (176), seq=1, seed 0x78f74a68, lo all 0" and concluded rekordbox writes
|
||||
> lo=0 / seed 0x78f74a68 for unanalyzed tracks. **That file
|
||||
> (`data/usb/.../export.pdb`) is maple's OWN output, not a rekordbox export**
|
||||
> (it is the compare_pdb.py "ours" path). Reading it back was circular; that
|
||||
> claim is WRONG and has been dropped. Provenance rule: maple output = seed
|
||||
> `0x78f74a68` + ALL per-track lo == 0; genuine rekordbox = per-install nonzero
|
||||
> seed + nonzero per-track lo.
|
||||
|
||||
| export (tracks) | header.seq | opaque_seed | opaque_track_id_lo |
|
||||
|----------------------------|-----------:|---------------|-------------------------------|
|
||||
| acemo (6) | 353 | `0x21b4a7ab` | 6/6 nonzero |
|
||||
| acemo_fresh (7, same lib) | 35 | `0x21b4a7ab` | 7/7 nonzero |
|
||||
| lib_multi (10, same lib) | 56 | `0x21b4a7ab` | 10/10 nonzero |
|
||||
| MAPLE (176, install B) | 546 | `0xd6d3a567` | 176/176 nonzero |
|
||||
| data/PIONEER (847, inst. B)| 4211 | `0xd6d3a567` | 847/847 nonzero |
|
||||
|
||||
Across **all 1046 real tracks in 5 genuine exports: 100% nonzero per-track lo,
|
||||
exactly one nonzero per-install seed per file. There is NO evidence rekordbox
|
||||
ever writes lo=0 or seed `0x78f74a68`.** The lo values span nearly the full
|
||||
28-bit space (`0x0003edeb`..`0x0ffe7f4e`).
|
||||
|
||||
Findings:
|
||||
1. **`opaque_seed` is constant within a single export** (exactly one value per
|
||||
file) and is **per-rekordbox-library/install**: acemo/acemo_fresh/lib_multi
|
||||
are three exports of the same library and all share `0x21b4a7ab`; the separate
|
||||
install behind MAPLE + data/PIONEER uses `0xd6d3a567`. (The old maple hardcode
|
||||
`0xa567`/`0xd6d3` = `0xd6d3a567` was correct only for install B.) The seed is
|
||||
NOT a binary constant (proven: no `0xa7ab`/`0x21b4`/`0xd6d3`/`0xa567`
|
||||
immediate or data-byte sequence in rekordbox.exe), so it is derived at runtime
|
||||
from the per-library DB (most likely a hash/truncation of the master.db's
|
||||
per-library UUID or creation marker). maple cannot reproduce a specific
|
||||
install's seed (see recommendation).
|
||||
2. **`opaque_track_id_lo` is a stable per-track value within a library.** Every
|
||||
track that appears in both the acemo and acemo_fresh exports has an
|
||||
**identical** lo (matched by title): "All My Life"→`0x07ff5a56`,
|
||||
"Smooth Move"→`0x0a20de34`, "Jelly Fish Jam"→`0x0f8fd598`, etc. It is NOT
|
||||
per-export-random.
|
||||
3. **It is NOT derivable from the track's file metadata.** Tested and ruled out
|
||||
(no match, 28-bit and full, utf-8/utf-16, several hash funcs): crc32 / adler32
|
||||
/ md5 / sha1 / fnv1 / fnv1a / djb2 of the title, filename, full
|
||||
`/Contents/...` path, and crc32 of the ANLZ `.DAT`/`.EXT` and the `.mp3`
|
||||
bytes. Also not the USBANLZ folder id, not sequential, not evenly spaced.
|
||||
4. **The values are runtime-derived, not binary constants.** A full
|
||||
instruction-immediate scan of `rekordbox.exe` found NO occurrence of either
|
||||
real seed (`0x21b4a7ab`, `0xd6d3a567`), the per-track lo values, or the
|
||||
track-row `bitmask` constant `0xc0700`. So these come from rekordbox's
|
||||
internal database (master.db / `djmdContent` + a per-library seed), copied
|
||||
into the row by the DeviceSQL record builder (vtable-dispatched, not
|
||||
statically anchorable). (`0x78f74a68` does not appear either — and per the
|
||||
correction above it was never a real rekordbox value here, just maple's
|
||||
placeholder.)
|
||||
|
||||
**Round-2 evidence (2026-06-09, after team-lead supplied acemo_fresh and asked
|
||||
to chase an audio-content hash):**
|
||||
5. **Audio-content-hash hypothesis: DISPROVEN.** Hashed the actual `.mp3`
|
||||
bytes in 8 framings — full file, ID3v2-stripped (audio frames only),
|
||||
first 1k/2k/4k/64k, last 64k, ID3-stripped-first-64k — each with crc32,
|
||||
adler32, md5 (lo/hi, LE+BE), sha1 (LE+BE), sha256, fnv1a, both full-32 and
|
||||
masked-to-28-bit. NONE matched any track's `opaque_track_id_lo`. So it is
|
||||
not a hash of the audio file content (at least not of the file bytes; a hash
|
||||
of decoded PCM cannot be ruled out but there is no evidence for it and it is
|
||||
not reproducible anyway).
|
||||
6. **Source DB confirmed, and it is ENCRYPTED.** Both `exportLibrary.db` (next
|
||||
to the pdb) and the captured `master.db`
|
||||
(`maple-py/captures/p17328_master.db`) fail to open as plain SQLite
|
||||
("file is not a database") — rekordbox 6/7 encrypts them with SQLCipher.
|
||||
The `opaque_track_id_lo` originates there; it cannot be read without the key.
|
||||
7. **The value is a pure stored field, used nowhere else.** It appears in the
|
||||
export.pdb ONLY at each track row's own offset 0x14 (present and stale/deleted
|
||||
rows alike); it does NOT appear in exportExt.pdb (no tag_tracks/ContentID
|
||||
link), nor in the ANLZ `.DAT`/`.EXT` (searched LE+BE), nor does it equal the
|
||||
USBANLZ folder id. So it is not a foreign key the player resolves.
|
||||
8. **Range note (suggestive, not conclusive):** across the 5 real exports the
|
||||
lo values span nearly the full 28-bit space (`0x0003edeb`..`0x0ffe7f4e`),
|
||||
the band of rekordbox-6/7 9-digit `djmdContent.ID` integers, so the most
|
||||
likely identity is "the rekordbox internal Content ID" — a stored
|
||||
random/sequential DB id, NOT a content-derived hash. Consistent with every
|
||||
other finding and with it being unreadable (encrypted DB).
|
||||
|
||||
**Conclusion:** this 8-byte blob is a **stored rekordbox-internal id (most
|
||||
likely the encrypted-DB Content ID), not a function of the track metadata or
|
||||
audio bytes maple has. It cannot be reproduced byte-for-byte** for an arbitrary
|
||||
track without rekordbox's own (encrypted) DB. The audio-content-hash idea is
|
||||
ruled out. There is **no "unanalyzed/zero" form** in any real export — every
|
||||
real track carries a nonzero seed and a nonzero lo.
|
||||
|
||||
**Recommendation for maple's writer:**
|
||||
- Players/crate-digger treat this blob as opaque (never decoded) and play fine
|
||||
regardless — so any value works for playback.
|
||||
- For maximum faithfulness to genuine rekordbox output (which never has
|
||||
lo=0/seed `0x78f74a68`), maple should write a **nonzero per-install seed and a
|
||||
nonzero, stable per-track lo**:
|
||||
- `opaque_seed`: use a real per-install seed. If maple has no install of its
|
||||
own, `0x21b4a7ab` (this RE machine's install) is a known-real value;
|
||||
keep it constant across all tracks in one export. Do NOT use `0x78f74a68`
|
||||
(that was maple's own placeholder, not a rekordbox value).
|
||||
- `opaque_track_id_lo`: a nonzero 28-bit value, stable per track. Since the
|
||||
real value is an opaque DB id maple can't recover, a deterministic stand-in
|
||||
(e.g. a 28-bit hash of a stable per-track key, or the track's own pdb id
|
||||
offset into the ContentID band) is fine — the goal is "looks like a real
|
||||
nonzero ContentID", not byte-parity.
|
||||
- Either way these 8 bytes will NOT match a specific reference export
|
||||
byte-for-byte (the real ids live in that install's encrypted DB) — record the
|
||||
divergence in re/KNOWN_DIVERGENCE.md.
|
||||
|
||||
**Confidence:** high (5-export empirical proof: 1046/1046 real tracks nonzero;
|
||||
exhaustive negative hash search incl. audio bytes; binary const-absence). The
|
||||
earlier "write 0 / 0x78f74a68" advice was based on a maple-output file and is
|
||||
retracted.
|
||||
|
||||
**Round-3 (task #10): can pyrekordbox read the real ids out of master.db?**
|
||||
User insight: pyrekordbox holds the SQLCipher key, so the DBs are not truly
|
||||
unreadable. Validated — partly true, but blocked on the data we have:
|
||||
- pyrekordbox 0.4.4 + `sqlcipher3-wheels` installs and wires up correctly. Its
|
||||
master.db key is the single **global** rekordbox-6 key
|
||||
`402fd482c38817c35ffa8ffb8c7d93143b749e7d315df7a81732a1ff43608497`
|
||||
(`deobfuscate(BLOB)`, must start with `402fd`) — same for every install, NOT
|
||||
per-install. So a VALID master.db *would* open and expose `DjmdContent`.
|
||||
- BUT the captured `maple-py/captures/p17328_master.db` is **corrupt/truncated**:
|
||||
1,024,000 bytes but only 4.4% non-zero, and the first 32 KB (the mandatory
|
||||
SQLCipher salt + page 1) is **all zeros**. No key can decrypt a DB with a
|
||||
zeroed salt. `p17328_master.backup.db` is 0 bytes. The Frida NtWriteFile
|
||||
capture only caught scattered later pages, never the header. → unusable.
|
||||
- The genuine `exportLibrary.db` shipped next to export.pdb (valid 16-byte salt
|
||||
`ed0d3a3c…`) does NOT open with the global master.db key (different key
|
||||
scheme) and pyrekordbox has **no exportLibrary.db support**. → can't read it
|
||||
here.
|
||||
- Strong inference (untested for lack of a readable DB): the per-track
|
||||
`opaque_track_id_lo` is almost certainly `DjmdContent.ID` (the 9-digit
|
||||
rekordbox Content ID; our lo range 0x0003edeb..0x0ffe7f4e fits). If maple can
|
||||
open the user's LIVE `%APPDATA%/Pioneer/rekordbox6/master.db` with pyrekordbox
|
||||
(global key), `db.get_content()` would give `(ID, …)` per track, and the blob
|
||||
would become EXACTLY reproducible: `opaque_track_id_lo = DjmdContent.ID & 0xFFFFFFFF`.
|
||||
- The per-install `opaque_seed` (0x21b4a7ab here) is NOT a DjmdContent column;
|
||||
it is one value for the whole export and most likely derived from a
|
||||
master.db-level per-library marker (a USB/library UUID or an `agentRegistry`/
|
||||
`local_usn`-style value). Identifying it needs a readable master.db to inspect
|
||||
candidate columns; unconfirmed.
|
||||
|
||||
**pyrekordbox write / export capability (asked in #10):**
|
||||
- It CAN write the desktop master.db: `add_content`/`add_album`/`add_artist`/
|
||||
`create_playlist`/`commit`, plus `set_local_usn`, `update_content_path`. But
|
||||
`add_content` lets rekordbox ASSIGN the Content ID; there is no API to force a
|
||||
specific ID. So it is a source of real ids, not a way to dictate them.
|
||||
- It does NOT drive a USB export — only `get_device` (read). The actual
|
||||
`export.pdb`/`exportLibrary.db` writing is rekordbox.exe's DeviceSQL engine
|
||||
(the thing maple reimplements). pyrekordbox manipulates only the desktop
|
||||
library DB, not the USB export. So pyrekordbox cannot replace maple's writer;
|
||||
its only use to maple is as a READ source of the real `DjmdContent.ID` (and
|
||||
potentially the seed) from a valid master.db.
|
||||
|
||||
**Net for #10:** the approach is sound (pyrekordbox + global key reads a real
|
||||
master.db → Content IDs), so `opaque_track_id_lo` is likely reproducible *if
|
||||
maple reads the live master.db*. It is NOT reproducible from the corrupt
|
||||
captured DB we have, and the seed source is still unidentified. pyrekordbox
|
||||
cannot produce the USB pdb itself. Recommended next step for maple/RE: point
|
||||
pyrekordbox at the user's LIVE rekordbox6 master.db
|
||||
(`%APPDATA%/Pioneer/rekordbox6/master.db`), confirm
|
||||
`DjmdContent.ID == opaque_track_id_lo` for the AceMo tracks, then hunt the seed
|
||||
in that DB. Until then, the nonzero stand-in (above) stays.
|
||||
|
||||
---
|
||||
|
||||
## 3. File-header `sequence` (and per-page `sequence`)
|
||||
|
||||
**Field:** `RekordboxPdb.sequence` (u4 @0x14 of the file header) and
|
||||
`Page.sequence` (u4 in each page header).
|
||||
|
||||
**Rule / meaning (confirmed from schema docs + the 5 genuine exports above):**
|
||||
- `sequence` is a monotonically increasing **edit counter** for the database.
|
||||
The file-header value is the **next** page-sequence number; each page records
|
||||
the counter value as of its last edit (so `header.sequence >=
|
||||
max(page.sequence)`). Observed header values in genuine rekordbox exports:
|
||||
35, 56, 353, 546, 4211 — scales with edit history, not track count. (No real
|
||||
export with sequence==1 is available; `data/usb`'s seq==1 is maple's own
|
||||
output, not a rekordbox sample — see §2 correction.)
|
||||
- The firmware/players (and crate-digger) use page `sequence` only as
|
||||
informational ordering metadata; **they do not validate it** for playback.
|
||||
No special value is required beyond keeping `header.sequence` greater than any
|
||||
page's `sequence`.
|
||||
- For maple it is enough to (a) pick a header `sequence` larger than every page
|
||||
`sequence` it writes, and (b) give each page a `sequence` <= header. A simple
|
||||
scheme (all pages = N, header = N+1) is accepted. This is what task #3
|
||||
(incrementing sequence) implements.
|
||||
|
||||
**Confidence:** high for semantics and "not validated by players"; the exact
|
||||
increment rekordbox uses per edit is not needed for a valid export.
|
||||
|
||||
---
|
||||
|
||||
## 4. File-header offset 0x10 (u4, schema `_unnamed4`) — HISTORY-DEPENDENT; not byte-reproducible
|
||||
|
||||
> CORRECTION (2026-06-09): a prior draft cited a "usb fresh, sequence==1,
|
||||
> _unnamed4==0" row as proof that 0 is the fresh value. That file is maple's own
|
||||
> output, not a rekordbox export, so it is removed below. The remaining proof
|
||||
> (same-library divergence) does not depend on it.
|
||||
|
||||
**Field:** the unnamed u4 between `next_unused_page` (0x0c) and `sequence`
|
||||
(0x14). maple writes 0; the AceMo ref has 4.
|
||||
|
||||
**Finding (5 genuine rekordbox exports):** an **edit/allocator accumulator**,
|
||||
not a structural value derivable from the tables. It does NOT equal track count,
|
||||
page count, or `next_unused_page - npages`:
|
||||
|
||||
| export | sequence | _unnamed4@0x10 | next_unused | npages |
|
||||
|---------------|---------:|---------------:|------------:|-------:|
|
||||
| acemo_fresh | 35 | 5 | 52 | 49 |
|
||||
| lib_multi | 56 | 4 | 57 | 52 |
|
||||
| acemo | 353 | 4 | 53 | 49 |
|
||||
| MAPLE | 546 | 5 | — | — |
|
||||
| data/PIONEER | 4211 | 1 | 174 | 174 |
|
||||
|
||||
**Decisive (same-library proof):** acemo and acemo_fresh are the SAME library
|
||||
(identical track set + identical per-track opaque ids) yet have `_unnamed4` =
|
||||
4 vs 5. A structural field would be identical; differing proves it is edit
|
||||
state. (We have NO genuine rekordbox export with `sequence==1`, so the exact
|
||||
never-edited value is unconfirmed — but it is clearly not derivable from the
|
||||
file's structure.)
|
||||
|
||||
**Rule for maple:** there is no reproducible rule. Write **0** (maple already
|
||||
does) as a **reasonable minimal default — NOT a confirmed match** (every genuine
|
||||
export we have is nonzero here, and we have no never-edited rekordbox export to
|
||||
check what fresh rekordbox writes). Record the divergence in
|
||||
re/KNOWN_DIVERGENCE.md. It cannot be matched to the AceMo ref's `4` without that
|
||||
DB's edit history.
|
||||
|
||||
**Confidence:** high that it is edit-history/allocator state and not
|
||||
structurally reproducible; the specific fresh-write value is unconfirmed (no
|
||||
seq==1 rekordbox sample available).
|
||||
|
||||
---
|
||||
|
||||
## 5. Page-header offset 0x26 (u2, "unknown7"/strange-page) — HISTORY-DEPENDENT; maple's 0 is a reasonable default
|
||||
|
||||
> CORRECTION (2026-06-09): do NOT cite `data/usb` (sequence==1) here — it is
|
||||
> maple's OWN output, so "maple's 0 matches usb" would be circular. The
|
||||
> observations below are from the five GENUINE rekordbox exports only.
|
||||
|
||||
**Field:** the second of the two trailing u2s in the page header (after
|
||||
`transaction_row_index` @0x22 and the "1004 for strange blocks" u2 @0x24).
|
||||
@flesniak: "always 0 except 1 for history pages, num entries for strange
|
||||
pages?". maple writes 0; the AceMo ref has 0/1/2 on various tables' first
|
||||
("strange") pages.
|
||||
|
||||
**Finding (5 genuine rekordbox exports — all incrementally edited):**
|
||||
- On DATA pages it is **always 0** (all five exports). This is a solid
|
||||
observation about genuine rekordbox output.
|
||||
- It is nonzero only on a table's first/"strange" page (flags 0x64), and the
|
||||
value does NOT correlate with that table's data-page count or row count
|
||||
(acemo tracks: 2 data pages → 2; data/PIONEER tracks: 105 data pages → 23).
|
||||
It is transaction/edit-tracking state.
|
||||
- **Decisive (same-library proof):** acemo and acemo_fresh hold the identical
|
||||
tracks table, yet its strange-page unknown7 = 2 (acemo) vs 1 (acemo_fresh).
|
||||
Same data → different value ⇒ not structural, it is edit state. (lib_multi:
|
||||
tracks strange unk7 = 2 with 3 data pages / 10 rows — again no count relation.)
|
||||
|
||||
**Rule for maple:** write **0** on every page (maple already does). The data
|
||||
pages being 0 across all five genuine exports makes 0 a well-supported value
|
||||
*for data pages*. For strange pages the real value is per-export edit state
|
||||
(0/1/2…) that cannot be reproduced — maple's 0 there is a **reasonable minimal
|
||||
default, NOT a confirmed match** (we have no never-edited rekordbox export to
|
||||
check what fresh rekordbox stamps on a strange page). Record the strange-page
|
||||
divergence in re/KNOWN_DIVERGENCE.md.
|
||||
|
||||
**Confidence:** high that it is history-dependent and that data-page 0 matches
|
||||
real rekordbox; the fresh-write strange-page value is unconfirmed (no seq==1
|
||||
rekordbox sample).
|
||||
|
||||
---
|
||||
|
||||
## 6. Row-group trailing u2 (`transaction_row_flags`) — write `present_flags` (NOT 16); residual mismatch is edit history
|
||||
|
||||
> CORRECTION (2026-06-09): an earlier draft of this section claimed "a fresh
|
||||
> export writes 16 everywhere, so maple's 16 is correct." That was based on
|
||||
> `data/usb/.../export.pdb`, which on re-check is **maple's OWN output**, not a
|
||||
> rekordbox export (opaque seed `0x78f74a68` + all-zero per-track lo +
|
||||
> `sequence`==low). Reading it back was circular. The analysis below uses ONLY
|
||||
> the five genuine rekordbox exports (provenance verified by nonzero per-track
|
||||
> opaque ids + per-install seed): acemo, acemo_fresh, lib_multi, MAPLE,
|
||||
> data/PIONEER.
|
||||
|
||||
**Field:** the last u2 of each 36-byte row-group block in the page row index
|
||||
(crate-digger `transaction_row_flags`, at `base`; in maple's `pager.py` it is
|
||||
`pack("<H", 16) # unknown (rekordcrate default)`).
|
||||
|
||||
**Finding (5 genuine rekordbox exports):** this u2 is the
|
||||
**"rows touched by the last transaction" bitmask** (same bit layout as
|
||||
`row_present_flags`), and is therefore **edit-history state, not reproducible**:
|
||||
- **No genuine rekordbox export writes 16 as the field's value** on settled
|
||||
pages (count of groups == 16 across the five exports: 0, 0, 0, 1, 8 — and
|
||||
those few are coincidental transaction bitmasks, not a default). So maple's
|
||||
constant **16 does NOT match rekordbox** — it is a rekordcrate invention.
|
||||
- The dominant value in heavily-edited tables is **0x0** (last transaction
|
||||
touched no rows on that page), e.g. data/PIONEER 170/294 groups = 0x0.
|
||||
- Three DIFFERENT patterns coexist in ONE file, proving it is not structural:
|
||||
(a) some pages `trailing == present_flags` (columns/unk17/unk18); (b) artwork
|
||||
pages consistently use `last group = 1<<(rows_in_last_group-1)`, earlier groups
|
||||
= 0 (data/PIONEER p28 g6 10rows→0x200, p147 g2 13rows→0x1000; acemo_fresh
|
||||
artwork 7rows→0x40); (c) churned tracks pages use an arbitrary single bit
|
||||
(0x1/0x2/0x4/0x8/0x10/0x40…) = whatever row that page's last edit touched.
|
||||
A candidate clean rule `[last grp=1<<(rows-1), others=0]` matches only
|
||||
**181/257** fully-present pages in data/PIONEER — confirming NO single formula.
|
||||
- **Same-library proof:** acemo and acemo_fresh hold identical data, yet the
|
||||
tracks page-2 group differs (acemo present=0x5e trailing=0x20; acemo_fresh
|
||||
present=0xbb trailing=0x4) — same rows, different value ⇒ not structural.
|
||||
The previously-flagged artwork `0x40` is likewise transaction state
|
||||
(acemo_fresh artwork=0x40 vs lib_multi artwork p49=0x4), not an open
|
||||
reproducible byte.
|
||||
|
||||
**Rule for maple (DECISION): write `present_flags`, NOT the constant 16.**
|
||||
maple's old constant 16 (rekordcrate's placeholder) matches **0/14** real groups;
|
||||
`trailing = row_present_flags` matches **12/14** clean groups in the real
|
||||
`data/acemo_fresh` export (8 rows→0xff, 16→0xffff, 11→0x7ff, 6→0x3f, 1→0x1). The
|
||||
2 misses (page2→0x4, page28→0x40, page40→0xc0) are the last-transaction bitmask
|
||||
on pages that were edited after creation — i.e. residual edit history, not a
|
||||
different rule. So for a from-scratch single-transaction write the correct value
|
||||
IS `present_flags` (all rows touched in the one creating transaction). maple now
|
||||
ships `present_flags` (pager.py) — this is the best achievable; reverting to 16
|
||||
would regress 12 groups. Remaining mismatch vs an EDITED reference (the per-page
|
||||
last-tx bitmask) is unavoidable edit history → KNOWN_DIVERGENCE.
|
||||
|
||||
> NOTE: the earlier "keep 16 / fresh usb writes 0x10 everywhere" framing was
|
||||
> WRONG — it read `data/usb`, which is MAPLE'S OWN OUTPUT (an old build that
|
||||
> emitted the rekordcrate-default 16), not a real export. `data/usb` must NOT be
|
||||
> used as a 'fresh rekordbox' baseline for ANY field (same contamination that
|
||||
> produced the wrong opaque-blob default in §2). The fresh-write value is
|
||||
> established from the REAL `data/acemo_fresh` as `present_flags`.
|
||||
|
||||
**Confidence:** high that it is history-dependent and that real rekordbox never
|
||||
uses 16; the correct fresh-write value is an open question (no seq==1 sample).
|
||||
|
||||
---
|
||||
|
||||
## Note on the three "reproducible header fields" (task #9)
|
||||
|
||||
After multi-export analysis across the FIVE genuine rekordbox exports (acemo,
|
||||
acemo_fresh, lib_multi, MAPLE, data/PIONEER — provenance verified by nonzero
|
||||
per-track opaque ids + per-install seed), all three fields are
|
||||
**edit-history / transaction state, NOT structurally reproducible**:
|
||||
- §4 file-header 0x10: allocator/edit accumulator. → write 0, DIVERGENCE.
|
||||
- §5 page 0x26 unknown7: 0 on all data pages (exact); per-table 0/1/2 on strange
|
||||
pages is edit state. → write 0, strange-page DIVERGENCE.
|
||||
- §6 row-group trailing u2: "rows touched by last transaction" bitmask. Fresh
|
||||
single-transaction value = `present_flags` (12/14 in real data/acemo_fresh;
|
||||
constant 16 = 0/14). → maple writes `present_flags`; residual mismatch vs an
|
||||
EDITED ref is the per-page last-tx bitmask = edit history, DIVERGENCE.
|
||||
|
||||
The decisive evidence is the **same-library pair** (acemo vs acemo_fresh:
|
||||
identical track data, yet header 0x10 = 4 vs 5, tracks strange unk7 = 2 vs 1,
|
||||
tracks-page trailing differs) — a structural field would be identical.
|
||||
|
||||
maple's values: §4 header0x10 = 0 and §5 unknown7 = 0 are reasonable minimal
|
||||
defaults (confirmed = real exports on data pages; fresh strange-page value
|
||||
unconfirmed). §6 trailing u2 = `present_flags` is the established fresh-write
|
||||
value (NOT the old constant 16). No further writer change needed; remaining
|
||||
mismatches vs edited references are unavoidable edit history.
|
||||
|
||||
IMPORTANT corrections to the earlier draft: (1) `data/usb/.../export.pdb` is
|
||||
maple's OWN output (an old build that emitted the rekordcrate-default 16), not a
|
||||
rekordbox export — so any "fresh usb" claim was circular; `data/usb` is NOT an
|
||||
authority for ANY field (same contamination that produced the wrong opaque-blob
|
||||
default in §2). (2) We have NO genuine rekordbox export with sequence==1, so the
|
||||
never-edited header0x10 / strange-page-unknown7 values are unconfirmed. (3) The
|
||||
row-group trailing u2 fresh value is `present_flags`, established from the REAL
|
||||
data/acemo_fresh (12/14); the rekordcrate "Default=16" is a placeholder that
|
||||
matches 0/14 real groups.
|
||||
|
||||
All three go to re/KNOWN_DIVERGENCE.md, not the writer.
|
||||
|
||||
---
|
||||
|
||||
## 7. Artwork + USBANLZ path schemes (task #11)
|
||||
|
||||
Decoded from genuine `data/MAPLE` (176 tracks/176 artwork) + acemo. The guiding
|
||||
principle (per the task): for **player acceptance only PATH SELF-CONSISTENCY
|
||||
matters** — every path stored in the pdb must resolve to a real on-disk file.
|
||||
Matching rekordbox's exact id-derivation is NOT required; maple's own consistent
|
||||
scheme is acceptable.
|
||||
|
||||
### 7a. Artwork — SOLVED, exactly reproducible (maple already matches)
|
||||
- Path: `/PIONEER/Artwork/{folder:05d}/a{artwork_id}.jpg` (80×80) plus a
|
||||
`a{artwork_id}_m.jpg` (240×240) variant in the same folder (rekordbox writes
|
||||
both; verified on disk in data/MAPLE).
|
||||
- **Folder bucket rule: `folder = artwork_id // 20 + 1`** (20 ids per bucket;
|
||||
bucket 00001 = ids 1–19 because ids are 1-based, 00002 = 20–39, 00003 = 40–59,
|
||||
…). Verified **176/176** in data/MAPLE.
|
||||
- maple's `artwork._bucket()` is literally `f"{artwork_id // 20 + 1:05d}"` and
|
||||
`extract_artwork` writes both jpg variants at that path → **byte-identical to
|
||||
rekordbox AND self-consistent**. No change needed.
|
||||
|
||||
### 7b. USBANLZ — opaque id; maple's own scheme is self-consistent (DIVERGENCE from rekordbox)
|
||||
- Path: `/PIONEER/USBANLZ/P{bucket}/{8-hex}/ANLZ0000.DAT` (+`.EXT`).
|
||||
- rekordbox's `{8-hex}` is a per-track **opaque analysis id** (range seen
|
||||
0x117..0x309fd over 176 tracks; NOT == track id, NOT == the row-0x14
|
||||
`opaque_track_id_lo`, NOT a crc of path/title — tested) and the `P{bucket}`
|
||||
(0–127, 94 distinct over 176) is a spreading bucket that is **NOT a derivable
|
||||
function** of the 8-hex or any track field (tested: `&0x7f`, `>>k`, `%127/128`,
|
||||
low-byte — all fail). Like the row-0x14 blob, these come from rekordbox's
|
||||
internal DB and are **not reproducible** without it.
|
||||
- maple (cli.py `build()`): generates its OWN deterministic scheme —
|
||||
`bucket = P{(crc32(file_path) >> 20) & 0xFFF:03X}`,
|
||||
`analyze_id = {crc32(file_path):08X}`, then `write_anlz()` writes the .DAT at
|
||||
exactly that path. This is **self-consistent by construction** (the path it
|
||||
records is the path it writes) and is the correct approach per the
|
||||
self-consistency principle. It does NOT match rekordbox's id/bucket (expected
|
||||
DIVERGENCE).
|
||||
- Minor cosmetic note (not a correctness issue): maple's bucket is 3 hex
|
||||
digits `P000..PFFF` whereas rekordbox uses `P00..P7F` (0–127). Harmless;
|
||||
optionally narrow to `& 0x7F` (`P{..:02X}`) to look more rekordbox-like.
|
||||
|
||||
### 7c. Self-consistency verification
|
||||
- **Genuine rekordbox (data/MAPLE): 176/176 ANLZ paths and 176/176 artwork
|
||||
paths resolve to on-disk files.** (rekordbox is fully self-consistent — sanity
|
||||
check that confirms the parser and the principle.)
|
||||
- **maple production `build()` (cli.py): self-consistent BY CONSTRUCTION** —
|
||||
`extract_artwork` (artwork.py:62-65) writes `a{id}.jpg`/`a{id}_m.jpg` at the
|
||||
recorded path; `write_anlz` (cli.py:93) writes `ANLZ0000.DAT` at the recorded
|
||||
`analyze_path`. Each pdb path is computed and the file is written there in the
|
||||
same step, so every path resolves.
|
||||
- CAVEAT — the byte-diff harness `tools/build_acemo.py` is NOT the production
|
||||
path: it MIRRORS a reference rekordbox export (copies its `analyze_path`/
|
||||
artwork `path` strings verbatim, writes no ANLZ/artwork files), so the
|
||||
`data/usb` export it produces references the reference's USBANLZ files which
|
||||
are not co-located → those paths don't resolve. This is expected for a
|
||||
structural-diff fixture and says nothing about production self-consistency.
|
||||
|
||||
**Confidence:** high. Artwork rule is exact (176/176). USBANLZ rekordbox ids are
|
||||
opaque/non-reproducible (consistent with the row-0x14 finding); maple's own
|
||||
USBANLZ scheme is self-consistent, which is all the player requires.
|
||||
|
||||
---
|
||||
|
||||
## 8. Page-manager free-space: `empty_candidate` + free pool (task #13 — THE FREEZE)
|
||||
|
||||
DECK TEST: maple's pdb FROZE the player (parser hang, not reject). Decoded the
|
||||
page-manager/free-space divergence against genuine rekordbox exports
|
||||
(`data/working` 9trk seq44, `data/MAPLE` 176trk, `data/PIONEER` 847trk — all
|
||||
load) vs maple output (`captures/maple_acemo.pdb`).
|
||||
|
||||
### Data-page `next_page` SELF-LOOP — ROOT CAUSE (confirmed divergence; high confidence)
|
||||
- The player reads each table by following `first_page → next_page → …`. maple's
|
||||
**last data page of each multi-page chain set `next_page == its own index`**
|
||||
(page 2→2, 6→6, 8→8, 12, 14, 28, 34, 36, 38, 40 — 10 self-loops in the 9-track
|
||||
build). A page whose `next_page` is itself is an **infinite loop → FREEZE**.
|
||||
- **Genuine rekordbox has ZERO self-loops.** A terminal data page uses
|
||||
`next_page = 0` (the "no next" sentinel; seen on pages 10/16/18/20/…/41-47 in
|
||||
`data/working`) OR points FORWARD to a distinct free page (never to itself).
|
||||
- FIX (maple, done in writer.py): point every chain's last page at a single
|
||||
dedicated zero-filled free-pool page (`free_sentinel`, just past the overflow
|
||||
region) instead of itself; materialise that page and set `next_unused` beyond
|
||||
it. Verified on the rebuilt pdb: 10 self-loops → 0; all 20 chains terminate at
|
||||
the blank sentinel; player tolerates last-page→free-page (genuine exports do
|
||||
the same). This is the primary freeze fix.
|
||||
|
||||
### Table-dir `empty_candidate` (u4 after page_type) — SECONDARY divergence (also wrong; lower-confidence freeze contributor)
|
||||
- Layout: each 16-byte table-dir entry is `type:u4, empty_candidate:u4,
|
||||
first_page:u4, last_page:u4`.
|
||||
- `empty_candidate` is the head of that table's **empty/free-page list** (the
|
||||
page the Page Manager grabs when the table needs to grow).
|
||||
- **In EVERY genuine rekordbox export all 20 `empty_candidate`s are NONZERO and
|
||||
distinct**, and each points to an **all-zero "free" page** — either an
|
||||
in-file allocated-but-empty page (e.g. `last_page+1`, or a pooled page like
|
||||
41–47/49) or a **beyond-EOF sentinel** (index ≥ npages, e.g. 50/51/52 when
|
||||
npages=49 — the firmware tolerates beyond-EOF here). It NEVER points to a
|
||||
used data page and NEVER to 0.
|
||||
- **maple writes `empty_candidate = 0` for all 20 tables.** Page 0 is the FILE
|
||||
HEADER. The firmware Page Manager, walking a table's empty-page list, follows
|
||||
`0` → reads the 4 KB file header AS a page → its "next_page" field (header
|
||||
offset 0x0c = `next_unused_page`) and bogus type/flags send the walk into a
|
||||
loop or out-of-bounds read → **hang/FREEZE**. This is the highest-confidence
|
||||
cause: it is the only systematic, data-independent divergence, and `0` is
|
||||
never valid (collides with the header).
|
||||
|
||||
### Free pool / page layout
|
||||
- rekordbox keeps the data pages in chains `1..40,48,…` and pre-allocates a
|
||||
pool of all-zero "free" pages past them; `next_unused_page` = one past the
|
||||
highest allocated page (so `next_unused > npages` is normal, e.g. 53 > 49).
|
||||
- maple **compacts**: 41 pages, every page used, `next_unused == npages == 41`,
|
||||
no free pool. The compaction itself is structurally legal (chains are clean),
|
||||
but it left `empty_candidate = 0` with no free page to point at.
|
||||
- Beyond-EOF links (a page's `next_page` ≥ npages, or `empty_candidate` ≥
|
||||
npages) are TOLERATED — the genuine `working` file has many (page 4→53,
|
||||
46→52, empty_candidate 51/52/50/49 all ≥ 49). So beyond-EOF is a valid
|
||||
"no-more-pages / no-free-page" sentinel; it is NOT the freeze.
|
||||
|
||||
### Strange-page free-map — NOT the cause (decoded, maple matches)
|
||||
- Each table's first/"strange" page (flags 0x64) carries, after a small prefix
|
||||
`[this_page, next_or_0x03ffffff, 0x03ffffff, 0, 0x1fff0000]`, a heap filled
|
||||
with **1004 × `0x1ffffff8`** (`f8 ff ff 1f` LE) — the empty-heap free-map
|
||||
(count == the `m1004`=1004 marker at header 0x24). `0x03ffffff` in the 2nd u4
|
||||
is the legitimate **"this table has no data page"** sentinel (genuine
|
||||
rekordbox writes it for every empty table; e.g. labels/pltree/plentries).
|
||||
- maple reproduces this free-map correctly; its strange-page prefixes match
|
||||
rekordbox's pattern byte-for-byte (data table → u1=first_data_page; empty
|
||||
table → u1=0x03ffffff). **So the free-map is NOT the freeze.**
|
||||
|
||||
### FIX (for maple → deck test)
|
||||
PRIMARY (high confidence, DONE): **eliminate the data-page `next_page` self-loop**
|
||||
— last page of each chain must point to a blank free page (or use `next_page=0`),
|
||||
never to itself. maple now points all last pages at one zero-filled `free_sentinel`
|
||||
page. Verified: 0 self-loops in the rebuilt pdb.
|
||||
|
||||
SECONDARY (cheap, also a real divergence — fold into the same build): **stop
|
||||
writing `empty_candidate = 0`.** Point it at the same `free_sentinel`
|
||||
(`pack("<I", free_sentinel)` instead of 0). Genuine rekordbox NEVER writes 0
|
||||
(0 = the header page); it always points each table's empty_candidate at a blank
|
||||
free page. Lower-confidence as a freeze cause for a read-only player than the
|
||||
self-loop, but wrong and trivially fixable with the page maple already
|
||||
materialised.
|
||||
|
||||
If the deck STILL hangs after both, next suspects (lower confidence):
|
||||
`next_unused_page == npages` (give a little slack), and whether the single shared
|
||||
sentinel needs to be per-table-distinct (rekordbox uses distinct free pages).
|
||||
|
||||
**Confidence:** HIGH that the self-loop was the freeze (self-referential
|
||||
next_page is an unconditional infinite loop; 0/all genuine exports self-loop).
|
||||
empty_candidate=0 is independently wrong (0/all genuine exports use it; collides
|
||||
with header). Exact firmware behaviour is inferred (no firmware to trace); the
|
||||
deck test confirms.
|
||||
is the confirmation.
|
||||
Reference in new issue
Block a user