Add a feedback for every controllable state, matrix send presets

A matrix-send knob looked dead on the booth Companion although the console
followed it (verified through Companion's rotate API: -90 -> -60 -> -59):
the module had matrix send actions but no matrix send feedbacks.

- Every action with a readable state now has a matching feedback: matrix
  send on/level text/level bar, matrix and control group solo and fader
  bars, graphic EQ in and band gain text/bar, aux send pan text, and value
  text for trim, gain and filter frequencies. A test checks the parity.
- New action: matrix output solo.
- Matrix sends use the console's -90 dB floor: adjusting down from -inf no
  longer writes -150 there.
- Presets: "Matrix sends to <output>" (on/off and knob per matrix input)
  and graphic EQ in/out. 2970 presets, 1.8 MB.

npm test: 368 checks pass on Node 25 and on Companion's Node 22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 15:44:43 -04:00
co-authored by Claude Opus 5.5
parent 57645be1e9
commit 90dee14444
9 changed files with 351 additions and 57 deletions
+67 -15
View File
@@ -9,8 +9,11 @@ const { white: WHITE, dark: DARK, red: RED, green: GREEN, amber: AMBER, bar: BAR
const fb = (feedbackId, options, bg, color = WHITE) => ({ feedbackId, options, style: { bgcolor: bg, color } })
const act = (actionId, options) => ({ actionId, options })
/** Build every preset from the live state. `withSendKnobs` is the first thing dropped when the list is too big. */
function build(inst, withSendKnobs) {
/**
* Build every preset from the live state. When the list is too big `grMeters` (the gate / compressor reduction
* meter presets) is dropped first, then `sendKnobs` (the per-input aux send level knobs).
*/
function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {}) {
const s = inst.state
const L = inst.label
const presets = {}
@@ -69,6 +72,7 @@ function build(inst, withSendKnobs) {
['gate', 'input_gate_gr', 'Gate reduction'],
['comp', 'input_comp_gr', 'Compressor reduction'],
]) {
if (meter !== 'level' && !grMeters) continue
presets[`${prefix}_${n}`] = simple(
`${label}: ${tag}`,
nameVar('input', n),
@@ -97,8 +101,7 @@ function build(inst, withSendKnobs) {
description: 'Meters subscribe on the console when the button first renders.',
definitions: [
group('meters_input', 'Level', meterIds.level),
group('meters_gate', 'Gate gain reduction', meterIds.gate),
group('meters_comp', 'Compressor gain reduction', meterIds.comp),
...(grMeters ? [group('meters_gate', 'Gate gain reduction', meterIds.gate), group('meters_comp', 'Compressor gain reduction', meterIds.comp)] : []),
],
})
}
@@ -217,6 +220,48 @@ function build(inst, withSendKnobs) {
if (muteIds.length) structure.push({ id: 'cgs', name: 'Control groups', definitions: [group('cgs_mute', 'Mute', muteIds), group('cgs_knob', 'Fader (knob)', knobIds)] })
}
// ---- Matrix sends: one section per matrix output, per matrix input an on/off button and a level knob
for (const out of s.numbers('matrix')) {
const oname = s.displayName('matrix', out)
const onIds = []
const knobIds = []
for (const inN of s.numbers('minput')) {
const tag = `Matrix In ${inN} → ${out} · ${oname}`
const text = `Matrix In ${inN}\\n$(local:level) dB`
const options = { matrixIn: inN, matrixOut: out }
const lv = [localFeedback('level', 'matrix_send_level_value', options)]
presets[`msend_on_${inN}_${out}`] = simple(`Matrix send on/off: ${tag}`, text, [act('matrix_send_on', { ...options, state: 'toggle' })], [fb('matrix_send_on', options, GREEN)], { localVariables: lv })
presets[`msend_knob_${inN}_${out}`] = knob(
`Matrix send level (knob): ${tag}`,
text,
[act('matrix_send_on', { ...options, state: 'toggle' })],
'matrix_send_level_adjust',
options,
lv,
[fb('matrix_send_on', options, GREEN), { feedbackId: 'matrix_send_level_bar', options: { ...options, color: BAR } }],
)
onIds.push(`msend_on_${inN}_${out}`)
knobIds.push(`msend_knob_${inN}_${out}`)
}
if (onIds.length) {
structure.push({
id: `msends_${out}`,
name: `Matrix sends to ${oname}`,
definitions: [group(`msends_${out}_on`, 'On/off', onIds), group(`msends_${out}_knob`, 'Level (knob)', knobIds)],
})
}
}
// ---- Graphic EQs: in / out
{
const ids = []
for (const n of s.numbers('geq')) {
presets[`geq_in_${n}`] = simple(`In/out: ${n} · ${s.displayName('geq', n)}`, `${s.displayName('geq', n)}\\nIN`, [act('geq_on', { geq: n, state: 'toggle' })], [fb('geq_on', { geq: n }, GREEN)])
ids.push(`geq_in_${n}`)
}
if (ids.length) structure.push({ id: 'geqs', name: 'Graphic EQs', definitions: [group('geqs_in', 'In/out', ids)] })
}
// ---- Snapshots
{
const fireIds = []
@@ -247,19 +292,26 @@ function build(inst, withSendKnobs) {
}
/**
* Presets for the current state: { structure, presets, stats }. If the list would exceed the agreed
* limits the per-input send level knobs (the largest group) are left out and stats.droppedSendKnobs is set.
* Presets for the current state: { structure, presets, stats }. If the list would exceed the agreed limits the
* gate / compressor reduction meter presets are left out first, then the per-input aux send level knobs;
* stats.dropped says which.
*/
export function getPresetDefinitions(inst) {
let built = build(inst, true)
let count = Object.keys(built.presets).length
let bytes = JSON.stringify(built.presets).length
let droppedSendKnobs = false
if (count > MAX_PRESETS || bytes > MAX_PRESET_BYTES) {
built = build(inst, false)
droppedSendKnobs = true
export function getPresetDefinitions(inst, limits = { count: MAX_PRESETS, bytes: MAX_PRESET_BYTES }) {
const tiers = [
{ options: { grMeters: true, sendKnobs: true }, dropped: [] },
{ options: { grMeters: false, sendKnobs: true }, dropped: ['the gate / compressor reduction meter presets'] },
{ options: { grMeters: false, sendKnobs: false }, dropped: ['the gate / compressor reduction meter presets', 'the per-input send level knobs'] },
]
let built
let count = 0
let bytes = 0
let dropped = []
for (const tier of tiers) {
built = build(inst, tier.options)
count = Object.keys(built.presets).length
bytes = JSON.stringify(built.presets).length
dropped = tier.dropped
if (count <= limits.count && bytes <= limits.bytes) break
}
return { ...built, stats: { count, bytes, droppedSendKnobs } }
return { ...built, stats: { count, bytes, dropped } }
}