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
+28 -10
View File
@@ -3,8 +3,7 @@
* choice builders and the query plans used to load it.
*
* Entities: input, buss (aux and group are two views of the same 25 busses, named and levelled
* on the side matching the buss mode), matrix (matrix outputs), minput (matrix inputs, no
* name address), cg (control groups), geq (graphic EQs). All numbers are 1-based.
* on the side matching the buss mode), matrix (matrix outputs), minput (matrix inputs), cg (control groups), geq (graphic EQs). All numbers are 1-based.
*
* applyMessage(address, args) is the single entry point for console traffic (and for the
* module's own optimistic writes): it returns the parsed event annotated with
@@ -13,7 +12,7 @@
* stereo/mono flags alone never are)
*/
import { LIMITS, K, addr, query, parse, fmtCue } from './protocol.js'
import { LIMITS, K, addr, query, parse, fmtCue, cleanName } from './protocol.js'
const EPS = 1e-4
@@ -25,6 +24,10 @@ function setField(obj, field, value) {
return old !== value
}
function clampInt(v, lo, hi) {
return Math.min(hi, Math.max(lo, v))
}
function rec(map, key) {
let r = map.get(key)
if (!r) {
@@ -77,6 +80,9 @@ export class Buss {
}
}
/** Peak tracking keeps, per meter, the last time a reading was at most k steps below full scale, for k = 0..LOUD_STEPS. */
const LOUD_STEPS = 20
const FALLBACK_NAME = { input: 'Input', matrix: 'Matrix', cg: 'CG', geq: 'Graphic EQ', minput: 'Matrix In' }
export class State {
@@ -93,6 +99,7 @@ export class State {
this.inputs = new Map()
this.busses = new Map()
this.matrix = new Map()
this.minputs = new Map() // matrix inputs: { name }
this.cgs = new Map()
this.geqs = new Map()
this.sends = new Map() // "ch:buss" -> { level, on, pan }
@@ -100,7 +107,7 @@ export class State {
this.matrixSends = new Map() // "in:out" -> { level, on }
this.params = new Map() // K.param(...) -> boolean | number (per-channel switches and values, loaded lazily)
this.snapshots = { current: undefined, count: undefined, list: new Map() } // index -> { name, cue }
this.meters = new Map() // meter id -> { value, at, peakAt }
this.meters = new Map() // meter id -> { value, at, loud } (loud[k]: last time a reading was k steps below full scale or louder)
}
// ---------- inbound ----------
@@ -112,6 +119,7 @@ export class State {
ev.structural = false
switch (ev.kind) {
case 'consoleName':
ev.value = cleanName(ev.value)
ev.changed = setField(this.console, 'name', ev.value)
break
case 'session':
@@ -143,6 +151,7 @@ export class State {
})
break
case 'name':
ev.value = cleanName(ev.value) // the surface pads and wraps names: trim and collapse whitespace once, here
ev.changed = this.#applyName(ev)
ev.structural = ev.changed
break
@@ -189,6 +198,7 @@ export class State {
this.snapshots.count = ev.value
break
case 'snapName': {
ev.name = cleanName(ev.name)
const old = this.snapshots.list.get(ev.index)
this.snapshots.list.set(ev.index, { name: ev.name, cue: ev.cue })
ev.changed = !old || old.name !== ev.name || old.cue !== ev.cue
@@ -198,10 +208,10 @@ export class State {
case 'meters': {
const now = Date.now()
for (const [id, value] of ev.pairs) {
const m = this.meters.get(id) || { value: 0, at: 0, peakAt: 0 }
const m = this.meters.get(id) || { value: 127, at: 0, loud: new Float64Array(LOUD_STEPS + 1) }
m.value = value
m.at = now
if (value >= 127) m.peakAt = now
for (let k = Math.max(0, value); k <= LOUD_STEPS; k++) m.loud[k] = now
this.meters.set(id, m)
}
ev.changed = ev.pairs.length > 0
@@ -254,6 +264,8 @@ export class State {
return this.inputs
case 'matrix':
return this.matrix
case 'minput':
return this.minputs
case 'cg':
return this.cgs
case 'geq':
@@ -286,7 +298,7 @@ export class State {
}
}
/** An entity exists when its name answered (matrix inputs have no name: they exist while matrix outputs do). */
/** An entity exists when its name answered. */
has(ent, n) {
if (!(n >= 1 && n <= this.limit(ent))) return false
switch (ent) {
@@ -294,8 +306,6 @@ export class State {
const b = this.busses.get(n)
return !!b && (b.auxName !== undefined || b.groupName !== undefined)
}
case 'minput':
return this.limit('matrix') > 0 && (this.counts.matrixIn !== undefined || this.matrix.size > 0)
default:
return this.#map(ent).get(n)?.name !== undefined
}
@@ -319,7 +329,6 @@ export class State {
/** The name as the console reports it (may be ""), or undefined while unknown. */
rawName(ent, n) {
if (ent === 'buss') return this.busses.get(n)?.name
if (ent === 'minput') return undefined
return this.#map(ent).get(n)?.name
}
@@ -391,6 +400,13 @@ export class State {
return this.meters.get(id)
}
/** Last time meter `id` read at or above `thresholdDb` dBFS (0 = never; thresholds from -20 to 0 are tracked). */
meterPeakAt(id, thresholdDb) {
const m = this.meters.get(id)
if (!m || !Number.isFinite(thresholdDb)) return 0
return m.loud[clampInt(Math.floor(-thresholdDb), 0, LOUD_STEPS)]
}
snapshotName(index) {
return this.snapshots.list.get(index)?.name
}
@@ -458,6 +474,7 @@ export class State {
if (want('buss', n, 'group')) q.push(query(addr.name('buss', n, 'group')))
}
for (let n = 1; n <= this.limit('matrix'); n++) if (want('matrix', n)) q.push(query(addr.name('matrix', n)))
for (let n = 1; n <= this.limit('minput'); n++) if (want('minput', n)) q.push(query(addr.name('minput', n)))
for (let n = 1; n <= this.limit('cg'); n++) if (want('cg', n)) q.push(query(addr.name('cg', n)))
for (let n = 1; n <= this.limit('geq'); n++) if (want('geq', n)) q.push(query(addr.name('geq', n)))
return q
@@ -493,6 +510,7 @@ export class State {
aux: this.namedBusses('aux').length,
group: this.namedBusses('group').length,
matrix: found('matrix'),
matrixIn: found('minput'),
cgs: found('cg'),
geqs: found('geq'),
snapshots: this.snapshots.list.size,