@wenathlan/debonair 2.0.93 → 2.0.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/docs/.gitkeep +0 -0
- package/docs/competitor-dissection.md +572 -0
- package/docs/enginemap.md +94 -0
- package/docs/planning.ladder.md +1107 -0
- package/docs/planning.v2.28-completo.md +69 -0
- package/docs/planning.versions.2.0.97-3.0.md +48 -0
- package/package.json +25 -1
- package/tests/.gitkeep +0 -0
- package/tests/analyze.test.ts +54 -0
- package/tests/arrangement.test.ts +109 -0
- package/tests/arranger.test.ts +116 -0
- package/tests/beatgrid.test.ts +0 -0
- package/tests/dawview.test.ts +104 -0
- package/tests/ducking.test.ts +0 -0
- package/tests/family.test.ts +80 -0
- package/tests/familyurl.test.ts +32 -0
- package/tests/fingerprint.test.ts +61 -0
- package/tests/genrematrix.test.ts +116 -0
- package/tests/harmonyengine.test.ts +204 -0
- package/tests/katexis.test.ts +97 -0
- package/tests/loudness.test.ts +0 -0
- package/tests/midiwriter.test.ts +183 -0
- package/tests/mixergraph.test.ts +0 -0
- package/tests/motionpref.test.ts +82 -0
- package/tests/musictheory.test.ts +231 -0
- package/tests/onboardingstate.test.ts +68 -0
- package/tests/pianoroll.test.ts +336 -0
- package/tests/remixplan.test.ts +0 -0
- package/tests/rhythmgrammar.test.ts +205 -0
- package/tests/solcss.test.ts +148 -0
- package/tests/spectrum.test.ts +59 -0
- package/tests/speechalign.test.ts +0 -0
- package/tests/stems.test.ts +127 -0
- package/tests/takeevents.test.ts +129 -0
- package/tests/takestore.test.ts +93 -0
- package/tests/timelinemodel.test.ts +0 -0
- package/tests/tuning.test.ts +27 -0
- package/tests/vocalsynth.test.ts +91 -0
- package/tests/wavwriter.test.ts +137 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// # vocalsynth.test — the formant voice: the render is deterministic under
|
|
2
|
+
// the same seed, the syllables stay finite and clamped, the vowel table
|
|
3
|
+
// answers the documented formants and the line splitter lands syllables
|
|
4
|
+
// inside the span.
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { describe, it } from "vitest";
|
|
7
|
+
import { renderVoice, syllablesOfLine, VOWEL_FORMANTS } from "../vocalsynth.ts";
|
|
8
|
+
|
|
9
|
+
/** The deterministic rng the tests ride (mulberry32, the engine's law). */
|
|
10
|
+
function seeded(seed: number): () => number {
|
|
11
|
+
let a = seed >>> 0;
|
|
12
|
+
return () => {
|
|
13
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
14
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
15
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
16
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("vocalsynth — the vowel table", () => {
|
|
21
|
+
it("carries the documented a-vowel formants", () => {
|
|
22
|
+
assert.deepEqual([...VOWEL_FORMANTS.a], [730, 1090, 2440]);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("vocalsynth — renderVoice", () => {
|
|
27
|
+
const syllables = [
|
|
28
|
+
{ vowel: "a" as const, midi: 64, start: 0, duration: 0.4, velocity: 0.9, consonant: true },
|
|
29
|
+
{ vowel: "o" as const, midi: 67, start: 0.45, duration: 0.4, velocity: 0.8 },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
it("is deterministic for the same seed", () => {
|
|
33
|
+
// the random stream is consumed per render — fresh seed, same take
|
|
34
|
+
const first = renderVoice(syllables, { sampleRate: 44100, random: seeded(2026) });
|
|
35
|
+
const again = renderVoice(syllables, { sampleRate: 44100, random: seeded(2026) });
|
|
36
|
+
assert.equal(first.samples.length, again.samples.length);
|
|
37
|
+
let identical = true;
|
|
38
|
+
for (let i = 0; i < first.samples.length; i++) {
|
|
39
|
+
if (first.samples[i] !== again.samples[i]) {
|
|
40
|
+
identical = false;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
assert.ok(identical, "the same seed rendered a different take");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders a finite, clamped, non-silent take", () => {
|
|
48
|
+
const voice = renderVoice(syllables, { sampleRate: 44100, random: seeded(2026) });
|
|
49
|
+
assert.ok(voice.samples.length >= Math.ceil(0.85 * 44100));
|
|
50
|
+
let sum = 0;
|
|
51
|
+
for (const v of voice.samples) {
|
|
52
|
+
assert.ok(Number.isFinite(v));
|
|
53
|
+
assert.ok(Math.abs(v) <= 1);
|
|
54
|
+
sum += Math.abs(v);
|
|
55
|
+
}
|
|
56
|
+
assert.ok(sum > 0, "the voice rendered silence");
|
|
57
|
+
assert.ok(voice.peak > 0.1, "the voice rides the normalized level law");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("changes the timbre with the vowel", () => {
|
|
61
|
+
const energy = (vowel: "a" | "i") => {
|
|
62
|
+
const voice = renderVoice([{ vowel, midi: 64, start: 0, duration: 0.3, velocity: 1 }], {
|
|
63
|
+
sampleRate: 44100,
|
|
64
|
+
random: seeded(9),
|
|
65
|
+
});
|
|
66
|
+
let sum = 0;
|
|
67
|
+
for (const v of voice.samples) sum += v * v;
|
|
68
|
+
return Math.round(sum * 1e4);
|
|
69
|
+
};
|
|
70
|
+
assert.notEqual(energy("a"), energy("i"));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("vocalsynth — syllablesOfLine", () => {
|
|
75
|
+
const options = { sampleRate: 44100, random: seeded(7) };
|
|
76
|
+
|
|
77
|
+
it("lands every syllable inside the span", () => {
|
|
78
|
+
const syllables = syllablesOfLine("neon rain over the city", 60, 2, 3.2, options);
|
|
79
|
+
assert.ok(syllables.length >= 1 && syllables.length <= 16);
|
|
80
|
+
for (const s of syllables) {
|
|
81
|
+
assert.ok(s.start >= 2 && s.start + s.duration <= 5.3);
|
|
82
|
+
assert.ok(s.velocity > 0 && s.velocity <= 1);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("answers a fallback vowel for glyph-less text", () => {
|
|
87
|
+
const syllables = syllablesOfLine("123 456", 60, 0, 1, options);
|
|
88
|
+
assert.equal(syllables.length, 1);
|
|
89
|
+
assert.ok(["a", "e", "i", "o", "u"].includes(syllables[0].vowel));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// # wavwriter.test — the wav encoder contract: the magic bytes and the chunk
|
|
2
|
+
// sizes the format pins, the round trip wavInfo proves from the bytes alone,
|
|
3
|
+
// the clamp (and the anti-NaN law) that keeps the bus inside full scale, the
|
|
4
|
+
// float32 marker, the mono/stereo interleave, the 24-bit byte order and the
|
|
5
|
+
// hard failures the export seam must see with their machine codes.
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { describe, it } from "vitest";
|
|
8
|
+
import { EXPORT_SIZE_LIMIT_BYTES, encodeWav, validateEncoding, WAV_FORMAT, WavError, wavInfo } from "../wavwriter.ts";
|
|
9
|
+
|
|
10
|
+
/** Reads a little-endian u16 out of the bytes. */
|
|
11
|
+
const u16le = (bytes: Uint8Array, offset: number): number => bytes[offset] | (bytes[offset + 1] << 8);
|
|
12
|
+
|
|
13
|
+
/** Reads a little-endian u32 out of the bytes. */
|
|
14
|
+
const u32le = (bytes: Uint8Array, offset: number): number =>
|
|
15
|
+
(bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24)) >>> 0;
|
|
16
|
+
|
|
17
|
+
describe("wavwriter — encodeWav", () => {
|
|
18
|
+
const mono = new Float32Array([0, 0.25, -0.5, 1]);
|
|
19
|
+
const bytes = encodeWav([mono], { sampleRate: 48000, bitDepth: 16 });
|
|
20
|
+
|
|
21
|
+
it("opens with the riff/wave magic", () => {
|
|
22
|
+
assert.equal(String.fromCharCode(...bytes.subarray(0, 4)), "RIFF");
|
|
23
|
+
assert.equal(String.fromCharCode(...bytes.subarray(8, 12)), "WAVE");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("lays the chunks at the documented offsets with exact sizes", () => {
|
|
27
|
+
assert.equal(String.fromCharCode(...bytes.subarray(12, 16)), "fmt ");
|
|
28
|
+
assert.equal(u32le(bytes, 16), 16); // the pcm fmt content is exactly 16 bytes
|
|
29
|
+
assert.equal(String.fromCharCode(...bytes.subarray(36, 40)), "data");
|
|
30
|
+
assert.equal(u32le(bytes, 40), 8); // 4 frames x 1 channel x 2 bytes
|
|
31
|
+
assert.equal(u32le(bytes, 4), bytes.length - 8); // the riff size is everything after the field
|
|
32
|
+
assert.equal(bytes.length, 44 + 8); // the classic 44 byte header
|
|
33
|
+
assert.equal(u32le(bytes, 28), 48000 * 1 * 2); // the byte rate agrees
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("round trips through wavInfo with the exact duration", () => {
|
|
37
|
+
const info = wavInfo(bytes);
|
|
38
|
+
assert.equal(info.sampleRate, 48000);
|
|
39
|
+
assert.equal(info.channels, 1);
|
|
40
|
+
assert.equal(info.bitDepth, 16);
|
|
41
|
+
assert.equal(info.frames, 4);
|
|
42
|
+
assert.equal(info.durationSeconds, 4 / 48000);
|
|
43
|
+
assert.equal(info.isFloat, false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("clamps the bus into full scale and never writes a NaN word", () => {
|
|
47
|
+
const loud = encodeWav([new Float32Array([1.5, -1.5, Number.NaN])], { sampleRate: 8000, bitDepth: 16 });
|
|
48
|
+
const view = new DataView(loud.buffer, loud.byteOffset + 44, 6);
|
|
49
|
+
assert.equal(view.getInt16(0, true), 32767);
|
|
50
|
+
assert.equal(view.getInt16(2, true), -32767);
|
|
51
|
+
assert.equal(view.getInt16(4, true), 0);
|
|
52
|
+
assert.equal(wavInfo(loud).sampleRate, 8000);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("marks the float32 format with code 3 and the 18 byte fmt chunk", () => {
|
|
56
|
+
const floats = encodeWav([mono], { sampleRate: 48000, bitDepth: 32 });
|
|
57
|
+
assert.equal(u32le(floats, 16), 18);
|
|
58
|
+
assert.equal(u16le(floats, 20), 3);
|
|
59
|
+
assert.equal(u16le(floats, 36), 0); // cbSize = 0 closes the float fmt chunk
|
|
60
|
+
const info = wavInfo(floats);
|
|
61
|
+
assert.equal(info.isFloat, true);
|
|
62
|
+
assert.equal(info.bitDepth, 32);
|
|
63
|
+
assert.equal(String.fromCharCode(...floats.subarray(38, 42)), "data");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("interleaves stereo frame by frame", () => {
|
|
67
|
+
const left = new Float32Array([1, 0.5]);
|
|
68
|
+
const right = new Float32Array([-1, 0]);
|
|
69
|
+
const stereo = encodeWav([left, right], { sampleRate: 48000, bitDepth: 16 });
|
|
70
|
+
const view = new DataView(stereo.buffer, stereo.byteOffset + 44, 8);
|
|
71
|
+
assert.equal(wavInfo(stereo).channels, 2);
|
|
72
|
+
assert.equal(view.getInt16(0, true), 32767); // left first
|
|
73
|
+
assert.equal(view.getInt16(2, true), -32767); // right second
|
|
74
|
+
assert.equal(view.getInt16(4, true), 16384);
|
|
75
|
+
assert.equal(view.getInt16(6, true), 0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("writes pcm24 across three little-endian bytes", () => {
|
|
79
|
+
const full = encodeWav([new Float32Array([1])], { sampleRate: 48000, bitDepth: 24 });
|
|
80
|
+
assert.equal(full[44], 0xff);
|
|
81
|
+
assert.equal(full[45], 0xff);
|
|
82
|
+
assert.equal(full[46], 0x7f);
|
|
83
|
+
assert.equal(wavInfo(full).bitDepth, 24);
|
|
84
|
+
assert.equal(u32le(full, 40), 3);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("is deterministic for the same bus", () => {
|
|
88
|
+
assert.deepEqual(encodeWav([mono], { sampleRate: 48000, bitDepth: 16 }), bytes);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("wavwriter — failures", () => {
|
|
93
|
+
it("refuses rates outside 8000..192000", () => {
|
|
94
|
+
assert.throws(
|
|
95
|
+
() => encodeWav([new Float32Array(4)], { sampleRate: 7999, bitDepth: 16 }),
|
|
96
|
+
(error: unknown) => error instanceof WavError && error.code === "bad-rate",
|
|
97
|
+
);
|
|
98
|
+
assert.throws(
|
|
99
|
+
() => encodeWav([new Float32Array(4)], { sampleRate: 192001, bitDepth: 16 }),
|
|
100
|
+
(error: unknown) => error instanceof WavError && error.code === "bad-rate",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("refuses broken magic and a truncated data chunk in wavInfo", () => {
|
|
105
|
+
assert.throws(
|
|
106
|
+
() => wavInfo(new Uint8Array(64)),
|
|
107
|
+
(error: unknown) => error instanceof WavError && error.code === "bad-magic",
|
|
108
|
+
);
|
|
109
|
+
const bytes = encodeWav([new Float32Array(4)], { sampleRate: 48000, bitDepth: 16 });
|
|
110
|
+
assert.throws(
|
|
111
|
+
() => wavInfo(bytes.subarray(0, 44)), // the data header survives, the payload does not
|
|
112
|
+
(error: unknown) => error instanceof WavError && error.code === "bad-truncation",
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("answers the validateEncoding invariants with machine codes", () => {
|
|
117
|
+
assert.deepEqual(validateEncoding([new Float32Array(4)], { sampleRate: 48000, bitDepth: 16 }), []);
|
|
118
|
+
assert.equal(validateEncoding([new Float32Array(4)], { sampleRate: 1000, bitDepth: 16 })[0].code, "bad-rate");
|
|
119
|
+
assert.equal(validateEncoding([], { sampleRate: 48000, bitDepth: 16 })[0].code, "bad-channels");
|
|
120
|
+
assert.equal(
|
|
121
|
+
validateEncoding([new Float32Array(4), new Float32Array(5)], { sampleRate: 48000, bitDepth: 16 })[0].code,
|
|
122
|
+
"bad-shape",
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("flags a bounce past the 50 MB export cap before any bytes exist", () => {
|
|
127
|
+
const huge = [
|
|
128
|
+
new Float32Array(3400000),
|
|
129
|
+
new Float32Array(3400000),
|
|
130
|
+
new Float32Array(3400000),
|
|
131
|
+
new Float32Array(3400000),
|
|
132
|
+
];
|
|
133
|
+
assert.equal(validateEncoding(huge, { sampleRate: 48000, bitDepth: 32 })[0].code, "oversize");
|
|
134
|
+
assert.equal(EXPORT_SIZE_LIMIT_BYTES, 52428800);
|
|
135
|
+
assert.equal(WAV_FORMAT.pcm24.bytesPerSample, 3);
|
|
136
|
+
});
|
|
137
|
+
});
|