Use this reference when browser editor controls are not enough: you need to write a mix into HTML, know a parameter’s exact range, automate a knob, or understand why something behaves differently in the render than in preview. For everyday use start with Mix audio and apply effects, and for what to ask an agent for, Audio effects and mixing. A composition carries its whole mix in the HTML. There is no session file and nothing to load beside the markup.

The four attributes

data-audio-group is the odd one: a bare string, and ignored on a muted <video> (muted, or data-has-audio="false"). 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 &quot; and & as &amp; — as every example on this page does. The browser reads them through getAttribute and does not mind either way, but 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

Unescaped, that chain reads: a high-pass labelled Remove Rumble, a peaking cut labelled Reduce Mud, and a bypassed limiter.
  • Order is signal order. Each node processes what the one before produced.
  • type is an effect id from the registry. params are 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.
  • id is 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. The browser editor hands out the first free n1, n2, …
  • label replaces the effect’s own name in the rack. Write one whenever a node is doing a named job — two peaking nodes otherwise show the same row twice and you cannot tell the mud cut from the clarity lift.
  • enabled: false is bypass: the node stays in the chain, out of the signal path. Absent means enabled.
  • fromCarve: true marks a node the carve analysis generated. fromPreset carries the id of the preset that wrote the node, which is how re-applying a preset replaces its own nodes in place. Do not set fromCarve by hand — the next carve deletes exactly those nodes.

data-automation

That is two lanes: the track’s volume dropping to 0.4 by 2.5 s, and node 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. The lane multiplies data-volume, the way clip gain offsets volume keyframes in an editor: data-volume="0.5" under a lane at 1 plays at 0.5. The product shares the same +12 dB ceiling. This holds on clips and on groups. On a clip, data-volume is applied ahead of its effects, so a compressor or limiter in its data-fx-chain reacts to it; the lane comes after them. A group’s data-volume stays after its effects, like a track fader. 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.

Tracks add up; nothing averages them

The render sums every track at unity. A track at data-volume="1" sits in the mix at its source level whether it plays alone or beside twenty others, and a clip ending does not raise the ones still playing. Adding a track never turns the others down.
  • data-volume, a volume lane, fades and a group fader each scale only their own track or group. Doubling a voice moves the voice +6 dB; how much the whole mix moves depends on what else is playing at that moment.
  • Nothing limits the sum. Loud tracks that overlap can pass 0 dBFS. Leave headroom, or put a truepeak on the master bus; a limiter can let fast peaks through.
  • Export keeps the delivered file under -1 dBTP. A mix that passes it goes through a truepeak at -3 dBTP, so only its peaks come down. If a peak still gets over, export lowers the whole mix by one flat amount, quiet passages included, and the CLI prints an audio_lowered warning.
  • The render’s master gain, PRODUCER_AUDIO_GAIN (default 1), multiplies the finished sum once, before the master bus chain.
  • To check a level, compare the export against its source scaled by the data-volume you wrote, in a window where only that track plays.

data-fx-carve

Unescaped: {"enabled":true,"sources":["voiceover"],"strength":0.25}.
  • sources names what this bed makes room for — element ids, or a group id, which expands to its current members on every analysis.
  • strength is 0–1 and derives the whole mechanism.
  • enabled: false keeps 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.
This attribute is not read at playback — the chain and lanes it produced are what play. It exists so strength can be changed on an existing carve instead of being guessed back out of the filters. Older projects may carry six mechanism numbers (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

Membership is held by the member, not by the group nesting its members, so a deleted clip simply disappears from the group on the next resolve and nothing dangles.
  • <hf-audio-group> is optional metadata: data-label, data-volume, data-hidden, and its own data-fx-chain / data-automation. A group with members but no element still resolves, using its id as the label.
  • Audio and audible video. A <video> with sound (data-has-audio="true", no muted) joins its group in preview and render; on a muted video data-audio-group is ignored. Groups do not nest.
  • data-audio-group="" is no group at all.
  • The element must be inert. The runtime injects hf-audio-group{display:contents!important} so an unknown custom element cannot take a flex gap, while anything inside it still renders unless the group is muted (data-hidden hides it as before) — but author the rule yourself so a bare preview never lays it out. Close it with </hf-audio-group>: HTML ignores /> on a custom element.
  • Group ids and element ids share one namespace.

The master bus: data-fx-chain on the composition root

The master bus is the data-fx-chain on the top-level composition root. It runs once over the sum of every clip and every group, after each group’s own chain and fader, so a limiter here sees the peak of the whole mix. A clip can belong to only one group, so this is the way to put one effect across everything.
  • Only the top-level composition root is read as the master bus. On a clip or a group data-fx-chain keeps its own meaning, and on a sub-composition’s root it is not applied while a parent composition contains it. A sub-composition file previewed or rendered on its own is the top-level root, so its chain is the master bus there.
  • There is no master automation: data-automation on the root is not read as the master’s.
  • The render sums the tracks to a float WAV, runs the chain, and only then encodes AAC, so the chain reads unclipped samples. Preview inserts the chain between the clips’ sum and the monitor.
  • With a master chain on the root, preview plays every audible <video> through that bus too, as the render does. Without one, a video at volume 1 with no chain, group or automation of its own stays on native output.
  • An unreadable master chain fails the whole mix: the render stops with an audio_processing_failed error under every strictness setting, and exports nothing. Preview plays dry with no message. audio_attribute_unreadable catches a chain that is not JSON, has no "version": 1 or no nodes array; an unknown effect type still only shows up at render. Check a master chain in preview before rendering.

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. limiter is an envelope follower: it reacts to the level of the signal, has no lookahead, and can let a sample peak or an inter-sample peak through. truepeak is the one to use for a delivery ceiling. It estimates the true peak on a 4x oversampled signal, reads lookahead ms ahead so the gain is already down when the peak arrives, and holds that estimate at ceiling. Channels share one gain. It delays its output by lookahead plus about 0.3 ms (3.3 to 10.3 ms across the range): the offline render trims that delay, live preview plays it, so the sound is that much behind the picture. A seek clears the limiter’s delay line and held gain on a group or master bus. Changing lookahead rebuilds the node. The ceiling is held against that 4x estimate, not against the waveform itself. A 4x interpolator cannot see the peaks of content close to Nyquist, so the true peak can end above ceiling. Measured against an ideal 16x interpolation, tones, pink noise and a dense mix ended at most 0.6 dB over. Full-band white noise driven 8 dB or more into the limiter ended 1.2 to 1.7 dB over (median 1.4 dB, 28 runs of 10 to 30 s). A meter that also reads at 4x, such as ffmpeg’s ebur128=peak=true, reports that same noise about 0.1 dB over, so it will not show the miss. Under a hard delivery limit set ceiling at least 2 dB below it: −3 dBTP for a −1 dBTP limit.

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 native AudioParam 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 named peaking 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

The script ships with the media-use skill; <SKILL_DIR> is that skill’s folder.
That is the whole command — it finds the voice and the bed itself and prints what it decided:
Name tracks with --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 the browser editor’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 and mute

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.

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. Four rules exist: Nothing checks a lane’s target or an effect’s type and parameters. What enforces those is the render. Beyond that, a mix is verified by rendering and listening.