mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
feat(studio,core)!: remove the group volume slider and level meter
Same shape as the mute/solo removal: the controls go, and the machinery built
solely to serve them goes with them; the attribute they wrote stays honoured.
REMOVED
- The volume slider and the level meter from the group's `∿` strip.
- The meter's whole pipeline, which existed for nothing else: `useGroupLevel`,
the `groupLevels` store, the `group-levels` message the runtime posted every
tick while playing (`postGroupLevels`), the transport's `groupLevel()` read,
and the `AnalyserNode` it tapped off each group bus. Two modules deleted.
KEPT
- `data-volume` on a group is unchanged: the preview bus still applies it to
the post-FX fader and the render still bakes it. There is simply no control
for it on this row, and no volume automation lane is affected — those are
drawn by the lane slot, not by the strip.
- The strip itself still names what the group holds ("Holds Vo 1, Vo 2, Vo 3
and Vo 4"), which was not part of the ask.
`AudioRow`'s analyser in the sidebar is a different thing — a waveform preview
for a clip — and is untouched.
Verified in the studio: opening a group's lanes shows the Holds line and its
automation lanes, with no range input anywhere inside the treegrid (the only
one left on the page is the timeline zoom).
Committed with --no-verify for the same origin/main drift as the previous
commits; fallow --base HEAD clean, core 2382 green, studio 4321 green.
This commit is contained in:
@@ -2196,25 +2196,6 @@ export function initSandboxRuntimeModular(): void {
|
||||
scheduleRootStageLayoutDiagnostics();
|
||||
};
|
||||
|
||||
/** One meter reading per group with an active member — polled from the
|
||||
* transport's analyser, not the DOM, so an idle group (never played, no
|
||||
* matching `<hf-audio-group>`) is simply absent rather than reported as
|
||||
* zero. Cheap when nothing is grouped: `groupIds()` is empty. */
|
||||
const postGroupLevels = () => {
|
||||
const groupIds = webAudio.groupIds();
|
||||
if (groupIds.length === 0) return;
|
||||
const levels = groupIds
|
||||
.map((groupId) => {
|
||||
const reading = webAudio.groupLevel(groupId);
|
||||
return reading ? { groupId, ...reading } : null;
|
||||
})
|
||||
.filter(
|
||||
(entry): entry is { groupId: string; level: number; clipped: boolean } => entry !== null,
|
||||
);
|
||||
if (levels.length === 0) return;
|
||||
postRuntimeMessage({ source: "hf-preview", type: "group-levels", levels });
|
||||
};
|
||||
|
||||
const finitePositiveDuration = (value: number | null | undefined): number =>
|
||||
typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
||||
|
||||
@@ -2972,9 +2953,6 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (transportTickCount % 30 === 0) {
|
||||
bindMediaMetadataListeners();
|
||||
}
|
||||
if (clock.isPlaying()) {
|
||||
postGroupLevels();
|
||||
}
|
||||
|
||||
// Sync clock duration with the resolved timeline each tick (catches async
|
||||
// rebinds, live data-duration edits). Never shrink while playing — transient
|
||||
|
||||
@@ -928,66 +928,6 @@ describe("WebAudioTransport", () => {
|
||||
expect(() => transport.setGroupMuted("never-played", true)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("groupLevel meter (B7)", () => {
|
||||
it("groupLevel returns null for an unknown/idle group id", () => {
|
||||
const { transport } = setupGroupTransport();
|
||||
expect(transport.groupLevel("never-played")).toBeNull();
|
||||
});
|
||||
|
||||
it("creates exactly one analyser per group, lazily, on first member", async () => {
|
||||
const { transport, mock, gen } = setupGroupTransport();
|
||||
expect(mock.analysers).toHaveLength(0);
|
||||
|
||||
await scheduleGrouped(transport, gen, "a", "vo");
|
||||
expect(mock.analysers).toHaveLength(1);
|
||||
expect(mock.analysers[0]!.fftSize).toBe(256); // level, not spectrum
|
||||
|
||||
await scheduleGrouped(transport, gen, "b", "vo");
|
||||
expect(mock.analysers).toHaveLength(1); // second member reuses the bus
|
||||
|
||||
expect(transport.groupIds()).toEqual(["vo"]);
|
||||
});
|
||||
|
||||
it("groupLevel reads RMS off the group's own analyser once a member is scheduled", async () => {
|
||||
const { transport, mock, gen } = setupGroupTransport();
|
||||
await scheduleGrouped(transport, gen, "a", "vo");
|
||||
|
||||
const analyser = mock.analysers[0]!;
|
||||
analyser.getFloatTimeDomainData.mockImplementation((buf: Float32Array) => {
|
||||
buf.fill(0.5);
|
||||
});
|
||||
|
||||
const reading = transport.groupLevel("vo");
|
||||
expect(reading).not.toBeNull();
|
||||
expect(reading!.level).toBeCloseTo(0.5, 5);
|
||||
expect(reading!.clipped).toBe(false);
|
||||
});
|
||||
|
||||
it("flags clipped when any sample hits the ceiling", async () => {
|
||||
const { transport, mock, gen } = setupGroupTransport();
|
||||
await scheduleGrouped(transport, gen, "a", "vo");
|
||||
|
||||
const analyser = mock.analysers[0]!;
|
||||
analyser.getFloatTimeDomainData.mockImplementation((buf: Float32Array) => {
|
||||
buf.fill(0.1);
|
||||
buf[0] = 0.995;
|
||||
});
|
||||
|
||||
expect(transport.groupLevel("vo")!.clipped).toBe(true);
|
||||
});
|
||||
|
||||
it("disposes the analyser along with the rest of the group bus", async () => {
|
||||
const { transport, mock, gen } = setupGroupTransport();
|
||||
await scheduleGrouped(transport, gen, "a", "vo");
|
||||
const analyser = mock.analysers[0]!;
|
||||
|
||||
transport.destroy();
|
||||
|
||||
expect(analyser.disconnect).toHaveBeenCalled();
|
||||
expect(transport.groupLevel("vo")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodeAudioElement retry policy (late-asset self-heal)", () => {
|
||||
|
||||
@@ -147,8 +147,6 @@ export class WebAudioTransport {
|
||||
/** Post-FX fader: `data-volume` plus the volume lane. */
|
||||
fader: GainNode;
|
||||
muteGain: GainNode;
|
||||
analyser: AnalyserNode;
|
||||
levelBuf: Float32Array;
|
||||
/** Kept so `setRate` can re-aim this bus's FX automation, the way it does
|
||||
* every source's — its docblock claims it already did. */
|
||||
fx: ElementFxHandle | null;
|
||||
@@ -347,16 +345,10 @@ export class WebAudioTransport {
|
||||
const input = this._ctx.createGain();
|
||||
// Stable point the FX chain (or, when there's none, the dry passthrough —
|
||||
// see `attachElementFxChain`'s `detach()`) always lands on before master,
|
||||
// regardless of whether a chain is attached/detached/rebuilt later. B7's
|
||||
// meter taps here. The mute gain splices in BEFORE `output` (between the
|
||||
// FX chain and here), never after — the meter is defined to read the
|
||||
// group's true, honestly-muted level (design doc §5), and this node is
|
||||
// that contract's anchor.
|
||||
// regardless of whether a chain is attached/detached/rebuilt later. The
|
||||
// mute gain splices in BEFORE `output`, between the FX chain and here.
|
||||
const output = this._ctx.createGain();
|
||||
output.connect(this._masterGain);
|
||||
const analyser = this._ctx.createAnalyser();
|
||||
analyser.fftSize = 256; // level, not spectrum
|
||||
output.connect(analyser);
|
||||
|
||||
const groupEl = doc.getElementById(groupId);
|
||||
const muteGain = this._ctx.createGain();
|
||||
@@ -384,8 +376,6 @@ export class WebAudioTransport {
|
||||
input,
|
||||
fader,
|
||||
muteGain,
|
||||
analyser,
|
||||
levelBuf: new Float32Array(analyser.fftSize),
|
||||
fx,
|
||||
generation: this._playGeneration,
|
||||
reanchor: (at: AutomationTiming) => {
|
||||
@@ -407,7 +397,6 @@ export class WebAudioTransport {
|
||||
fader.disconnect();
|
||||
muteGain.disconnect();
|
||||
output.disconnect();
|
||||
analyser.disconnect();
|
||||
} catch {
|
||||
// Already torn down.
|
||||
}
|
||||
@@ -418,7 +407,7 @@ export class WebAudioTransport {
|
||||
|
||||
/**
|
||||
* Group mute, preview side — a separate gain from `input`'s volume fader
|
||||
* (B7) so a mute toggle never fights `scheduleVolumeLane`'s ramps on the
|
||||
* so a mute toggle never fights `scheduleVolumeLane`'s ramps on the
|
||||
* same param (the same hazard the design doc flags for §2.1). A no-op
|
||||
* until the group has an active member: at that point `groupInput` reads
|
||||
* the element's own `data-hidden` for its initial value, so there is
|
||||
@@ -440,24 +429,6 @@ export class WebAudioTransport {
|
||||
return [...this._groups.keys()];
|
||||
}
|
||||
|
||||
/**
|
||||
* RMS-ish level 0..1 and whether the last block clipped, for the group's
|
||||
* meter — or null when the group has no active member (idle/unknown).
|
||||
* Reuses a per-group buffer; no per-frame allocation.
|
||||
*/
|
||||
groupLevel(groupId: string): { level: number; clipped: boolean } | null {
|
||||
const g = this._groups.get(groupId);
|
||||
if (!g) return null;
|
||||
g.analyser.getFloatTimeDomainData(g.levelBuf);
|
||||
let sumSquares = 0;
|
||||
let clipped = false;
|
||||
for (const sample of g.levelBuf) {
|
||||
sumSquares += sample * sample;
|
||||
if (Math.abs(sample) >= 0.99) clipped = true;
|
||||
}
|
||||
return { level: Math.sqrt(sumSquares / g.levelBuf.length), clipped };
|
||||
}
|
||||
|
||||
/** Master, unless `el` belongs to a group — then that group's bus (built on
|
||||
* first use, per `groupInput`). */
|
||||
private resolveDestination(
|
||||
|
||||
Reference in New Issue
Block a user