fix(studio): make the volume fader tell the truth about the gain it writes (#3305)

* fix(studio): make the volume fader tell the truth about the gain it writes

The fader travels in dB, so its stops are irrational values; serializing them
through the generic two-decimal numeric formatter collapsed the bottom quarter
of its travel onto "0" — a hard mute — and made the knob jump on release
everywhere below unity. Both panels now use the exact serializer, which
round-trips every integer stop back to itself.

Raise the volume automation lane to the same ceiling the fader reaches.
Clamping the lane at unity meant automating a boosted clip silently discarded
the boost, and the panel disables the fader while a lane owns the level, so
there was no way back. This rescales the lane's vertical axis: unity now sits
a quarter of the way up rather than at the top.

Add audio_volume_tween_overrides_gain. Tween values on `volume` are absolute —
they replace the authored gain rather than scaling it — so a clip carrying both
plays at whatever the tween names, and the fader gives no sign of it. The rule
reuses the tween detector the sibling lane/tween rule already has.

* fix(lint): treat a missing data-volume as unity, not as silence

readAttr returns null when the attribute is absent, and Number(null) is 0 —
finite, and not 1 — so a clip carrying NO data-volume cleared both filters and
was reported as authored at silence. Both halves of that were false: absent
means unity everywhere else in the runtime.

It fired on exactly the case the rule exists to bless. The docs this PR edits
say data-volume is the baseline for elements no tween touches, so a tweened
clip is expected not to carry one — the common audio fade. A warning does not
fail check, but an agent reading the fixHint would have written a gain to
correct a level that was never wrong.
This commit is contained in:
Miguel Ángel
2026-08-19 18:08:23 -04:00
committed by GitHub
parent b3c43e2480
commit 228eabd43f
15 changed files with 243 additions and 108 deletions
+5 -2
View File
@@ -18,6 +18,7 @@ import {
type HfAutomationLane,
} from "./audioAutomation.js";
import { mintAudioFxNodeId, parseAudioFxChain, type HfAudioFxChain } from "./audioFx.js";
import { MAX_AUDIO_GAIN } from "./audioGain.js";
const chain: HfAudioFxChain = {
version: 1,
@@ -116,7 +117,7 @@ describe("normalisation", () => {
]);
});
it("clamps volume into 0..1 at parse time", () => {
it("clamps volume into the authoring gain range at parse time", () => {
const parsed = parseAutomation(
JSON.stringify({
version: 1,
@@ -131,7 +132,9 @@ describe("normalisation", () => {
],
}),
);
expect(parsed.lanes[0]!.points.map((p) => p.v)).toEqual([1, 0]);
// The lane shares the fader's ceiling. Clamping it at unity discarded the
// boost of any clip authored above 0 dB the moment it was automated.
expect(parsed.lanes[0]!.points.map((p) => p.v)).toEqual([MAX_AUDIO_GAIN, 0]);
});
it("refuses malformed input instead of silently losing an envelope", () => {