Timed fades, mute-style matrix sends, meters on matrix send knobs

- Every set-level action (input, bus, matrix and control group faders,
  aux and matrix sends) has a fade time and a curve: fader travel
  (natural), fader travel with ease in/out, or linear in dB. Fades step
  at 20 Hz, land exactly on the target, never overshoot, and are
  cancelled by any new move on the same fader; new "Stop all fades"
  action, fades_running variable and fade_running feedback. Verified on
  the S31 with a switched-off matrix send: 20 writes in 1 s, ending
  exactly on target, then restored.
- Matrix sends now look like mutes in the presets: red when the send is
  off, black when it is on (new matrix_send_off feedback, true only when
  the send is known to be off).
- Matrix send knobs carry a meter column on the right. The S31 has no
  meter for matrix inputs, so it shows the destination output's meter.
  Meter gauges use a faint track so an empty meter reads as empty.
- Test fixes: the meter-resubscribe check no longer depends on a silent
  channel changing value, plus back-to-back /Meters/clear coverage.

npm test: 504 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-20 11:20:07 -04:00
co-authored by Claude Opus 5.5
parent 52777f3d7c
commit 205f91e7d2
8 changed files with 634 additions and 64 deletions
+52 -20
View File
@@ -54,24 +54,34 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
const text = (id, x, y, width, height, value) => ({ type: 'text', id, x, y, width, height, text: value, fontsize: 100, fontsizeAllowShrink: true, color: TEXT, halign: 'center', valign: 'center' })
/** Boolean feedback on a layered preset: it colours the background box. */
const tint = (feedbackId, options, color) => ({ feedbackId, options, styleOverrides: [{ elementId: 'bg', elementProperty: 'color', override: color }] })
/** Green / amber / red zones for a meter gauge on the -60..0 dBFS scale. */
const meterStops = () => [
{ value: 0, color: GAUGE_GREEN, gradient: false },
{ value: 70, color: METER_AMBER, gradient: false },
{ value: 90, color: METER_RED, gradient: false },
]
/** Width (%) of the optional meter column at the right edge of a knob preset. */
const KNOB_METER_WIDTH = 13
/** Track brightness (%) behind multi-colour meter gauges: faint, so an empty meter reads as empty. */
const METER_TRACK_AMOUNT = 20
/**
* Layered rotary preset with a native Companion gauge along the bottom: black background, the entity name on top, the
* dB text below it, and a gauge fed by the `pos` local variable. `level` and `pos` are the text and the numeric value
* feedbacks for the same thing.
* feedbacks for the same thing. With `meter: [feedbackId, options]` a vertical meter column (fed by the `meter` local
* variable, a meter position feedback) runs down the right edge and the rest of the button narrows to make room.
*/
const layeredKnob = ({ name, title, press, adjust: [adjustActionId, adjustOptions], level: [levelId, levelOptions], pos: [posId, posOptions], feedbacks }) => ({
type: 'layered',
name,
elements: [
const layeredKnob = ({ name, title, press, adjust: [adjustActionId, adjustOptions], level: [levelId, levelOptions], pos: [posId, posOptions], meter, feedbacks }) => {
const w = meter ? 100 - KNOB_METER_WIDTH : 100
const elements = [
{ type: 'box', id: 'bg', x: 0, y: 0, width: 100, height: 100, color: BLACK },
text('title', 0, 0, 100, 55, title),
text('level', 0, 52, 100, 32, '$(local:level) dB'),
text('title', 0, 0, w, 55, title),
text('level', 0, 52, w, 32, '$(local:level) dB'),
{
type: 'gauge',
id: 'gauge',
x: 0,
y: 84,
width: 100,
width: w,
height: 16,
value: { isExpression: true, value: '$(local:pos)' },
min: 0,
@@ -82,11 +92,30 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
multiColour: false,
stops: [{ value: 0, color: GAUGE_LEVEL, gradient: false }],
},
],
steps: knobSteps(press, adjustActionId, adjustOptions),
feedbacks,
localVariables: [localFeedback('level', levelId, levelOptions), localFeedback('pos', posId, posOptions)],
})
]
const localVariables = [localFeedback('level', levelId, levelOptions), localFeedback('pos', posId, posOptions)]
if (meter) {
elements.push({
type: 'gauge',
id: 'meter',
x: w + 1,
y: 0,
width: KNOB_METER_WIDTH - 1,
height: 100,
value: { isExpression: true, value: '$(local:meter)' },
min: 0,
max: 100,
orientation: 'vertical',
trackStyle: 'dimmed',
fillEnabled: true,
multiColour: true,
trackAmount: METER_TRACK_AMOUNT,
stops: meterStops(),
})
localVariables.push(localFeedback('meter', meter[0], meter[1]))
}
return { type: 'layered', name, elements, steps: knobSteps(press, adjustActionId, adjustOptions), feedbacks, localVariables }
}
/**
* Layered meter: name on the left, a vertical gauge (green / amber / red zones) at the right edge; the position comes
* from `posFeedback`. A signal feedback tints the background dark green and, for level meters, a peak feedback red.
@@ -111,11 +140,8 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
trackStyle: 'dimmed',
fillEnabled: true,
multiColour: true,
stops: [
{ value: 0, color: GAUGE_GREEN, gradient: false },
{ value: 70, color: METER_AMBER, gradient: false },
{ value: 90, color: METER_RED, gradient: false },
],
trackAmount: METER_TRACK_AMOUNT,
stops: meterStops(),
},
],
steps: [{ down: [], up: [] }],
@@ -306,7 +332,11 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
const text = `${nameVar('minput', 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 })
// A send that is off looks like a mute (red); an enabled send keeps the black background.
presets[`msend_on_${inN}_${out}`] = simple(`Matrix send on/off: ${tag}`, text, [act('matrix_send_on', { ...options, state: 'toggle' })], [fb('matrix_send_off', options, MUTED_RED)], {
style: { text, size: 'auto', color: WHITE, bgcolor: BLACK },
localVariables: lv,
})
presets[`msend_knob_${inN}_${out}`] = layeredKnob({
name: `Matrix send level (knob): ${tag}`,
title: nameVar('minput', inN),
@@ -314,7 +344,9 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
adjust: ['matrix_send_level_adjust', options],
level: ['matrix_send_level_value', options],
pos: ['matrix_send_level_pos', options],
feedbacks: [tint('matrix_send_on', options, ON_GREEN)],
// The console has no meter for matrix inputs (no address answers), so the column shows the destination output.
meter: ['matrix_meter_pos', { matrix: out }],
feedbacks: [tint('matrix_send_off', options, MUTED_RED)],
})
onIds.push(`msend_on_${inN}_${out}`)
knobIds.push(`msend_knob_${inN}_${out}`)