Balance levels, place sounds in stereo, route shared effects and shape the combined output of a song.
1. The surface#
bus hall = reverb 0.85 0.4 |> width 1.3 -- shared aux bus (effect sections)
snareTrack = play snare snarePat
hatTrack = play hat hatPat
padTrack = play pad padPat
group drums = [kickTrack, snareTrack, hatTrack] -- VCA over member strips
mixer {
kickTrack = { level = -1.5dB, sends = { hall = -18dB } }
hatTrack = { level = -10dB, pan = -0.3, width = 1.4 }
bassTrack = { level = -3dB, polarity = inverted, mute = false, solo = false }
padTrack = { presends = { hall = -12dB } } -- pre-fader send
drums = { level = -1dB } -- group VCA entry
master = { level = -0.5dB, mono = false, dim = -20dB, refLevel = -18dB,
console = true, inserts = [compress 0.12] } -- or "9"/"LA"/"MC"
}
bassTrack = play rubber !"c2 ~ eb2 ~" |> bypassed tape (1c / 8) 0.3 0.25 -- warm, skipped
- Mixer records, bus sections and group lists use ordinary expressions and enter the type checker. Field values may use scalar/record bindings, arithmetic, booleans, strings and record overrides. Insert sections have type
Track -> Track; track pipes, buses and master inserts use the same argument checker and effect lowerer.polarity = "inverted"is a string; the barenormal/invertedspellings remain contextual aliases. - Persistent expressions. Live effect, strip, send and master parameters accept multi-macro arithmetic and control graphs, with their existing read cadences. Graph values carry surface units and do not mint user macros. See Bring a sound to life for scope, units and a runnable example.
- Macro binding (automation-completeness.md). Macros are the automation currency across the mix surface:
- a send level or a strip
level/pan/widthmay be a bare macro ident (level = fader,sends = { hall = wet }) — the param then rides the knob live (drivable by MIDI-learn, shred lanes, arrangement motion). Ordinary scalar bindings work too. Range checks apply to constants only; runtimes clamp a bound value to the param's own range (level ≥ 0, pan [-1, 1], width [0, 2]) at application. masterandgroupleveltake the same forms (production-gaps.md §19 N2):master = { level = close }with a scene-rampedcloseis the whole-mix fade, which no per-patchgaincan fake for sample tracks.- bus/master insert args use the same expression grammar as track arguments. Parenthesize arithmetic when it is one positional argument:
plate 0.4 (haze * 0.5),tilt (1 - haze). Named field values need no parentheses:plate 0.4 { wet = haze * 0.5 }. Constant-only slots (times/decays, baked at build) diagnose a proven live macro or signal. - mixer field values are full expressions —
level = close * -6dB,pan = tilt * 0.5; the older parenthesized forms work too. - re-assert rule: a macro-bound send/strip param OWNS its smoothed target — the engine re-asserts the block's macro value at the top of
mix_track, so a liveSetSend/SetStriplands but is overridden next block — the same engine code on every target. - control rate: a knob re-asserts once per block into a per-sample one-pole, which is what the smoother was designed around. A macro carrying a modulation graph re-asserts
leveland each send gain per sample from the dense curve (Macros and automation §4, 2026-08-05) — both are plain full-scale multiplies, and a block-quantised target left 2.51e-3 of export-vs-live disagreement at a 96-frame device period.pan/widthunder a knob stay on the block grid: they build a mix matrix at the block's two endpoints and lerp its four coefficients per sample, which is the §2.3 design. Under a modulation graph they too follow the curve per sample — a per-sample target into the same one-pole and a matrix per sample — since the endpoint matrix left block lines at −80.7 dBc on a 7 Hz pan sweep (now −152.2;engine/tests/effect_param_motion.rs, measured offline). Before that change, the export-vs-live test readpan2.05e-3 on a 0.120 peak andwidth4.5e-8 (the width matrix is near-linear in the knob).
- a send level or a strip
Scene addressing. A song scene's
with { … }may name a mixer site by dotted path instead of a macro, so "open the reverb send in the breakdown" is one line and no hand-minted knob is needed (closes production-gaps D2):padStrip = play pad padPat |> tape (1c / 8) 0.3 0.25 drumStrip = play kick kickPat bus hall = reverb 0.85 0.4 mixer { padStrip = { sends = { hall = -12dB } } master = { inserts = [plate 0.4] } } breakdown = [padStrip, drumStrip] with { padStrip.sends.hall = ramp 0.1 0.6, -- a declared send's level padStrip.level = ramp 0dB (-9dB),-- a strip fader padStrip.pan = ramp (-0.4) 0.4, padStrip.width = 1.6, padStrip.tape = ramp 0.3 0.75, -- a TRACK pipe's insert arg master.plate.mix = ramp 0.2 0.7, -- a master insert arg hall.reverb.mix = 0.9 -- a bus insert arg }- Grammar.
<track>.level|pan|width·<track>.sends.<bus>/.presends.<bus>·<track|bus|master>.<effect>[.<param>]. The path mirrors the mixer block's own nesting;master.inserts.<effect>…is accepted as a synonym formaster.<effect>….<param>may be omitted when the effect has exactly one modulatable slot (hall.reverb,padStrip.tape); the slot names are the IR field names (mix/feedback/amount/azimuth/…) and an unknown one is diagnosed with the effect's real list.<effect>matches the first insert of that name in the chain. A song that imports another names an imported send by the song's own spelling (melTrack.sends.room, resolved to the one send whose bus is….room) or by the qualified bus (melTrack.sends.pipeworks.room). The same key may also open with a patch name and reach inside it —keys.lpf.cutoff(a builtin stage written in the patch) orkeys.cutoff(a patch field), written in the site's own unit (with { keys.lpf.cutoff = ramp 400Hz 4000Hz }); see Draw a change, and record one § "Inside a patch". - Desugaring. Pure compiler-side (
lower.rs::apply_scene_mixer_addresses): each distinct address mints an internal macromix:<address>seeded from the site's currently declared constant, substitutes it at the site, and the scene emits the ordinaryMacroAutoset/ramp against that name. Both engines then honour it through the shipped macro-ramp path — no runtime change, which is the whole point of "macros are the automation currency". A module that writes no address is bit-identical to before (every golden snapshot unmoved), and a render with no arrangement reproduces the declared constant exactly. The knob is real: it shows up in the workbench macro panel, annotated@mixer "<address>", and is MIDI-learnable like any other. - Units — and the travel convention. A level address (
….level,….sends.<bus>) reads-6dB/ a non-negative linear ratio exactly as the mixer grammar does; every other slot takes a plain number and refuses a unit suffix rather than silently reading-6dBas −6. You always write surface numbers — but note what that costs the compiler. Strip and send params are read RAW by the engine, so surface is travel there. Insert args are not: per theParamconvention (§2), aConstcarries surface units while aMacro's value is [0, 1] knob travel mapped across the slot's range. For most slots that range is [0, 1] and the two coincide, but forwidth[0, 2],tilt[-1, 1],formantShift[-12, 12] andplace(az [-180, 180] / el [-90, 90] / dist [0, 4]) it does not, so an address on those inverts both the seed and the scene's endpoints (TrackEffect::travel_range, mirroringengine/src/effect.rs::apply_macros). Hencemaster.width.amount = ramp 1.0 1.6means what it says, while a hand-written macro in the same slot (width (w)) still takes travel — the difference is exactly the const-vs-macro rule, and out-of-range endpoints clamp to the slot's rails likeclamped_constdoes. Pinned end to end byengine/src/offline.rs::a_scene_addressed_insert_renders_exactly_what_was_declared. - Collisions are an error, not a compose. A site already bound to a user macro in the mixer declaration refuses the address: multiplying a scene ramp into somebody's knob would make the scene's own numbers lie (and does not even mean anything for a bipolar
pan). The diagnostic names the macro and the one-line fix — ramp it directly (with { wet = ramp 0.1 0.6 }). Two spellings of one site (master.platevsmaster.plate.mix) are refused the same way. - A dotted key that IS a declared macro name stays macro automation — namespaced library macros (
with { lib.glow = ramp … }) keep their old meaning; the mixer namespace only claims dotted keys that name no macro. The precedence is unconditional, so a macro literally spelled like a path (macro "padStrip.level") shadows that address silently — don't name macros after mixer paths. - Shadowing. On a track,
level/pan/widthalways mean the channel STRIP, even when the track's pipe chain carries an insert of that name (|> width 1.3). The insert stays reachable under its full path —padStrip.width.amount— and the diagnostic forhall.widthon a bus (which has no strip at all) points there. - One slot, one entry per scene. Addressing the same site twice in one scene is an error, including via two spellings that canonicalise together (
master.inserts.plate.mix+master.plate.mix) — otherwise the two motions would resolve last-writer-wins with nothing in the source to show it. Different scenes addressing one site is the normal case and shares the single minted knob. - Dotted keys are scene-only. The parser accepts the path shape in any
with-block (it cannot tell a scene from a record update), so the checker rejects one whose base is not a scene's track list —r with { a.b = 7 }stays the error it always was. master.levelIS addressable — the whole-mix fade. The master fader is aParam(§5.1), and the engine reads it through the sameParamBinda channel strip's fader uses (apply_master), so the address behaves exactly like a strip level: dB endpoints linearise, the declared constant seeds the minted knob, and a module that never wrote amaster = { … }entry gets the neutral one materialised for it (unity, no inserts) — which is the runtime default and so changes nothing on its own. Pinned byengine/src/offline.rs::a_scene_addressed_master_level_fades_the_render.- Not addressable: a group
level(group graph controls do not add scene addressing; §1.1 describes their engine VCA and the legacy host trim),master.pan/master.width(the master entry has no such fields — its placement lives on its inserts),mute/solo/polarity(booleans/enums), constant-only insert slots (times, decays, crossover frequencies), andeqbands. A send must already be declared in the mixer block — routing is declared, the scene only rides it; the diagnostic spells the mixer line to add. A track with no mixer entry gets a neutral strip materialised for it, which is the runtime default and so changes nothing.
- Grammar.
- Fields per track entry:
level(dB or linear ratio ≥ 0, or a macro ident),pan∈ [-1,1],width∈ [0,2] (both macro-bindable),mute/solo,polarity=normal|inverted,sends/presendsmaps. Groups acceptlevel/mute/soloonly; a buslevelonly (its return fader, the chain's last stage).masteracceptslevel,mono,dim(dB),refLevel(dB),inserts(track-rate effects only — per-voice names are diagnosed). Alevelexpression that FOLDS is evaluated in dB and converted to a linear trim once, so it follows Language reference § 2 like any other arithmetic:level = (-6dB) + (-6dB)is −12 dB, a quarter of the amplitude, not the sum of two gains — and so is a NAME for it (trim = (-6dB) + (-6dB)…level = trim). An expression that does not fold —level = close * -6dBwith a live macro — keeps its macro and its authored trim. A live signal that is itself in dB (fader = macro -6dB…level = fader) is refused: the strip reads a linear ratio, so a dB-valued macro would otherwise reach the engine as a linear gain of −6. The track, bus and mastergainread their level through this same reader (lower::mixer::linear_level), so|> gain -6dBis the strip's −6 dB. varispeed kis master-only (Mastering and delivery §5): the whole-mix tape ratio. It rides ininsertslike everything else and is macro-bindable and scene-addressable (master.varispeed.ratio, the ratio itself over 0.05–4) — but the engine hoists it out of the chain, because it re-times the render rather than filtering it, and no same-rate box can do that. A bus or a track pipe diagnoses, and a mix may declare at most one.
- Dotted names are names. A mixer entry, a
groupmember, and an insert /buseffect name are eachident(.ident)*, joined into one string. Two things need that: a collision-qualified track flattens as<module>.<name>(Names and library imports), somixer { drums.kickT = { level = -6dB } }andgroup kicks = [kickT, drums.kickT]address it; and every builtin has a qualified spelling beside its flat one, soinserts = [track.crush 0.4]≡crush 0.4andbus hall = fx.plate 0.4≡plate 0.4. An effect'smodule.membermaps throughbuiltins::qualified_builtinbefore the insert whitelist, so the two spellings lower byte-identically and a qualified name that isn't a track-rate insert (track.gain) is rejected by the whitelist, not the grammar. An unknown module or member diagnoses by name. (A.was a hard parse error in all three slots before, so this is strictly additive — no existing program's meaning moves. The scene automation ADDRESS grammar,with { padStrip.width.amount = … }, has its own dotted-key rule and is untouched.)
1.1 How a macro level composes with an authored dB#
A level is a linear ratio. A bare macro ident is the level, so it replaces whatever was written there — level = -6dB changed to level = close has silently discarded the −6 dB balance, and at close = 1 the strip is now at unity, 6 dB hot. That is the one sharp edge in this grammar and it is why the affine form exists.
Write the trim into the expression and it multiplies instead:
close = macro "close" 1
mixer {
hatTrack = { level = (close * -10dB) } -- rides `close`, tops out at -10dB
master = { level = close } -- whole-mix fade, unity at close=1
}
A dB literal inside a level slot converts to its linear ratio (10^(v/20)), exactly as a bare level = -6dB does, and becomes the affine scale. So (close * -6dB) is 0.501 · close: at close = 1 it is bit-for-bit the authored −6 dB (measured ratio 1.00 against a plain level = -6dB master); at close = 0.5 it is 6 dB under it; at close = 0 it is silence. The macro scales the balance, it does not stand in for it.
A dB level that is a compile-time constant converts however it is spelled: a literal, a name (trim = -6dB), arithmetic in dB ((-6dB) + (-3dB) is −9 dB, not the sum of two linear gains), a let, or a field of a record of constants. So a song can keep its balance in one place:
levels = { kick = 3dB, hat = -6dB }
bus hall = reverb 0.85 0.4
mixer {
kickTrack = { level = levels.kick }, -- exactly `level = 3dB`
hatTrack = { level = levels.hat, sends = { hall = levels.hat } }
}
The same holds for a track's |> gain levels.kick. What is refused is a dB value that is live — a macro -6dB, a dB ramp, or a record field holding one — because no level reader has a per-sample dB law; the error says so and points at the linear-travel form (fade * -6dB).
This linear conversion is specific to strip/master/group/send levels. Insert parameters retain the unit their slot reads: a limiter ceiling takes raw dB, a delay reads seconds, and a frequency reads Hz. limiter (-1dB) (60ms) and limiter -1dB 60ms agree. Parenthesized unit expressions such as (200ms) and (3c/8) work on tracks, buses and master inserts; incompatible units diagnose through the shared parameter schema. The legacy bare limiter release scale is described in §1.2.
Two consequences worth stating:
masteris engine-side and rides live.MasterStrip::level_bindfollows the same declared-macro-wins re-assert rule as a strip (§1 above): the bound macro owns the smoothed target, a liveSetMasterlands but is overridden next block, and a macro carrying a modulation graph moves the fader per sample. A scene-rampedclosetherefore takes the whole mix — sends, samples, sample-only tracks, everything past the master sum — to digital zero with no step (measured: −20.5 dBFS → −200 dBFS across the ramp, descent |Δx| within the body's own maximum).- Legacy macro-bound group levels seed the host VCA. Bare-macro and affine declarations still emit
GroupLevelMacroInertand use the initial value. This preserves existing songs. A graph-valued group level instead runs once in the engine and multiplies each member's strip gain per sample. The host group fader starts at unity and acts as an additional trim; it does not apply the graph value a second time. For example,level = a * bcan ride two macros together across a group. Group mute/solo remain host-side. Layering rule (§1c of the spec): placement is authored (
panTrack/jux/Pan— per-voice), balance is mixed (the strip — bus-level). Strip pan is a per-channel balance of the already-placed stereo bus —L' = L·min(1, 1−p),R' = R·min(1, 1+p)— and strip level multiplies aftergainTrack. The balance law's properties, all load-bearing:- unity at centre —
p = 0is a bit-exact pass-through (the combined pan+width stage atwidth = 1is the identity matrix, verified end to end byengine/tests/mixer_strip.rs::strip_pan_centre_is_a_bit_exact_no_op); - monotone, non-negative, no wrap — a channel can only be attenuated on its way off-centre; the pan law itself never feeds one channel into the other, so at width 1 already-placed content (
panTrack,jux, a hard-panned stereo sample) keeps its polarity at every setting. (The width stage's mid/side math does mix the channels — its off-diagonals are0.5(1−w)·g, negative for width > 1 — which is inherent to width, not to pan.); - mono fold — a centred source folds to
1 − |p|/2(−6.02 dB at the extremes, never a null); a source already hard-panned to the side you pan toward folds unchanged; panning away from it attenuates it smoothly to silence at the opposite extreme, because that extreme mutes the channel it lives in. Nothing cancels. - corpus impact of the law change — ~80 shipped songs declare a non-zero strip pan (max |pan| 0.45); at those settings the rebalance vs the old rotation measures ≲1 dB of per-channel level shift. Audible on A/B, pinned by nothing; ride
level/panper song if a mix drifted. - not equal-power for the bus. A centred source loses 3.01 dB of total power at a hard edge (one channel muted, the other at unity) — the equal-power requirement is a per-source one, satisfied by the authored layer (
panTrack/Pan,voice/util.rs::pan_gains_at, §3.5), not by a bus-level balance. Ridelevelif a hard strip pan needs make-up.
This replaced an M/S rotation (θ = pan·π/4), which was power-preserving for the summed bus but cyclic for every non-centred source inside it: a hard-right element at
pan = 1rotated past the edge to(−0.7071x, +0.7071x)— equal level in both speakers, anti-phase, a total mono null (measured −142 dB). Seedocs/notes/dsp-quality-audit-2026-08-03.md§A finding 1. Rotating the field is a genuinely different musical move; it is not available today and would need its own verb (see Mixing tracks and buses §2.4 Follow-up).- unity at centre —
1.2 Reusable expressions and effect sections#
A bus RHS and each master insert-list element are checked as Track -> Track. The context supplies the missing track receiver:
wet = 0.25
room = { bigness = 0.7, mix = wet }
finish : Track -> Track = \t -> t |> galactic 0.4 room
strip = { level = -6dB, pan = 0.1 }
bus hall = finish |> width 1.2
bus wideHall = hall |> haas 0.2
mixer {
bassTrack = strip with { pan = 0.3, polarity = "inverted" },
master = { inserts = [finish, bypassed compress 0.2] }
} with { master.level = -0.5dB }
- Options are ordinary records: bind, import and override them exactly as in track calls. Inherited fields keep their definition scope even inside a helper with shadowing local names. Record presets and insert-list bindings can be shared; inherited EQ handles are read-only in the consumer.
- Bus sections compose in declaration-independent order. Direct/indirect cycles diagnose. A section must transform its receiver with track audio inserts; replacing it with
play, or applying voice/event modifiers, is rejected.varispeedretains its master-only placement rule. - Comma-free legacy mixer fields, insert lists and group lists still parse. Use commas between new helper expressions for an unambiguous list. Dotted track addresses and qualified builtins keep working. Existing polarity aliases and spaced negative literals are retained.
- Explicit units agree across all three surfaces, including tempo-aware delays and cents/semitone conversion. For compatibility, a direct ambient limiter section reads an unsuffixed release in milliseconds (
limiter -1 60). An authored track-transform lambda keeps ordinary track-call semantics (bare release is seconds). Write60mswhen extracting or sharing the call so the meaning stays explicit. - Group member lists may use bindings.
mute/solo/mono,consoleandpolaritymay use compile-time values;dimandrefLevelalso require compile-time values. Live slots also accept persistent graphs; compile-time slots remain fixed. - The entry file owns its ambient mix, and its
masterabsolutely: a library'slevel/mono/dim/refLevel/consolenever apply to the song. Importing values used only by a mixer keeps those values reachable. An imported module's own channel strips are not inactive: they are DEFAULTS for that module's own tracks, which this block overrides field by field, and a module's masterinsertsride one adopted track. The whole rule, including whysolonever travels and when a master chain is refused with a warning, is Names and library imports §"Imported strips". The source mixer editor edits literal fields only and asks authors to edit computed/shared records in Source.
A typed section also composes outside the mixer when piped into a supported track-effect function. For example, base = eq [peak 1kHz (-2dB)] and finish = base |> width 1.2 define a Track -> Track function; apply it as finish (play voice pattern). The effect function can be an alias or a value returned by a helper. The compiler supplies the missing receiver in an ordinary lambda, so the section itself never becomes an audio track.
The mixer record itself must be a literal record or record binding with with overrides; arbitrary functions returning mixer records/lists are not elaborated in these contextual slots. Unrestricted DSP signals on buses, unrestricted implicit sections outside mix context and inline track strips remain outside this stage. Timing is unchanged.
1.3 Sub-mixes: a bus tracks output INTO#
tone = patch { source = saw (freq pitch), gain = vel * 0.3 }
lead = play tone !"c4 e4 g4 e4"
pad = play tone !"[c3,g3]"
az = macro 0.5
submix flyers = [lead, pad, tone] -- a patch here means its LIVE voices
mixer { flyers = { inserts = [place az 0 1], level = -3dB } }
A send copies a track into a bus, and the track still reaches the master in full. A sub-mix member is moved: its finished output — after its own fader, strip and inserts — lands in the sub-mix instead of the master sum, and reaches the master only through the sub-mix's chain and fader. An empty-chain sub-mix is a pure re-route and nulls against no sub-mix at all.
submix <name> = [<member>, …]is a soft keyword withgroup's shape. A member is a track, or a patch — and a patch means the voices a host plays on it by hand (preview_trigger/volta_note_on,one_shot, a shred trigger): everything that would otherwise sum on the default bus and see the master section and nothing else. A track's own pattern-driven voices follow the track, whatever patch they play. A track or patch has ONE output: naming it in two sub-mixes is an error, and a sub-mix is not a member of a sub-mix.mixer { <name> = { inserts = […], level = … } }is its chain and its fader — those two fields only; a member keeps its ownpan,width,muteandsends.insertsis the master's grammar (§1.2's sections, userTrack -> Trackfunctions included) minusvarispeed, which re-times the whole render.leveltakes every form a strip level does (§1.1) and lowers to the chain's last insert, againTrack, wherever it is written: a fader is post-effect.- It is a bus.
tap "flyers",follower "flyers",onsets, the bus meters, chain reuse across a recompile and parameter addresses ("flyers.place.azimuth") all work as they do forbus, and another track may stillsends = { flyers = -6dB }a copy into it. Sub-mixes are listed after the aux buses inModule.buses, so an aux bus keeps its index. placefinally has a home above one track. It collapses its input to a point source, which is meaningless on the master and exactly right on the sum of a few tracks — or on an instrument's live keys (Position sounds in space).- A patch member is how a hand-played instrument shares a chain with its track. The MIRA window (Play a track through MIRA) wires every adopted track as
submix mira<Track>Out = [track, <alias>.inst, <alias>.voice, <alias>.voice1, …voice3]withmixer { mira<Track>Out = { inserts = [<alias>.chain] } }, so the song's notes and the notes played on MIRA's keyboard pass through the patch's effects together. List every patch a host may preview: an imported patch nothing else reaches is not published, so the sub-mix entry is what makes it playable. Because a member has one output and sub-mixes do not nest, a track already in a sub-mix cannot be adopted. Measured offline, not listened to: a chain as a sub-mix insert came out about 2 LU quieter than the same chain piped on the track (one library preset; mira-window Stage 5). The cause is not yet found. - An imported module's master chain rides a sub-mix the compiler mints for that module (
<module>.master), which also takes the returns of the module's own aux buses —BusDef.buses, the one member kind only the compiler can write, because a bus name is not an expression. See Names and library imports § "Imported strips" for the rule, theDoubledMasterChainwarning and theinserts = []opt-out. - Engine. One seam on all three mix paths: where a finished track (or a member aux return) would sum into the master, it accumulates into the sub-mix instead (
finish_track/finish_aux; the staged plan ordersBusStart(submix)after each member'sFinish). Sub-mixes sit after the aux buses, so the ordinary bus pass reaches them last. The anonymous voices of a member patch render straight into the accumulator (VoiceRoutes, resolved from the template a voice holds), which is why the aux accumulators are zeroed at the top of the block with the track buses; held preview notes take the monitor gain first, and a module with a voice sub-mix renders its previews before the mix pass instead of after it. Stem export selects a member's sub-mix the way it selects a send's bus.
2. IR#
Module.mixer: MixerState { tracks: [MixerEntry], groups_mix: [GroupMix], master: MasterEntry?, source_offset/len }, Module.buses: [BusDef], Module.groups: [GroupDef]. BusDef { name, effects, tracks?, voices?, buses? } — a bus with any of the three member lists is a sub-mix (§1.3); each is skipped when empty, so a plain aux bus serializes as it always did. MixerEntry { track, level, pan, width, mute, solo, polarity, sends: [Send { bus, level, pre? }] } — all canonical (linear level, ±1 polarity). level/pan/width, Send.level, MasterEntry.level and GroupMix.level are all Params (untagged: a constant serializes as a bare number — zero golden churn, verified by engine/tests/export_endings.rs::const_master_and_group_levels_stay_bare_numbers_in_the_ir; a macro binding as { macro_ref, initial }, an affine one as { macro_ref, scale, offset, initial }; a persistent graph as { control_ref, unit, scale, offset, initial }, indexing Module.control_graphs). Track.effects and MasterEntry.inserts are Vec<TrackInsert { #[serde(flatten)] effect: TrackEffect, bypass }> — serialized flat, so the wire format is the pre-mixing one plus an optional "bypass": true. MasterEntry.console is a ConsoleFlavor with bool-or-string serde (§3.1): Off is skipped, Purest is the bare true it has always been, C9/La/Mc are "9"/"LA"/"MC". Everything defaults empty/absent: a mixer-less module round-trips byte-identically (golden-tested).
3. Signal topology (native = reference)#
voices → Σ → fader → STRIP (polarity → level·mute → pan-balance → width)
→ [meter tap: rms/peak/clips] → sends (post) ──→ aux input
→ insert chain (bypass-aware) → master sum │
(a sub-mix member: → its sub-mix's input instead, §1.3)
(presends tap pre-strip) ▼
aux: input → TrackInsert chain → [bus meter] → master sum; post-effect
output snapshotted per block for `follower "bus"` / `tap "bus"`
master: encoded Σ → console decode → level·dim → master inserts
→ MONO FOLD → [RMS]
→ master gain 0.7 → soft-knee limiter → soft clip (safety net)
→ out → [LUFS + true-peak taps + clip events]
With a console set, each track/default/aux/monitor summand is encoded on its way into the master accumulator and the bounded sum is decoded once, before ordinary master gain and inserts. A single in-range channel is transparent; overlapping channels interact. Absent/false preserves the ordinary digital sum. Because this is a topology edit, changing it on dense material at a module-swap boundary can click. Which encode/decode pair runs is §3.1.
Levels are relative. Every voice is scaled by VOICE_GAIN (0.5, engine/src/voice/mod.rs) before it reaches its track, and the master applies its own 0.7 gain and limiter, so the absolute level of a render reflects headroom policy rather than the patch alone. Compare levels between windows or tracks of the same render, not against a fixed number.
Strip changes ramp, including mute. Level, mute and polarity chase their targets through a per-sample one-pole with a 10 ms time constant (LEVEL_SMOOTH_TC, engine/src/lib.rs) so a fader move or a mute never clicks. A track muted from the start of a render therefore still passes a short, decaying amount of signal in its first block. This anti-zipper ramp is separate from the ~15 ms smoother on macro knobs. A part that must be silent from the very first sample is written as a rest (!"~ ~ ~ ~"), not muted.
Invalid audio (NaN/Inf) is fenced, not latched. A voice whose block holds any non-finite sample is muted for that block and its node state reset (engine/src/voice/eval.rs). Every active insert slot — track, bus and master chains — scrubs non-finite input to 0, and if its own output goes non-finite the block is muted and the effect reset; a slot that trips again on the very next block (state its reset cannot reach) plays dry until the chain is rebuilt (TrackFx::process_slot, engine/src/effect.rs). A trip also makes the next Run build every chain fresh instead of reusing live ones, and the desktop shows a notice naming the effect and its track or bus. Before the fence, one NaN latched in a reverb or delay: the device stage scrubbed every sample to silence and Stop, which keeps effect tails, never brought it back.
Rendering reads frozen history. The tracks a block has already mixed must not change what later readers in that block see, on any render path (the default all-tracks mix, tapLive ordering, routed returns). Block levels for follower/onsets come from the previous block's snapshot (ordered_rms_prev, read by every track, return and master effect chain), delayed tap reads from the previous block's buses and the insert-tap snapshot (engine/src/control.rs), and only current-block readers (tapLive) read the live endpoint bank. A stem export keeps the original scene's tracks as routing context while only the stem's tracks are audible, and a dependency never starts a track the scene left inactive. An isolated patch audition, which removes the song's track and bus tables, drops the prepared routing catalogue with them (engine/src/audition.rs).
3.1 Console flavours (true / "9" / "LA" / "MC")#
console takes a bool or a flavour name:
master = { console = true } -- PurestConsole (the original; unchanged)
master = { console = "9" } -- Airwindows Console9
master = { console = "LA" } -- Airwindows ConsoleLA — the Quad Eight colour
master = { console = "MC" } -- Airwindows ConsoleMC — the bright MCI colour
master = { console = false } -- ordinary digital sum (the default)
Anything else is a compile error naming the legal set. true is the historical spelling and still lowers to PurestConsole, byte-identically — the IR field serializes as the bare JSON true it always did, and an off console is absent from the wire entirely (compiler/src/lower/tests.rs:: console_round_trips_through_the_ir_wire_format).
| flavour | encode (per summand) | decode (on the sum) |
|---|---|---|
true | sin(x) | asin(clamp(Σ, −1, 1)) |
"9" | Console9Channel's golden-ratio expander, 1 − (1 − 0.618034·x)^φ | Console9Buss's 1 − (1 − Σ)^(1/φ), ×C9_MAKEUP |
"LA" | the whole ConsoleLAChannel: Hull2/Pear three-band split → tone (flat) → SubTight added → Console7 Spiral/Density shaper | the whole ConsoleLABuss: SubTight subtracted → ConsoleZero 2Σ/(3−Σ) → Sinew slew clip, ×LA_MAKEUP |
"MC" | the whole ConsoleMCChannel: fader (pre-EQ, unity) → two Pear crossovers into treble/mid/bass → rate-gated treble averaging → tone (flat) → ResEQ2 mid boost (off) → SubTight subtracted from the bass band → BitShiftPan → sin(clamp(x, ±1.57079633)) | the whole ConsoleMCBuss: four-stage SubTight subtracted → the Console7 buss 0.618034·asin(Σ|Σ|)/|Σ| + 0.381966·asin(Σ) → Sinew slew clip, ×MC_MAKEUP (= 1.0) |
What the flavours sound like. Console9 is the transparent one: its two shapers are exact inverses, so a solo channel comes back bit for bit and only overlapping channels squeeze. ConsoleLA is the opinionated one — a whole Quad Eight channel strip per summand, so it colours even a single source: SubTight tightens the lows (added per channel, subtracted on the buss, which is what makes the summed bottom end tighten rather than each channel's), and the Console7 blend adds the Spiral/Density overtone mix.
ConsoleMC is the bright one, and the one that reshapes the bottom. It is Chris Johnson's initial take on the MCI desk: the fader sits before the EQ ("for MCI consoles, the fader is before the EQ, which overdrives easily"), every summand is split into three bands by a pair of Pear filters, and a two-stage SubTight is subtracted from the bass band alone — then the buss subtracts a four-stage SubTight from the whole sum. The result is a pronounced, deliberate low tightening under an essentially transparent midband and top. Measured on a single channel at small signal, 44.1 kHz (pinned in engine/tests/console_port.rs::console_mc_is_unity_up_top_and_tightens_the_lows):
| 20 Hz | 60 Hz | 100 Hz | 200 Hz | 1 kHz | 8 kHz | 15 kHz |
|---|---|---|---|---|---|---|
| −5.31 dB | −3.47 dB | −2.00 dB | −1.36 dB | +0.11 dB | +0.01 dB | −0.00 dB |
Above 400 Hz the pair is flat to a tenth of a dB. The shape of the tightening is fixed in samples, not in Hz, because upstream scales SubTight's input trim with the sample rate but not its one-pole coefficients — so the 1 kHz reading moves to −0.05 dB at 48 kHz and −0.51 dB at 96 kHz. That is upstream's behaviour, kept.
Level. Every flavour is normalised to unity for a single in-range channel — the transparency contract console = true already advertised — so switching one never silently rebalances a mix against the master fader or the safety limiter. For "MC" read that as unity through the midband and the top: its low tightening is tone, not staging, and it is not compensated. The Airwindows plugins set their pair gain in a pan-law section and a fader, which Volta's strip owns (level, pan) and which the seam therefore drops; one constant on the decode replaces that staging, applied after every nonlinearity so no operating point moves:
- Console9:
C9_MAKEUP = 1.618033988749895, the reciprocal of the channel's own×0.618033988749895input trim. Without it the pair is a flat −4.18 dB at every level (the shapers being exact inverses, the trim is all that survives). With it,decode(encode(x)) == xto the last bit. - ConsoleLA:
LA_MAKEUP = 1.5, the reciprocal of the ConsoleZero buss's 2/3 slope at the origin — exactly the makeup Console0's own channel supplies in the original pairing, which ConsoleLA drops when it swaps in the unity-slope Console7 channel. Small-signal exact only; the LA pair compresses above that. - ConsoleMC:
MC_MAKEUP = 1.0— none, and none is available to derive. Its BitShiftPan (bitshift = 0 ⇒ ×1), its channel fader (F×2 = 1.0) and its buss fader (sqrt(1.0) = 1.0, behind threeif (gain < 1.0)guards) are all exactly transparent at the plugin defaults, and neither shaper carries a trim: the channel ends onsin, slope 1 at the origin, and the Console7 buss weights sum to0.999999999999999848, slope 1 to within 1.5e-16. The pair is therefore the identity at small signal — checked in both directions, which is Console9's lesson — compresses to ×0.9598 around x = 1.015, and is unity again at the channel's ±1.57079633 clamp ceiling, whichsinsends to exactly 1.0 and the buss sends back to π/2. Any constant here would be invented.
Measured on the three-track mix in engine/tests/console_flavors.rs, true, "9" and "LA" land within 0.6 dB of the plain sum. "MC" measures −2.90 dB there — that mix's loudest element is a 33 Hz sine, which is exactly what SubTight is for; filtered above 300 Hz the same comparison is within 0.6 dB like the others, which is what the test actually gates.
Cost. true and "9" are memoryless — one transcendental per sample per summand, no state, no allocation. "LA" keeps a full ConsoleLAChannel per summand (tracks, the default bus, each aux bus, and the two monitor paths), each with a 60-slot Hull ring, ten Pear stages and three SubTight stages: about ten sin calls per sample per summand. "MC" is comparable in arithmetic — nine Pear stages per side, a two-stage SubTight and the output sin on each channel, plus a four-stage SubTight, two asin and a cos per side once on the buss — but it carries a much larger state: the ResEQ2 delay lines are 2005 slots per side, ~32 KB per summand, allocated even though the default MidPeak = 0 makes that stage exactly neutral (the port keeps it so the seam runs the same code the bit-exactness proof covers). Both stateful flavours size their bank control-side at load/swap (AudioEngine::console_slots), never on the audio thread; only the active flavour's arm is populated, so switching "LA" ⇄ "MC" never keeps two banks alive, and both stay empty for true / "9" / false.
Provenance and fidelity. engine/src/console.rs — a port of Airwindows Console9/ConsoleLA/ConsoleMC (c) Chris Johnson, MIT. All six processors are bit-exact against a standalone build of the original processDoubleReplacing (clang++ -O2 -ffp-contract=off): 1 228 800 sample comparisons for Console9/ConsoleLA over 3 sample rates × 5 signal kinds × 5 drive levels, max abs diff 0.0, and 0.0 again for the composed three-strip seam. ConsoleMC adds a fourfold parameter axis on top — its defaults leave both tone controls and the ResEQ2 dormant, so its corpus also drives Treble ±, Bass ±, a 64-tap ResEQ2, an off-centre BitShiftPan and buss faders that make all three of its attenuations live — for 2 519 040 sample comparisons, 0 differing, max abs diff 0.0.
Upstream quirks are kept verbatim and listed in the module header — notably that ConsoleLAChannel clamps its mid and bass bands to ±1 unconditionally but its treble band only when the treble control is negative, and that its freqMid switch has no case 4, so ≥176.4 kHz silently reverts to the 44.1 kHz crossover; and, on the ConsoleMC side, that its ResEQ2 switch has the mirror-image dead case 4 its 1..3 clamp can never reach, that its treble averaging is gated on the raw overallscale rather than the tier (so 48 kHz gets none of it while 88.2 kHz gets two of three stages), that its mid band is never clamped at all, and that its Sinew clamps its stored state at the other end of the sample than ConsoleLA's does. The fpd registers are dropped: they feed only the 32-bit output dither (already commented out in the double path) and the |x| < 1.18e-23 denormal substitution — no DSP, unlike ClipOnly3 — and dropping the substitution is what keeps a stopped transport digitally silent instead of parked on a −146 dBFS floor.
Demo: examples/songs/consoleflavors.volta (one dense mix, one line to swap).
The mono fold is the last thing the master section does, after the inserts. It used to run with level/dim, before the chain, which meant any master insert that manufactures side content from the mid re-widened the "mono" output and the check silently checked nothing: on a mono source, master = { mono = true, inserts = [decorrelate 1] } measured side/mid 0.7833 — bit-identical to mono = false — and haas 0.6 measured 0.3540 (dsp-quality-audit-2026-08-03.md §A5). Folding last is also the only meaning the control can have: what a mono listener receives is the sum of what leaves the master, not of what entered its inserts. It stays a smoothed engage (the same ~15 ms one-pole as level/dim), so toggling it live is a crossfade into the fold, not a switch. Pinned by engine/tests/pan_law.rs::a5_master_mono_survives_a_master_bus_widener and ::a5_engaging_mono_live_is_smoothed_not_switched.
The LUFS / true-peak taps sit after Mixer::write, on the interleaved output, because LUFS and dBTP are delivery quantities: a meter that reads them anywhere else is not describing the file or the device. They used to tap the pre-limiter master bus and so read 3.1 LU hot (the 0.7 output trim), plus up to 7 dB of true peak once the safety limiter engaged — see docs/notes/dsp-quality-audit-2026-08-03.md §B2. The clip count and master RMS stay on the pre-limiter bus on purpose: those are bus warnings ("this mix is hot going into the safety limiter"), and the safety chain is precisely what stops that reaching the output, so measuring them after it would report "fine" forever.
- Engine (
mix_buses, engine/src/lib.rs): strip params are one-pole smoothed per sample (level/mute/polarity) or block-lerped (pan/width — the two fold into ONE 2×2 mix matrix,strip_matrix, built at both block endpoints and lerped across the block; four multiply-adds, no transcendentals); a neutral fast path keeps mixer-less songs at the old cost. Sends accumulate post-strip/pre-effects (presends pre-strip), aux chains run after the track loop, andtrack_levels/track_tapsgain appended bus slots soSourceBus::Bus(n+b)reads followers/taps uniformly (prior-block, like tracks). - Browser: no separate strip DSP. The old Web Audio strip graph (a splitter → 4 gains → merger matrix) was deleted with that path (2026-07-24) — the workbench plays through the native engine's wasm build in an AudioWorklet (Engine Wasm), so the strip surface only pushes canonical values:
ui/src/mixer.rs→ui-wasm/src/web.rs→runtime/public/studio-host.js→runtime/src/worklet-bridge.ts→engine-wasm'sset_strip→ the very samemix_track. One strip law, both backends, by construction. Sends are per-(track,bus) gains off the strip output; every insert is wrapped in a dry/wet selector so bypass toggles without a rebuild — a swap whose only change is a writtenbypassedkeeps the chain and flips that slot (reuse comparesinsert_chain_keywithbypasscleared). - Groups:
GroupBoard(ui/src/groups.rs) folds the live host trim, mute and solo into member strip commands. Graph-valued authored levels add one shared engine control, multiplied into each member's strip gain (§1.1). The engine still has no group mute/solo logic.
3.5 The pan law (one law, everywhere)#
Every placement in the engine — panTrack (constant, pattern or macro), jux, a per-event pan, a unison/supersaw/jp8000 width, a granular cloud's spray, and a hosted plugin's bus — reads exactly one function, engine/src/voice/util.rs::pan_gains_at:
θ = (p + 1)·π/4 p ∈ [−1, 1]
(L, R) = √2·(cos θ, sin θ)
It is the W3C StereoPannerNode mono-input curve, normalised so its centre is unity rather than 0.7071. Three properties, all load-bearing:
p = 0is(1, 1), bit-exactly — the same pair the unpanned path uses. So|> panTrack 0is bit-identical to writing nopanTrackat all, andunison 7 18 0.8 0is level-matched tounison 7 18 0.8.- Constant power,
L² + R² = 2at every position — the same total the unpanned pair carries.panTrackis therefore a pure imaging control: sliding a part across the field never changes what it contributes to the mix. The rails are(√2, 0)— a hard-panned part is +3.01 dB in its surviving channel, which is exactly what keeps its total constant with one channel gone. It is a balance, not a rotation: hard over, the far channel's content is attenuated away rather than folded into the near one (the same thing the strip'spandoes one stage later — §1). - Mono fold: a centred part folds unchanged; a hard-panned one folds to −3.01 dB. Nothing cancels at any position.
Release note — what changed and what songs will hear#
Three sites ran different laws before 2026-08-05 (dsp-quality-audit-2026-08-03.md §A3/§A4). Measured through AudioEngine::process at 48 kHz:
| before | after | |
|---|---|---|
|> panTrack 0 vs no panTrack | −3.010 dB | bit-identical |
|> panTrack p total, any p | −3.010 dB | ±0.000 dB |
|> panTrack 1 mono fold | −6.021 dB | −3.010 dB |
width 0 vs no width (unison, supersaw, jp8000) | −3.010 dB | ±0.000 dB (± the stack's own phase-seed scatter) |
macro panTrack vs the same position as a constant | +3.010 / +5.333 / +6.021 dB at 0 / 0.5 / 1 | 0.000 dB |
hosted plugin panTrack vs a native patch (peak ch.) | +3.01 / +4.42 / +5.33 / +5.85 / +6.02 dB at 0 / 0.25 / 0.5 / 0.75 / 1 | 0.000 dB |
What you will hear. Any part carrying a panTrack or an authored pan is now 3.01 dB louder than it was, and a part carrying a width argument is too — that is the bug being removed, not a new gain stage: those parts were being attenuated by the act of placing them. A song where everything is panned comes up ~3 dB as a whole and just needs the master fader; a song where only some parts are panned will have its balance shift by up to 3 dB in favour of the panned ones, and those are the faders to ride. 291 of 492 shipped songs mention panTrack or width (79 use panTrack). Songs with a macro-driven panTrack, or with a hosted plugin on a panned track, move the other way — those paths were up to 6 dB hot and come down.
Pinned end-to-end in engine/tests/pan_law.rs; the law itself in voice::tests::the_pan_law_is_unity_at_centre_and_constant_power, the plugin call site in lib::tests::plugin_bus_pan_is_the_engine_pan_law, the macro auto-pan in effect::tests::auto_pan_sweeps_the_stereo_field.
4. Live control#
One command family per layer (engine/src/command.rs), all name-resolved control-side and smoothed on the audio thread: Command::SetStrip(track, Level|Pan|Width|Mute|Polarity, v), SetSend(track, bus, v), SetInsertBypass(track, slot, on), SetMaster(Level|Dim|Mono, v), SetSpectra(on). The browser reaches these commands through ui-wasm/src/web.rs and runtime/public/studio-host.js. Right-click a strip's name for ⟲ history of … (the track's versions — Save ideas, compare versions and record takes) and its Automate addresses. Its Channel window's insert dots write bypassed (○), one Undo step; inserts not on the track's line toggle live. Right-click a fader or send bar to type dB (−60…+6, sends …0); Escape cancels. A presend and a send to one bus are two bars (post hall, pre hall), and each previews live: SetSend names its side of the fader (set_send_side; without one it moves the track's first send to the bus). Each bus is a return strip: its fader writes <bus> = { level = … } (live through SetLive once written), its meter reads after the chain, and Scopes gives it a tile. Track faders use SetStrip(Level); the separate SetLevel performance multiplier is not a mixer declaration and is not group-folded. VCAs multiply the track's own strip level exactly once. GroupBoard retains live values while declarations are unchanged; an actual source-level change, including undo, replaces its cached own value.
5. Source authority (native Mixer controls)#
Studio's track level, pan, send and bus-return controls and the Master fader edit the live song's entry source, in the browser too (§5.1). A drag previews sound only; release commits one validated source transaction and one undo step. Typed Enter, keyboard steps, clicks and double-click resets are single commits. Escape cancels a preview. Numeric-token splices preserve comments, unrelated formatting and dotted names; missing fields, entries or a mixer block are created (sends = { hall = -12dB }). The fader writes its own level, never the group-folded level. A literal keeps its author's style (dB or linear); a level or send the Mixer creates is written in dB (silence: 0). Writes are at the strips' resolution — 0.1 dB, hundredths of pan (level = -4.9dB, pan = 0.02). Send bars and the Master fader follow the declaration at every install.
Run current source/import edits first. Formula-, macro- and scene-controlled parameters refuse literal replacement and explain where to edit; a send bar names the macro it rides. A stale revision, changed song or rejected compile leaves source untouched and cancels the preview; old-song commands never target a same-named new track. Mixer undo/redo validates and submits its target module before moving text or history. Failure leaves the history step available to retry. Ordinary typing undo does not run source, and typing immediately after a mixer edit gets a separate undo step. Parked-song mix history requires making it live.
Native mixer source/history commits wait for the audio installation receipt. A refusal leaves text and history unchanged and restores a preview only on its original module. Typing or a newer Run while waiting cannot be overwritten by an older mixer receipt. Save still uses the ordinary source dirty/recovery path; the mixer does not claim the file has been saved. Library songs remain editable copies under the existing Save-a-copy rule.
Boundaries: a host without authored-edit support (the plugin) keeps them live, and every install puts its hand-set levels and pans back. Connected rooms refuse authored mixer edits until origin-aware collaborative mix undo exists. Room transitions retire local mixer snapshot history. Groups, mute/solo, width/polarity, DIM/MONO and other monitoring controls remain transient; an install re-asserts them over the swap's declared targets (MixerUi::reassert_live), and a changed declaration wins. Native Save/Save As/Export settle an active mixer gesture before using its source (§5.2). Complete save/reopen/export agreement, automated overrides and artifact/browser acceptance remain open in Language Studio Consolidation Stage 3.
5.1 Browser installation confirmation#
Run says submitted · waiting for audio engine until the host returns a matching request/owner receipt. Candidate rack/track metadata, successful takes, new-track offers and deferred auto-start actions are not published on submission. Acknowledging older source does not replace newer typing or claim that newer imports ran. Song/room changes retire stale requests; Save As changes the persistence address without clearing unchanged audio metadata.
The page serializes boot, Run, Open, room installs, module-dependent controls, asset-bank mutations and complete autosample zone transactions. Open and room compiles use captured source/import bytes, not whichever buffers exist after an await. Stop is ordered behind an in-flight install: it can wait for asset work, but a completed Stop cannot be undone by that earlier swap's restart.
Receipts distinguish accepted, rejected before installation, superseded before installation, and unknown runtime state. Explicit queue/IR rejection preserves the prior module. A lost/failed worklet is not assumed to preserve it. Optional post-install failures produce warnings while keeping accepted metadata; a fatal worklet failure invalidates runtime state. Acceptance means entry into the audio command stream, not a rendered audio block, complete assets, a saved file or a verified export.
The Mixer's authored edits commit on these receipts; collaborative mixer history and the complete same-snapshot round trip remain open. Evidence: Language Studio Consolidation Browser Install 2026 09 05.
5.2 Save and export snapshots#
Native Save, Save As and Export commit the latest value of an active mixer drag once, including its undo step. If the source, imports or song changed during the drag, the action stops with an error and cancels the preview. If the mix installation is pending, Save/Save As and Export ask for a retry after its decision; they do not snapshot the previous source as if the mix had committed. A failed save keeps the editor dirty and retains its recovery state. Save As dependency handling still needs acceptance when imports have unsaved edits or the destination directory changes.
Export compiles the live song's current editor text, including unrun edits. A compile error never falls back to the last successful module. Make a parked song live before exporting; if a Run is pending, wait for it to finish and retry. Export reflects authored settings, rather than transient monitor/VCA adjustments.
Native export captures source, resolved import bytes and immutable references to loaded sample/wavetable data before starting its worker. The WAV's source stamp uses that same import closure. Browser export compiles its source/import snapshot before queueing, and serializes asset loading and rendering with other bank operations. Both paths refuse missing requested sample/file-wavetable assets; Run the song and resolve those assets before retrying. These checks do not qualify every plugin, live-input or multisample-zone workflow.
The browser delivers 16-bit WAV, a two-second tail and a −1 dBTP ceiling. The dialog offers duration, sample rate and filename; desktop-only stems, container/bit-depth choices, loudness targets and adjustable tails are hidden. Browser WAVs do not embed the source/import archive: save the song separately. Native delivery options and provenance remain available; see Mastering and delivery.
Browser Save, Save As, Keep as song and import saving require a successful document-store write before reporting success. A refused index/body write does not clear the previous saved source or recovery. Save As retains the current import snapshot. This remains browser-local storage, not an external backup or a general cross-tab storage transaction. Tests and remaining host journeys: Launch Work Log.
6. Meters#
- Per track/bus: RMS + per-block peak + cumulative clip events (a contiguous run of blocks whose peak is ≥ −0.1 dBFS counts once; reset at transport start), post-strip/pre-effects (a bus: after its chain), in
VizSnapshot.analysis(trackRms/trackPeak/trackClips+busRms/busPeak/busClips). Peak-hold (1.5 s / 12 dB/s) is frontend. The strip's latched clip lamp shows the count and names its ceiling in the hover. - Ballistics are frontend, and mandatory. The engine writes those numbers for every track every block, unconditionally (
mix_track) — there is no metering "interest" to arm — but they are ONE block wide (~3 ms), so between onsets they are exactly zero. Measured onexamples/m0bench.volta, a raw poll reads 0 in 66 % (kick) to 88 % (bell) of frames. The native strip therefore integrates control-side (ui/src/mixer.rs): instant attack, 26 dB/s release on the RMS bar and on the inspector'srmsreadout, peak-hold for itspeakreadout, and-inf(not−120.0 dB) at the floor. Printing the raw block value is what made the strip inspector look dead on a playing song. - Master:
engine/src/meter.rs— BS.1770-4 K-weighting (tan-prewarped, any SR), momentary/short-term/integrated LUFS (two-stage gating, O(1) histogram spanning −70 … +24 LUFS), 4× polyphase true peak — inVizSnapshot.master(lufsM/S/I,truePeak(Max)dBTP,clips,refLevelDb) — metered on the device output (§3), so the readout is what a BS.1770 meter would report on a capture.clipscounts events at the true-peak tap (an event opens on the first oversampled sample over 0 dBTP and closes after 100 ms under, so one overload burst = 1 andclips > 0 ⇔ truePeakMax > 0 dBTP); the UI holds an acknowledged baseline shared by the timeline master row, the mixer MASTER card and the bay master card — clicking any clears all three (producer audit 2026-08-12 #2). The engine-side reset command is still deferred (§8).ExportRenderreportslufs_integrated+true_peak_maxmeasured over its own finished buffer for the same reason (mastering.md §4). The browser publishes a labelled≈pseudo-LUFS approximation; native numbers are authoritative. - Two honesty rules on the integrated readout: the histogram runs to +24 LUFS (a pre-trim master bus reaches the old +5.0 top in ordinary use) and a block past it counts as saturated (
saturated_blocks()) rather than believed; and material shorter than one 400 ms gating block, where BS.1770 defines no integrated value, reports an ungated short-window measure flagged byintegrated_is_short()instead of reading as−100 LUFSsilence (mastering.md §3).
6.5 Plugin delay compensation (PDC)#
Native engine only (Production tools in Studio Stage 5). Every hosted plugin's declared latency is queried once at activate — CLAP latency extension, VST3 getLatencySamples (post-setupProcessing), AU kAudioUnitProperty_Latency — cached on the backend (PluginBackend::latency_samples) and carried in PluginManifest/LoadedPlugin.latency (reused instances keep theirs). At load/hot-swap the control side computes each track's total (instrument + insert sum) and ships per-track CompDelay integer stereo rings (effect.rs) in the LoadPayload; the audio thread delays each track by max − own post-effects / pre-master-sum (bench taps see the aligned signal), retiring old rings to the graveyard. The io panel lists per-plugin latency and the compensation target. Limits: live bypass toggles don't recompute until the next swap; aux-bus send paths are reported, not compensated; mid-flight latency changes refresh at the next load.
6.6 Correlation + goniometer (phase metering)#
engine/src/correlation.rs, published on the viz frame's phase trailer and drawn by ui/src/phase.rs. Answers the two questions a mono fold-down raises: will the sum survive? and is the sub mono?
- What is measured. The zero-lag normalized cross product
ρ = Σ l·r / √(Σ l² · Σ r²)∈[−1, +1]—+1mono,0uncorrelated,−1cancels outright on a mono sum. Read off the same post-limiter device output the BS.1770 meter reads (§6), so the phase readout and the loudness readout can never be describing different signals. - Ballistics. The three products are integrated with a one-pole exponential window (≈300 ms) and
ρis resolved from the smoothed accumulators, not as a smoothed ratio. Smoothing the ratio would let a near-silent instant (a wild ratio off two tiny numbers) whip the needle; smoothing numerator and denominator separately weights each instant by its own energy, which is what a needle meter's inertia does. Below a mean-square floor (≈ −100 dBFS)ρis undefined and the meter holds its last reading rather than snapping to zero in every gap. - Bands — the actionable part. Four coefficients ship: broadband plus
< 120 Hz,120 Hz – 2 kHz,> 2 kHz, split by two cascaded one-poles per channel (low = lp120,mid = lp2k − lp120,high = x − lp2k). This is a metering split, not a crossover: gentle slopes, no inter-band phase guarantee, never heard. The< 120 Hznumber is the one the guides mean by "keep the sub near +1" — a mono body keeps the broadband reading positive while a polarity-flipped sub vanishes on a club rig, and only the band bar catches that (pinned incorrelation.rs'sout_of_phase_sub_is_caught_by_the_band_readout). - Goniometer. A stride-decimated
(l, r)scatter — 64 points per block, spanning the block rather than its first samples — accumulated display-side over ~24 frames with an age fade, plotted rotated 45° so mono stands vertical and anti-phase lies horizontal (the studio orientation). Anti-phase points draw in the alert colour. - Surfaces. The mixer MASTER strip carries two compact bars (
CORRbroadband,SUB< 120 Hz); the monitor view (Scopes) carries the fullPHASEpanel — goniometer plus all four bars with a slow-release worst-case hold tick. Every readout carries an accessible name (correlation <band> <value>,goniometer · N points). - Transport. The numbers ride an append-only trailer on the binary viz frame (
viz::encode_frame→ui/src/frame.rs::PhaseFrame), so both shells get them from one encoder. A frame from an engine that predates the trailer decodes withphase: Noneand the UI reads "unavailable" rather than a confident+0.00. Browser parity needs a./build-engine-wasm.sh.
6.7 Reference-track A/B#
Load a commercial mix, level-match it to the session, flip. ui/src/app/reference.rs plus three defaulted EngineBridge methods (pick_reference, load_reference, play_reference).
- Loading. Any format the sample decoder handles (symphonia: wav/mp3/flac/ogg/m4a/aac), resampled to the engine rate and measured with
offline::measure_loudness_detail. The decoded buffer stays shell-side; the UI only ever holds aReferenceTracksummary (name, path, seconds, LUFS, dBTP). - The flip is loudness-matched, not level-matched. Gain is
session LUFS − reference LUFS, recomputed at every flip off the master meter's live short-term reading (falling back to integrated, then to −14 LUFS when the transport has never reported one), clamped to ±24 dB. A reference 3 LU louder does not get to win the comparison.Command::PlayPcm's gain ceiling is 8.0 (+18 dB) for this reason — matching a −16 LUFS reference into a −6 LUFS session needs +10 dB, and a silently clamped match is a broken comparison, not a safe one. Nothing is at risk: the PCM sums before the master chain and the safety limiter. - Soloed, not summed.
PlayPcm { solo: true }— the same seam the take-history scrubber uses — ducks the live mix under the reference on a smoothed one-pole, so the boundary is click-free and no mix bleeds through. - Everything downstream follows. The PCM sums in before the master tap, so the analyser, the bus scopes, the LUFS readout and §6.6's phase meter all show the reference while it is flipped in. That is the reason to route it through the engine rather than a side channel.
- Continuity. The reference keeps its own playhead across flips (and wraps at its end), so A/B-ing the same eight bars twenty times compares the same eight bars.
- Controls. The
◇ MIX/◆ REFchip on the monitor view's REFERENCE strip,Transport ▸ Reference A/Bin the native menu, or ⌘⇧R from any view — all three route through onetoggle_reference, so they cannot diverge. - Persistence. The chosen path is remembered per song in the shell state (
refab=<song>*<path>|…, besidesongfav/recent, bounded to 12 and dropping any path carrying a separator — the blob has no escaping). It is adopted lazily on song open and decoded on the first flip, so a launch never stalls decoding a five-minute master. - Degradation. Browser shells decline through the bridge defaults (no wasm file picker yet); the panel draws and explains itself instead of offering a dead control.
7. Auto-mix (diagnose + propose; never act)#
- Dominance (Patch Cable Bay Sum cards): % of post-gate energy per summand, amber "dominating" badge ≥ 70%. Browser: per-gate analysers behind a consumer-refcounted enable. Native: preview-scoped summand accumulators in a trailing (back-compatible)
engine_preview_tapssection. Diagnostic only. - Headroom (transport bar):
headroom −x dB · clips nfrom the master true-peak; clip-log popover ranks offenders; a hot master offers "trim master −3 dB" (or a single hot track), applied through the commit path, dismissal re-arms on new clips. Uniform trims only. - Conflicts (panel next to headroom): 8-band weighted min-overlap pair scores, 8-tick average with hysteresis; rhythmic (envelope CoV ≥ 0.6) ⇒ a
followerduck on the target's gain (the anthem/overclock idiom, 8ms/100ms kick-like else 30ms/300ms, depth 0.4); tonal ⇒ a per-voice|> eqcut (−3 dB in the mapped band). Proposals are diff cards with Apply (source splice; imported synths ⇒ not applicable, text provided) and Dismiss (keyed on target-span text). Live input: browser per-bus FFTs, or the native 8-band biquad bank (engine/src/spectra.rs, gated byengine_set_spectra,analysis.trackBands). Full-song scan renders per-track stems offline on either backend. Never auto-balances, never auto-pans, never edits silently.
8. Deferred#
Browser LUFS/true-peak worklets (§5.3); a wet/dry engine command + commit; native clip-reset command; bus-level fader (no IR field); native live conflict-threshold calibration vs the browser scale; the open §11 questions (0 dB fader law, LUFS reset gesture beyond transport-start, mute mid-tail).
9. Tests#
compiler/tests/mixer_expressions.rs covers shared effect IR, reusable values, scopes, units/liveness, cycle/receiver checks and inherited EQ handles. mixer_dotted_names.rs, insert_spans.rs, fmt_corpus.rs, automation.rs and the unchanged golden IR corpus pin compatibility; ui::mixer_edit tests pin literal source edits and refusal of computed/shared records.
engine/tests/{mixer_strip,master_section,insert_bypass,bus_sends,spectra,automation_mix}.rs (audible/behavioral, full process() path), engine/tests/export_endings.rs (the §1.1 macro/affine master fader: ramped_macro_master_level_fades_the_whole_mix, affine_master_level_multiplies_into_the_authored_trim, and the golden-churn guard const_master_and_group_levels_stay_bare_numbers_in_the_ir), engine/src/meter.rs unit tests (BS.1770 coefficients < 1e-9, exact −20 LU tracking, intersample recovery), engine/tests/console_port.rs (the six Airwindows processors bit-exact vs the C++, plus the §3.1 makeup/transparency properties) and engine/tests/console_flavors.rs (all four flavours end to end, with console = true / "9" / "LA" pinned bit-identical across every flavour addition), compiler lowering tests + the console / consoleflavors golden snapshots, and the control-side Rust suites: ui/tests/mixer_console.rs (the console surface), ui/src/groups.rs unit tests (the group-cascade truth table) and ui/src/app/mixer_transaction_tests.rs (the §5 source transactions).