Skip to content

feat(studio): the rack redesign — named jobs, two faces, ruler, schematic - #3160

Open
vanceingalls wants to merge 21 commits into
wa-20c-rack-copyfrom
wa-20d-rack-design
Open

feat(studio): the rack redesign — named jobs, two faces, ruler, schematic#3160
vanceingalls wants to merge 21 commits into
wa-20c-rack-copyfrom
wa-20d-rack-design

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

The rack redesign the review page had been showing all along. The PR below this
wired the language; this is the drawing and the structural rules it serves.

Prompted by a good question — is everything designed as seen in the preview?
It was not: half of EFFECT_COPY was still unread and none of the visual
direction had been built.

The one that changes the catalogue

d7d36de35 — the range IS the module.

"Shape One Range" has three controls — where, how much, how wide — and no honest
way to nominate one as the knob that matters. The copy nominated how much,
which is incoherent: boosting an unspecified frequency means nothing. The range
is the first decision, not the second.

So the add menu offers the decision instead of the machine: Tame Boominess,
Reduce Mud, Reduce Boxiness, Add Clarity, Soften Harshness
, each a peaking
filter with its frequency already chosen, ordered low to high because that is
the order an author hears them in. peaking is no longer offered as itself —
two doors to the same effect, one of them the incoherent one, is worse than
either alone.

Every job is one the preset catalogue already ships, at the settings it ships
it with, and a test asserts that.
The list names the vocabulary the presets
were written in rather than inventing a second one; a job nobody's preset does
is a guess about what authors want.

It needs no new node field — label already names a node for the work it does,
so a job node and a preset node are indistinguishable afterwards, which is
right. It also dissolves the duplicate-name problem at the root rather than
papering it with a role label: Clean Voice reads Remove Rumble · Reduce Mud ·
Even Out Loudness · Add Clarity · Peak Ceiling
, and nothing repeats.

The three rules

Two faces (736597572) — a module opens on its name, a line saying what it
is for, and the single control that carries it; everything else is behind a
Details disclosure, which is also where the DSP name now lives. Nothing is
hidden, it is ordered.

Ten of fifteen effects get this from a real parameter. The other five get a
derived one — see below.

Worth naming: peaking's primary is "how much", and that only became coherent
in the commit before this one. Two faces is honest for it because the jobs
landed first
, which is why the order in this PR is the order it is.

The shared ruler (04a28135a) — BANDS has named seven ranges since the
copy layer landed and nothing showed them; the rack spoke entirely in Hz, which
mean nothing to somebody who has not been taught them. Every spectral module now
shows where it acts on one ruler, with the range named underneath. Log-spaced,
because hearing is: rumble is 20–80 Hz, three tenths of one percent of the range
linearly and about a fifth of it by ear, so a linear layout collapses the bottom
six bands into a sliver and teaches nothing. A test asserts that, and a linear
layout fails it.

Family lettering and the tint step (05c09053f) — a rack of eight modules
is eight lines of text, and reading it should not mean reading eight names. The
sans carries four families apart by weight, case and tracking; the serif is
spent on the one family that generates signal rather than measuring or shaping
what is there; monospace goes to the measuring modules, because what they show
IS a readout. Alongside it a tint step per module within its family, derived
from registry position so adding an effect never re-colours its siblings. It
sits on the card's left edge rather than the text, which already carries the
family in its lettering.

One knob for the five that cannot have one

29d1cff42. A compressor has seven controls and an author wants one, but
unlike a filter or a delay no single one of them can be its face: threshold
means nothing without ratio, ratio means nothing without make-up gain. So the
knob is derived, exactly as carveProfile already turns one number into six.

Continuous, not the three-point tables the design proposed — a table makes
gentle/middle/strong three settings to pick between, and the thing being
modelled is one axis.

Three of the five sets of figures were wrong, and only rendering showed it.
Measured through the real engine path on 20 s of narration; full numbers and
method in ~/audio-fx-profiles-ab/README.md.

  • Compressor make-up was too small, and not linear. The proposed 1/3/7 dB
    left the track 2.5 dB quieter at full evenness — an evenness knob that
    turns the track down as it goes up. Gain reduction accelerates as threshold
    and ratio move together, so linear was wrong too. Now s² × 9.5: spread
    falls 19.1 → 8.8 dB with the level unmoved.
  • Saturation's trim went the wrong way. A soft clipper at −18 dB threshold
    is a limiter at −18 dB: the proposed −3 dB trim took the peak from 0.496 to
    0.089 and nearly halved RMS, so "Warmth" mostly meant "much quieter".
    Reversed; RMS now holds within 0.4% across the knob while the peak comes
    down, which is saturation rather than attenuation.
  • The gate did essentially nothing. The design derived threshold and range
    only; at the effect's 100 ms default release it moved the gaps 0.1 dB
    against a range asking for 30. Swept, release dominates everything else —
    0.2 dB at 140 ms, 13.4 dB at 10 ms — so it joins the profile. Gaps now fall
    29.6 dB with speech unmoved to a tenth of a dB.

Reverb and bitcrush measured correctly as proposed and are unchanged.

Worth one line for a reviewer: the first measurement reported the gate as
working when it was doing nothing at all.
A speaking-window measure excludes
exactly the windows a gate acts on, so the gaps are measured separately.

In the panel the derived knob writes mechanism, never itself: the chain stores
what renders, and audioFxProfileStrength reads the knob back by inverting the
curve — the same contract normalizeCarveSettings has. It renders through the
ordinary FxParamRow rather than FxNodeParams, because it has no AudioParam
behind it and nothing to automate; the parameters it sets are automatable
individually under Details.

The schematic

9ae8f03fd, translated to one column. IN and OUT terminals — two lines,
and they change what the rack is: without them the order reads as a list, and a
list is the one reading that makes "move up" look cosmetic when it is the most
consequential control here. Every step numbered, counted over what the rack
shows rather than the chain, because the carve's filters and an EQ's bands live
inside their own modules and counting raw nodes would leave the rack jumping
from 02 to 07. A preset draws as one thing — consecutive nodes only, since a
preset pulled apart by a reorder is no longer a unit and a bracket around the
gap would claim an adjacency the signal path does not have.

Tests

core 1762 (113 files) · studio 3695 + 18 todo. Every non-trivial change was
falsified — the notes are in the commit bodies.

One flaky test seen once in eight full-suite runs and not from this branch:
timeline performance fixture > generates an identical 50k keyframe-heavy-expanded fixture times out at 5.7 s under full-suite load, and passes 5/5 in isolation.
Its file imports nothing from the audio code.

Known debt, and one thing not done

propertyPanelFxSection.tsx is back over the 600-line cap at 670. Extracting
its add menu was tried and abandoned: the shelf needs about fourteen props
to lift out, and pulling it apart splits the hover-audition wiring across two
files — which is exactly where both audition bugs in the PR below came from. If
it is done, do it with a context rather than props.

None of this has been looked at in a running studio yet. Everything is
unit-tested and falsified, but tests do not tell you whether the tint step is
too subtle at 32% saturation or the ruler is noise under a filter you are
dragging.

🤖 Generated with Claude Code

The rack was renamed and the add menu was not, so the author picked
"High-pass" and a module called "Remove Rumble" appeared. That is the
exact confusion the plain-language layer exists to remove, reintroduced
one gesture upstream of it.

The menu now reads `EFFECT_COPY[…].title`, and the tooltip carries the
copy's own "what it is for" line with the registry name in brackets —
taught rather than withheld, which is rule 3 in
`plans/audio-fx-ux/README.md`.

Tests that clicked add-menu entries by their registry label now look the
name up, for the same reason the knob rows already do.

Falsified: putting `d.label` back fails three tests.

studio 3685 passing, 18 todo.
"Shape One Range" has three controls and no honest way to nominate one of
them as the knob that matters. The copy nominated *how much*, which is
incoherent: boosting an unspecified frequency means nothing. The range is
the first decision, not the second — `plans/audio-fx-ux/README.md`
§"The hole in the single-knob rule", where B is named as the answer.

So the add menu offers the decision instead of the machine. Five jobs —
Tame Boominess, Reduce Mud, Reduce Boxiness, Add Clarity, Soften
Harshness — each a peaking filter with its frequency already chosen, low
to high, which is the order an author hears them in. Picking the module
is picking the range, so one knob is honest rather than a simplification
hiding the real choice. `peaking` is no longer offered as itself: two
doors to the same effect, one of them the incoherent one, is worse than
either alone.

Every job is one the preset catalogue already ships, at the settings it
ships it with — a test asserts that. The list names the vocabulary the
presets were written in rather than inventing a second one beside it, and
a job nobody's preset does would be a guess about what authors want.

It needs no new node field. `label` already names a node for the work it
does, so a job node and a preset node are indistinguishable afterwards —
which is right, because they are the same idea. It also dissolves the
duplicate-name problem at the root rather than papering it: Clean Voice
reads Remove Rumble · Reduce Mud · Even Out Loudness · Add Clarity · Peak
Ceiling, and nothing repeats.

Underneath it is an ordinary peaking node. Details opens on the same
three controls it always had, and the frequency is a starting point
rather than a cage.

Falsified: offering `peaking` alongside its jobs fails the menu test.

Note for the next session: adding a core subpath needs `bun run build` in
packages/core, not just the subpath sync. Studio's PropertyPanel tests
resolve `@hyperframes/core/*` through the `node` condition, which points
at `dist/` — so a new module resolves in the FX tests and fails in those
with a "does the file exist?" that looks like a config error.

core 1750 passing (112 files), studio 3687 passing / 18 todo.
…ick away

Rule 1 of `plans/audio-fx-ux/README.md`. A module opens on its name, a
line saying what it is for, and the single control that carries it; every
other parameter is behind a Details disclosure. Nothing is hidden — it is
ordered. `EFFECT_COPY.primary` names that control and `primaryEnds` says
what its two ends sound like, which is the question an author actually
has: a number tells them where the knob is, not which way to move it.

The DSP name moves onto the disclosure itself, so it is read at the
moment the author asks what this really is and never before.

Ten of the fifteen effects get this. The other five — compressor, gate,
saturate, reverb, bitcrush — name their primary as "strength", meaning
they want a single DERIVED control over several parameters, which is
`PROFILES`, whose figures are proposed rather than measured and which has
not shipped. They open on all their controls until it does. That is the
honest state: inventing one knob for them now would be a knob that lies
about what it sets. The lookup needs no special case for it either —
"strength" names no parameter today, and the day it does name one, this
starts using it without being told.

Worth naming: `peaking`'s primary is "how much", and that only became
coherent in the commit before this one. Boosting an unspecified frequency
means nothing, so one knob was a lie while the module was generic; now
that picking the job is what picks the range, it is the whole truth.

The disclosure is local state, keyed by node like the row itself, so it
stays with its effect across a reorder — and an effect arriving in the
open slot arrives closed, like any other.

Tests that address a non-primary knob now open Details first, which is
also the gesture a user makes. Falsified: expanding Details unconditionally
fails the two-faces test, and dropping the "is it a real parameter" guard
fails three.

studio 3688 passing, 18 todo.
`BANDS` has named the ranges since the copy layer landed — rumble,
weight, mud, middle, presence, edge, air — and nothing showed them. The
rack spoke entirely in Hz, which mean nothing to somebody who has not
been taught them, and the words that would teach them sat unread in core.

Every spectral module now shows where it acts on one ruler. Two things at
once, deliberately: the bar says where this module works relative to
everything else, and the caption names the range and what lives there.
A bar alone is decoration; a caption alone does not teach the shape.

Log-spaced, because hearing is. Rumble is 20–80 Hz — three tenths of one
percent of the range linearly and about a fifth of it by ear — so laid
out linearly the bottom six bands collapse into a sliver and the ruler
teaches nothing. A test asserts the bottom segment is over a tenth of the
bar, which a linear layout fails.

Segments outside what the module can reach are dimmed rather than hidden:
a filter that only works in the bottom three bands should not look like
it could move anywhere, and it should still be legible against the whole
range.

`audioBandAt` clamps past both ends rather than returning nothing — 15 Hz
is still rumble to anybody who can hear it, and the alternative is a
filter parked at the edge of its range having no name at all. Boundaries
belong to the band they open, or a filter at exactly 250 Hz reads as
Weight while the ruler beside it highlights Mud.

Nothing under an effect with no range to place: there is nothing spectral
about a limiter, and a bar under one would be a decoration claiming to be
information.

core 1754 passing (112 files), studio 3690 passing / 18 todo.
A rack of eight modules is eight lines of text, and reading it top to
bottom should not mean reading eight names. Each family letters
differently, so an author knows what KIND of module they are looking at
with the label out of focus and the word only confirms it.

Two faces of type, as budgeted in `plans/audio-fx-ux/README.md`. The sans
carries four families apart by weight, case and tracking — filters light
and widely tracked like a measuring instrument, dynamics heavy and tight
because it grips the signal, time thin and very wide because it is
atmosphere rather than control. The serif is spent on the single family
that behaves differently from all of them: non-linear is the only one
that generates signal rather than measuring or shaping what is there, and
it should not look like the others.

`smart` is the fifth and is not a registry group — the carve, the Tone EQ
and the leveller, which measure the audio and write their own settings.
Monospace, because what they show IS a readout: numbers something else
decided.

Alongside it a tint step per module WITHIN its family, derived from
position in the registry rather than assigned by hand, so two filters
read as two different modules without reading as two different families
and adding an effect upstream never re-colours its siblings. It sits on
the card's left edge rather than on the text: the name already carries
the family in its lettering, and colouring it too would fight the panel's
own tokens for automated and bypassed. Kept to 62–76% lightness at low
saturation for the same reason.

Falsified: flattening the tint step fails one test, and lettering
non-linear like dynamics fails the other.

studio 3692 passing, 18 todo.
The last of the schematic direction, translated to a one-column panel
rather than the wide diagram the review page draws.

**Both ends named.** IN — this track, OUT — to mix. Two lines, and they
change what the rack is: without them the order reads as a list, and a
list is the one reading that makes "move up" look cosmetic. It is the
most consequential control in the panel — chain order is audible.

**Every step numbered**, counted over what the rack SHOWS rather than
over the chain. The carve's filters and an EQ's bands live inside their
own modules, so counting raw nodes would leave the visible rack jumping
from 02 to 07, and the numbers would read as a bug rather than a
position.

**A preset draws as one thing.** Applying one used to drop five loose
rows in with nothing saying they arrived together — the same failure the
carve module exists to fix, one level down. Consecutive nodes only: a
preset pulled apart by a reorder is no longer a unit, and a bracket
around the gap would claim an adjacency the signal path does not have.

Falsified: numbering by chain index fails the path test, and grouping a
preset's nodes regardless of adjacency fails the run test.

studio 3695 passing, 18 todo.
@vanceingalls vanceingalls changed the title wa 20d rack design feat(studio): the rack redesign — named jobs, two faces, ruler, schematic Aug 10, 2026
The last unbuilt piece of the rack design. A compressor has seven
controls and an author wants one — but unlike a filter or a delay, no
single one of them can be its face: threshold means nothing without ratio,
ratio means nothing without make-up gain. So the knob is derived, exactly
as `carveProfile` already turns one number into six for the carve.

`EFFECT_COPY[id].primary === "strength"` was already the copy layer saying
"this module wants this treatment", and a test now asserts the profile set
matches that list exactly — a missing profile leaves a module opening on
all seven controls, and an extra one is a knob nobody asked for.

**Continuous, not the three-point tables the design proposed.** A table
makes gentle/middle/strong three settings to pick between, and the thing
being modelled is one axis — which the carve's knob has already proved
reads. The proposal's figures survive as anchors where they held up.

## Three of the five figures were wrong, and rendering is what showed it

Full numbers and method in `~/audio-fx-profiles-ab/README.md`.

- **Compressor make-up was too small and not linear.** 1/3/7 dB left the
  track 2.5 dB QUIETER at full evenness — an evenness knob that turns the
  track down as it goes up. Gain reduction accelerates as threshold and
  ratio move together, so linear was wrong too; solved as s² × 9.5. Spread
  now falls 19.1 → 8.8 dB with the level unmoved.
- **Saturation's trim went the wrong way.** A soft clipper at −18 dB IS a
  limiter at −18 dB: the proposed −3 dB trim took the peak from 0.496 to
  0.089 and nearly halved RMS, so "Warmth" mostly meant "much quieter".
  Reversed to s² × 2.8 up; RMS now holds within 0.4% while the peak comes
  down, which is the signature of saturation rather than attenuation.
- **The gate did essentially nothing.** The design derived threshold and
  range only; left at the effect's 100 ms default release it moved the
  gaps 0.1 dB against a range asking for 30. Swept, release dominates
  everything else — 0.2 dB at 140 ms, 13.4 dB at 10 ms — so it joins the
  profile. Gaps now fall 29.6 dB with speech unmoved to a tenth of a dB.

Reverb and bitcrush measured correctly as proposed and are unchanged.

The first measurement reported the gate as working when it was doing
nothing at all: a speaking-window measure excludes exactly the windows a
gate acts on. Gaps are measured separately for that reason.

## In the panel

The derived knob renders through the ordinary `FxParamRow`, not through
`FxNodeParams` — it has no AudioParam behind it and nothing to automate.
What automation there is belongs to the parameters it sets, under Details,
where they can be aimed at individually.

It writes mechanism, never the knob: the chain stores what renders, and
`audioFxProfileStrength` reads the knob back by inverting the curve — the
same contract `normalizeCarveSettings` has. A profile merges over what is
there, so a compressor's knee and a saturation's curve type survive the
knob moving.

Falsified: a derived parameter that turns around mid-sweep, one that runs
past its range, a profile that replaces rather than merges, and a panel
that writes the knob instead of the mechanism each fail a test.

core 1762 (113 files) · studio 3695 + 18 todo.
…aults

Found in a running studio, which is the only place it could be found: add
a compressor and the module opens reading **Evenness 0.67** with its
make-up gain at 0 dB.

The registry's defaults are not a point on the profile's curve, and
`audioFxProfileStrength` reads the knob back by inverting that curve — so
a default-seeded compressor reports a strength it was never set to, and
every parameter under the knob disagrees with it. That is the
"quieter as you turn it up" failure the measurement pass fixed, arriving
on the very first frame instead.

Adding a profiled effect now seeds it through the profile at 0.5, so the
knob and the mechanism agree from the start. Everything else still
arrives exactly as the registry declares it.

Falsified: restoring the default seed fails the new test. The old
"seeded with its declared defaults" case asserted the behaviour being
fixed, and now uses an effect that has no derived knob — which is what it
was really about.

studio 3696 passing, 18 todo · core 1762.
The bracket said a preset was one thing the author added, and then made
them treat it as five: every member module had its own On / ↑ / ↓ / ×
and the preset itself had none. Switching off Telephone meant reaching
into seven modules and toggling each — exactly the bookkeeping the
bracket exists to remove.

The run head now carries the two controls that belong to the whole:

- **On/Off bypasses every node it wrote**, and leaves anything the author
  added themselves alone. A bypass, not a delete: the settings survive,
  which is what makes a preset worth trying rather than committing to.
- **× takes it back out whole**, with its lanes. An orphaned lane keeps
  driving a parameter that is no longer in the graph, and with ids minted
  lowest-free the next effect added inherits it — the same contract
  removing a single node already has.

It reads as On while *any* of it is still running. "Some of it is
bypassed" is not a state an author set, it is one they arrived at by
toggling a member, and the switch has to offer to stop the preset rather
than claim it has already stopped.

Also fixes the mount helper in the section tests, which never passed
`onRemoveNodeAutomation` — so nothing in that file could have caught a
lane leak on removal.

studio 3700 passing, 18 todo.
A preset's nodes share no automatable parameter, and its worklet effects
(compressor, limiter) expose no AudioParams at all — Clean Voice could
only ever have automated 3 of its 5 nodes. So there was nothing to aim a
lane at, and no way to bring a preset in gradually.

**The graph wraps each preset's run in a wet/dry pair.** The rest of the
chain stays a strict series, which is right for an effect the author
placed: it is either in the path or it is not. A preset is not one effect
— it is several added as a unit, and "how much of it is applied" is a
question about the unit. One crossfade answers it for every preset
including the worklet ones, and it cannot go half-wrong the way seven
lanes can. The dry leg bridges the whole run, so amount 0 is the
untouched signal rather than a quieter version of the processed one.

Consecutive nodes only, matching what the rack already brackets: a preset
pulled apart by a reorder is no longer a unit, and wrapping across the
gap would route the effect between its members through the dry leg too.

**`fx.preset.<id>` is a new lane target.** Parsed before the 3-part fx
form, which it structurally is — with a reserved node id an effect can
never have, since ids are minted `n1`, `n2`, …. It resolves only for a
preset the chain actually carries, so a lane left behind by a removed
preset is dropped at read time, the same contract an orphaned node lane
has.

**Off is amount 0, not `enabled: false`.** The switch and the lane are
now the same value — one notion of how much of a preset is applied, with
the switch at its two ends. Writing `enabled` would take the nodes out of
the graph, which a lane cannot do part-way and cannot do without a
rebuild that restarts the audio. The panel also grows an Amount slider,
so half-applied is something an author can just set.

Changing the amount is a values-only update, pushed into the running
graph. A `presetAmount` in a chain is clamped to 0..1 on the way in: two
gains in opposition, so past 1 the dry leg goes negative rather than the
preset getting louder.

Falsified: a blend ignoring the stored amount, an amount not pushed on
update, and the clamp each fail a test. The round-trip — the §4 invariant
that a new node field must be in BOTH the parser and the writer — had no
test until a mutation survived one; dropping it from either half now
fails.

core 1770 (113 files) · studio 3703 + 18 todo · engine services 739 + 3.
Hovering a preset auditions it on the running audio, and that was
invisible: the shelf looked identical whether the audition was playing or
the pointer just happened to be resting there. An affordance nobody can
see they triggered is one they do not learn.

The hovered row lights in the accent at low alpha, and four bars animate
at its right edge. Deliberately the accent rather than a neutral grey —
the row is lit BY the thing that is playing, so the highlight and the
bars should read as one event rather than as a hover style that happens
to sit near an animation.

Only rendered when there is an audition channel to hear it through, so
the panel never claims audio that is not happening. Four bars because at
this size that reads as a level meter; three reads as an ellipsis. They
are staggered so it reads as a waveform travelling rather than four bars
pumping in unison, which reads as a spinner — and a spinner would say
"working", the opposite of what is true.

Under `prefers-reduced-motion` the bars hold at a fixed height instead of
disappearing: they are telling the author something is playing, and that
fact does not go away because the animation does.

Verified in a running studio — synthetic `mouseover` does not trigger CSS
`:hover`, so this was driven with a real pointer move: opacity 1, bars
`rgb(60, 230, 172)`, row `rgba(60, 230, 172, 0.12)`, and nothing left lit
after the pointer leaves.

Falsified: rendering the wave unconditionally fails the new test.

studio 3704 passing, 18 todo.
Opening either menu hid both buttons, and the only thing that set them
back was picking something. So an author who opened one and changed their
mind had two ways out: add an effect they did not want, or deselect the
clip and lose their place. Escape did nothing.

The buttons now stay and close what they opened — same control, toggled,
reading "Close" while its menu is up. Opening one closes the other, since
two menus at once is two surfaces over the rack with nothing saying which
the next click belongs to.

Escape closes whichever is open. Bound on the section rather than the
window, because a keystroke aimed at the timeline is not aimed at this,
and it only stops propagation when it actually has a menu to close — the
panel has its own Escape handling and swallowing the key unconditionally
would break it.

Closing reverts an audition in flight, for the same reason leaving the
shelf with the pointer does: a preview left playing is audible over a
chain the document does not have.

Verified live, stepping one render at a time — clicking through the whole
cycle in a single synchronous pass batches into one React render and only
shows the final state, which is what made this look fixed when it was not.

Falsified: a button that only opens, an Escape that does nothing, opening
one menu without closing the other (in EITHER direction — the first test
only covered one, and the reverse mutation survived it), and closing
without reverting the audition each fail a test.

studio 3710 passing, 18 todo.
Hovering a preset writes it to the running graph — which is silent while
the transport is paused. So the whole affordance only worked mid-playback:
a paused author hovering the shelf heard nothing at all and had no way to
know the feature existed.

Hovering now starts playback from wherever the playhead sits, and leaving
stops it and returns the playhead to exactly where it was found. Browsing
the shelf is not an edit and must not cost the author their place.

Two guards, each with a test:

- **A transport the author started is left alone**, in both directions.
  Stopping their playback because they passed over a preset would be the
  panel taking a decision nobody offered it.
- **Every path that ends an audition stops the transport** — leaving,
  applying, and the panel unmounting. A click means "keep this", not "and
  carry on playing from wherever the audition reached".

Order matters at both ends: the chain goes into the graph before playback
starts, or the first moment heard is the un-auditioned mix; and playback
stops before the chain reverts, so the last thing heard is the preset
rather than a frame of the old chain coming back.

`playbackRequest` on the player store follows `requestedSeekTime`: the
panel cannot reach `useTimelinePlayer`, which is a single instance owned
by the shell. It carries a nonce because two hovers in a row both want
play, and without one the second is indistinguishable from the first
already having been served.

Falsified: not returning the playhead, and hijacking a transport the
author started, each fail a test.

studio 3716 passing, 18 todo.
A chorus with its wobble dialled up past width and into the effect
itself: 14.6 ms spread, 2.57 ms depth, 10 Hz — the top of the speed
range — and fully wet, so no straight signal is left to anchor the pitch.
Character family, beside Telephone and Megaphone.

Measured on a steady 440 Hz tone, which is where pitch modulation is
legible: dry holds at 420 Hz across the whole take, worbled swings
**380–500 Hz**.

Worth recording, because it nearly read as a broken preset: the first
measurement used short-window energy and reported almost no change (0.723
dry against 0.732). That is correct and irrelevant — a chorus at mix 1
modulates PITCH, and its effect on amplitude is close to nil. The metric
was wrong, not the preset.

`PRESET_PROBLEM` gains its line in the same commit — `audioFxCopy.test.ts`
requires one for every shipped preset, so the catalogue cannot grow an
entry the shelf would render without plain language.

core 1770 · studio 3716 + 18 todo.
"Megaphone sounds just like AM Radio" — correct, and the measurement
found a bigger problem behind it. A log sweep through each preset showed
BOTH flattened to a dead -19 dB line across the whole midband: the
saturation thresholds were low enough that hard clipping erased the
resonances underneath. The same chain with its clipper removed showed the
horn peaks standing +12 dB proud, so the shaping had always been there
and was being destroyed on the way out.

Every character preset's clipper is backed off to where its shaping
survives — telephone -18→-9, tannoy -16→-10, radio -15→-8, megaphone
-14→-5. That alone is most of the fix.

Then the ones that remained alike, by what physically distinguishes them:

- **AM Radio** was a second telephone: same band, same mid honk. A
  receiver DIPS where a phone honks (the IF droop) and is boxy where a
  phone is thin, so it gets a -5 dB scoop at 1.2 kHz and a low shelf, and
  its band moves down and darker. The telephone keeps its band unchanged
  — 300-3400 IS the G.712 passband, and it is the one preset whose
  identity is the band itself.
- **Megaphone** sat inside the telephone's band. A bullhorn has no low end
  at all: the band moves up to 700 Hz and the honk gets narrower and
  louder at 1.9 kHz.
- **Tannoy** was a telephone with reverb. A concourse horn is bigger in
  every direction — band out to 5 kHz, honk higher and harder at 2.4 kHz.

Measured every character preset against every other, before and after. No
pair now sounds more alike than the signal itself; the worst were
telephone/AM at -5.9 dB and AM/tannoy at -4.8 dB, and telephone/megaphone
reached -3.2 mid-fix. Now every pair is positive — telephone/megaphone
-3.2 → +5.6, AM/megaphone -0.2 → +11.3.

Also sets Tannoy's Concourse reverb to the requested size 0.54, damping
0.48 (was 0.5 / 0.7).

Worth recording for whoever tunes these next: measuring on narration is
almost useless here. Speech has little energy above 3 kHz, so a spectrum
probe on it reported the two presets as near-identical whatever I changed.
A log sweep is what showed the flattening, and an A-vs-B difference on
speech is what confirms it.

core 1770 passing.
**Collapse.** A preset is one thing the author added, and once it is set
the seven modules inside are detail — two presets in a rack was thirteen
cards deep before anything hand-built appeared. The title is now the
disclosure, and folded it carries the node count, which is what keeps a
collapsed preset reading as a chain rather than one opaque effect.

Open by default: a preset that arrives already hidden is one nobody
learns they can edit. Collapsed by preset id rather than by index, so it
survives a reorder. The whole-preset controls stay reachable while
folded — collapsing hides the detail, not the preset.

**A title treatment per preset.** The rack already letters by FAMILY,
which answers "what kind of thing is this". This answers a different
question: a preset is a character, and the point of Telephone or
Megaphone is that you know what it sounds like before you play it. A
phone's band is narrow, so is its tracking; a bullhorn is heavy, tight
and leaning forward; a tape is worn and slightly off. The bracket's left
edge carries the same colour.

Deliberately not a colour free-for-all. Every hue sits in the same narrow
lightness band as the family tints — a test enforces saturation ≤ 50%
and lightness 66–78% — because the panel already spends saturation on
"automated" and "bypassed", and a per-preset colour outside that reads as
status. The corrective families are plainer than the character ones on
purpose: a costume on "Cut Rumble" promises a character it does not add.

`fxPresetStyle` falls back rather than failing, so the catalogue can grow
without this file — but tests assert every shipped preset has an entry
and no entry outlives its preset, and that no two character presets share
a treatment.

Verified in a running studio: Telephone renders mono/400/3px in cyan and
Megaphone sans/900/italic/tight in warm orange; folding Telephone left 0
of its 7 nodes showing with the count reading 7, and left Megaphone's 6
alone.

Falsified: a collapse that hides nothing, two presets sharing a
treatment, and a colour outside the band each fail a test.

studio 3725 passing, 18 todo · core 1770.
The first pass styled titles with Tailwind's three generic families, so
eighteen presets came out as variations of the same two faces — weight
and tracking doing all the work. And at 10px a title was the same size as
the parameter rows under it, which is not a title.

Titles are 12–17px now, sized by character rather than uniformly: the
Megaphone is the loudest thing in the rack at 17px, Hall is 15px with
0.4em tracking so the word itself opens out, and the workshop presets
stay at 12px because a costume on "Cut Rumble" promises a character it
does not add.

Eight named faces, grouped by what they read as rather than by name —
condensed signage, geometric, typewriter, editorial serif, bookish serif,
theatrical display, engraved caps, terminal. Telephone gets the face with
no warmth, the tape gets one that looks struck, the Tannoy gets something
bolted to a wall, and Doofus Worble gets a display face with no restraint
at all.

They are SYSTEM faces, and that is a constraint rather than a preference:
the studio has no webfont pipeline. The 169 Google Fonts cached under
`~/.cache/hyperframes` belong to the CLI's composition build, and reaching
into them from a panel would be inventing a second one. Every stack ends
in a generic keyword and carries at least two fallbacks, which a test
enforces — a machine without Haettenschweiler lands on Arial Narrow, then
Impact, then sans-serif, rather than on the browser's default serif.

The face is applied as data rather than a class: Tailwind cannot name a
stack its config does not know, and adding eight to the config to style
one panel would put them in every autocomplete in the studio.

Verified in a running studio — five presets applied at once resolved to
five distinct faces (SF Mono, Haettenschweiler, Luminari, American
Typewriter, Copperplate) at 13–17px. Tests can assert the stack is
well-formed; they cannot tell you a face exists on the machine.

Falsified: a stack with no generic fallback, a title back at panel-row
size, and two character presets sharing a face each fail a test.

studio 3727 passing, 18 todo.
Saturation roughly doubles — the muted 26-46% band was chosen to stay out
of the way, and stayed so far out of it that the titles read as grey text
with a hint of tint. They are 68-92% now, and light enough (58-78%) to
carry on the panel's near-black ground.

Two hues moved rather than brightened. The voice presets sat at hue 155,
which is the accent (#3CE6AC is hue 160) — saturating them there would
have made "Clean Voice" read as automated or playing, which is what that
green means everywhere else in the panel. They are blues now, and a test
keeps every title at least 20° clear of the accent.

**Each preset also gets its own background**, derived from its title hue
rather than picked: nineteen hand-chosen pairs is nineteen chances for
one to clash with its own title, and a hue rotation cannot. Same hue at
22% saturation and 11% lightness, so a rack with several presets reads as
several regions instead of one long list, while staying dark enough that
every control on top of it is unaffected.

The repair family deliberately shares one colour and therefore one
background — they are workshop tools, not characters — so the uniqueness
test covers the character family only.

Verified in a running studio with five presets applied: five distinct
title colours over five matching washes, and the accent still reads as
the only green in the panel.

Falsified: a washed-out colour, a title on the accent's hue, a background
bright enough to fight the controls, and a background hue unhooked from
its title each fail a test.

studio 3730 passing, 18 todo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant