Skip to content
Back to skills

Build Preset Routing

ASecurity

Build Quad Cortex presets with correct grid routing, I/O, and multi-amp/parallel topologies — and verify them. Use when creating or editing presets (especially multi-amp, parallel, or shared-front rigs), wiring inputs/outputs, sharing one pedal chain across parallel amps, or whenever a build "doesn't take" / reads come back empty. Encodes the routing rules, I/O conventions, and the reliability tricks that make building actually work over the bridge.

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 2, 2026
ai-agentsrustgogit

Works with

  • cli
  • mcp

Security analysis

A100/100

Scanned September 20, 2026

npx -y skills add lexasoft123/qc-mcp --skill build-preset-routing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Build Preset Routing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Build Preset Routing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lexasoft123-build-preset-routing/badge)](https://www.skillsdirectory.com/skills/lexasoft123-build-preset-routing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: build-preset-routing
description: Build Quad Cortex presets with correct grid routing, I/O, and multi-amp/parallel topologies — and verify them. Use when creating or editing presets (especially multi-amp, parallel, or shared-front rigs), wiring inputs/outputs, sharing one pedal chain across parallel amps, or whenever a build "doesn't take" / reads come back empty. Encodes the routing rules, I/O conventions, and the reliability tricks that make building actually work over the bridge.
---

# Build QC presets: routing, I/O, and parallel topologies

The grid is **4 rows × 8 columns**, fed by two DSP cores (see `optimize-preset-cpu` /
`docs/CPU.md`). Building reliably needs three things right: the **routing model**, the
**I/O conventions**, and the **read/verify loop**. Get any wrong and you ship silent or
mis-wired presets — every failure below was hit for real.

## 0. Always verify — never trust the write
`build_preset`'s own trailing read is unreliable (it fires amid the edit-echo storm).
After any build/edit, **read back with `get_current_preset`** (now includes
`split_points` per chain) AND, for topology, **screenshot the grid**:
`tools/gui/gui.py home && tools/gui/gui.py shot out.png` then look at the wiring.
Cross-check with `cpu_load` (its block list reveals duplicates / orphaned blocks).

## 1. Routing model
- **Lane ports** — `in_port` / `out_port` per chain (row). Full `out_port` map (verified by
  clicking through Cortex Control's output block and reading `chains[i].out_port`):

  | out | dest | out | dest | out | dest |
  |----|------|----|------|----|------|
  | 1 | Out 1/2 | 8–9 | Send 1 / 2 | 16 | **Row 3** |
  | 2 | Out 3/4 | 10–13 | USB 5–8 | 17 | **Row 4** |
  | 3 | Send 1/2 | 14 | USB 5/6 | 18 | **Row 3/4** |
  | 4–7 | Out 1 / 2 / 3 / 4 | 15 | USB 7/8 | 19 | **Multi Out** |
  |     |         | 20–22 | USB 3 / 4 / 3‑4 |    |          |

  So **`out_port=16/17/18` route a lane's output *into* Row 3/4** (the "patch‑cable to a
  row" jumper), `out_port=19` = Multi Out, and physical jacks are 1‑9 — e.g. `out=7` is the
  **Out 4 jack**, NOT a row (a trap). On the INPUT side, **`in_port=7` = "Prev. row"** (take
  the row directly above's output); `in_port=1` = In 1. Idle branch lanes: `in=0, out=0`.
- **Parallel split** — `split_control_points {split, mix}` per row (in `build_preset`:
  `"splitter":{"split_col":S,"mix_col":M}`). `split=S` branches the signal **before the
  block at column S**; `mix=M` merges the branch back at column M. `(-1,-1)` = none.
  **A split is 1→2 only.** The underlying law (per the official manual): **rows 1&3 are
  "Path A", rows 2&4 are "Path B"; splitters only route Path A → Path B.** That's WHY a
  branch (Path B) row can't host its own split (rejected), while row 3 — even when
  patch-fed via in=7 — CAN split (it's Path A).
- **Read-vs-delta indexing:** in a full READ, row = chain-array index, column =
  models-array index, param index = params-array index (the `.row/.column/.index`
  *fields* are 0 there). In edits you set those fields explicitly.

## 2. Multi-amp / parallel patterns
- **N parallel amps, no shared front** (simplest, most robust — the "SRV multiamp"
  skeleton): each amp lane `in=1` taps the input directly; one lane is the **merge/output
  lane** `in=0, out=19` carrying shared post-FX. All amp lanes `out=0` sum into it.
- **Shared front-end feeding parallel amps** (saves CPU — one pedal chain, not N):
  put the shared blocks on row 0, then **split AFTER them**. `split_col` = the column of
  the **first block that should NOT be shared** (pedals at cols 0-2 → `split_col=3`, so
  the amp at col 3 and the branch both get the post-pedal signal). Splitting at col 2
  would leave the col-2 block (e.g. the TS) out of the branch — a classic bug.
- **3+ parallel amps sharing ONE pedal group before all of them** (verified against a
  hand-built reference). Do NOT nest `scp` splits on a *split-branch* (rejected). The
  working recipe uses a **no-merge tap → empty jumper row → split a patch-fed lane**:
  1. **Row 1:** In 1 → shared pedals → amp 1 → cab → reverb → `out=19` (Multi Out), with
     **`split_col` after the pedals and `mix_col = -1` (NO merge)**. The `-1` is critical:
     the split *only taps* the post-pedal signal down to row 2; row 1 continues to amp 1
     independently. (A real `mix_col` would merge the tap back and pollute the amp-1 lane —
     the classic bug.)
  2. **Row 2 = empty jumper:** receives row 1's tap; set its output to **`out=16` ("Row 3")**
     → feeds row 3.
  3. **Row 3 (amp 2):** `in_port=7` ("Prev. row" = the jumper). Give THIS lane its own
     **split `scp=(0, M)`** — split at col 0 (its input, pre-amp) to feed row 4, merge the
     return at col M (after the cab, before this lane's reverb). A **patch-fed lane (in=7)
     CAN be split** even though a split-branch can't — this is how the 3rd amp gets the
     shared signal in parallel. Then amp 2 → cab → reverb → `out=19`.
  4. **Row 4 (amp 3):** `in_port=7`, fed by row 3's `scp` split; amp 3 → cab → merges back
     into row 3 at col M.
  So one `comp→Klon→TS` feeds all three: amp 1 on row 1, amps 2+3 via jumper→row 3→split.
  Put reverb per amp-group (row 1's amp; the row 3+4 merge) so every amp is wet. Verify by
  screenshot: each amp row reads "Prev. row"/In → "Multi Out"; none should be **series**
  ("Prev. row" pointing at another amp's output) or **orphaned** (output on `+`).
  (`out=17`="Row 4", `out=18`="Row 3/4" also route to rows but the split-the-lane method
  above is the confirmed-correct one. Simpler alternative that does NOT share the front: the
  SRV 2-stage merge — amps tap `in=1`, drive on one lane, mix `out=16`→`out=19`.)
- **Branch-row column budget:** a branch that merges at column M reserves the columns
  between its last block and M as "runway"; it can't hold blocks all the way to M. If a
  block silently won't land at the end of a branch row, move the merge column out or
  place the block earlier.
- **Post-merge (shared) FX — e.g. a stereo reverb for the whole blend:** place it on
  the output row **after `mix_col`** so it processes the merged signal, not one lane. FX
  before the merge only affect that lane (why one amp ended up "dry" of the delay once).

## 2b. Transparent Blend — parallel/side-chain without spending lanes
`Transparent Blend` (hash **16013**, Utility; sibling `Plugin Blend` 16008) is a
**single-slot** block that taps another point in the grid and blends it into the current
slot — so you get a parallel/return path **without** consuming a splitter lane + mixer
lane. Params (display): `SOURCE` (0–44 — picks the tap), `BLEND` 0–100 %, `OUTPUT`
−60..+12 dB, `Delay` 0–255 (samples, for time-alignment), plus `InputChannel`/
`StereoInput` for stereo handling.
- **`SOURCE` is a picklist**, not a fixed enum: physical **Input 1/2/1+2**, **FX Return
  1/2/1+2**, **USB inputs 5–8/5+6/7+8**, then **every block on the grid by position**
  (shown as `R{row}C{col} <name>`). So it can pull the output of any block anywhere.
- `SOURCE` can be **assigned to Scenes** (long-press in the app) — the tap point itself
  becomes scene-varying. Verify the exact stored index by reading the block back
  (`get_current_preset`): the list is dynamic (grid blocks are appended after the fixed
  input/return/USB entries), so the integer depends on the current grid.
- **CONFIRMED multi-amp recipe (built & saved "4 Amp Blend", 2026-07-24):** this beats
  the jumper/split method and enables layouts it CAN'T do — e.g. **4 amps sharing one
  pedal**, impossible the old way (4 rows, all consumed by amps). Recipe:
  1. Row 0: `In 1 → shared pedal(s) → amp1 → cab → out=19` (amp1 inline, normal).
  2. Rows 1–3: `Transparent Blend(col0) → ampN → cab → out=19`, each blend
     `BLEND=100`, `SOURCE = R1C1 <first shared pedal>`. All amps then get the same
     post-pedal signal; pan the lanes + drop volumes (4-way sum) as usual.
  - **Selecting a side-chain SOURCE auto-sets that lane's `in_port` to 0** — the lane is
    fed *purely* by the blend (BLEND=100 = 100% source), so you don't wire its input.
    Confirmed by read-back (in=0) + the on-screen side-chain taps + signal on the meter.
  - **`SOURCE` is fiddly to set programmatically** (the stored value is not a clean list
    index; a linear display write lands on the wrong entry — `SOURCE=12`→"Input 1/2").
    Robust method: set ONE blend's source via the app dropdown, read its stored value
    back, then replicate that exact value to the other blends (worked here: display 38.5
    = R1C1 Myth Drive, same for all three since the pedal shares a list position). Verify
    each in the app (the block editor shows the resolved source name). See the
    `transparent-blend-block` memory.

## 3. I/O and stereo
- Output "Multi Out" (`out_port=19`) is this device's main out — leave it unless told.
- **Stereo comes from panning the lanes**, not from the output alone. Two places to pan:
  - **Lane Output Control** — params `VOLUME=0, PAN=1, MUTE=2, SOLO=3` (PAN 0.0=hard L,
    0.5=center, 1.0=hard R). Use `set_lane_output(row, pan=…, volume=…)`.
  - **Splitter / mixer blocks** — these also carry a pan/balance param, so you can
    position the branches at the split/merge instead of (or with) the lane output.
  Pan the **branch** amp lanes (e.g. one left, one right) and leave the **main/output
  row centered** — panning the output row pans the whole mix. "Not 100%" → ~0.3 / 0.7.
- Mono (M) cabs + a stereo (ST) reverb is a cheap way to get width; ST cabs cost ~2×.
- **Gain-staging — don't clip the sum.** Parallel lanes **add** at the mixer, so N amps
  each at unity overloads the output. **Reduce each parallel lane's output volume** (Lane
  Output Control VOLUME, or the mixer level) so the summed level stays below clipping —
  order of −20·log₁₀(N) dB, e.g. ≈1/3 for three amps (default lane VOLUME is 0.769 → drop
  the amp lanes to ~0.4–0.5). **Verify with `output_meter(hold_s=2)` while playing** —
  `limiters.*` must all be false; leave headroom for the reverb. For a real number
  rather than a meter, `measure_loudness()` gives LUFS + true peak (needs `.[audio]`).

## 4. Build gotchas that cause silent failures
- **`clear_grid` needs working reads.** It deletes what it reads; if reads are empty,
  it clears nothing and the next `build_preset` **merges onto the old grid** (ghost
  blocks, duplicated amps). Confirm reads work first.
- **`add_block` de-dupes:** re-adding a hash already present on that row is a no-op. To
  move a block, delete it first (or place a different hash); don't "overwrite" with the
  same model.
- **Whole-preset Grid UPDATE merges**, doesn't replace — build incrementally or clear.
- Recalling an empty slot doesn't always reset the live grid over the bridge; use
  `clear_grid` + verify.

## 5. Reliability — reads must work or nothing above is verifiable
The bridge (sharing Cortex Control's session) is what makes live building possible; it
was made robust this way (`bridge.py`, `transport.py`):
- Out FIFO opened **O_RDWR** so the reader never hits EOF when the app's forwarder
  blinks; the reader thread **self-heals** (reopens, never dies).
- Reads correlate on **`request_id`** — the device echoes it on solicited replies, so a
  buffered/older push or the app's own traffic can't be mistaken for our response. Our
  ids use a high base to avoid colliding with the app's.
- **Streamed telemetry** (CPULoad, IOMeter) is `request_id=0` broadcast — take the
  **latest** message, not the first buffered one (drain, then read).
  `transport.latest_broadcast(name, hold_s)` does this; `cpu_load` and `output_meter`
  both use it.
- If reads still come back empty, **reconnect** (`disconnect`→`connect`) to revive a
  stale bridge session; `get_current_preset` also auto-reconnects+retries once.
- **Only one reader on the bridge at a time.** The out FIFO is a single stream: if a
  second process (a script alongside the MCP server) also reads it, the two steal each
  other's frames and reads return empty while telemetry keeps flowing — it looks
  exactly like a dead session. This is another reason to do everything through the
  MCP tools rather than side scripts.
- **After a CorOS update, re-instrument the app** (`interceptor/build.sh`): the bridge
  runs a *copy* of Cortex Control, so an updated app leaves the old binary talking to
  new firmware. Then refresh the schema and catalog — see CLAUDE.md.

Bottom line: **build → read back split_points + routing → screenshot → cpu_load**. Only
then is a preset "done."

## 6. Device presets — don't dial from scratch (CorOS 4.1+)
Every device carries saved settings: ~2750 factory ones across 602 models, plus up to 32
user presets each. `list_device_presets("<model>")` shows them;
`load_device_preset(row, column, "<name>")` applies one to the block at that position,
replacing all its parameters at once.

Use it as the **first pass on a block** — load the closest-named preset, then tweak — and
for utility blocks (Output EQ, I/O Settings) where the factory presets encode real
voicings. `device_info` reports `firmware` and `protocol_generation`; on CorOS 4.0 these
tools return a "needs CorOS 4.1" error rather than failing silently. Loading is a Grid
UPDATE under the hood, so verify it the same way as any other write (read the block back
— `load_device_preset` returns `params_changed` and the new values).

`save_device_preset(row, column, name)` stores a block's current knobs as a reusable
user preset (32 per device); `delete_device_preset` removes one. The device **refuses
a save whose parameters already match an existing preset** for that model — change
something, or just load the preset that already holds those values.

## 7. Footswitch (stomp) assignments
`assign_stomp(row, column, "E")` binds a block to a footswitch (A-H), so it can be
toggled live in Stomp or Hybrid mode; `unassign_stomp` removes it, and
`get_current_preset` reports `stomp_assignments`. A block holds one assignment —
assigning again moves it. `momentary=True` = active only while held.

`kind="secondary"` is the CorOS 4.1 Dual Footswitch feature: a second function on its
own switch, and only meaningful on devices that have one (Vintage Digital, Aeons
Reverb). For a player in Hybrid mode, plan stomps alongside scenes — drives, boosts
and delay toggles are what a footswitch is for, while scenes carry the whole tone.

## MCP tools — the complete interface (no scripts needed)
The bridge is reliable now, so **do everything through MCP tools** — never fall back to
ad-hoc scripts for device control. The full toolbox:
- **Blocks:** `build_preset` (whole topology), `add_block` / `remove_block`, `clear_grid`,
  `add_capture`, `add_ir`.
- **Params (display units):** `set_parameter(row, col, index, value)`. Read a model's param
  names/order from `get_current_preset` or `find_devices` before setting indices.
- **Routing / stereo:** `set_lane_routing(row, in_portid, out_portid)`,
  `set_lane_output(row, pan, volume, mute, solo)`, `add_split` (splits are also set via
  `build_preset`'s `splitter`).
- **Scenes:** `set_parameter_scenes(row, col, index, values[8])` (assign-to-scenes + write;
  the tool confirms each scene switch via Scene READ — racing drops values). Scene the AMP
  params too (gain ladder, lead master bump, EQ trims), not just drive bypass.
  `set_block_bypass(row, col, bypassed, scenes=[…])` (per-scene bypass — works on drives/
  amps, silent NO-OP on delays: scene the delay MIX instead), `switch_scene(0-7)`,
  `set_preset_meta(scene_labels=[…], scene_colors=[…])` — labels/colors go over dedicated
  SceneLabel/SceneColor ops (verified); scenes with data but no label show "Undefined".
  Preset `name` is NOT settable live — it's set by the save op (pass it to save_preset/_as).
- **Slot picking:** `list_empty_slots(setlist_key)` — free "Unsaved" positions (with
  bank labels like 4E). Always build/save into one of these, never a named preset's slot.
- **Save:** `save_preset(name)` / `save_preset_as(name, setlist_key, position)`. BOTH now
  persist via the app's real op — **File CREATE** (`File{action=CREATE, folder{key,
  files{index=<position>, name}}}`, **no preset_payload**; the device commits its live
  working grid). Decoded from Cortex Control's own Save. **Never** save via *RecallPreset
  UPDATE reason=SAVE* — it **hangs the device** on an empty/Unsaved slot (had to reboot).
  The tool's success string is not proof of commit: verify with `current_preset_position`
  + the app header showing the name **without a `*`** (dirty marker). See the
  `save-empty-slot-hangs` memory.
- **Device presets (CorOS 4.1+):** `list_device_presets(model)` /
  `load_device_preset(row, column, preset)` — a whole knob set on one block (§6) —
  plus `save_device_preset` / `delete_device_preset`.
- **Footswitches:** `assign_stomp(row, column, 'A'-'H', kind, momentary)` /
  `unassign_stomp` (§7).
- **Read / verify:** `get_current_preset`, `cpu_load(detail)`, `get_io_settings`,
  `current_preset_position`, `device_info` (firmware + which features this unit has),
  **`output_meter(hold_s, detail)`** (per-port peak-hold in dB + limiter flags — the
  fastest check for "is signal reaching the outputs / is it clipping"). With the
  `.[audio]` extra: `measure_loudness()` and `suggest_levels()` compare presets by
  measured loudness without writing anything (docs/LEVELING.md). The only non-MCP helper is `tools/gui/gui.py shot` for a
  *visual* wiring screenshot — verification, not control.

If an operation has no tool yet, **add the MCP tool** rather than scripting it — keep the
MCP the single functional interface. When a write seems to succeed but doesn't take
(e.g. Grid UPDATE with preset-level scene_labels was a silent no-op), **capture the op
from Cortex Control** (logmark → do it in the app → decode) and match it exactly.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.