engine-dj-mcp 0.9.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/blobs/index.d.ts +337 -0
  4. package/dist/blobs/index.js +483 -0
  5. package/dist/blobs/qcompress.d.ts +44 -0
  6. package/dist/blobs/qcompress.js +146 -0
  7. package/dist/discovery.d.ts +36 -0
  8. package/dist/discovery.js +111 -0
  9. package/dist/errors.d.ts +30 -0
  10. package/dist/errors.js +49 -0
  11. package/dist/guard.d.ts +31 -0
  12. package/dist/guard.js +236 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +13 -0
  15. package/dist/library-select.d.ts +63 -0
  16. package/dist/library-select.js +97 -0
  17. package/dist/paths.d.ts +18 -0
  18. package/dist/paths.js +34 -0
  19. package/dist/probe.d.ts +7 -0
  20. package/dist/probe.js +20 -0
  21. package/dist/proc/query-client.d.ts +36 -0
  22. package/dist/proc/query-client.js +249 -0
  23. package/dist/proc/query-worker.d.ts +1 -0
  24. package/dist/proc/query-worker.js +72 -0
  25. package/dist/semantics.d.ts +43 -0
  26. package/dist/semantics.js +95 -0
  27. package/dist/server.d.ts +31 -0
  28. package/dist/server.js +439 -0
  29. package/dist/sidecar/build.d.ts +19 -0
  30. package/dist/sidecar/build.js +85 -0
  31. package/dist/sidecar/schema.d.ts +25 -0
  32. package/dist/sidecar/schema.js +36 -0
  33. package/dist/store/connections.d.ts +28 -0
  34. package/dist/store/connections.js +116 -0
  35. package/dist/store/index-manager.d.ts +29 -0
  36. package/dist/store/index-manager.js +187 -0
  37. package/dist/tools/audit.d.ts +15 -0
  38. package/dist/tools/audit.js +148 -0
  39. package/dist/tools/libraries.d.ts +40 -0
  40. package/dist/tools/libraries.js +30 -0
  41. package/dist/tools/performance.d.ts +15 -0
  42. package/dist/tools/performance.js +47 -0
  43. package/dist/tools/refresh.d.ts +8 -0
  44. package/dist/tools/refresh.js +3 -0
  45. package/dist/tools/search.d.ts +60 -0
  46. package/dist/tools/search.js +328 -0
  47. package/dist/tools/sql.d.ts +14 -0
  48. package/dist/tools/sql.js +21 -0
  49. package/dist/tools/tracks.d.ts +12 -0
  50. package/dist/tools/tracks.js +49 -0
  51. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikhail Chereshnev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # engine-dj-mcp
2
+
3
+ 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.
5
+
6
+ > **Not affiliated with, endorsed by, or sponsored by inMusic Brands, Denon
7
+ > DJ, or the Engine DJ product.** "Engine DJ" is used here only to name the
8
+ > software whose library this tool reads. No logos or brand artwork from
9
+ > inMusic or Denon DJ are used in this project.
10
+
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).
34
+
35
+ ## Choosing a library
36
+
37
+ 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.
59
+
60
+ ## Safety
61
+
62
+ 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
64
+ 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/`.
66
+
67
+ `run_sql` accepts arbitrary SQL, but only ever the first statement of it is
68
+ executed, and `VACUUM`, `ATTACH` and `DETACH` are rejected outright, so a
69
+ chained or exfiltrating statement cannot slip past the read-only connection.
70
+
71
+ 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.
76
+
77
+ ## Limitations
78
+
79
+ Read this before deciding what to trust.
80
+
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)
150
+
151
+ ## Install
152
+
153
+ ```bash
154
+ npx engine-dj-mcp
155
+ ```
156
+
157
+ Claude Desktop configuration:
158
+
159
+ ```json
160
+ {
161
+ "mcpServers": {
162
+ "engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp"] }
163
+ }
164
+ }
165
+ ```
166
+
167
+ ## Licence
168
+
169
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Whether a field's binary layout has been checked against real Engine data.
3
+ *
4
+ * `"verified"` means the layout was derived from a real library and confirmed
5
+ * by predictions that could have failed: cue positions landing inside the
6
+ * track, a beatgrid whose implied tempo matches `Track.bpmAnalyzed`, a
7
+ * waveform whose declared bucket spacing multiplies back out to the track's
8
+ * sample count. `status: "ok"` on a verified field is a claim about the
9
+ * values, not merely about the parse.
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.
15
+ *
16
+ * The marker is about the *bytes*: which offset holds which field, and what
17
+ * the numbers there mean. It is not a claim about every English word this
18
+ * module attaches to them. Three labels are inferred rather than measured and
19
+ * say so at their own definitions — the cue colour's channel order, the
20
+ * beatgrid's `grid: "adjusted"` and `main_cue.is_adjusted` naming, and the
21
+ * waveform's low/mid/high band naming. Each names a field whose *value* is
22
+ * pinned by the evidence below; only the name is a reading of it.
23
+ */
24
+ export declare const LAYOUT_VERIFIED: "verified";
25
+ export declare const LAYOUT_UNVERIFIED: "unverified";
26
+ export type LayoutStatus = typeof LAYOUT_VERIFIED | typeof LAYOUT_UNVERIFIED;
27
+ export type Decoded<T, X = object> = {
28
+ layout: LayoutStatus;
29
+ } & (({
30
+ status: "ok";
31
+ items: T[];
32
+ total: number;
33
+ truncated: boolean;
34
+ } & X) | {
35
+ status: "empty";
36
+ } | {
37
+ status: "unsupported";
38
+ detail: string;
39
+ bytes: number;
40
+ } | {
41
+ status: "corrupt";
42
+ detail: string;
43
+ });
44
+ export interface Cue {
45
+ /** Hot-cue slot, 0-based: `index: 7` is the pad labelled "Cue 8". */
46
+ index: number;
47
+ label: string;
48
+ position_samples: number;
49
+ /** position_samples converted with beatData's sample rate; null if unknown. */
50
+ position_seconds: number | null;
51
+ /**
52
+ * The slot's four colour bytes as one big-endian unsigned 32-bit value.
53
+ * Which byte is which channel is NOT confirmed — see decodeCues.
54
+ */
55
+ colour: number;
56
+ }
57
+ /** The track's main cue (the CUE button), which lives in the quickCues blob. */
58
+ export interface MainCue {
59
+ position_samples: number | null;
60
+ position_seconds: number | null;
61
+ /**
62
+ * The second of the blob's two main-cue doubles. Read as Engine's stored
63
+ * default, before any adjustment — see `is_adjusted` on why that reading is
64
+ * inferred rather than measured.
65
+ */
66
+ default_samples: number | null;
67
+ /**
68
+ * The single byte between the two main-cue doubles, as a boolean.
69
+ *
70
+ * Not verified: that "adjusted" is what the byte means. Its *position* is
71
+ * pinned (the layout parses to the last byte on all 281 blobs, and moving
72
+ * this byte would misalign the double after it), and it is 0 or 1 with no
73
+ * other value seen. The name comes from a correlation that mostly holds
74
+ * and does not quite: the byte is set on 10 of the 281 real blobs, and on
75
+ * 8 of those the two doubles differ — but on 2 it is set while they are
76
+ * identical, and on 17 more the doubles differ with the byte clear (25
77
+ * differ in total). Treat it as "a flag Engine sets about the main cue",
78
+ * not as a measured meaning.
79
+ */
80
+ is_adjusted: boolean;
81
+ }
82
+ export interface Loop {
83
+ index: number;
84
+ label: string;
85
+ start_samples: number;
86
+ end_samples: number;
87
+ start_seconds: number | null;
88
+ end_seconds: number | null;
89
+ }
90
+ export interface BeatAnchor {
91
+ /** Sample offset of the anchor; may be negative (Engine anchors before 0). */
92
+ sample: number;
93
+ /** Beat number at that sample; may be negative for the same reason. */
94
+ beat: number;
95
+ seconds: number | null;
96
+ }
97
+ export type CuesResult = Decoded<Cue, {
98
+ slots: number;
99
+ main_cue: MainCue;
100
+ }>;
101
+ export type LoopsResult = Decoded<Loop, {
102
+ slots: number;
103
+ }>;
104
+ export type BeatgridResult = Decoded<BeatAnchor, {
105
+ sample_rate: number;
106
+ sample_count: number;
107
+ duration_seconds: number;
108
+ /** Tempo implied by the anchors, or null if fewer than two anchors. */
109
+ bpm: number | null;
110
+ /**
111
+ * Which of the blob's two grids `items` holds: always the second one.
112
+ *
113
+ * Not verified: that "adjusted" is Engine's own name for it. That the
114
+ * second grid is the one Engine plays *is* measured — on 30 of the 281
115
+ * real blobs the two grids differ, and on seven of those the first runs
116
+ * at exactly half `Track.bpmAnalyzed` while the second matches it. The
117
+ * word is the standard one for that role in the reverse-engineering
118
+ * literature, not a string read out of the blob.
119
+ */
120
+ grid: "adjusted";
121
+ }>;
122
+ export type WaveformSummary = {
123
+ layout: LayoutStatus;
124
+ } & ({
125
+ status: "ok";
126
+ /** How many peak values `profile` holds — one per bucket. */
127
+ peaks: number;
128
+ /** Waveform points the blob carries (1024 in every blob measured). */
129
+ entries: number;
130
+ /** Audio samples each point covers; entries × this is the track length. */
131
+ samples_per_entry: number;
132
+ /** Size of the decompressed waveform blob, in bytes. */
133
+ bytes: number;
134
+ /** Track length in seconds, from Track.length; null when unknown. */
135
+ duration_seconds: number | null;
136
+ profile: number[];
137
+ } | {
138
+ status: "empty";
139
+ } | {
140
+ status: "unsupported";
141
+ detail: string;
142
+ bytes: number;
143
+ } | {
144
+ status: "corrupt";
145
+ detail: string;
146
+ });
147
+ /**
148
+ * quickCues — qCompress frame, big-endian, 129 bytes inflated for a track
149
+ * whose cue labels are all empty:
150
+ *
151
+ * int64 slot count (8 bytes, always 8)
152
+ * per slot: uint8 label length, label bytes,
153
+ * float64 sample offset (-1.0 = slot unused),
154
+ * 4 colour bytes (13 bytes with an empty label)
155
+ * float64 main cue, uint8 main-cue-adjusted flag, float64 default main cue
156
+ * (17 bytes)
157
+ *
158
+ * Evidence. 8 + 8×13 + 17 = 129, the size of 278 of the 281 real blobs, and
159
+ * every one parsed to exactly its last byte. The remaining three inflate to
160
+ * 134: 5 bytes longer because one slot carries the 5-character label
161
+ * "Cue 8", which is only consistent with a *variable*-length slot — a
162
+ * fixed-stride layout cannot produce two sizes. The -1.0 sentinel appears at
163
+ * the offset this layout predicts in all 2245 unused slots, so the 13-byte
164
+ * stride is confirmed 2245 times over, not once. The three populated slots
165
+ * decode to 244.94 s of a 300 s track and 0.05 s of a 369 s track — inside
166
+ * the track, which a wrong offset would not be. The main-cue triple's first
167
+ * double distributes as: 122 blobs at the -1.0 sentinel, 51 at exactly 0,
168
+ * 105 at a positive offset inside the track, and 3 slightly negative — two
169
+ * at -1.455e-11 samples and one at -598.8 samples (-0.0136 s), all three
170
+ * within a beat of zero rather than anywhere random, which is what a
171
+ * misaligned read would produce. Its two doubles differ from each other on
172
+ * 25 tracks, which they could not if the layout had merged one field with
173
+ * its neighbour.
174
+ *
175
+ * Not verified: which of the four colour bytes is which channel. Reading
176
+ * them as (alpha, red, green, blue) makes the one slot carrying an unedited
177
+ * Engine default agree with djinterop's `pad_8` constant {0x15, 0x8E, 0xE2}
178
+ * on red and blue but not green, and the alternative reading gives a cue
179
+ * marker 12% opaque, so alpha-first is likely — but "likely" is not
180
+ * measured, and two colours from two tracks cannot settle it. The bytes are
181
+ * therefore reported as stored, as one big-endian u32, with no channel
182
+ * claim attached.
183
+ */
184
+ export declare function decodeCues(buf: Buffer | null, sampleRate?: number | null): CuesResult;
185
+ /**
186
+ * Whether this track actually has a hot cue set — the question "which tracks
187
+ * still need cue points?" is really asking.
188
+ *
189
+ * It exists because the cheap SQL answer is not an answer at all. Engine
190
+ * writes a full eight-slot `quickCues` blob to every analysed track whether
191
+ * or not a pad is used, so `length(quickCues) > 0` is true for all 281 blobs
192
+ * in the reference library while exactly 3 of them (two tracks, one of which
193
+ * is exported to a second library) hold a cue. A check that structurally
194
+ * cannot report a problem is worse than no check: it answers "none of your
195
+ * tracks need cue points" for a library where 255 of 257 do.
196
+ *
197
+ * The main cue is deliberately excluded. It is set on 159 of the 281 blobs,
198
+ * including all 71 tracks the library records as played and 88 that it does
199
+ * not — Engine writes it as a playback start marker, not as something a DJ
200
+ * placed. Counting it would make this flag answer a third question, closer
201
+ * to "has this been loaded on a deck" than to "has a cue been set".
202
+ *
203
+ * A blob that fails to decode answers `false`: an undecodable blob is not
204
+ * evidence that a cue exists, and the direction that errs toward flagging a
205
+ * track for a human to look at is the safe one for an audit.
206
+ */
207
+ export declare function hasCueSet(buf: Buffer | null): boolean;
208
+ /**
209
+ * loops — 192 raw bytes, uncompressed and unframed, little-endian:
210
+ *
211
+ * int64 slot count (8 bytes, always 8)
212
+ * per slot: uint8 label length, label bytes,
213
+ * float64 start, float64 end (-1.0 = slot unused),
214
+ * uint8 start-set, uint8 end-set, 4 colour bytes
215
+ * (23 bytes with an empty label)
216
+ *
217
+ * 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;
219
+ * the little-endian count reads as 8, and the little-endian -1.0 sentinel
220
+ * (`000000000000f0bf`) appears at the offsets this layout predicts in all
221
+ * 2248 slots. Reading the count big-endian gives 134217728, which is what
222
+ * made every real track decode as `unsupported` before.
223
+ *
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.
231
+ */
232
+ export declare function decodeLoops(buf: Buffer | null, sampleRate?: number | null): LoopsResult;
233
+ /**
234
+ * beatData — qCompress frame, 138 bytes inflated on every track measured,
235
+ * and mixed-endian:
236
+ *
237
+ * float64 BE sample rate
238
+ * float64 BE sample count
239
+ * uint8 beat data present
240
+ * int64 BE default-grid marker count, then that many markers
241
+ * int64 BE adjusted-grid marker count, then that many markers
242
+ * 9 trailing bytes (zero on every track measured)
243
+ *
244
+ * A marker is 24 bytes and little-endian: float64 sample offset, int64 beat
245
+ * number, int32 beats until the next marker, int32 unknown.
246
+ *
247
+ * Evidence. 17 + 8 + 2×24 + 8 + 2×24 + 9 = 138 for all 281. The two leading
248
+ * doubles read big-endian as 44100 (276 tracks) or 48000 (5); read the other
249
+ * way they are denormals. sample count ÷ sample rate equals `Track.length`
250
+ * to within a second for all 281 — a check on both doubles at once that no
251
+ * other offset or endianness passes. Within a grid, the beat numbers of
252
+ * consecutive markers differ by exactly the preceding marker's "beats until
253
+ * next" field on all 281, which pins the marker stride and three of its four
254
+ * fields simultaneously. The tempo implied by the adjusted grid's first and
255
+ * last anchor matches `Track.bpmAnalyzed` to within 0.5 BPM on all 281 —
256
+ * across tempos from 102 to 170 and durations from 92 s to 602 s, a
257
+ * prediction a wrong offset or a wrong endianness has no way to satisfy.
258
+ *
259
+ * `items` holds the *adjusted* grid, which is what Engine plays. That choice
260
+ * is itself measured, not stylistic: the two grids are byte-identical on 251
261
+ * tracks and differ on 30, and on seven of those the default grid runs at
262
+ * exactly half `bpmAnalyzed` (85.0000 against 170, 80.0000 against 160)
263
+ * while the adjusted grid matches it. The trailing int32 of each marker is
264
+ * read and discarded:
265
+ * it holds 0-12 on first markers and 1 or 2 on last ones, with two tracks
266
+ * carrying values that look like float bit patterns, and nothing in the
267
+ * library explains it. Reporting a field nobody can interpret would be
268
+ * padding a model's context with noise.
269
+ */
270
+ export declare function decodeBeatgrid(buf: Buffer | null): BeatgridResult;
271
+ /**
272
+ * overviewWaveFormData — qCompress frame, big-endian header, 3099 bytes
273
+ * inflated on every track measured:
274
+ *
275
+ * int64 BE number of waveform points
276
+ * int64 BE the same number again
277
+ * float64 BE audio samples per point
278
+ * three bytes per point (three band levels)
279
+ * three trailing bytes: the maximum of each band over the whole track
280
+ *
281
+ * Evidence. Both counts read 1024 on all 281 blobs, and 24 + 3×1024 + 3 =
282
+ * 3099 accounts for every byte. The overview is a fixed 1024 points
283
+ * regardless of track length, so it is the *spacing* that scales with
284
+ * duration, not the size: samples-per-point × 1024 equals the sample count
285
+ * in `beatData` on all 281 — for a 345-second track that is 14858.0 × 1024 =
286
+ * 15214592 samples, the same value beatData carries. The three trailing
287
+ * bytes equal the per-band maximum computed over the 1024 points on all 281,
288
+ * a prediction with 255³ ways to fail per track that failed on none. That
289
+ * last check is what makes the three-bytes-per-point *stride* measured
290
+ * rather than assumed, and it is why this field carries `layout: "verified"`
291
+ * like cues and the beatgrid.
292
+ *
293
+ * Not verified: that the three bytes per point are the low, mid and high
294
+ * bands, in that order. Three parallel level channels is what the byte
295
+ * evidence shows; naming them is a reading of Engine's own display, and
296
+ * nothing in the library distinguishes one ordering from another. Nothing
297
+ * downstream depends on it — `profile` takes the loudest of the three,
298
+ * which is order-independent — so the naming is kept out of the response
299
+ * rather than asserted in it.
300
+ *
301
+ * The raw waveform is never returned to the model; it is reduced to a coarse
302
+ * per-bucket profile (loudest band value in the bucket, normalised to 0..1).
303
+ * That bucketing now runs over the waveform points rather than over the
304
+ * decompressed bytes, which previously mixed the 24-byte header and the
305
+ * trailing maxima into the first and last buckets.
306
+ *
307
+ * `duration_seconds` still comes from Track.length rather than from the
308
+ * blob: the overview carries a sample *count* but no sample rate, so a
309
+ * duration derived from it alone would be a guess.
310
+ */
311
+ export declare function summariseWaveform(buf: Buffer | null, buckets?: number, durationSeconds?: number | null): WaveformSummary;
312
+ export interface PerformanceRow {
313
+ quickCues: Buffer | null;
314
+ loops: Buffer | null;
315
+ beatData: Buffer | null;
316
+ overviewWaveFormData: Buffer | null;
317
+ /** Track.length in seconds, if known; reported by the waveform summary. */
318
+ durationSeconds?: number | null;
319
+ }
320
+ /**
321
+ * Decodes every PerformanceData field independently. A failure in one field
322
+ * (e.g. a corrupt beatgrid) never prevents the others from decoding — each
323
+ * carries its own status rather than the call as a whole throwing or failing.
324
+ *
325
+ * The one dependency between fields is the sample rate, which only beatData
326
+ * carries and which cue and loop offsets need to become seconds. It is
327
+ * passed in as a value, so a beatData that fails to decode costs the cues
328
+ * their `position_seconds` and nothing else: `position_samples` is still
329
+ * reported, and the cue list still decodes.
330
+ */
331
+ export declare function decodePerformance(row: PerformanceRow): {
332
+ sample_rate: number | null;
333
+ cues: CuesResult;
334
+ loops: LoopsResult;
335
+ beatgrid: BeatgridResult;
336
+ waveform_summary: WaveformSummary;
337
+ };