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
+55 -14
View File
@@ -1,4 +1,4 @@
import { LEVEL, LIMITS, PARAMS } from '../protocol.js'
import { FADE_CURVES, LEVEL, LIMITS, PARAMS } from '../protocol.js'
export const STATE_CHOICES = [
{ id: 'on', label: 'On' },
@@ -41,6 +41,30 @@ export const opt = {
step: 0.5,
showMinAsNegativeInfinity: true,
}),
/** Fade time of a "set level" action; the curve choice shows only while it is above 0 (so `duration` cannot be an expression). */
duration: (id = 'duration') => ({
type: 'number',
id,
label: 'Fade time (ms, 0 = instant)',
default: 0,
min: 0,
max: 60000,
step: 100,
asInteger: true,
disableAutoExpression: true,
}),
curve: (id = 'curve') => ({
type: 'dropdown',
id,
label: 'Fade curve',
default: 'fader',
choices: [
{ id: 'fader', label: 'Fader travel (natural)' },
{ id: 'ease', label: 'Fader travel, ease in/out' },
{ id: 'db', label: 'Linear in dB' },
],
isVisibleExpression: '$(options:duration) > 0',
}),
delta: (id = 'delta', label = 'Change (dB, for encoders)') => ({ type: 'number', id, label, default: 1, min: -30, max: 30, step: 0.5 }),
pan: (id = 'pan', label = 'Pan (0 = left, 0.5 = centre, 1 = right)') => ({ type: 'number', id, label, default: 0.5, min: 0, max: 1, step: 0.01 }),
pdelta: (id = 'pdelta', label = 'Pan change (for encoders)') => ({ type: 'number', id, label, default: 0.05, min: -1, max: 1, step: 0.01 }),
@@ -50,6 +74,12 @@ export const opt = {
/** Said in every level adjust action: a relative step cannot start from -inf, so the first step up jumps to -60 dB. */
const ADJUST_NOTE = 'From −∞ the first step up jumps to −60 dB.'
/** Said in every level set action: the optional fade. */
const FADE_NOTE = 'With a fade time above 0 the level moves from where it is now to the target in steps of 50 ms; another set, adjust or fade on the same level ends it.'
/** [fade time in ms (0 = instant), curve] from a set-level action's options; missing options (buttons made before fades existed) mean instant. */
const fadeArgs = (options) => [Number(options?.duration) > 0 ? Math.min(60000, Math.round(Number(options.duration))) : 0, FADE_CURVES.includes(options?.curve) ? options.curve : 'fader']
export const numbers = (v) => (Array.isArray(v) ? v : v === undefined || v === '' ? [] : [v]).map(Number).filter(Number.isFinite)
/** Which entity kinds get actions / feedbacks for a parameter: 'input', 'buss', 'matrix' and/or 'minput' (matrix input). */
@@ -132,8 +162,9 @@ export function getActionDefinitions(inst) {
},
input_fader_set: {
name: 'Input: set fader',
options: [opt.channel(inst), opt.level()],
callback: async ({ options }) => inst.setFader('input', n(options.channel), n(options.level)),
description: FADE_NOTE,
options: [opt.channel(inst), opt.level(), opt.duration(), opt.curve()],
callback: async ({ options }) => inst.setFader('input', n(options.channel), n(options.level), ...fadeArgs(options)),
},
input_fader_adjust: {
name: 'Input: adjust fader (relative)',
@@ -164,8 +195,9 @@ export function getActionDefinitions(inst) {
},
send_level_set: {
name: 'Aux send: set level',
options: [opt.channel(inst), opt.buss(inst, 'aux'), opt.level()],
callback: async ({ options }) => inst.setSendLevel(n(options.channel), n(options.buss), n(options.level)),
description: FADE_NOTE,
options: [opt.channel(inst), opt.buss(inst, 'aux'), opt.level(), opt.duration(), opt.curve()],
callback: async ({ options }) => inst.setSendLevel(n(options.channel), n(options.buss), n(options.level), ...fadeArgs(options)),
},
send_level_adjust: {
name: 'Aux send: adjust level (relative)',
@@ -201,8 +233,9 @@ export function getActionDefinitions(inst) {
},
buss_fader_set: {
name: 'Buss: set fader',
options: [opt.buss(inst), opt.level()],
callback: async ({ options }) => inst.setFader('buss', n(options.buss), n(options.level)),
description: FADE_NOTE,
options: [opt.buss(inst), opt.level(), opt.duration(), opt.curve()],
callback: async ({ options }) => inst.setFader('buss', n(options.buss), n(options.level), ...fadeArgs(options)),
},
buss_fader_adjust: {
name: 'Buss: adjust fader (relative)',
@@ -223,8 +256,9 @@ export function getActionDefinitions(inst) {
},
matrix_fader_set: {
name: 'Matrix output: set fader',
options: [opt.matrix(inst), opt.level()],
callback: async ({ options }) => inst.setFader('matrix', n(options.matrix), n(options.level)),
description: FADE_NOTE,
options: [opt.matrix(inst), opt.level(), opt.duration(), opt.curve()],
callback: async ({ options }) => inst.setFader('matrix', n(options.matrix), n(options.level), ...fadeArgs(options)),
},
matrix_fader_adjust: {
name: 'Matrix output: adjust fader (relative)',
@@ -239,9 +273,9 @@ export function getActionDefinitions(inst) {
},
matrix_send_level_set: {
name: 'Matrix send: set level',
description: 'A matrix send goes down to −90 dB (−∞).',
options: [opt.matrixIn(inst), opt.matrix(inst, 'matrixOut'), opt.level('level', 'Level (dB)', LEVEL.matrixSendMin)],
callback: async ({ options }) => inst.setMatrixSendLevel(n(options.matrixIn), n(options.matrixOut), n(options.level)),
description: `A matrix send goes down to −90 dB (−∞). ${FADE_NOTE}`,
options: [opt.matrixIn(inst), opt.matrix(inst, 'matrixOut'), opt.level('level', 'Level (dB)', LEVEL.matrixSendMin), opt.duration(), opt.curve()],
callback: async ({ options }) => inst.setMatrixSendLevel(n(options.matrixIn), n(options.matrixOut), n(options.level), ...fadeArgs(options)),
},
matrix_send_level_adjust: {
name: 'Matrix send: adjust level (relative)',
@@ -262,8 +296,9 @@ export function getActionDefinitions(inst) {
},
cg_fader_set: {
name: 'Control group: set fader',
options: [opt.cg(inst), opt.level()],
callback: async ({ options }) => inst.setFader('cg', n(options.cg), n(options.level)),
description: FADE_NOTE,
options: [opt.cg(inst), opt.level(), opt.duration(), opt.curve()],
callback: async ({ options }) => inst.setFader('cg', n(options.cg), n(options.level), ...fadeArgs(options)),
},
cg_fader_adjust: {
name: 'Control group: adjust fader (relative)',
@@ -305,6 +340,12 @@ export function getActionDefinitions(inst) {
},
snapshot_next: { name: 'Snapshot: next', options: [], callback: async () => inst.fireNextSnapshot() },
snapshot_prev: { name: 'Snapshot: previous', options: [], callback: async () => inst.firePrevSnapshot() },
fade_stop: {
name: 'Stop all fades',
description: 'Ends every running level fade where it is.',
options: [],
callback: async () => inst.stopFades(),
},
reload: {
name: 'Console: reload names and levels',
options: [],