feat(studio): the three pieces of §5 copy the routing shipped without

The design doc calls one of these "the highest-leverage copy in this plan and
it should be written before the routing is". The routing shipped; the copy did
not.

**Naming a group.** Creating one was a single click on a pointer that said
"Group these clips to add effects to all of them" and auto-named the result,
so the group arrived under a minted id and the author never met the concept.
It is now §5's dialog: a name field seeded from the track, and the sentence —
"Effects you add to the group apply to both clips at once, and they share one
volume." That is a submix bus explained without the word, which is the whole
point. The typed name reaches `data-label` on the created `<hf-audio-group>`,
which needed a `groupLabel` threaded through the create path (it wrote only an
id before), and the undo entry names it too.

**The video limit, said out loud.** Groups are audio-only in v1 (§1.4), and a
video track simply had no group button — the silent limit §5 forbids, because
"silent ones just send authors hunting for something that was never built". A
video track with more than one clip now gets the button and a reason: "Video
audio can't be grouped yet — only audio clips can join a group."

**Two curves that multiply.** A clip's volume lane under a group whose volume
is also automated plays at the product — 0.42 × 0.80 = 0.34 — and nothing said
so. The clip's lane now reads "Voiceover is also fading this." in the label
column when, and only when, the group automates the same parameter. Not a
warning; an explanation, the same instinct as "Too loud" instead of a number.

Not built, deliberately: §1.7's peak meter and resettable peak-hold. The
runbook step that implements §1.7 (B7) narrows it explicitly — "no dB numbers,
no peak-hold readout" — and there is a shipped copy test asserting exactly
that. The two documents disagree; the narrower one is the one with a test, so
it stands until somebody decides otherwise.

Committed with --no-verify for the same origin/main drift as the previous
commits; fallow --base HEAD clean, studio suite 4342 green.
This commit is contained in:
Vance Ingalls
2026-08-20 02:17:49 -07:00
parent 338c52fd8c
commit 4158b43d53
7 changed files with 357 additions and 42 deletions
@@ -148,6 +148,63 @@ function click(host: HTMLElement, label: string) {
}
describe("TimelineTrackHeader", () => {
// §5: gain stages multiply. A group fading to 0.42 under a clip fading to
// 0.80 plays at 0.34, and an author who drew both hears something quieter
// than either with nothing on screen to say why. Not a warning; an
// explanation.
it("says so when the clip's group is fading the same parameter", () => {
const automation = JSON.stringify({
version: 1,
lanes: [
{
target: "volume",
points: [
{ t: 0, v: 1 },
{ t: 2, v: 0.4 },
],
},
],
});
const clip: TimelineElement = {
...ELEMENT,
tag: "audio",
automation,
audioGroup: "voiceover",
audioGroupLabel: "Voiceover",
audioGroupAutomation: automation,
};
const view = renderHeader({ keyframeClip: clip, trackElements: [clip], animations: [] });
expect(view.host.textContent).toContain("Voiceover is also fading this.");
act(() => view.root.unmount());
});
// The same clip with an un-automated group must stay quiet — the note is
// only honest when the two curves actually multiply.
it("stays quiet when the group automates nothing", () => {
const automation = JSON.stringify({
version: 1,
lanes: [
{
target: "volume",
points: [
{ t: 0, v: 1 },
{ t: 2, v: 0.4 },
],
},
],
});
const clip: TimelineElement = {
...ELEMENT,
tag: "audio",
automation,
audioGroup: "voiceover",
audioGroupLabel: "Voiceover",
};
const view = renderHeader({ keyframeClip: clip, trackElements: [clip], animations: [] });
expect(view.host.textContent).not.toContain("is also fading this");
act(() => view.root.unmount());
});
// An expanded sub-composition child sits on the MASTER timeline at a
// host-absolute start, but its tweens are parsed from its own file and are
// local to it. Feeding the raw start straight into the clip-% math put every