Skip to content

Commit aa53c6d

Browse files
committed
feat(core): land the plain-language layer, and test that it covers the rack
`copy.mts` lived in `plans/` and was read by one build script. Moving it to `packages/core/src/audioFxCopy.ts` puts it beside the registry it describes, and turns the coverage into `audioFxCopy.test.ts` — every shipped effect, every one of its parameters, and every preset has to have copy. That check existed before as a step in `build-preview.mts`, which means it only fired when somebody remembered to rebuild the review page. Now it fires on the commit that adds an effect without a plain name for it, which is the only moment it can still be cheap to fix. Four more assertions the build step never made, each of which was a real hole: copy for an effect the registry no longer ships (dead text that reads as coverage), a `SUMMARY` missing for an effect that has one everywhere else, a summary that renders `undefined` or `NaN` at the effect's own defaults — which is the first thing an author reads after adding one — and a gap or overlap in the shared frequency ruler, which would be a band the rack can name in one module and not in another. `PROFILES` deliberately did NOT come along. Its figures are proposed, not measured, nothing derives from them yet, and they want the same before/after listen the clip-before-duck fix got before a knob is wired to them. So it stays in `plans/audio-fx-ux/copy.mts`, which is now all that file holds, and `build-preview.mts` imports the shipped four from core and that one from beside itself. Landing the data is not wiring it: nothing in the studio reads this yet, and it should not until the three open UX questions are settled — whether the plain name replaces the DSP name or sits beside it decides what the rack renders. The README says so where the status used to say the layer had not landed. Falsified: deleting one parameter's entry fails the highpass case. core 1745 passing (111 files), studio unchanged at 3674 / 18 todo.
1 parent a0e3a75 commit aa53c6d

5 files changed

Lines changed: 488 additions & 16 deletions

File tree

packages/core/package-subpaths.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@
9292
"types": "./dist/audioFx.d.ts",
9393
"environments": ["browser", "bun", "node"]
9494
},
95+
"./audio-fx-copy": {
96+
"source": "./src/audioFxCopy.ts",
97+
"runtime": "./dist/audioFxCopy.js",
98+
"types": "./dist/audioFxCopy.d.ts",
99+
"environments": ["browser", "bun", "node"]
100+
},
95101
"./audio-fx-eq": {
96102
"source": "./src/audioFxEq.ts",
97103
"runtime": "./dist/audioFxEq.js",

packages/core/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@
106106
"import": "./src/audioFx.ts",
107107
"types": "./src/audioFx.ts"
108108
},
109+
"./audio-fx-copy": {
110+
"bun": "./src/audioFxCopy.ts",
111+
"node": "./dist/audioFxCopy.js",
112+
"import": "./src/audioFxCopy.ts",
113+
"types": "./src/audioFxCopy.ts"
114+
},
109115
"./audio-fx-eq": {
110116
"bun": "./src/audioFxEq.ts",
111117
"node": "./dist/audioFxEq.js",
@@ -410,6 +416,10 @@
410416
"import": "./dist/audioFx.js",
411417
"types": "./dist/audioFx.d.ts"
412418
},
419+
"./audio-fx-copy": {
420+
"import": "./dist/audioFxCopy.js",
421+
"types": "./dist/audioFxCopy.d.ts"
422+
},
413423
"./audio-fx-eq": {
414424
"import": "./dist/audioFxEq.js",
415425
"types": "./dist/audioFxEq.d.ts"
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, expect, it } from "vitest";
2+
import { defaultAudioFxParams, HF_AUDIO_FX } from "./audioFx.js";
3+
import { HF_AUDIO_FX_PRESETS } from "./audioFxPresets.js";
4+
import { BANDS, EFFECT_COPY, PRESET_PROBLEM, SUMMARY } from "./audioFxCopy.js";
5+
6+
/**
7+
* The copy layer is only worth having if it covers everything that ships. A gap
8+
* is not a missing nicety — it is a rack panel labelled `highpass` in front of
9+
* somebody who came here to stop a hum, which is the exact failure this layer
10+
* exists to prevent.
11+
*
12+
* This was a build step in `plans/audio-fx-ux/build-preview.mts`, which meant it
13+
* only caught a gap when somebody remembered to rebuild the review page. Here it
14+
* catches it on the commit that adds the effect.
15+
*/
16+
describe("every shipped effect has plain-language copy", () => {
17+
for (const def of HF_AUDIO_FX) {
18+
it(`${def.id}`, () => {
19+
const copy = EFFECT_COPY[def.id];
20+
expect(copy, `${def.id} has no copy`).toBeDefined();
21+
if (!copy) return;
22+
for (const param of def.params) {
23+
expect(copy.params[param.key], `${def.id}.${param.key} has no plain name`).toBeDefined();
24+
}
25+
// "strength" is the one legal fiction: it means the module gets a single
26+
// derived knob and its real parameters live behind Details. Anything else
27+
// has to name a parameter the effect actually has, or the panel would put
28+
// its headline control on a knob that does not exist.
29+
if (copy.primary !== "strength") {
30+
expect(
31+
def.params.map((p) => p.key),
32+
`${def.id}'s primary "${copy.primary}" is not one of its parameters`,
33+
).toContain(copy.primary);
34+
}
35+
expect(SUMMARY[def.id], `${def.id} has no closed-state summary`).toBeDefined();
36+
});
37+
}
38+
});
39+
40+
it("every preset says which everyday problem it answers", () => {
41+
const missing = HF_AUDIO_FX_PRESETS.filter((p) => !PRESET_PROBLEM[p.id]).map((p) => p.id);
42+
expect(missing).toEqual([]);
43+
});
44+
45+
it("describes no effect the registry does not ship", () => {
46+
const shipped = new Set(HF_AUDIO_FX.map((d) => d.id));
47+
// The other direction. Copy for an effect that has been removed or renamed is
48+
// dead text that reads as covered, and the count in the review page would say
49+
// so too.
50+
expect(Object.keys(EFFECT_COPY).filter((id) => !shipped.has(id))).toEqual([]);
51+
expect(Object.keys(SUMMARY).filter((id) => !shipped.has(id))).toEqual([]);
52+
});
53+
54+
it("summarises every effect at its own defaults without throwing", () => {
55+
for (const def of HF_AUDIO_FX) {
56+
const summary = SUMMARY[def.id];
57+
if (!summary) continue;
58+
// The first thing an author reads after adding an effect, so it has to be a
59+
// sentence at the values it arrives with — not "undefined dB".
60+
const text = summary(defaultAudioFxParams(def.id));
61+
expect(text, `${def.id} summarised as "${text}"`).toMatch(/^[^u].*[^ ]$/);
62+
expect(text).not.toContain("undefined");
63+
expect(text).not.toContain("NaN");
64+
}
65+
});
66+
67+
it("covers the spectrum without a gap or an overlap", () => {
68+
// The ruler is shared by every spectral module, so a hole in it is a frequency
69+
// the rack can name in one place and not in another.
70+
expect(BANDS[0]?.from).toBe(20);
71+
expect(BANDS.at(-1)?.to).toBe(20000);
72+
for (let i = 1; i < BANDS.length; i++) {
73+
expect(BANDS[i]?.from, `gap or overlap before ${BANDS[i]?.name}`).toBe(BANDS[i - 1]?.to);
74+
}
75+
});

0 commit comments

Comments
 (0)