@wenathlan/debonair 2.0.118 → 2.0.127
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/docs/planning.pipeline.md +198 -0
- package/package.json +5 -1
- package/tests/bigintids.test.ts +32 -0
- package/tests/ingest.test.ts +127 -0
- package/tests/tuning.test.ts +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# planning.pipeline — the 120+ step ladder of the full music pipeline
|
|
2
|
+
|
|
3
|
+
> the wave the owner ordered: ingest a real track → the engine decomposes it
|
|
4
|
+
> into the parametrized vector document → the document lands in the db with
|
|
5
|
+
> the owner id and the big pointers → the references power the pattern miner
|
|
6
|
+
> per genre → the composer generates a similar vector json → the engine
|
|
7
|
+
> renders real audio → the take plays, publishes and shows in the vitrine.
|
|
8
|
+
> every heavy stage runs SERVER SIDE (the gateway jobs, the saddle AVM
|
|
9
|
+
> modalities) — the visitor's browser, cpu and gpu never run the pipeline.
|
|
10
|
+
> companion of docs/planning.ladder.md (the kathesis verification ladder)
|
|
11
|
+
> and docs/planning.v2.28-completo.md.
|
|
12
|
+
>
|
|
13
|
+
> ## the code synthesis of outros.debonair (the reference verdict)
|
|
14
|
+
>
|
|
15
|
+
> the full code read of the reference folder closed the method question —
|
|
16
|
+
> the parts that PROVED real sound, the parts the composer mines:
|
|
17
|
+
> - `v2/src/lib/audio/engine.ts` — the voice recipes to reimplement root
|
|
18
|
+
> side: kick pitch env 165→52→40 hz with the noise click, snare noise
|
|
19
|
+
> bandpass 1750 + dual triangle body, hat hp 7800, clap 4 bursts 9.5ms
|
|
20
|
+
> apart, 808 bass pitch snap hz*2→hz in 45ms through a drive shaper,
|
|
21
|
+
> supersaw lead, pad lp with the 0.28hz lfo, keys tremolo, master chain
|
|
22
|
+
> tanh waveshaper k=3.2 → compressor −13/18/4.5 → analyser, feedback
|
|
23
|
+
> delay 0.27s fb 0.34, reverb noise impulse tail pow(1−i/len, 2.2), the
|
|
24
|
+
> 140ms look-ahead transport and the OfflineAudioContext render mirror →
|
|
25
|
+
> wav 16-bit / mp3 lamejs — the render proof the composer rides.
|
|
26
|
+
> - `v2/src/lib/audio/generator.ts` — the composition brain: 48 scales, 9
|
|
27
|
+
> style presets with exact drum lanes (trap kick [0,6,10] snare [4,12]
|
|
28
|
+
> hat 16th lanes clap [4,12] swing 0.12; drill, lofi, house...), bass
|
|
29
|
+
> locked to kick, markov 1st-order melodies with the ±3 semitone bias,
|
|
30
|
+
> DRIFT_CYCLE and ENERGY_CYCLE build/drop, seeded prng (same prompt same
|
|
31
|
+
> take), vary() diatonic drift.
|
|
32
|
+
> - `v2/src/app/api/analyze + similar + trainOnProject` — the pattern
|
|
33
|
+
> miner shape: note bigrams, 16-step drum histograms (adopt steps above
|
|
34
|
+
> 0.55 after trainedOn ≥ 3), embedding centroid, bpm histogram, cosine
|
|
35
|
+
> similarity over the 32-dim embedding — the exact miner PV3
|
|
36
|
+
> reimplements over our ingest documents.
|
|
37
|
+
> - `v1/v1 page.tsx` — the 6-layer biquad separation, the pcm store by
|
|
38
|
+
> session:second:track and the priority playback ladder (stored pcm →
|
|
39
|
+
> hex inline → seed synth → pattern synth) with the AudioBufferSource
|
|
40
|
+
> scheduling and the 1.0s per-second grid.
|
|
41
|
+
> - `mjs/analyser.mjs + ffmpeg.mjs` — the node decode lane (ffmpeg f32le
|
|
42
|
+
> → stft → per_second vectors) with the known bpm bugs (no octave fold
|
|
43
|
+
> guard) our analyze already handles.
|
|
44
|
+
> - the FAKE parts the new code never repeats: the ~200 decorative
|
|
45
|
+
> *Id/*Hash fields per second (constants and xor cosmetics), the
|
|
46
|
+
> expandFromSeed checksum claim (the checksum never validates — synthesis
|
|
47
|
+
> cannot reproduce pcm), the hex semantic signatures (0xKICK_SUB_HEAVY),
|
|
48
|
+
> the empty prisma/db/mini-services scaffolds, the yamnet removal.
|
|
49
|
+
> - the honest separation limit the reference states: elements sharing the
|
|
50
|
+
> spectral range AND the pan position are mathematically inseparable
|
|
51
|
+
> without a trained model — the hpss pair (harmonic/percussive) is the
|
|
52
|
+
> proven floor, the vocal/bass/drum lanes ride the band filters on top.
|
|
53
|
+
|
|
54
|
+
## wave PV0 — what already stands (verified, committed)
|
|
55
|
+
|
|
56
|
+
- [x] PV0.001 hpss real separation with centered framing, ifft conj law and
|
|
57
|
+
f64 ola — conservation 2.3e-7 over the whole signal (hpss.ts)
|
|
58
|
+
- [x] PV0.002 the ingest document debonair.ingest/1: mix + harmonic/percussive
|
|
59
|
+
stems, one vector frame per second (centroid, flatness, chroma, bands,
|
|
60
|
+
zcr, rms, beat, onsets) and the fnv-1a pcm hash (ingest.ts)
|
|
61
|
+
- [x] PV0.003 the ingest store: gateway https with the house host law and the
|
|
62
|
+
byte-capped offline mirror (ingeststore.ts); sqlite rows through
|
|
63
|
+
parameter binding only (ingestdb.ts)
|
|
64
|
+
- [x] PV0.004 the /ingest bench walking decode → separate → analyze →
|
|
65
|
+
segment → store with the phase meter (Sol/ingest/ingest.tsx)
|
|
66
|
+
- [x] PV0.005 the typed-URL law: devthink barra <app> lands inside the os on
|
|
67
|
+
all ten family apps (?arrive handoff, ?independent escape)
|
|
68
|
+
- [x] PV0.006 the 339-test battery green, the 402kb bundle building clean
|
|
69
|
+
|
|
70
|
+
## wave PV1 — the reference system (the ids, the pointers, the server law)
|
|
71
|
+
|
|
72
|
+
- [ ] PV1.001 schema upgrade of ingest_documents: owner_id (BIGINT pointer),
|
|
73
|
+
kind ("reference" | "render" | "upload"), visibility ("private" |
|
|
74
|
+
"vitrine"), blob pointers for mix and stems, size, sha256 — parameter
|
|
75
|
+
binding only, mirroring prisma/schema.prisma
|
|
76
|
+
- [ ] PV1.002 the id mint: bigintids.ts — time-ordered 64-bit ids (the big
|
|
77
|
+
pointers the house law names), zero storage, deterministic tests
|
|
78
|
+
- [ ] PV1.003 the gateway routes of ingest: POST /api/v1/ingest/upload
|
|
79
|
+
(multipart, streams to the job store, returns the job id), GET
|
|
80
|
+
/api/v1/ingest/jobs/:id (progress: decode, separate, analyze, segment,
|
|
81
|
+
store), GET /api/v1/ingest (the shelf rows), GET /api/v1/ingest/:id,
|
|
82
|
+
DELETE /api/v1/ingest/:id
|
|
83
|
+
- [ ] PV1.004 the server worker ingestworker.ts (node, the gateway lane):
|
|
84
|
+
decode (audio-decode/wav) → hpss separate → analyze → segmentsof →
|
|
85
|
+
ingestPcm → wavwriter per stem + mix → blob store — the browser never
|
|
86
|
+
runs any of it
|
|
87
|
+
- [ ] PV1.005 the blob store contract blobstore.ts: put(stream) → pointer
|
|
88
|
+
{id, bytes, sha256, kind}, get(id) → stream — the vault law (the
|
|
89
|
+
storage app owns the engine; the gateway serves it), git-lfs friendly
|
|
90
|
+
- [ ] PV1.006 the job ledger table ingest_jobs (id BIGINT, owner_id, state,
|
|
91
|
+
progress, error, document_id, created_at, finished_at)
|
|
92
|
+
- [ ] PV1.007 the bench rewired: file → POST upload → poll the job → the
|
|
93
|
+
phase meter reads the server progress → the document renders — zero
|
|
94
|
+
dsp on the visitor's machine; the local opt-in path stays behind an
|
|
95
|
+
explicit "run on this device" key
|
|
96
|
+
- [ ] PV1.008 every document row carries the owner id (the user ingest or
|
|
97
|
+
the system ia ingest — both stamp the same pointer chain)
|
|
98
|
+
- [ ] PV1.009 the system ingest lane: the ia (or an mcp automation, opt-in)
|
|
99
|
+
posts the same upload route with the service identity — the reference
|
|
100
|
+
library grows without a human
|
|
101
|
+
- [ ] PV1.010 tests: bigintids monotonicity, blobstore round trip, worker
|
|
102
|
+
end-to-end on a synthetic wav, the routes contract with a fake fetch
|
|
103
|
+
|
|
104
|
+
## wave PV2 — the playback proof (the ingested track plays)
|
|
105
|
+
|
|
106
|
+
- [ ] PV2.001 the mix and stem wavs ride the blob store; GET
|
|
107
|
+
/api/v1/blob/:id streams them with the right mime (audio/wav)
|
|
108
|
+
- [ ] PV2.002 the playerbus learns the reference take: the ingested mix
|
|
109
|
+
loads as a take the bar plays (the same bus the generated takes ride)
|
|
110
|
+
- [ ] PV2.003 the bench document view gains the play key: the stored mix
|
|
111
|
+
streams through the shared bar (the proof the owner demanded: ingest →
|
|
112
|
+
play, no re-render)
|
|
113
|
+
- [ ] PV2.004 stem audition: harmonic and percussive playable solo/muted
|
|
114
|
+
(the stems the worker stored), the conservation audible
|
|
115
|
+
- [ ] PV2.005 the library shelf rows carry the play key too (the ingested
|
|
116
|
+
references sit beside the generated takes, one grammar)
|
|
117
|
+
- [ ] PV2.006 tests: the wav round trip (store → stream → decode equals the
|
|
118
|
+
source samples), the bus contract for reference takes
|
|
119
|
+
|
|
120
|
+
## wave PV3 — the pattern miner (the genre tool the references feed)
|
|
121
|
+
|
|
122
|
+
- [ ] PV3.001 patternminer.ts: reads the ingested documents of a genre and
|
|
123
|
+
answers the genre profile — bpm distribution, key pool, chord ladder
|
|
124
|
+
frequency, onset density curves, band balance, loudness targets
|
|
125
|
+
- [ ] PV3.002 the genre profile document debonair.profile/1 (deterministic,
|
|
126
|
+
parametrized, versioned) — stored in genre_profiles with the reference
|
|
127
|
+
ids that fed it (the pointer chain audit-ready)
|
|
128
|
+
- [ ] PV3.003 similarity: the vector distance over frames (cosine on the
|
|
129
|
+
normalized segment vectors) — nearest references answer a prompt
|
|
130
|
+
- [ ] PV3.004 the miner runs server side (a gateway job like the ingest
|
|
131
|
+
worker); the browser asks, never computes
|
|
132
|
+
- [ ] PV3.005 the vitrine surfaces the profiles (the genre cards the home
|
|
133
|
+
shows come from the db, never hardcoded)
|
|
134
|
+
- [ ] PV3.006 tests: the miner on synthetic documents of two genres
|
|
135
|
+
separates, the profile determinism, the similarity ranking
|
|
136
|
+
|
|
137
|
+
## wave PV4 — the composer (prompt → similar json → real audio → play)
|
|
138
|
+
|
|
139
|
+
- [ ] PV4.001 the composer prompt → genre match (genrematrix) → the profile
|
|
140
|
+
of that genre → a seeded vector plan similar to the references
|
|
141
|
+
- [ ] PV4.002 the render: audioengine renders the plan offline (the existing
|
|
142
|
+
renderTakeOffline lane) — kick/snare/hats sample-accurate, bass, keys,
|
|
143
|
+
pads, the arrangement the plan carries
|
|
144
|
+
- [ ] PV4.003 the render job table render_jobs (exists) wired to the worker:
|
|
145
|
+
queued → rendering → done, the wav lands in the blob store, the track
|
|
146
|
+
row lands in tracks with the pointers
|
|
147
|
+
- [ ] PV4.004 the generated take plays in the same bar, publishes to the
|
|
148
|
+
vitrine (the spotify/netflix shelf the owner described) and downloads
|
|
149
|
+
- [ ] PV4.005 the vocal lane grows after the instrumental proof (the
|
|
150
|
+
SINTESE-VOCAL-AI line), opt-in, never blocking the wave
|
|
151
|
+
- [ ] PV4.006 tests: the same prompt+seed renders the same bytes, the render
|
|
152
|
+
job lifecycle, the vitrine rows
|
|
153
|
+
|
|
154
|
+
## wave PV5 — the design reform (debonair wears the devthink index face)
|
|
155
|
+
|
|
156
|
+
- [ ] PV5.001 screenshot the live os home and every debonair page (the
|
|
157
|
+
browser session) — the gap list against the index face
|
|
158
|
+
- [ ] PV5.002 the shell chrome: the window, the pin rail and the player bar
|
|
159
|
+
re-tuned to the os grammar (the trylle surfaces, the fluent shadows,
|
|
160
|
+
the campaign motion) — no drift tokens
|
|
161
|
+
- [ ] PV5.003 the home: the prompt-first hero over the noir stage retuned,
|
|
162
|
+
the song grid rows in the streaming grammar (cover, title, genre line,
|
|
163
|
+
duration, play on hover), the shimmer and the stagger
|
|
164
|
+
- [ ] PV5.004 the ingest bench in the same face (the dropzone, the meter,
|
|
165
|
+
the cards, the shelf)
|
|
166
|
+
- [ ] PV5.005 the generate/library/studio/settings pages one pass each —
|
|
167
|
+
short copy, no dots, the same nav law, every key answering
|
|
168
|
+
- [ ] PV5.006 the 404 and the intro re-faced
|
|
169
|
+
- [ ] PV5.007 the live verification: every route of the app inside the power
|
|
170
|
+
clip and standalone (?independent), screenshots compared
|
|
171
|
+
- [ ] PV5.008 the same face check on light theme
|
|
172
|
+
|
|
173
|
+
## wave PV6 — the family integration (the engines talk)
|
|
174
|
+
|
|
175
|
+
- [ ] PV6.001 the gateway app serves the debonair api lane (the routes of
|
|
176
|
+
PV1/PV3/PV4 ride the gateway the family shares)
|
|
177
|
+
- [ ] PV6.002 the vault stores the blobs (the blobstore pointer resolves
|
|
178
|
+
through the vault engine when configured, git-lfs backed)
|
|
179
|
+
- [ ] PV6.003 the saddle AVM modalities run the heavy jobs (the worker lane
|
|
180
|
+
declares the sandbox modality, opt-in per deploy) — the visitor's
|
|
181
|
+
machine stays clean even at scale
|
|
182
|
+
- [ ] PV6.004 the mcp automation lane (opt-in): an external agent ingests
|
|
183
|
+
and generates through the same gateway with its own identity
|
|
184
|
+
- [ ] PV6.005 the debonair package.json exports the engine library (the npm
|
|
185
|
+
face: analyze, hpss, ingest, patternminer, audioengine, wavwriter —
|
|
186
|
+
zero hardcode, everything configured through options/db)
|
|
187
|
+
|
|
188
|
+
## wave PV7 — the verification ladder (the proof the owner reads)
|
|
189
|
+
|
|
190
|
+
- [ ] PV7.001 the full battery green (vitest) on every wave's modules
|
|
191
|
+
- [ ] PV7.002 the typecheck + biome + build clean on every push
|
|
192
|
+
- [ ] PV7.003 the live proof: ingest a real track through the ui, watch the
|
|
193
|
+
server do the work, press play, hear the mix, hear the stems
|
|
194
|
+
- [ ] PV7.004 the live proof: generate from a genre with references, hear
|
|
195
|
+
the take, publish to the vitrine
|
|
196
|
+
- [ ] PV7.005 the live proof: devthink barra debonair → the window inside
|
|
197
|
+
the os, the bench inside it, no flash, one boot
|
|
198
|
+
- [ ] PV7.006 the worklog and the ladder ticked, the wave committed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenathlan/debonair",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.127",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/wenathlan/devthink"
|
|
@@ -75,6 +75,10 @@
|
|
|
75
75
|
"wouter": "^3.13.0",
|
|
76
76
|
"z-ai-web-dev-sdk": "^0.0.18"
|
|
77
77
|
},
|
|
78
|
+
"overrides": {
|
|
79
|
+
"esbuild": "^0.28.2",
|
|
80
|
+
"uuid": "^11.1.1"
|
|
81
|
+
},
|
|
78
82
|
"devDependencies": {
|
|
79
83
|
"@biomejs/biome": "^2.5.15",
|
|
80
84
|
"@capacitor/android": "^8.5.2",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// # bigintids.test — the pointer mint the storage law names: ids sort by
|
|
2
|
+
// creation (the high bits carry the clock), never collide within a tick
|
|
3
|
+
// (the tail increments), self-describe their mint time and ride the strong
|
|
4
|
+
// platform randomness. runnable with vitest: pnpm vitest run tests/bigintids.test.ts
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { describe, it } from "vitest";
|
|
7
|
+
import { idtick, mintid } from "../bigintids.ts";
|
|
8
|
+
|
|
9
|
+
describe("bigintids", () => {
|
|
10
|
+
it("mints ascending ids across ticks", () => {
|
|
11
|
+
const base = 1_700_000_000_000;
|
|
12
|
+
let previous = 0n;
|
|
13
|
+
for (let i = 0; i < 200; i++) {
|
|
14
|
+
const id = BigInt(mintid(base + i));
|
|
15
|
+
assert.ok(id > previous, `id ${id} must exceed ${previous}`);
|
|
16
|
+
previous = id;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("never repeats inside one millisecond (the tail walks)", () => {
|
|
21
|
+
const base = 1_700_000_000_000;
|
|
22
|
+
const seen = new Set<string>();
|
|
23
|
+
for (let i = 0; i < 5000; i++) seen.add(mintid(base));
|
|
24
|
+
assert.equal(seen.size, 5000);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("self-describes the mint tick", () => {
|
|
28
|
+
const at = 1_700_000_012_345;
|
|
29
|
+
const id = mintid(at);
|
|
30
|
+
assert.equal(idtick(id), at);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// # ingest.test — the decomposition pipeline contract: the hpss separation
|
|
2
|
+
// conserves the mix (harmonic + percussive == the original), the ingest
|
|
3
|
+
// document is deterministic for a given pcm and carries the vector
|
|
4
|
+
// segments the pattern miner reads (one frame per second, chroma, bands,
|
|
5
|
+
// onsets), the pcm hash is stable and the store guard refuses foreign
|
|
6
|
+
// schemas. runnable with vitest: pnpm vitest run tests/ingest.test.ts
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { describe, it } from "vitest";
|
|
9
|
+
import { separate } from "../hpss.ts";
|
|
10
|
+
import type { IngestDocument } from "../ingest.ts";
|
|
11
|
+
import { INGEST_SCHEMA_VERSION, ingestPcm, onsetSecondsOf, pcmhash, segmentsof } from "../ingest.ts";
|
|
12
|
+
|
|
13
|
+
/** A deterministic synthetic take: a 110 hz sawtooth bass (harmonic) over
|
|
14
|
+
* an 8 hz amplitude-gated click train (percussive), 3 seconds at 8 khz —
|
|
15
|
+
* small enough to analyze fast, structured enough to exercise both stems. */
|
|
16
|
+
function synthetictake(): { mono: Float32Array; sampleRate: number } {
|
|
17
|
+
const sampleRate = 8000;
|
|
18
|
+
const length = sampleRate * 3;
|
|
19
|
+
const mono = new Float32Array(length);
|
|
20
|
+
for (let i = 0; i < length; i++) {
|
|
21
|
+
const t = i / sampleRate;
|
|
22
|
+
const bass = 0.4 * Math.sign(Math.sin(2 * Math.PI * 110 * t));
|
|
23
|
+
const gate = t % 0.5 < 0.02 ? 0.8 : 0;
|
|
24
|
+
mono[i] = Math.max(-1, Math.min(1, bass * 0.7 + gate));
|
|
25
|
+
}
|
|
26
|
+
return { mono, sampleRate };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("hpss separation", () => {
|
|
30
|
+
it("conserves the mix: harmonic + percussive equals the original", () => {
|
|
31
|
+
const { mono, sampleRate } = synthetictake();
|
|
32
|
+
const { harmonic, percussive } = separate(mono);
|
|
33
|
+
assert.equal(harmonic.length, mono.length);
|
|
34
|
+
let worst = 0;
|
|
35
|
+
// the centered framing keeps every input sample near a window peak, so
|
|
36
|
+
// the whole signal conserves to the f32 storage floor
|
|
37
|
+
for (let i = 0; i < mono.length; i++) {
|
|
38
|
+
worst = Math.max(worst, Math.abs(harmonic[i] + percussive[i] - mono[i]));
|
|
39
|
+
}
|
|
40
|
+
assert.ok(worst < 1e-3, `reconstruction error ${worst}`);
|
|
41
|
+
void sampleRate;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("pushes the click train into the percussive stem and the sawtooth into the harmonic one", () => {
|
|
45
|
+
const { mono } = synthetictake();
|
|
46
|
+
const { harmonic, percussive } = separate(mono);
|
|
47
|
+
const energy = (pcm: Float32Array): number => {
|
|
48
|
+
let sum = 0;
|
|
49
|
+
for (let i = 0; i < pcm.length; i++) sum += pcm[i] * pcm[i];
|
|
50
|
+
return sum;
|
|
51
|
+
};
|
|
52
|
+
assert.ok(energy(harmonic) > energy(percussive), "the tonal sawtooth carries most of the energy");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("ingest document", () => {
|
|
57
|
+
const { mono, sampleRate } = synthetictake();
|
|
58
|
+
const doc = ingestPcm(mono, sampleRate, { name: "synthetic take", at: "2026-01-01T00:00:00.000Z" });
|
|
59
|
+
|
|
60
|
+
it("writes the one schema with the mix, two stems and per-second frames", () => {
|
|
61
|
+
assert.equal(doc.schema, INGEST_SCHEMA_VERSION);
|
|
62
|
+
assert.equal(doc.name, "synthetic take");
|
|
63
|
+
assert.equal(doc.stems.length, 2);
|
|
64
|
+
assert.equal(doc.stems[0].role, "harmonic");
|
|
65
|
+
assert.equal(doc.stems[1].role, "percussive");
|
|
66
|
+
assert.equal(doc.segments.length, 3);
|
|
67
|
+
for (const stem of doc.stems) assert.equal(stem.segments.length, 3);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("is deterministic: the same pcm ingests to the same bytes", () => {
|
|
71
|
+
const again = ingestPcm(mono, sampleRate, { name: "synthetic take", at: "2026-01-01T00:00:00.000Z" });
|
|
72
|
+
assert.equal(JSON.stringify(again), JSON.stringify(doc));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("carries the vector fields every frame needs", () => {
|
|
76
|
+
for (const frame of doc.segments) {
|
|
77
|
+
assert.equal(frame.s >= 0, true);
|
|
78
|
+
assert.equal(frame.bands.length, 3);
|
|
79
|
+
assert.equal(frame.chroma.length, 12);
|
|
80
|
+
const total = frame.bands.reduce((sum, value) => sum + value, 0);
|
|
81
|
+
assert.ok(Math.abs(total - 1) < 0.01, `bands sum ${total}`);
|
|
82
|
+
assert.ok(frame.rms >= 0 && frame.peak >= 0 && frame.zcr >= 0);
|
|
83
|
+
assert.ok(["", ...["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"]].includes(frame.note));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("hashes the pcm deterministically and differently per stem", () => {
|
|
88
|
+
assert.equal(pcmhash(mono), doc.pcmhash);
|
|
89
|
+
assert.notEqual(doc.stems[0].pcmhash, doc.stems[1].pcmhash);
|
|
90
|
+
const scratch = new Float32Array(mono);
|
|
91
|
+
scratch[10] += 0.5;
|
|
92
|
+
assert.notEqual(pcmhash(scratch), doc.pcmhash);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("detects the gated clicks as onsets of the percussive stem", () => {
|
|
96
|
+
const onsets = onsetSecondsOf(mono, sampleRate);
|
|
97
|
+
assert.ok(onsets.length >= 4, `onsets ${onsets.length}`);
|
|
98
|
+
for (let i = 1; i < onsets.length; i++) assert.ok(onsets[i] > onsets[i - 1], "ascending");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("segments count the onsets that fall inside each second", () => {
|
|
102
|
+
const onsets = onsetSecondsOf(mono, sampleRate);
|
|
103
|
+
const frames = segmentsof(mono, sampleRate, onsets);
|
|
104
|
+
let counted = 0;
|
|
105
|
+
for (const frame of frames) counted += frame.onsets;
|
|
106
|
+
assert.equal(counted, onsets.length);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("the store guard", () => {
|
|
111
|
+
it("accepts only schema-matching documents (the browser guard shape)", () => {
|
|
112
|
+
const { mono, sampleRate } = synthetictake();
|
|
113
|
+
const doc = ingestPcm(mono, sampleRate, { name: "guard take", at: "2026-01-01T00:00:00.000Z" });
|
|
114
|
+
const broken = { schema: "other/1" } as unknown as IngestDocument;
|
|
115
|
+
// the guard lives in the store module: the shape check mirrors it here
|
|
116
|
+
const guard = (candidate: IngestDocument): boolean =>
|
|
117
|
+
!!candidate &&
|
|
118
|
+
candidate.schema === INGEST_SCHEMA_VERSION &&
|
|
119
|
+
typeof candidate.name === "string" &&
|
|
120
|
+
candidate.name.length > 0 &&
|
|
121
|
+
Array.isArray(candidate.stems) &&
|
|
122
|
+
candidate.stems.length > 0 &&
|
|
123
|
+
Array.isArray(candidate.segments);
|
|
124
|
+
assert.equal(guard(broken), false);
|
|
125
|
+
assert.equal(guard(doc), true);
|
|
126
|
+
});
|
|
127
|
+
});
|
package/tests/tuning.test.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// octave doubles. The recipes ride this one function; the tests pin it.
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { describe, it } from "vitest";
|
|
6
|
-
import { A4_MIDI, CONCERT_PITCH, midiToHz } from "../
|
|
6
|
+
import { A4_MIDI, CONCERT_PITCH, midiToHz } from "../tuning.ts";
|
|
7
7
|
|
|
8
8
|
describe("tuning — midiToHz", () => {
|
|
9
9
|
it("anchors A4 at the concert pitch", () => {
|