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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/blobs/index.d.ts +337 -0
- package/dist/blobs/index.js +483 -0
- package/dist/blobs/qcompress.d.ts +44 -0
- package/dist/blobs/qcompress.js +146 -0
- package/dist/discovery.d.ts +36 -0
- package/dist/discovery.js +111 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +49 -0
- package/dist/guard.d.ts +31 -0
- package/dist/guard.js +236 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/library-select.d.ts +63 -0
- package/dist/library-select.js +97 -0
- package/dist/paths.d.ts +18 -0
- package/dist/paths.js +34 -0
- package/dist/probe.d.ts +7 -0
- package/dist/probe.js +20 -0
- package/dist/proc/query-client.d.ts +36 -0
- package/dist/proc/query-client.js +249 -0
- package/dist/proc/query-worker.d.ts +1 -0
- package/dist/proc/query-worker.js +72 -0
- package/dist/semantics.d.ts +43 -0
- package/dist/semantics.js +95 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +439 -0
- package/dist/sidecar/build.d.ts +19 -0
- package/dist/sidecar/build.js +85 -0
- package/dist/sidecar/schema.d.ts +25 -0
- package/dist/sidecar/schema.js +36 -0
- package/dist/store/connections.d.ts +28 -0
- package/dist/store/connections.js +116 -0
- package/dist/store/index-manager.d.ts +29 -0
- package/dist/store/index-manager.js +187 -0
- package/dist/tools/audit.d.ts +15 -0
- package/dist/tools/audit.js +148 -0
- package/dist/tools/libraries.d.ts +40 -0
- package/dist/tools/libraries.js +30 -0
- package/dist/tools/performance.d.ts +15 -0
- package/dist/tools/performance.js +47 -0
- package/dist/tools/refresh.d.ts +8 -0
- package/dist/tools/refresh.js +3 -0
- package/dist/tools/search.d.ts +60 -0
- package/dist/tools/search.js +328 -0
- package/dist/tools/sql.d.ts +14 -0
- package/dist/tools/sql.js +21 -0
- package/dist/tools/tracks.d.ts +12 -0
- package/dist/tools/tracks.js +49 -0
- 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
|
+
};
|