Skip to content

Commit 86d8d5d

Browse files
committed
fix(core): stop the rack telling a music bed it will thin the voice out
Applying Cut Rumble to a bass line said "Thins the voice out" under the knob. The rack sits on whatever the author selected — a bed, a sound effect, a room tone — and every effect, job and profile is offered on all of them, so copy that names the material describes something the author does not have. This is the rule the copy layer's own header already states — describe a control by what changes in THE SOUND — applied to the words rather than to the mechanism. The offending strings were written one at a time over months and each reads fine beside a voiceover; the assumption only shows when the module lands somewhere else. Fifteen strings, audited across the whole surface rather than fixing the one that was reported: - `highpass` ends (x2): "Thins the voice out" → "Thins it out" - `compressor`: "Some words are much louder" → "Some parts…" - `gate`: blurb, reach-for, both ends and the release ends — pauses and gaps rather than words and sentences - `gate` profile "Tightness": "Cuts quiet words too" → "…parts too" - Jobs: "Too much chest" → "Too much low-end body"; "Words are hard to make out" → "It is hard to make out — it sits back" - Presets: room-gate and boom-tame, which are in the Fix family and so are still offered on a bed - BANDS: Weight, Middle, Presence and Edge — these get taught once and reused by every spectral module, so a voice-only word here spreads The three `voice-*` presets keep their language: they are voice by definition, and the shelf now hides them on a track that classifies as music or as an effect. Encoded the audit as tests over EFFECT_COPY, BANDS, PRESET_PROBLEM, the jobs and the profiles, since the next entry added would otherwise reintroduce it. All three fail against the old strings — checked by reverting them. The voice-preset exception is asserted too, so the rule cannot pass vacuously on a catalogue that stopped saying "voice" at all. Deliberately left: `highpass`'s "traffic, footsteps, air conditioning, hands on the mic" names what rumble IS in the world, matching the BANDS entry — it teaches the range rather than claiming anything about the track.
1 parent 1cf14db commit 86d8d5d

6 files changed

Lines changed: 128 additions & 16 deletions

File tree

packages/core/src/audioFxCopy.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,74 @@ describe("audioBandAt", () => {
103103
expect(audioBandAt(Number.NaN)).toBeUndefined();
104104
});
105105
});
106+
107+
/**
108+
* Copy that assumes the track is a voice.
109+
*
110+
* The rack sits on whatever the author selected — a music bed, a sound effect,
111+
* a room tone. Every effect, every named job and every profile is offered on
112+
* all of them, so a control that reads "Thins the voice out" on a synth pad is
113+
* describing something the author cannot hear and does not have. It is the same
114+
* rule this file's header already states — describe a control by what changes in
115+
* THE SOUND — applied to the words rather than to the mechanism.
116+
*
117+
* The voice presets are the deliberate exception: they are voice by definition,
118+
* and the shelf hides them on a track that classifies as music or as an effect,
119+
* so "My voice sounds amateur" is only ever read next to a voice.
120+
*
121+
* This is a lint on the words, not a judgement about mixing. It exists because
122+
* the offending strings were written one at a time over months and read fine in
123+
* isolation — nobody notices the assumption until they apply Cut Rumble to a
124+
* bass line and the panel tells them it will thin their voice out.
125+
*/
126+
describe("no copy assumes the track is a voice", () => {
127+
/** Words that only mean something if the material is speech. */
128+
const SPEECH =
129+
/\b(voice|vocal|voices|speech|spoken|word|words|sentence|sentences|syllable|syllables|narration|narrator|talking|chest)\b/i;
130+
131+
/** Presets whose whole purpose is a voice, so their copy may say so. */
132+
const VOICE_PRESETS = new Set(
133+
HF_AUDIO_FX_PRESETS.filter((p) => p.family === "voice").map((p) => p.id),
134+
);
135+
136+
const offenders = (entries: [string, string][]): string[] =>
137+
entries.filter(([, text]) => SPEECH.test(text)).map(([where, text]) => `${where}: "${text}"`);
138+
139+
it("not in an effect's name, blurb, reach-for line or any knob", () => {
140+
const entries: [string, string][] = [];
141+
for (const [id, copy] of Object.entries(EFFECT_COPY)) {
142+
entries.push([`${id}.title`, copy.title], [`${id}.does`, copy.does]);
143+
entries.push([`${id}.reachFor`, copy.reachFor]);
144+
entries.push([`${id}.primaryEnds.low`, copy.primaryEnds.low]);
145+
entries.push([`${id}.primaryEnds.high`, copy.primaryEnds.high]);
146+
for (const [key, param] of Object.entries(copy.params)) {
147+
entries.push([`${id}.${key}.label`, param.label]);
148+
if (param.hint) entries.push([`${id}.${key}.hint`, param.hint]);
149+
if (param.ends) {
150+
entries.push([`${id}.${key}.ends.low`, param.ends.low]);
151+
entries.push([`${id}.${key}.ends.high`, param.ends.high]);
152+
}
153+
}
154+
}
155+
expect(offenders(entries)).toEqual([]);
156+
});
157+
158+
it("not in the band vocabulary, which every spectral module shares", () => {
159+
// These names get taught once and then reused everywhere, so a voice-only
160+
// word here spreads to every filter in the rack.
161+
expect(offenders(BANDS.map((b) => [b.name, b.says]))).toEqual([]);
162+
});
163+
164+
it("not in the complaint a non-voice preset answers", () => {
165+
const entries = Object.entries(PRESET_PROBLEM).filter(([id]) => !VOICE_PRESETS.has(id));
166+
expect(offenders(entries as [string, string][])).toEqual([]);
167+
});
168+
169+
it("still lets the voice presets say what they are for", () => {
170+
// The exception has to be real, or the rule above is untested — a catalogue
171+
// where nothing said "voice" would pass every assertion here vacuously.
172+
const voiced = Object.entries(PRESET_PROBLEM).filter(([id]) => VOICE_PRESETS.has(id));
173+
expect(voiced.length).toBeGreaterThan(0);
174+
expect(voiced.some(([, text]) => SPEECH.test(text))).toBe(true);
175+
});
176+
});

packages/core/src/audioFxCopy.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ export const EFFECT_COPY: Record<string, EffectCopy> = {
6666
does: "Cuts the very bottom — traffic, footsteps, air conditioning, hands on the mic.",
6767
reachFor: "There's a low hum or thump under everything.",
6868
primary: "frequency",
69-
primaryEnds: { low: "Only the deepest", high: "Thins the voice out" },
69+
primaryEnds: { low: "Only the deepest", high: "Thins it out" },
7070
band: [20, 300],
7171
params: {
7272
frequency: {
7373
label: "Cut below",
7474
hint: "Everything under this is removed.",
75-
ends: { low: "Only the deepest", high: "Thins the voice out" },
75+
ends: { low: "Only the deepest", high: "Thins it out" },
7676
},
7777
q: { label: "Sharpness", hint: "How abruptly the cut starts." },
7878
poles: { label: "Steepness", hint: "How fast it falls away below the point." },
@@ -134,7 +134,7 @@ export const EFFECT_COPY: Record<string, EffectCopy> = {
134134
compressor: {
135135
title: "Even Out Loudness",
136136
does: "Brings the quiet parts up and holds the loud parts down, so nothing jumps out at the listener.",
137-
reachFor: "Some words are much louder than others.",
137+
reachFor: "Some parts are much louder than others.",
138138
primary: "strength",
139139
primaryEnds: { low: "Barely touched", high: "Very even, quite squashed" },
140140
params: {
@@ -168,10 +168,10 @@ export const EFFECT_COPY: Record<string, EffectCopy> = {
168168
},
169169
gate: {
170170
title: "Silence the Gaps",
171-
does: "Mutes the pauses between words. Room tone under speech stays — this closes the silences, it does not remove noise.",
172-
reachFor: "You can hear the room breathing between sentences.",
171+
does: "Mutes the pauses. Whatever sits underneath stays — this closes the gaps, it does not remove noise.",
172+
reachFor: "You can hear the room in the gaps.",
173173
primary: "strength",
174-
primaryEnds: { low: "Only true silence", high: "Cuts quiet words too" },
174+
primaryEnds: { low: "Only true silence", high: "Cuts quiet parts too" },
175175
params: {
176176
threshold: { label: "Quieter than this is a gap" },
177177
range: {
@@ -182,7 +182,7 @@ export const EFFECT_COPY: Record<string, EffectCopy> = {
182182
attack: { label: "How fast it opens" },
183183
release: {
184184
label: "How fast it closes",
185-
ends: { low: "Clips word endings", high: "Leaves tails intact" },
185+
ends: { low: "Clips tails short", high: "Leaves tails intact" },
186186
},
187187
knee: { label: "How gradual" },
188188
},
@@ -286,11 +286,11 @@ export const EFFECT_COPY: Record<string, EffectCopy> = {
286286
*/
287287
export const BANDS: { from: number; to: number; name: string; says: string }[] = [
288288
{ from: 20, to: 80, name: "Rumble", says: "traffic, footsteps, handling" },
289-
{ from: 80, to: 250, name: "Weight", says: "chest, body, warmth" },
289+
{ from: 80, to: 250, name: "Weight", says: "body, warmth, low end" },
290290
{ from: 250, to: 600, name: "Mud", says: "boxy, muffled, cardboard" },
291-
{ from: 600, to: 2000, name: "Middle", says: "the body of a voice" },
292-
{ from: 2000, to: 5000, name: "Presence", says: "consonants, intelligibility" },
293-
{ from: 5000, to: 10000, name: "Edge", says: "sibilance, harshness" },
291+
{ from: 600, to: 2000, name: "Middle", says: "the body of the sound" },
292+
{ from: 2000, to: 5000, name: "Presence", says: "definition, consonants" },
293+
{ from: 5000, to: 10000, name: "Edge", says: "harshness, sibilance" },
294294
{ from: 10000, to: 20000, name: "Air", says: "sparkle, openness" },
295295
];
296296

@@ -318,8 +318,8 @@ export const PRESET_PROBLEM: Record<string, string> = {
318318
"voice-broadcast": "I want it to sound like radio",
319319
"voice-warm": "I want it intimate and close",
320320
"rumble-cut": "There's a hum or thump underneath",
321-
"room-gate": "I can hear the room between sentences",
322-
"boom-tame": "My voice sounds boomy",
321+
"room-gate": "I can hear the room in the gaps",
322+
"boom-tame": "It sounds boomy",
323323
"harsh-tame": "It's harsh and tiring to listen to",
324324
telephone: "Make it sound like a phone call",
325325
"radio-am": "Make it sound like an old radio",

packages/core/src/audioFxJobs.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,20 @@ describe("named jobs", () => {
6161
for (const id of ids) expect(getAudioFxJob(id)?.id).toBe(id);
6262
});
6363
});
64+
65+
/**
66+
* A job's `does` is the complaint that leads to it, shown in the add menu on
67+
* whatever track is selected — so it is under the same rule as the effect copy
68+
* in `audioFxCopy.test.ts`: it may not assume the material is speech. Reduce Mud
69+
* is as right on a boxy guitar as on a boxy voice, and the menu should say so.
70+
*/
71+
describe("no job assumes the track is a voice", () => {
72+
const SPEECH =
73+
/\b(voice|vocal|speech|spoken|word|words|sentence|syllable|narration|talking|chest)\b/i;
74+
it("names the symptom without naming the source", () => {
75+
const bad = HF_AUDIO_FX_JOBS.filter((j) => SPEECH.test(`${j.label} ${j.does}`)).map(
76+
(j) => `${j.id}: "${j.does}"`,
77+
);
78+
expect(bad).toEqual([]);
79+
});
80+
});

packages/core/src/audioFxJobs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const HF_AUDIO_FX_JOBS: readonly HfAudioFxJob[] = [
5151
{
5252
id: "tame-boominess",
5353
label: "Tame Boominess",
54-
does: "Too much chest — it booms.",
54+
does: "Too much low-end body — it booms.",
5555
type: "peaking",
5656
params: { frequency: 200, gain: -4, q: 1.4 },
5757
},
@@ -72,7 +72,7 @@ export const HF_AUDIO_FX_JOBS: readonly HfAudioFxJob[] = [
7272
{
7373
id: "add-clarity",
7474
label: "Add Clarity",
75-
does: "Words are hard to make out.",
75+
does: "It is hard to make out — it sits back.",
7676
type: "peaking",
7777
params: { frequency: 3000, gain: 2.5, q: 1 },
7878
},

packages/core/src/audioFxProfiles.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,27 @@ describe("derived one-knob profiles", () => {
138138
expect(audioFxProfileStrength("not-an-effect", {})).toBe(0.5);
139139
});
140140
});
141+
142+
/**
143+
* A profile's knob name and its two ends are read on every track that carries
144+
* the effect, so they fall under the same rule as the rest of the copy: say what
145+
* changes in the sound, not what it does to a voice. See the matching audit in
146+
* `audioFxCopy.test.ts`.
147+
*/
148+
describe("no profile assumes the track is a voice", () => {
149+
const SPEECH =
150+
/\b(voice|vocal|speech|spoken|word|words|sentence|syllable|narration|talking|chest)\b/i;
151+
it("labels the knob and both ends without assuming speech", () => {
152+
const bad: string[] = [];
153+
for (const [type, p] of Object.entries(HF_AUDIO_FX_PROFILES)) {
154+
for (const [where, text] of [
155+
["label", p.label],
156+
["ends.low", p.ends.low],
157+
["ends.high", p.ends.high],
158+
] as const) {
159+
if (SPEECH.test(text)) bad.push(`${type}.${where}: "${text}"`);
160+
}
161+
}
162+
expect(bad).toEqual([]);
163+
});
164+
});

packages/core/src/audioFxProfiles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export const HF_AUDIO_FX_PROFILES: Record<string, HfAudioFxProfile> = {
8181

8282
gate: {
8383
label: "Tightness",
84-
ends: { low: "Only true silence", high: "Cuts quiet words too" },
84+
ends: { low: "Only true silence", high: "Cuts quiet parts too" },
8585
derives: ["threshold", "range", "release"],
8686
at(strength) {
8787
const s = clamp01(strength);

0 commit comments

Comments
 (0)