feat(studio): the rack redesign — named jobs, two faces, ruler, schematic - #3160
Open
vanceingalls wants to merge 21 commits into
Open
feat(studio): the rack redesign — named jobs, two faces, ruler, schematic#3160vanceingalls wants to merge 21 commits into
vanceingalls wants to merge 21 commits into
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_COPYwas still unread and none of the visualdirection 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.
peakingis 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 —
labelalready 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 itis 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 coherentin 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) —BANDShas named seven ranges since thecopy 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 modulesis 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, butunlike 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
carveProfilealready 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.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: spreadfalls 19.1 → 8.8 dB with the level unmoved.
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.
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
audioFxProfileStrengthreads the knob back by inverting thecurve — the same contract
normalizeCarveSettingshas. It renders through theordinary
FxParamRowrather thanFxNodeParams, because it has no AudioParambehind 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 fixturetimes 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.tsxis back over the 600-line cap at 670. Extractingits 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