The four attributes
data-audio-group is the odd one: a bare string, and ignored on <video>.
The other three are JSON. Of those, only data-fx-chain and data-automation
can also sit on an <hf-audio-group>, where they apply to the group’s whole bus;
data-fx-carve belongs to a clip.
Write the JSON attributes double-quoted, escaping the JSON’s own quotes as
" and & as & — as every example on this page does. The browser
reads them through getAttribute and does not mind either way, but
scripts/carve.mjs finds them with a name="..." regex, so a single-quoted
attribute is invisible to it: a carve reports no existing chain and overwrites
work it could not see.
Nothing static validates a chain. Preview plays an unreadable chain dry so
the composition stays workable; the render fails the whole mix rather than
shipping a dry track that sounds plausible and is wrong.
data-fx-chain
- Order is signal order. Each node processes what the one before produced.
typeis an effect id from the registry.paramsare in the units a person thinks in — dB, ms, Hz. Out-of-range values are clamped on read, so a chain that parses is always safe to realise.idis a stable handle. Automation addresses nodes by id, never by position, so reordering a chain cannot re-point a lane at a different effect. A node with no id loads fine but cannot be automated. Studio hands out the first freen1,n2, …labelreplaces the effect’s own name in the rack. Write one whenever a node is doing a named job — twopeakingnodes otherwise show the same row twice and you cannot tell the mud cut from the clarity lift.enabled: falseis bypass: the node stays in the chain, out of the signal path. Absent means enabled.fromCarve: truemarks a node the carve analysis generated.fromPresetcarries the id of the preset that wrote the node, which is how re-applying a preset replaces its own nodes in place. Do not setfromCarveby hand — the next carve deletes exactly those nodes.
data-automation
n2’s
gain moving to −6 dB by 1 s along a bent segment.
Two clocks
Getting this backwards is the most expensive mistake on the page: a group lane
written in clip-relative time lands wherever the first member happens to start.
Preview and render agree on both clocks.
Volume is not capped at 1
v on a volume lane is a linear gain multiplier with a ceiling of +12 dB,
which is about 3.981 — the same ceiling data-volume uses. A lane that boosts
above unity is valid and will play; 1 is unity, not the maximum.
A lane holds its first value backwards to the start of its clip and its last
value forward to the end. A bed that begins before the voice therefore needs an
explicit “no cut” point at t: 0, or it starts out already ducked. Maximum 512
points per lane. A lane whose node is gone is pruned on read rather than
erroring — so a mistyped nodeId costs you the envelope silently. Read ids back
out of the chain rather than assuming what was minted.
A volume lane and a GSAP tween on volume conflict: the lane wins and the
tween is ignored. The linter reports that as audio_volume_double_automation.
data-fx-carve
{"enabled":true,"sources":["voiceover"],"strength":0.25}.
sourcesnames what this bed makes room for — element ids, or a group id, which expands to its current members on every analysis.strengthis 0–1 and derives the whole mechanism.enabled: falsekeeps the settings and stops the carve. It exists because a bed with exactly one candidate voice is carved by default; with “off” as an absent attribute, switching it off would read as never-configured and the default would put it back.
maxCutDb, bands, q,
intelligibilityBias, duckDb, headroomDb) instead of strength. They still
load: the depth maps back onto a strength and the rest is re-derived. A stored
carve with no enabled reads as on, a single source reads as a one-voice
sources list, and a stored dynamic is ignored — every carve follows the
speech now.
data-audio-group
<hf-audio-group>is optional metadata:data-label,data-volume,data-hidden, and its owndata-fx-chain/data-automation. A group with members but no element still resolves, using its id as the label.- Audio only.
data-audio-groupon a<video>is ignored, and groups do not nest. data-audio-group=""is no group at all.- The element must be inert. The runtime injects
hf-audio-group{display:none!important}so an unknown custom element cannot take a flex gap or shift:nth-child— but author the rule yourself so a bare preview never lays it out. - Group ids and element ids share one namespace.
Effect registry
Sixteen effects. Values outside a range are clamped on read. AUTO marks a parameter an automation lane can drive; anything unmarked cannot move over time.HF_AUDIO_FX in @hyperframes/core/audio-fx is the source of truth.
Filter — which frequencies a track may occupy
q is bandwidth — higher is narrower. poles is the slope: 2 is the usual
biquad (12 dB/oct), 1 is gentler (6 dB/oct). Shelving filters have no q; the
Web Audio spec leaves it unused for them.
Dynamics — how level behaves over time
Cuts on
gain reach −60 dB, boosts stop at +12: it is a level stage for making
room, and a chain that could add 40 dB would clip long before that was useful.
knee of 1 is a hard corner. mix below 1 blends the dry signal back in
(parallel compression). range is how far down the gate pulls when closed.
Nonlinear — changes the waveform’s shape
tanh is the gentlest curve, hard is outright clipping. Higher oversample
costs more CPU and keeps aliasing down. samples repeats each sample N times — a
crude downsample, which is where the lo-fi character comes from.
Time — where a track sits, and how it moves
pitchshift moves pitch without changing playback speed, which is what
separates it from a playback-rate change. Neither of its parameters can be
automated — it is a worklet.
Reverb convolves a generated impulse and both preview and render generate the
same one, so a room is reproducible without shipping an impulse file. Higher
damping rolls the top off the tail faster. feedback is bounded below 1
because at 1 it never decays.
Why some parameters cannot be automated
Automation is handed to the audio thread once, as nativeAudioParam ramps and
curves — that is what keeps it sample-accurate and identical between preview and
render. A parameter can therefore only be automated if an AudioParam backs it.
Three kinds do not:
A lane on a non-automatable parameter is silently inert. To make one of those
behave differently over time, automate a
gain stage around it instead: a lane
on a gain before a compressor changes how hard the compressor is driven, which
is most of what automating its threshold would have done.
Presets, jobs, and one-knob profiles
Every one of these is a shortcut to a chain you could have built by hand. Open any of them and you find ordinary effects with their parameters showing.Presets
Twenty-two, in four families. Applying one appends; re-applying one already present replaces its own nodes in place, because position is signal order.chipmunk, giant, and monster are built on pitchshift, so they change the
speaker rather than the channel.
voice-clean is Remove Rumble → Reduce Mud → Even Out Loudness → Add Clarity →
Peak Ceiling. voice-broadcast is denser and more forward; voice-warm adds
body rather than cutting it.
A preset’s nodes are wrapped in a wet/dry blend, so presetAmount (0–1) fades
the whole thing, and fx.preset.<id> is an automation target that ramps it over
time. That is the only way to automate a preset as a unit — its nodes share no
common parameter.
Jobs
Five namedpeaking filters with the frequency already chosen. Picking the job
is picking the range, which is what makes a single “how much” knob honest.
Writing one by hand, carry the name in
label — the parameters alone are not
the job.
Every job also ships inside a preset at identical settings, which is where
the five came from. So check what a preset already contains before adding a job
on top: voice-clean plus a Reduce Mud job is −6 dB at 250 Hz where −3 was
meant.
One-knob profiles
Five effects have no single parameter that can honestly be their face — a compressor’s threshold means nothing without its ratio. They get a derived control instead, 0–1, setting several parameters together.
Evenness, Warmth, and Space are level-matched: the make-up gain, output trim,
and dry leg move with the drive, so turning the knob up does not also turn the
track up. Tightness and Crush are not, because neither has a trim to move.
The chain stores the mechanism values, not the knob position — the knob is read
back by inverting the curve, so hand-editing a parameter under a profile is
allowed and simply moves the knob.
Voiceover carve
A carve is a relationship, not an effect. The settings live on the bed — the track that gets processed — and name the voices to listen to, exactly as a sidechain compressor does. Never put a carve on a voice track.strength derives six numbers that move together in any real mix: how deep to
cut, how many bands, how wide, how far to favour intelligibility over raw voice
energy, how far the level may drop, and how far under the voice to aim.
Every value becomes an envelope of the speech’s own level: silence leaves the bed
alone, a loud passage pushes the carve to full depth. There is no static mode.
Inside a carved bed the signal runs through the dips first, then the level match,
then anything you built yourself — which is why a limiter you add still acts as
the last ceiling.
Voices are summed onto the bed’s clock before anything is measured, so one
analysis covers all of them. Voices that never play while the bed does are left
out; they cannot mask it.
Name a group, not a run of clip ids. A list of ids has to be exhaustively
right and stays right only until the next edit — a fourth narration clip added
later plays outside the carve’s awareness and the bed silently fails to duck
under it. A
sources list naming two or more plain clip ids is reported as
audio_carve_ungrouped_sources.
There are two arrangements where naming the group is worse than naming ids,
and both come from the group form resolving later and wider than the analysis
that wrote it:
Neither shows up on the run that writes it — the analysis sums the voices it
detected, so the first pass is correct however wrong the stored attribute is. It
surfaces on the next re-analysis, against a file the previous run declared good.
Keep the bed in its own group.
What a carve writes is an ordinary chain of
peaking filters plus a gain
stage, tagged fromCarve, and one automation lane per carved parameter. That
tagging is the whole trick: a re-run replaces the previous carve and leaves every
effect and lane you built by hand exactly where it was.
From the command line
--bed / --voice (repeatable) when the automatic choice is
wrong, --strength to push it, --dry-run to see the report and write nothing.
It refuses when it cannot tell which track is the bed rather than carving the
wrong one.
What it records in sources. When every voice it analysed shares exactly one
group, and that group is safe to name, it writes the group id. It falls back to
the individual clip ids — and says so — when the group contains the bed or a
member classified music or SFX, for the reasons in the table above. A group
member classified voice or unknown that this run did not analyse is not a
reason to fall back: picking up a clip that starts playing later is the whole
point of naming the group.
Track choice is by name first, using the same classifier Studio’s picker uses, so
the two cannot disagree: an id or filename that looks like music (music, bgm,
bed, score…) is the bed, and everything else playing over it that is not
SFX-shaped is a voice. Needs ffmpeg on PATH and @hyperframes/core
installed in the project.
Groups, mute, and solo
A group sums its members through one bus, and the bus carries the group’s own volume, FX chain, and automation. In the render each group is sub-mixed into a single track at full composition length before entering the main mix, so a group-level effect hears the sum rather than each member separately. Mute and solo are not symmetric, on purpose.
Solo is a listening tool. An element is audible while any solo is active only if
it is itself soloed or its own group is soloed; a group bus is never itself
attenuated by solo, so a soloed member’s path out stays open. A group that is not
soloed while one of its members is shows as half-lit — the display-only signal
that some of what is under it still plays.
Preview and render
Both read the same two attributes through the same builders, which is why preview predicts the render:
Effects with a tail —
reverb, delay — make the rendered track longer than
its source, and the mix is told how much. A bed with reverb therefore no longer
ends exactly at its data-duration. That is expected.
What the linter checks
Almost no static gate covers a mix. Three rules exist:
Nothing validates the chain or the effect lanes at all. What enforces those is
the render. Beyond that, a mix is verified by rendering and listening.