@wenathlan/debonair 2.0.89 → 2.0.94
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenathlan/debonair",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.94",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/wenathlan/devthink"
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"build": "vite build",
|
|
38
38
|
"start": "vite preview --host",
|
|
39
39
|
"preview": "vite preview --host",
|
|
40
|
-
"check": "tsc --noEmit"
|
|
40
|
+
"check": "tsc --noEmit",
|
|
41
|
+
"test": "vitest run"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@hono/node-server": "^2.1.3",
|
package/tests/beatgrid.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// # beatgrid.test — honest unit tests for the beat grid (tempo detection, beat
|
|
2
|
-
// tracking, quantize/swing, tempo map integration), runnable with the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// tracking, quantize/swing, tempo map integration), runnable with the
|
|
3
|
+
// vitest runner:
|
|
4
|
+
// pnpm test
|
|
5
5
|
// The fixtures synthesise a 120 BPM click track and piecewise tempo curves.
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import { describe, it } from "
|
|
7
|
+
import { describe, it } from "vitest";
|
|
8
8
|
import {
|
|
9
9
|
beatCount,
|
|
10
10
|
beatsElapsed,
|
package/tests/ducking.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// # ducking.test — honest unit tests for the sidechain ducking logic (level
|
|
2
2
|
// envelope, activity regions, per-sample follower gain, keyframes), runnable
|
|
3
|
-
// with the
|
|
4
|
-
//
|
|
3
|
+
// with the vitest runner:
|
|
4
|
+
// pnpm test
|
|
5
5
|
// The fixtures synthesise a 1 kHz voice-like trigger with two speech bursts.
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import { describe, it } from "
|
|
7
|
+
import { describe, it } from "vitest";
|
|
8
8
|
import {
|
|
9
9
|
activityRegions,
|
|
10
10
|
autoDuckRegions,
|
package/tests/loudness.test.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// # loudness.test — honest unit tests for the loudness meter (K-weighting,
|
|
2
|
-
// gating, LRA, peaks, normalisation gain), runnable with the
|
|
3
|
-
// runner
|
|
4
|
-
//
|
|
2
|
+
// gating, LRA, peaks, normalisation gain), runnable with the vitest
|
|
3
|
+
// runner:
|
|
4
|
+
// pnpm test
|
|
5
5
|
// The fixtures synthesise a 1 kHz sine at known amplitudes; every expected
|
|
6
6
|
// number is derived from the meter itself, never from a lookup table.
|
|
7
7
|
import assert from "node:assert/strict";
|
|
8
|
-
import { describe, it } from "
|
|
8
|
+
import { describe, it } from "vitest";
|
|
9
9
|
import {
|
|
10
10
|
analyzeLoudness,
|
|
11
11
|
applyGain,
|
package/tests/mixergraph.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// # mixergraph.test — honest unit tests for the mixing graph model, runnable
|
|
2
|
-
// with the
|
|
3
|
-
//
|
|
2
|
+
// with the vitest runner:
|
|
3
|
+
// pnpm test
|
|
4
4
|
// The desk fixtures reuse the real mixer strips the site seed serves.
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { describe, it } from "
|
|
6
|
+
import { describe, it } from "vitest";
|
|
7
7
|
import {
|
|
8
8
|
assertAcyclic,
|
|
9
9
|
assertGraph,
|
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
effectiveGains,
|
|
13
13
|
graphFromStrips,
|
|
14
14
|
linearToDb,
|
|
15
|
+
type MixerChannel,
|
|
15
16
|
MixerGraphError,
|
|
16
17
|
patchChannel,
|
|
17
18
|
resolvePaths,
|
|
18
|
-
type MixerChannel,
|
|
19
19
|
} from "../mixergraph.ts";
|
|
20
20
|
import { seedMixerStrips } from "../seed.ts";
|
|
21
21
|
|
|
@@ -29,34 +29,46 @@ describe("mixer graph construction", () => {
|
|
|
29
29
|
|
|
30
30
|
it("refuses a desk without a master row", () => {
|
|
31
31
|
const orphans = seedMixerStrips.map((strip) => ({ ...strip, master: false }));
|
|
32
|
-
assert.throws(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
assert.throws(
|
|
33
|
+
() => graphFromStrips(orphans),
|
|
34
|
+
(error: unknown) => {
|
|
35
|
+
assert.ok(error instanceof MixerGraphError);
|
|
36
|
+
assert.equal(error.code, "missing-master");
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
);
|
|
37
40
|
assert.throws(() => graphFromStrips([]), MixerGraphError);
|
|
38
41
|
});
|
|
39
42
|
|
|
40
43
|
it("validates duplicate ids, unknown sends and non finite gains", () => {
|
|
41
44
|
const graph = graphFromStrips(seedMixerStrips);
|
|
42
45
|
const duplicated: MixerChannel = { id: "Drums", name: "Drums", gaindb: 0, mute: false, solo: false, sends: [] };
|
|
43
|
-
assert.throws(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
assert.throws(
|
|
47
|
+
() => assertGraph({ ...graph, channels: [...graph.channels, duplicated] }),
|
|
48
|
+
(error: unknown) => {
|
|
49
|
+
assert.ok(error instanceof MixerGraphError);
|
|
50
|
+
assert.equal(error.code, "duplicate-channel");
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
);
|
|
48
54
|
const withbadsend = patchChannel(graph, "Drums", { sends: [{ targetid: "ghost", gaindb: -3 }] });
|
|
49
|
-
assert.throws(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
assert.throws(
|
|
56
|
+
() => assertGraph(withbadsend),
|
|
57
|
+
(error: unknown) => {
|
|
58
|
+
assert.ok(error instanceof MixerGraphError);
|
|
59
|
+
assert.equal(error.code, "unknown-send");
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
);
|
|
54
63
|
const withnangain = patchChannel(graph, "Drums", { gaindb: Number.NaN });
|
|
55
|
-
assert.throws(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
assert.throws(
|
|
65
|
+
() => assertGraph(withnangain),
|
|
66
|
+
(error: unknown) => {
|
|
67
|
+
assert.ok(error instanceof MixerGraphError);
|
|
68
|
+
assert.equal(error.code, "bad-gain");
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
);
|
|
60
72
|
});
|
|
61
73
|
});
|
|
62
74
|
|
|
@@ -64,17 +76,18 @@ describe("mixer graph cycles", () => {
|
|
|
64
76
|
const graph = graphFromStrips(seedMixerStrips);
|
|
65
77
|
|
|
66
78
|
it("rejects a send loop between two strips", () => {
|
|
67
|
-
const looped = patchChannel(
|
|
68
|
-
|
|
69
|
-
"Bass",
|
|
70
|
-
{ sends: [{ targetid: "Drums", gaindb: -3 }] },
|
|
71
|
-
);
|
|
72
|
-
assert.throws(() => assertAcyclic(looped), (error: unknown) => {
|
|
73
|
-
assert.ok(error instanceof MixerGraphError);
|
|
74
|
-
assert.equal(error.code, "cycle");
|
|
75
|
-
assert.equal(error.channelid, "Drums");
|
|
76
|
-
return true;
|
|
79
|
+
const looped = patchChannel(patchChannel(graph, "Drums", { sends: [{ targetid: "Bass", gaindb: -3 }] }), "Bass", {
|
|
80
|
+
sends: [{ targetid: "Drums", gaindb: -3 }],
|
|
77
81
|
});
|
|
82
|
+
assert.throws(
|
|
83
|
+
() => assertAcyclic(looped),
|
|
84
|
+
(error: unknown) => {
|
|
85
|
+
assert.ok(error instanceof MixerGraphError);
|
|
86
|
+
assert.equal(error.code, "cycle");
|
|
87
|
+
assert.equal(error.channelid, "Drums");
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
90
|
+
);
|
|
78
91
|
});
|
|
79
92
|
|
|
80
93
|
it("rejects a strip that sends to itself", () => {
|
|
@@ -94,7 +107,10 @@ describe("mixer graph gains", () => {
|
|
|
94
107
|
assert.ok(Math.abs(dbToLinear(-6) - 0.501) < 0.001);
|
|
95
108
|
assert.equal(dbToLinear(Number.NEGATIVE_INFINITY), 0);
|
|
96
109
|
assert.equal(linearToDb(1), 0);
|
|
97
|
-
assert.ok(
|
|
110
|
+
assert.ok(
|
|
111
|
+
Math.abs(linearToDb(dbToLinear(-6)) - -6) < 1e-9,
|
|
112
|
+
`expected the -6 dB roundtrip to hold, got ${linearToDb(dbToLinear(-6))}`,
|
|
113
|
+
);
|
|
98
114
|
assert.equal(linearToDb(0), Number.NEGATIVE_INFINITY);
|
|
99
115
|
});
|
|
100
116
|
|
|
@@ -139,7 +155,9 @@ describe("mixer graph routing paths", () => {
|
|
|
139
155
|
});
|
|
140
156
|
|
|
141
157
|
it("resolves the send path with the send and master gains chained", () => {
|
|
142
|
-
const graph = patchChannel(graphFromStrips(seedMixerStrips), "Drums", {
|
|
158
|
+
const graph = patchChannel(graphFromStrips(seedMixerStrips), "Drums", {
|
|
159
|
+
sends: [{ targetid: "Master", gaindb: -6 }],
|
|
160
|
+
});
|
|
143
161
|
const paths = resolvePaths(graph, "Drums");
|
|
144
162
|
assert.equal(paths.length, 2);
|
|
145
163
|
assert.ok(Math.abs(paths[0].gaindb - -6) < 1e-9); // direct: -4 + -2
|
|
@@ -156,10 +174,13 @@ describe("mixer graph routing paths", () => {
|
|
|
156
174
|
|
|
157
175
|
it("refuses an unknown channel", () => {
|
|
158
176
|
const graph = graphFromStrips(seedMixerStrips);
|
|
159
|
-
assert.throws(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
assert.throws(
|
|
178
|
+
() => resolvePaths(graph, "ghost"),
|
|
179
|
+
(error: unknown) => {
|
|
180
|
+
assert.ok(error instanceof MixerGraphError);
|
|
181
|
+
assert.equal(error.code, "unknown-channel");
|
|
182
|
+
return true;
|
|
183
|
+
},
|
|
184
|
+
);
|
|
164
185
|
});
|
|
165
186
|
});
|
package/tests/remixplan.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// # remixplan.test — honest unit tests for the remix planner (beat similarity,
|
|
2
|
-
// joint scoring, piece plans, crossfade gains), runnable with the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// joint scoring, piece plans, crossfade gains), runnable with the
|
|
3
|
+
// vitest runner:
|
|
4
|
+
// pnpm test
|
|
5
5
|
// The fixtures synthesise a 32-beat loop whose features repeat every 3 beats.
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import { describe, it } from "
|
|
7
|
+
import { describe, it } from "vitest";
|
|
8
8
|
import {
|
|
9
9
|
beatSimilarity,
|
|
10
10
|
bestTargetBeat,
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// # speechalign.test — honest unit tests for the speech alignment logic (RMS
|
|
2
2
|
// framing, silence threshold, phrase segmentation, grid fitting with stretch
|
|
3
|
-
// limits), runnable with the
|
|
4
|
-
//
|
|
5
|
-
// node --test tests/speechalign.test.ts
|
|
3
|
+
// limits), runnable with the vitest runner:
|
|
4
|
+
// pnpm test
|
|
6
5
|
// The fixture synthesises a 3 s "voice": two sine phrases (0.2–0.8 s and
|
|
7
6
|
// 1.2–1.7 s) with 50 ms fades over digital silence, so every phrase boundary
|
|
8
7
|
// the logic reports is checkable by hand.
|
|
9
8
|
import assert from "node:assert/strict";
|
|
10
|
-
import { describe, it } from "
|
|
9
|
+
import { describe, it } from "vitest";
|
|
11
10
|
import {
|
|
12
11
|
alignSpeech,
|
|
13
12
|
beatGridTimes,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// # timelinemodel.test — honest unit tests for the arrangement timeline
|
|
2
|
-
// model (snap, overlap, split, trim, move), runnable with the
|
|
3
|
-
//
|
|
2
|
+
// model (snap, overlap, split, trim, move), runnable with the vitest runner:
|
|
3
|
+
// pnpm test
|
|
4
4
|
// The clip fixtures mirror the shapes the studio timeline serves.
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { describe, it } from "
|
|
6
|
+
import { describe, it } from "vitest";
|
|
7
7
|
import {
|
|
8
8
|
activeAt,
|
|
9
9
|
clipsOverlap,
|
|
@@ -12,11 +12,11 @@ import {
|
|
|
12
12
|
snapClip,
|
|
13
13
|
snapToGrid,
|
|
14
14
|
splitClip,
|
|
15
|
+
type TimelineClip,
|
|
15
16
|
TimelineModelError,
|
|
16
17
|
totalDuration,
|
|
17
18
|
trimClip,
|
|
18
19
|
validatePlacement,
|
|
19
|
-
type TimelineClip,
|
|
20
20
|
} from "../timelinemodel.ts";
|
|
21
21
|
|
|
22
22
|
const GRID = { stepseconds: 0.25 };
|
|
@@ -46,11 +46,14 @@ describe("timeline grid snap", () => {
|
|
|
46
46
|
|
|
47
47
|
it("refuses a bad time or a bad grid", () => {
|
|
48
48
|
assert.throws(() => snapToGrid(-1, GRID), TimelineModelError);
|
|
49
|
-
assert.throws(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
assert.throws(
|
|
50
|
+
() => snapToGrid(1, { stepseconds: 0 }),
|
|
51
|
+
(error: unknown) => {
|
|
52
|
+
assert.ok(error instanceof TimelineModelError);
|
|
53
|
+
assert.equal(error.code, "bad-grid");
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
);
|
|
54
57
|
});
|
|
55
58
|
});
|
|
56
59
|
|
|
@@ -67,12 +70,15 @@ describe("timeline overlap", () => {
|
|
|
67
70
|
it("validates the placement and reports the offending pair", () => {
|
|
68
71
|
const arrangement = [clip("a", "Drums", 0, 2), clip("b", "Bass", 0, 4)];
|
|
69
72
|
assert.doesNotThrow(() => validatePlacement(clip("c", "Bass", 5, 1), arrangement));
|
|
70
|
-
assert.throws(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
assert.throws(
|
|
74
|
+
() => validatePlacement(clip("c", "Drums", 1, 2), arrangement),
|
|
75
|
+
(error: unknown) => {
|
|
76
|
+
assert.ok(error instanceof TimelineModelError);
|
|
77
|
+
assert.equal(error.code, "overlap");
|
|
78
|
+
assert.equal(error.clipid, "c");
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
);
|
|
76
82
|
});
|
|
77
83
|
|
|
78
84
|
it("skips the clip itself and honors the allowoverlap escape hatch", () => {
|
|
@@ -96,12 +102,15 @@ describe("timeline split", () => {
|
|
|
96
102
|
|
|
97
103
|
it("refuses a split on the edges or outside the span", () => {
|
|
98
104
|
const target = clip("c1", "Bass", 2, 1.5);
|
|
99
|
-
assert.throws(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
assert.throws(
|
|
106
|
+
() => splitClip(target, 2),
|
|
107
|
+
(error: unknown) => {
|
|
108
|
+
assert.ok(error instanceof TimelineModelError);
|
|
109
|
+
assert.equal(error.code, "split-outside");
|
|
110
|
+
assert.equal(error.clipid, "c1");
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
);
|
|
105
114
|
assert.throws(() => splitClip(target, 3.5), TimelineModelError);
|
|
106
115
|
assert.throws(() => splitClip(target, 9), TimelineModelError);
|
|
107
116
|
});
|
|
@@ -122,11 +131,14 @@ describe("timeline trim", () => {
|
|
|
122
131
|
|
|
123
132
|
it("refuses a trim that leaves the span or breaks the minimum duration", () => {
|
|
124
133
|
const target = clip("c1", "Drums", 2, 1.5);
|
|
125
|
-
assert.throws(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
assert.throws(
|
|
135
|
+
() => trimClip(target, "start", 1.9),
|
|
136
|
+
(error: unknown) => {
|
|
137
|
+
assert.ok(error instanceof TimelineModelError);
|
|
138
|
+
assert.equal(error.code, "trim-invalid");
|
|
139
|
+
return true;
|
|
140
|
+
},
|
|
141
|
+
);
|
|
130
142
|
assert.throws(() => trimClip(target, "start", 3.5 - MINIMUMDURATION + 0.01), TimelineModelError);
|
|
131
143
|
assert.throws(() => trimClip(target, "end", 2 + MINIMUMDURATION - 0.01), TimelineModelError);
|
|
132
144
|
assert.throws(() => trimClip(target, "end", 9), TimelineModelError);
|
|
@@ -147,11 +159,14 @@ describe("timeline move", () => {
|
|
|
147
159
|
});
|
|
148
160
|
|
|
149
161
|
it("rejects the landing on an occupied spot", () => {
|
|
150
|
-
assert.throws(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
assert.throws(
|
|
163
|
+
() => moveClip(clip("d", "Drums", 2, 1), 1.5, arrangement),
|
|
164
|
+
(error: unknown) => {
|
|
165
|
+
assert.ok(error instanceof TimelineModelError);
|
|
166
|
+
assert.equal(error.code, "overlap");
|
|
167
|
+
return true;
|
|
168
|
+
},
|
|
169
|
+
);
|
|
155
170
|
});
|
|
156
171
|
|
|
157
172
|
it("snaps the landing when a grid is passed and moves across tracks", () => {
|
|
@@ -166,8 +181,14 @@ describe("timeline queries", () => {
|
|
|
166
181
|
const arrangement = [clip("a", "Drums", 0, 1), clip("b", "Bass", 0.5, 2), clip("c", "Lead", 5, 1)];
|
|
167
182
|
|
|
168
183
|
it("answers the clips playing at one instant", () => {
|
|
169
|
-
assert.deepEqual(
|
|
170
|
-
|
|
184
|
+
assert.deepEqual(
|
|
185
|
+
activeAt(arrangement, 0.75).map((row) => row.id),
|
|
186
|
+
["a", "b"],
|
|
187
|
+
);
|
|
188
|
+
assert.deepEqual(
|
|
189
|
+
activeAt(arrangement, 1).map((row) => row.id),
|
|
190
|
+
["b"],
|
|
191
|
+
);
|
|
171
192
|
assert.deepEqual(activeAt(arrangement, 10), []);
|
|
172
193
|
});
|
|
173
194
|
|