Name matrix inputs, fix the meter scale, add output meters

Found on the live S31 (read-only, plus meter subscriptions):

- Matrix inputs are named at /Matrix_Inputs/N/Channel_Input/name (an
  address missing from DiGiCo's 2014 list). Their names now appear in
  every matrix-send dropdown, preset and the new matrix_in_N_name
  variables, falling back to "Matrix In N" when empty. Matrix inputs also
  get trim and phase.
- Console names are space-padded where the surface wraps them; every name
  is now trimmed and its whitespace runs collapsed ("LEAD V RESI").
- Meter values are inverted: 127 is silence and lower is louder, about
  1 dB per step below full scale (silent channels read 127, the room mic
  36-48). The module treated 127 as peak. Level meters now read dBFS
  (approximate), gauges span -60..0 dBFS, signal and peak thresholds are
  in dBFS; gain reduction reads dB of reduction, 127 meaning none.
- Bus and matrix outputs have meters (/Aux_Outputs, /Group_Outputs and
  /Matrix_Outputs/N/Output/meter): new signal, peak, value and gauge
  feedbacks and "Buss meters" / "Matrix meters" presets.

3002 presets, 2.7 MB. npm test: 425 checks pass on Node 25 and on
Companion's Node 22. Deployed and checked on the booth Companion 5.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 10:25:39 -04:00
co-authored by Claude Opus 5.5
parent 9f64d20a24
commit 52777f3d7c
11 changed files with 436 additions and 152 deletions
+39 -14
View File
@@ -584,6 +584,8 @@ const SD_GROUP_NAMES = [
'', '', '', '', '', '', '', '', 'BAND', 'VOCALS', 'MISC', 'DRUMS', 'DRUM CRUSH', '', '', '', '', 'Broadcast mix',
'', '', '', '', 'CAFE', 'Studio', 'Master',
];
/** Matrix input names as the console reports them: padded with trailing spaces, with runs of spaces inside (captured). */
const SD_MATRIX_IN_NAMES = ['TB ', 'Studio L', 'Studio R', 'Master L', 'Master R', 'GUIDE ', 'Studio TB ', 'Dante In 63 ', 'Dante In 64 ', ''];
const SD_MATRIX_NAMES = ['MAINS L', 'MAINS R', 'SUB L', 'SUB R', 'ProP LR', 'Matrix 6', 'Matrix 7', 'Studio'];
const SD_CG_NAMES = ['DRUMS', 'GTR', 'KEYS', 'TRKS', 'VOCALS', 'FX', '', 'ProP', 'MC & SPEAKER', ''];
/** Snapshot list: index n has cue number (n+1)*100 (reported as iiis [index, cue*100, 0, name]). */
@@ -644,6 +646,10 @@ const SD_PARAM_SPECS = {
matrix: {
'Buss_Trim/trim': { spec: sdValue(-40, 40), def: 0 },
},
minput: {
'Channel_Input/trim': { spec: sdValue(-40, 40), def: 0 },
'Channel_Input/phase': { spec: SD_SWITCH, def: 0 },
},
};
const SD_SEND_PROPS = { send_level: 'level', send_on: 'on', send_pan: 'pan', 'send_pre-post': 'prepost' };
@@ -655,11 +661,13 @@ const sdInputs = [null]; // 1-based: { name, fader, mute, solo, pan, sends:[null
const sdAuxes = [null]; // 1-based: { name, fader, mute, solo }
const sdGroups = [null]; // 1-based: { name, fader, mute, solo }
const sdMatrix = [null]; // 1-based matrix outputs: { name, fader, mute, solo }
const sdMatrixIn = [null]; // 1-based matrix inputs: { sends:[null,{level,on}...] }
const sdMatrixIn = [null]; // 1-based matrix inputs: { name, sends:[null,{level,on}...] }
const sdCgs = [null]; // 1-based control groups: { name, fader, mute, solo }
const sdGeqs = [null]; // 1-based graphic EQs: { name, on, gains:[null, ...] }
let sdSnapshot = 3; // /Snapshots/Current_Snapshot
const sdMeterSubs = new Map(); // meter id -> last value sent
/** meter id -> { last, kind: 'level' | 'gr', live }: what was last sent and what to send. Silence is 127; live channels read 20-60. */
const sdMeterSubs = new Map();
const SD_LIVE_INPUTS = 3; // inputs 1-3 carry signal; aux 1, group 9 and matrix output 1 do too
let sdMeterTimer = null;
const SD_METER_INTERVAL_MS = 200; // ~5 Hz
@@ -705,7 +713,7 @@ function buildStateSd() {
for (let k = 0; k < SD_MATRIX_IN; k++) {
const sends = [null];
for (let m = 1; m <= SD_MATRIX_OUT; m++) sends.push({ level: -90, on: 0 }); // captured: -90 / 0
sdMatrixIn.push({ sends });
sdMatrixIn.push({ name: SD_MATRIX_IN_NAMES[k], sends });
}
for (let k = 0; k < SD_CGS; k++) sdCgs.push({ name: SD_CG_NAMES[k], fader: tidy16(-20 + rng() * 20), mute: 0, solo: 0 });
for (let k = 1; k <= SD_GEQS; k++) {
@@ -738,8 +746,9 @@ const SD_RE_MATRIX_SEND = /^\/Matrix_Inputs\/(\d+)\/Matrix_Send\/(\d+)\/(send_le
const SD_RE_CG_NAME = /^\/Control_Groups\/(\d+)\/name$/;
const SD_RE_CG_PROP = /^\/Control_Groups\/(\d+)\/(fader|mute|solo)$/;
const SD_RE_GEQ = /^\/Graphic_EQ\/(\d+)\/(name|geq_in|geq_gain_(\d+))$/;
const SD_RE_PARAM = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs)\/(\d+)\/((?:Channel_Input|Filters|Channel_Delay|EQ|Dynamics|Insert|Buss_Trim)\/\w+)$/;
const SD_RE_METER_SUB = /^\/Input_Channels\/(\d+)\/(?:Channel_Input\/post_meter|Dynamics\/GR_meter)$/;
const SD_RE_MINPUT_NAME = /^\/Matrix_Inputs\/(\d+)\/Channel_Input\/name$/;
const SD_RE_PARAM = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs|Matrix_Inputs)\/(\d+)\/((?:Channel_Input|Filters|Channel_Delay|EQ|Dynamics|Insert|Buss_Trim)\/\w+)$/;
const SD_RE_METER_SUB = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs)\/(\d+)\/(Channel_Input\/post_meter|Dynamics\/GR_meter|Output\/meter)$/;
/** Map an SD leaf address to { target, prop, spec } or null when there is no such thing. */
function sdResolve(address) {
@@ -776,11 +785,13 @@ function sdResolve(address) {
const prop = fixedProp || m[2];
return { target, prop, spec: SD_PROPS[prop] };
}
m = SD_RE_MINPUT_NAME.exec(address);
if (m) return sdMatrixIn[Number(m[1])] ? { target: sdMatrixIn[Number(m[1])], prop: 'name', spec: SD_PROPS.name } : null;
m = SD_RE_PARAM.exec(address);
if (m) {
const kind = { Input_Channels: 'input', Matrix_Outputs: 'matrix' }[m[1]] || 'buss';
const kind = { Input_Channels: 'input', Matrix_Outputs: 'matrix', Matrix_Inputs: 'minput' }[m[1]] || 'buss';
const entry = SD_PARAM_SPECS[kind][m[3]];
const owner = { Input_Channels: sdInputs, Aux_Outputs: sdAuxes, Group_Outputs: sdGroups, Matrix_Outputs: sdMatrix }[m[1]][Number(m[2])];
const owner = { Input_Channels: sdInputs, Aux_Outputs: sdAuxes, Group_Outputs: sdGroups, Matrix_Outputs: sdMatrix, Matrix_Inputs: sdMatrixIn }[m[1]][Number(m[2])];
if (!entry || !owner) return null;
if (!owner.params) owner.params = {};
if (!(m[3] in owner.params)) owner.params[m[3]] = entry.def;
@@ -888,8 +899,16 @@ function sdSet(address, args, types) {
}
const sub = SD_RE_METER_SUB.exec(address);
if (sub) {
if (!sdInputs[Number(sub[1])] || typeof args[0] !== 'number') return ignore(`meter subscription ${address} ${formatArgs(args)}`);
sdMeterSubs.set(Math.trunc(args[0]), -1);
const [, tree, numText, path] = sub;
const owner = { Input_Channels: sdInputs, Aux_Outputs: sdAuxes, Group_Outputs: sdGroups, Matrix_Outputs: sdMatrix }[tree];
const input = tree === 'Input_Channels';
if (!owner[Number(numText)] || (input && path === 'Output/meter') || (!input && path !== 'Output/meter') || typeof args[0] !== 'number') {
return ignore(`meter subscription ${address} ${formatArgs(args)}`);
}
const num = Number(numText);
const kind = path === 'Dynamics/GR_meter' ? 'gr' : 'level';
const live = kind === 'level' && ((input && num <= SD_LIVE_INPUTS) || (tree === 'Aux_Outputs' && num === 1) || (tree === 'Group_Outputs' && num === 9) || (tree === 'Matrix_Outputs' && num === 1));
sdMeterSubs.set(Math.trunc(args[0]), { last: -1, kind, live });
return undefined;
}
@@ -906,14 +925,20 @@ function sdSet(address, args, types) {
return undefined;
}
/** ~5 Hz: random 0..127 for every subscribed id whose value changed, as /Meters/values [id, v, id, v ...]. */
/**
* ~5 Hz: /Meters/values [id, v, id, v ...] for every subscribed id whose value changed. The scale is inverted, as on the
* real console: a level meter reads about the dB below full scale, so silence is 127 and live channels read 20-60;
* a gain reduction meter reads the dB of reduction (0-12, now and then 127 = inactive).
*/
function sdMeterTick() {
if (!sdMeterSubs.size || !replyTarget) return;
const pairs = [];
for (const [id, last] of sdMeterSubs) {
const v = Math.random() < 0.15 ? 0 : Math.floor(Math.random() * 128);
if (v === last) continue;
sdMeterSubs.set(id, v);
for (const [id, meter] of sdMeterSubs) {
let v;
if (meter.kind === 'gr') v = Math.random() < 0.1 ? 127 : Math.floor(Math.random() * 13);
else v = meter.live ? 20 + Math.floor(Math.random() * 41) : 127;
if (v === meter.last) continue;
meter.last = v;
pairs.push(int(id), int(v));
}
for (let i = 0; i < pairs.length; i += 40) send('/Meters/values', pairs.slice(i, i + 40));