engine-dj-mcp 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,168 +1,294 @@
1
1
  # engine-dj-mcp
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/engine-dj-mcp)](https://www.npmjs.com/package/engine-dj-mcp)
4
+ [![licence](https://img.shields.io/npm/l/engine-dj-mcp)](./LICENSE)
5
+
3
6
  An MCP server that lets an AI assistant search and audit your **Engine DJ**
4
- libraries — the one on your Mac and the ones on your USB drives.
7
+ libraries — the one on your computer and the ones on your USB drives.
5
8
 
6
9
  > **Not affiliated with, endorsed by, or sponsored by inMusic Brands, Denon
7
10
  > DJ, or the Engine DJ product.** "Engine DJ" is used here only to name the
8
11
  > software whose library this tool reads. No logos or brand artwork from
9
12
  > inMusic or Denon DJ are used in this project.
10
13
 
11
- ## What it does
12
-
13
- - **Smart search** (`search_tracks`) — full text with diacritics folded,
14
- tempo windows, harmonic key matching, rating, and play history ("what have
15
- I not played in six months?").
16
- - **Track lookup** (`get_tracks`) full metadata for specific track ids.
17
- - **Cues, loops and beatgrids** (`get_track_performance`) — decoded from
18
- Engine's `PerformanceData`, with a per-field decode status. The cue and
19
- beatgrid layouts are reverse-engineered but **validated against a real
20
- Engine library**; the loop layout is not fully validated see
21
- [Limitations](#limitations).
22
- - **Collection audit** (`audit_library`) missing files, unanalysed tracks,
23
- tracks without cues or beatgrids, duplicates, suspicious tempos, orphaned
24
- playlist entries.
25
- - **Read-only SQL** (`run_sql`) — an escape hatch for anything the tools
26
- above do not cover.
27
- - **Library and index introspection** (`list_libraries`, `refresh_index`) —
28
- see what was discovered and force a search-index rebuild.
29
-
30
- It also exposes two MCP resources: `engine://schema` (the field semantics an
31
- assistant needs before writing SQL against the library) and
32
- `engine://libraries` (what was discovered, and whether each library's schema
33
- is supported).
14
+ Your library is opened **read-only at the operating-system level**. It is
15
+ never written to — see [Safety](#safety).
16
+
17
+ ## What you can ask
18
+
19
+ Once connected, these are ordinary questions in chat:
20
+
21
+ - *"Something dark around 124 in a minor key I haven't played in six months."*
22
+ - *"Which tracks still have no hot cue set?"*
23
+ - *"Find me anything harmonically compatible with 8A between 138 and 142."*
24
+ - *"What's in my ACID Beach playlist, in order?"*
25
+ - *"Anything around 128 in ACID Beach?"*
26
+ - *"What's broken in my collection — missing files, duplicates, bad tempos?"*
27
+ - *"Where are the cue points on this track, and what tempo did Engine analyse?"*
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npx engine-dj-mcp
33
+ ```
34
+
35
+ Claude Desktop add to your configuration:
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp"] }
41
+ }
42
+ }
43
+ ```
44
+
45
+ **Requirements:** Node.js 22.13 or newer (for the unflagged `node:sqlite`;
46
+ there are no native dependencies), and an Engine DJ library at schema 3.0.0
47
+ through 3.0.2 — Engine DJ 4.5 and 5.x.
48
+
49
+ ## Tools
50
+
51
+ Nine tools, all read-only. Every tool that reads library data also accepts
52
+ an optional `library` argument — see [Choosing a library](#choosing-a-library).
53
+
54
+ ### `search_tracks`
55
+
56
+ The main one. Full-text search with diacritics folded, plus filters for
57
+ tempo, key, rating, when a track was added and when it was last played.
58
+
59
+ | Argument | What it does |
60
+ | --- | --- |
61
+ | `q` | Full text over title, artist, album, genre, comment and label. Diacritics are folded, so `bjork` matches `Björk` — Engine's own search does not. |
62
+ | `bpm` | `{ min, max }` or `{ around, tolerance_pct }`. Resolved tempo, so an analysed BPM wins over the tag. |
63
+ | `key` | `{ camelot: [...] }` for exact keys, `{ compatible_with: "8A" }` for harmonic neighbours, `{ mode: "minor" }` for a whole side of the wheel. |
64
+ | `rating` | `{ min, max }`, 0–5. |
65
+ | `played` | `{ never: true }`, or `{ before, after }` taking an ISO date or a relative form like `-6 months`. |
66
+ | `added` | `{ before, after }`, same date forms. |
67
+ | `flags` | `analyzed`, `available`, `has_cues`, `has_beatgrid`. `has_cues` means a hot cue is genuinely set — see [Limitations](#limitations). |
68
+ | `playlist` | `{ id }` or `{ name }` — search inside one playlist. Results still come back by relevance or id; `get_playlist_tracks` is what preserves playlist order. |
69
+ | `fields` | Which columns to return. Defaults to `id, artist, title, bpm, camelot, rating`. |
70
+ | `limit`, `cursor` | Page size (default 25, max 200) and an opaque cursor for the next page. |
71
+ | `include_total` | Off by default because counting costs far more than the page. Capped at 1000 — a capped result carries `total_capped: true` and means "at least 1000". |
72
+
73
+ ### `get_tracks`
74
+
75
+ Full metadata for specific track ids, returned in the order you asked for.
76
+ Unknown ids are omitted rather than failing the call.
77
+
78
+ `ids` (required), `fields`, `redact_paths`.
79
+
80
+ ### `get_playlists`
81
+
82
+ Your playlist tree, in the order Engine DJ shows it — folders included.
83
+
84
+ The list is flat and in the order you would read down the sidebar with every
85
+ folder expanded: `depth` and `path` carry the nesting, `parent_id` names the
86
+ folder a list sits in.
87
+
88
+ | Field | Meaning |
89
+ | --- | --- |
90
+ | `name`, `id`, `path` | `path` is the full `Folder/Sub/Name`, and is unique — a bare `name` need not be. |
91
+ | `depth`, `parent_id` | The nesting. `parent_id` is `null` at the top level. |
92
+ | `is_folder` | The list has child lists. Engine has no folder flag — a folder *is* a playlist that other playlists sit under — so an emptied folder reads as an empty playlist. |
93
+ | `is_persisted` | Engine's own flag for a list saved to the device. Both values appear on lists Engine displays, so nothing is filtered on it. |
94
+ | `track_count` | Entries in that list alone, never rolled up from its children — the number Engine shows beside it. |
95
+ | `missing_count` | How many of those entries name a track this library does not have. |
96
+
97
+ `limit` (default 200, max 1000). `warnings` appears if a playlist's link
98
+ chain is damaged; nothing is ever dropped from the list because of one.
99
+
100
+ ### `get_playlist_tracks`
101
+
102
+ The tracks of one playlist, **in playlist order**.
103
+
104
+ Name it with `playlist_id` or with `playlist_name` — exactly one of the two.
105
+ Playlist names are unique only within a folder, so a name matching more than
106
+ one is refused with every candidate's id and full path rather than guessed
107
+ at; pass the `path` from `get_playlists` to say which you meant.
108
+
109
+ Every row carries `position`, its 1-based place in the playlist. Same
110
+ `fields`, `limit` and `cursor` conventions as `search_tracks`.
111
+
112
+ An entry whose track is not in this library keeps its slot and comes back as
113
+ `{ position, entry_id, track_id, missing: true }`, with `missing_count`
114
+ alongside `entry_count`. That is ordinary rather than corruption — playlist
115
+ entries outlive their tracks and travel between drives — and they are kept in
116
+ place so the number of rows still matches the playlist's own length. On one
117
+ reference library a 43-entry playlist holds exactly one track that library
118
+ can actually play.
119
+
120
+ ### `get_track_performance`
121
+
122
+ Decodes the binary `PerformanceData` Engine stores per track: hot cues, the
123
+ main cue, saved loops, the beatgrid, and a coarse waveform profile.
124
+
125
+ Every field carries its own decode status **and** its own `layout` marker.
126
+ `layout: "verified"` means the byte layout was confirmed against a real
127
+ library, so `status: "ok"` is a claim about the values. `layout: "unverified"`
128
+ would mean only that the bytes parsed; no field returns it today.
129
+
130
+ Positions are sample offsets; cue and loop items also carry seconds.
131
+ `items: []` with `slots: 8` means an analysed track with no cues set.
132
+
133
+ `id` (required).
134
+
135
+ ### `audit_library`
136
+
137
+ Ten collection health checks. Returns a count and a small sample of ids per
138
+ check, never the full result set — a library with thousands of unanalysed
139
+ tracks should not fill an assistant's context.
140
+
141
+ | Check | Finds |
142
+ | --- | --- |
143
+ | `missing_files` | Tracks whose file is gone from disk |
144
+ | `unavailable` | Tracks Engine has marked unavailable |
145
+ | `unanalyzed` | Tracks Engine has not analysed |
146
+ | `no_cues` | Tracks with no hot cue set |
147
+ | `no_beatgrid` | Tracks with no beatgrid data |
148
+ | `missing_key` | Tracks with no key detected |
149
+ | `suspicious_bpm` | Analysed and tagged tempo disagree, or tempo is outside 60–200 |
150
+ | `duplicates` | Same artist and title, or same size and length |
151
+ | `empty_metadata` | No artist or no title |
152
+ | `orphan_entries` | Playlist entries pointing at tracks not in this library — `get_playlist_tracks` shows where each one sits |
153
+
154
+ `checks` — omit it to run all ten.
155
+
156
+ ### `run_sql`
157
+
158
+ An escape hatch for questions the tools above do not cover. Read-only is
159
+ enforced by the kernel, not by this tool. Results are bounded whatever the
160
+ query says.
161
+
162
+ Prefer `side.track_derived.camelot` and `side.track_derived.tempo` in `WHERE`
163
+ clauses over the `camelot()` and `tempo()` SQL functions — the functions run
164
+ per row and defeat the indexes.
165
+
166
+ `sql` (required), `params`, `limit`.
167
+
168
+ ### `list_libraries`
169
+
170
+ Every library found, including ones whose schema is unsupported — listed
171
+ with their version, so you can tell a broken server from a missing library.
172
+ Re-scans on every call, so a drive plugged in after the server started shows
173
+ up without a restart. A library that is temporarily unreadable — Engine DJ
174
+ writing to it, say — stays listed with `status: "unreadable"` rather than
175
+ vanishing.
176
+
177
+ No arguments.
178
+
179
+ ### `refresh_index`
180
+
181
+ Rebuilds the search index if the library changed. Normally unnecessary; the
182
+ server checks staleness itself before answering.
183
+
184
+ ## Resources
185
+
186
+ - **`engine://schema`** — the field semantics an assistant needs before
187
+ writing SQL: how Engine encodes musical key, why tempo is
188
+ `COALESCE(bpmAnalyzed, bpm)`, that `Track.path` is relative, where playlist
189
+ order really lives, and which helper columns are indexed.
190
+ - **`engine://libraries`** — what was discovered at startup and whether each
191
+ library's schema is supported. A snapshot; `list_libraries` is the live view.
34
192
 
35
193
  ## Choosing a library
36
194
 
37
195
  Engine DJ keeps a library on your computer and another on every drive you
38
- export to, so more than one is usually connected at once. `list_libraries`
39
- reports each of them with a `uuid` and a `path`, and **every tool that reads
40
- library data takes an optional `library` argument**`search_tracks`,
41
- `get_tracks`, `get_track_performance`, `audit_library`, `run_sql` and
42
- `refresh_index`. Pass either the `uuid` or the `path` exactly as
43
- `list_libraries` printed it; the `~/…` form it reports is accepted alongside
44
- the absolute one. A value matching neither comes back as
45
- `library_not_found`, listing the libraries you can actually choose from.
46
-
47
- Leave `library` out and the server uses **the supported library holding the
48
- most tracks**, ties broken by the order the drives were scanned in. That
49
- matters: the local library Engine DJ creates on install is scanned first and
50
- is often empty, so "the first one found" would hide the drive you actually
51
- work from.
52
-
53
- Each library gets its own search index and its own read-only connection,
54
- opened the first time you ask that library a question — mounting four drives
55
- does not cost four connections. Every query, audit and file-path check stays
56
- inside the library selected for that call. Comparing two libraries against
57
- each other — "what is on this drive but not that one?" — is **not** something
58
- this server does.
196
+ export to, so more than one is usually connected. `list_libraries` reports
197
+ each with a `uuid` and a `path`, and every tool that reads library data takes
198
+ an optional `library` argument. Pass either form exactly as printed the
199
+ `~/…` path is accepted alongside the absolute one. A value matching neither
200
+ comes back as `library_not_found`, listing what you can choose from.
201
+
202
+ Leave it out and the server uses **the supported library holding the most
203
+ tracks**. That matters: the local library Engine DJ creates on install is
204
+ scanned first and is often empty, so "the first one found" would hide the
205
+ drive you actually work from.
206
+
207
+ Each library gets its own index and its own connection, opened the first time
208
+ you ask that library something. Comparing two libraries against each other
209
+ *"what is on this drive but not that one?"* — is **not** something this server
210
+ does.
59
211
 
60
212
  ## Safety
61
213
 
62
214
  Your library is opened **read-only at the operating-system level**, not by
63
- convention or by a `PRAGMA` that a query could turn back off. Writes are
215
+ convention and not by a `PRAGMA` a query could turn back off. Writes are
64
216
  refused by SQLite itself, and no file is ever created inside your `Engine
65
- Library` folder. The server keeps its search index in `~/.engine-dj-mcp/`.
217
+ Library` folder. The search index lives in `~/.engine-dj-mcp/`.
66
218
 
67
- `run_sql` accepts arbitrary SQL, but only ever the first statement of it is
219
+ `run_sql` accepts arbitrary SQL, but only the first statement is ever
68
220
  executed, and `VACUUM`, `ATTACH` and `DETACH` are rejected outright, so a
69
221
  chained or exfiltrating statement cannot slip past the read-only connection.
70
222
 
71
223
  If Engine DJ was closed uncleanly and left an unrecovered journal, this
72
- server will never open the library writably to "fix" it for you that
73
- would break the one guarantee this project makes. It reports
74
- `library_needs_recovery` instead and asks you to launch Engine DJ once so it
75
- can recover the library itself.
224
+ server will not open the library writably to "fix" it that would break the
225
+ one guarantee this project makes. It reports `library_needs_recovery` and
226
+ asks you to launch Engine DJ once so it can recover its own library.
76
227
 
77
228
  ## Limitations
78
229
 
79
230
  Read this before deciding what to trust.
80
231
 
81
- - **Cue, beatgrid and waveform layouts are validated; the loop layout is
82
- not.** The
83
- binary layouts inside `PerformanceData` are reverse-engineered, so every
84
- decoded field says which kind it is:
85
-
86
- - `layout: "verified"` **cues** (including the main cue), the
87
- **beatgrid**, and the **waveform summary**. These were derived from and
88
- checked against a real Engine DJ 3.0.x library of 281 analysed tracks:
89
- cue offsets land inside the track, the beatgrid's implied tempo matches
90
- `bpmAnalyzed` on all 281, and the waveform's declared point spacing
91
- multiplies back out to the track's sample count on all 281. A
92
- `status: "ok"` here is a claim about the values, not just the parse.
93
- - `layout: "unverified"` — **loops**. The blob's slot structure is known
94
- (eight slots, uncompressed, little-endian) and an empty one decodes
95
- correctly, but not one track in that library had a loop saved, so the
96
- meaning of a *populated* loop slot is untested. Do not report loop
97
- bounds to a user as fact.
98
-
99
- A `layout` marker is a claim about the **bytes** — which offset holds
100
- which field, and what the number there means. It is not a claim about
101
- every name this server puts on them. Three labels are inferred rather than
102
- measured, and the code says so where each is defined: which of a cue's
103
- four colour bytes is which channel (they are reported as stored, as one
104
- 32-bit value, with no channel claim attached); that the second beatgrid is
105
- the one Engine calls "adjusted" (that it is the one Engine *plays* is
106
- measured — on seven tracks the other grid runs at exactly half the
107
- analysed tempo); that `main_cue.is_adjusted` is what its flag byte means;
108
- and that the waveform's three bytes per point are the low, mid and high
109
- bands in that order. None of these affects a value you get back.
110
-
111
- Everything else the server reports — titles, artists, tempo, key, ratings,
112
- play history, file paths — is read straight from the database and carries
113
- no such caveat.
114
- - **`has_cues` and `no_cues` mean "a hot cue is set", and cost a little to
115
- say so.** Engine writes a `quickCues` blob to every analysed track whether
116
- or not any pad is used, so the cheap SQL test (`quickCues` present and
117
- non-empty) answers a question about *analysis*: in the 281-blob reference
118
- library all 281 would count as having cues, while two tracks (three rows,
119
- one track being exported to a second library) actually do. The blob is
120
- therefore decoded when the search index is built, and
121
- `search_tracks(flags: { has_cues: true })` and `audit_library`'s `no_cues`
122
- both read the decoded answer. That costs about 100 ms of extra rebuild
123
- time at 50,000 tracks, and a rebuild only runs when your library changes.
124
-
125
- The track's **main cue** does not count towards it: Engine sets that as a
126
- playback start marker rather than the DJ placing it (it is set on 159 of
127
- the 281 blobs, including every track the library records as played), so
128
- counting it would answer a third question again. `has_beatgrid` and
129
- `no_beatgrid` do still test for the blob — a `beatData` blob has no
130
- "written but empty" state, and on all 281 real blobs presence and a usable
131
- grid have never disagreed.
132
- - **It never writes to your library.** Not to add a cue, not to fix a tag,
133
- not even to recover a journal Engine DJ left behind. The connection is
134
- read-only at the kernel level, so a write is refused by SQLite itself
135
- rather than by a rule this code could get wrong.
136
- - **It does not read play history.** `Track.timeLastPlayed` is used to answer
137
- "what have I not played in six months?", but the separate Engine history
138
- database — individual sessions, decks, what followed what — is not opened
139
- at all.
140
- - **It does not build set lists**, reorder playlists, or suggest transitions.
141
- It answers questions about the collection; the mixing is yours.
142
- - **Schema 3.0.0 through 3.0.2 only** (Engine DJ 4.5 and 5.x). Older and
143
- newer libraries are listed with their version and reported as unsupported
144
- rather than read on a guess.
145
-
146
- ## Requirements
147
-
148
- - Node.js 22 or newer
149
- - Engine DJ with library schema 3.0.0–3.0.2 (Engine DJ 4.5 and 5.x)
232
+ **The `PerformanceData` layouts are reverse-engineered**, so every decoded
233
+ field says which kind it is. All four are marked `layout: "verified"` —
234
+ derived from and checked against a real Engine DJ 3.0.x library of 281
235
+ analysed tracks, where cue offsets land inside the track, the beatgrid's
236
+ implied tempo matches `bpmAnalyzed` on all 281, and the waveform's declared
237
+ point spacing multiplies back out to the track's sample count on all 281.
150
238
 
151
- ## Install
239
+ Loops were the last to earn it. The slot grid was pinned down by 2248
240
+ sentinels, but no library available had a loop saved in it, so a *populated*
241
+ slot stayed untested and loops carried `layout: "unverified"` through several
242
+ releases. One deliberately saved loop settled it: its slot spans 1.678321678 s
243
+ on a track Engine analysed at 143 BPM, which is four beats to within 4e-15 s.
244
+ Only the right field order, unit and endianness land on a whole beat count.
152
245
 
153
- ```bash
154
- npx engine-dj-mcp
155
- ```
246
+ A `layout` marker is a claim about the bytes, not about every name put on
247
+ them. Four labels are inferred rather than measured, and the code says so
248
+ where each is defined: which of a cue's four colour bytes is which channel
249
+ (they are returned as stored, one 32-bit value, with no channel claim); that
250
+ the second beatgrid is the one Engine calls "adjusted" (that it is the one
251
+ Engine *plays* is measured — on seven tracks the other runs at exactly half
252
+ the analysed tempo); that `main_cue.is_adjusted` is what its flag byte means;
253
+ and that the waveform's three bytes per point are low, mid and high in that
254
+ order. None affects a value you get back.
156
255
 
157
- Claude Desktop configuration:
256
+ Everything else — titles, artists, tempo, key, ratings, play history, file
257
+ paths — is read straight from the database and carries no such caveat.
158
258
 
159
- ```json
160
- {
161
- "mcpServers": {
162
- "engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp"] }
163
- }
164
- }
165
- ```
259
+ **`has_cues` and `no_cues` mean "a hot cue is set".** Engine writes a
260
+ `quickCues` blob to every analysed track whether or not a pad is used, so the
261
+ cheap SQL test would answer a question about *analysis* instead: in the
262
+ reference library all 281 blobs would count as having cues, while two tracks
263
+ actually do. The blob is therefore decoded while the index is built. That
264
+ costs roughly 100 ms extra at 50,000 tracks, and only when your library
265
+ changes. The track's **main cue** does not count towards it — Engine sets
266
+ that as a playback marker rather than the DJ placing it. `has_beatgrid` does
267
+ still test for the blob: `beatData` has no "written but empty" state.
268
+
269
+ **It never writes to your library.** Not to add a cue, not to fix a tag, not
270
+ even to recover a journal Engine DJ left behind.
271
+
272
+ **It does not read play history.** `Track.timeLastPlayed` answers "what have I
273
+ not played in six months?", but the separate Engine history database —
274
+ sessions, decks, what followed what — is not opened at all.
275
+
276
+ **Smartlists are not reported.** Engine's rule-based lists live in a separate
277
+ `Smartlist` table with its own ordering and a JSON rule column, and nothing
278
+ here reads it. `get_playlists` reports ordinary playlists and folders only, so
279
+ a smartlist you can see in Engine will not appear.
280
+
281
+ **An empty folder reads as an empty playlist.** Engine's schema has no folder
282
+ flag — a folder is simply a playlist that other playlists sit under — so
283
+ `is_folder` means "has child lists". A folder you have emptied is
284
+ indistinguishable from a playlist with no tracks.
285
+
286
+ **It reads playlists; it does not write them.** No creating, reordering,
287
+ renaming or adding to a playlist, and no set lists or suggested transitions.
288
+ It answers questions about the collection; the mixing is yours.
289
+
290
+ **Schema 3.0.0 through 3.0.2 only.** Older and newer libraries are listed with
291
+ their version and reported as unsupported rather than read on a guess.
166
292
 
167
293
  ## Licence
168
294
 
@@ -8,10 +8,12 @@
8
8
  * sample count. `status: "ok"` on a verified field is a claim about the
9
9
  * values, not merely about the parse.
10
10
  *
11
- * `"unverified"` still means what it always did: the bytes parsed without
12
- * contradicting the layout, and nothing more. It survives on `loops` because
13
- * no track in the 281 examined has a loop set, so while the slot structure is
14
- * pinned down, the meaning of a *populated* slot is untested.
11
+ * `"unverified"` means the bytes parsed without contradicting the layout, and
12
+ * nothing more. No field carries it today: `loops` was the last one, and it
13
+ * held the marker only for as long as no library available had a loop saved in
14
+ * it. One saved loop supplied the missing prediction see `decodeLoops` and
15
+ * the marker is kept here because it is what a future field decoded from an
16
+ * unpopulated example would deserve.
15
17
  *
16
18
  * The marker is about the *bytes*: which offset holds which field, and what
17
19
  * the numbers there mean. It is not a claim about every English word this
@@ -215,19 +217,23 @@ export declare function hasCueSet(buf: Buffer | null): boolean;
215
217
  * (23 bytes with an empty label)
216
218
  *
217
219
  * Evidence for the framing and the slot grid: 8 + 8×23 = 192, the size of
218
- * all 281 real blobs, every one of which parsed to exactly its last byte;
220
+ * every unpopulated real blob, each of which parsed to exactly its last byte;
219
221
  * the little-endian count reads as 8, and the little-endian -1.0 sentinel
220
222
  * (`000000000000f0bf`) appears at the offsets this layout predicts in all
221
223
  * 2248 slots. Reading the count big-endian gives 134217728, which is what
222
224
  * made every real track decode as `unsupported` before.
223
225
  *
224
- * This layout keeps `layout: "unverified"`. Not one of the 2248 slots is
225
- * populated this library has no saved loops so while the slot grid is
226
- * pinned down by 2248 sentinels, the six bytes after each slot's two doubles
227
- * are zero everywhere, and nothing here distinguishes start/end from
228
- * end/start, or fixes the order of the flag and colour bytes. A populated
229
- * loop is the one thing the available data cannot exercise, so it is not
230
- * claimed as verified.
226
+ * The sentinels pin the slot grid, but an empty slot cannot say which double
227
+ * is the start, what unit the doubles are in, or what the six trailing bytes
228
+ * mean so this layout carried `unverified` for as long as no library had a
229
+ * saved loop in it. The `saved-loop` golden fixture is that loop, and it
230
+ * settles all three at once. Its slot reads label "Loop 1", start 2129416.08
231
+ * and end 2203430.06, which at the 44100 Hz `beatData` declares for the track
232
+ * is 1.678321678 s — exactly four beats at the 143 BPM Engine analysed, to
233
+ * 3.6e-15 s. Nothing but the right field order, the right unit and the right
234
+ * endianness lands on a whole number of beats, so the layout is verified.
235
+ * The trailing bytes read `01 01` (start set, end set) and four colour bytes,
236
+ * which is the same tail `quickCues` slots carry.
231
237
  */
232
238
  export declare function decodeLoops(buf: Buffer | null, sampleRate?: number | null): LoopsResult;
233
239
  /**
@@ -6,13 +6,15 @@
6
6
  // The layouts below were derived from, and checked against, a real Engine DJ
7
7
  // library: 257 analysed tracks on a USB export plus 24 in the local history
8
8
  // database, 281 blobs of each kind. What each check proved is recorded next
9
- // to the decoder it justifies. `loops` is the one field whose *contents*
10
- // could not be checked see decodeLoops.
9
+ // to the decoder it justifies. `loops` was the last field to be checked: its
10
+ // contents needed a library with a loop actually saved in one, which arrived
11
+ // later than the rest — see decodeLoops.
11
12
  //
12
13
  // Framing: `quickCues`, `beatData` and `overviewWaveFormData` are Qt
13
14
  // qCompress frames (4-byte big-endian uncompressed length, then a raw zlib
14
15
  // stream); the declared length matched the inflated length for all 281 of
15
- // each. `loops` is NOT compressed and NOT framed — it is 192 raw bytes, and
16
+ // each. `loops` is NOT compressed and NOT framed — 192 raw bytes plus any
17
+ // label text its slots carry, and
16
18
  // running it through the qCompress path reads its little-endian int64 count
17
19
  // of 8 as a big-endian length of 134217728 and hands zlib bytes it rejects.
18
20
  //
@@ -35,10 +37,12 @@ import { qUncompress, Reader, DecodeError } from "./qcompress.js";
35
37
  * sample count. `status: "ok"` on a verified field is a claim about the
36
38
  * values, not merely about the parse.
37
39
  *
38
- * `"unverified"` still means what it always did: the bytes parsed without
39
- * contradicting the layout, and nothing more. It survives on `loops` because
40
- * no track in the 281 examined has a loop set, so while the slot structure is
41
- * pinned down, the meaning of a *populated* slot is untested.
40
+ * `"unverified"` means the bytes parsed without contradicting the layout, and
41
+ * nothing more. No field carries it today: `loops` was the last one, and it
42
+ * held the marker only for as long as no library available had a loop saved in
43
+ * it. One saved loop supplied the missing prediction see `decodeLoops` and
44
+ * the marker is kept here because it is what a future field decoded from an
45
+ * unpopulated example would deserve.
42
46
  *
43
47
  * The marker is about the *bytes*: which offset holds which field, and what
44
48
  * the numbers there mean. It is not a claim about every English word this
@@ -252,23 +256,27 @@ export function hasCueSet(buf) {
252
256
  * (23 bytes with an empty label)
253
257
  *
254
258
  * Evidence for the framing and the slot grid: 8 + 8×23 = 192, the size of
255
- * all 281 real blobs, every one of which parsed to exactly its last byte;
259
+ * every unpopulated real blob, each of which parsed to exactly its last byte;
256
260
  * the little-endian count reads as 8, and the little-endian -1.0 sentinel
257
261
  * (`000000000000f0bf`) appears at the offsets this layout predicts in all
258
262
  * 2248 slots. Reading the count big-endian gives 134217728, which is what
259
263
  * made every real track decode as `unsupported` before.
260
264
  *
261
- * This layout keeps `layout: "unverified"`. Not one of the 2248 slots is
262
- * populated this library has no saved loops so while the slot grid is
263
- * pinned down by 2248 sentinels, the six bytes after each slot's two doubles
264
- * are zero everywhere, and nothing here distinguishes start/end from
265
- * end/start, or fixes the order of the flag and colour bytes. A populated
266
- * loop is the one thing the available data cannot exercise, so it is not
267
- * claimed as verified.
265
+ * The sentinels pin the slot grid, but an empty slot cannot say which double
266
+ * is the start, what unit the doubles are in, or what the six trailing bytes
267
+ * mean so this layout carried `unverified` for as long as no library had a
268
+ * saved loop in it. The `saved-loop` golden fixture is that loop, and it
269
+ * settles all three at once. Its slot reads label "Loop 1", start 2129416.08
270
+ * and end 2203430.06, which at the 44100 Hz `beatData` declares for the track
271
+ * is 1.678321678 s — exactly four beats at the 143 BPM Engine analysed, to
272
+ * 3.6e-15 s. Nothing but the right field order, the right unit and the right
273
+ * endianness lands on a whole number of beats, so the layout is verified.
274
+ * The trailing bytes read `01 01` (start set, end set) and four colour bytes,
275
+ * which is the same tail `quickCues` slots carry.
268
276
  */
269
277
  export function decodeLoops(buf, sampleRate) {
270
278
  const rate = usableRate(sampleRate);
271
- return guard(buf, LAYOUT_UNVERIFIED, "raw", (r) => {
279
+ return guard(buf, LAYOUT_VERIFIED, "raw", (r) => {
272
280
  const slots = boundedCount(r, true, MAX_ITEMS, `loop slot count`);
273
281
  const items = [];
274
282
  for (let i = 0; i < slots; i++) {