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
+27 -10
View File
@@ -75,8 +75,10 @@ fader, mute and solo are on the Aux_Outputs or Group_Outputs side that matches i
## Actions ## Actions
Level actions take dB (`-inf` at the bottom of the number field). Every level has a **set** and a Level actions take dB (`-inf` at the bottom of the number field). Every level has a **set** and a
relative **adjust** version for rotary encoders; adjusting from -inf lands at -60 dB on the first step up. relative **adjust** version for rotary encoders. A relative step cannot start from -inf: from −∞ the first
Toggles read the current state first when it is not known yet. step up jumps to −60 dB. A matrix send has its floor at **-90 dB** (the console reports off matrix sends as -90, and
the module never writes lower); faders and aux sends go down to -150 dB. Toggles read the current state first when it
is not known yet.
| Group | Actions | | Group | Actions |
| --- | --- | | --- | --- |
@@ -106,14 +108,26 @@ snapshot's list position (0 = the first); verify it once on your console before
## Feedbacks ## Feedbacks
* Boolean (button colour): input muted / solo, aux send on, group assigned, buss muted / Every action that changes something the console reports has a feedback for it: a boolean for every switch, a value
solo, matrix muted, control group muted, snapshot is current, fader level above/below a threshold, send (text for a button's local variable) and a bar for every level. The state a feedback needs (sends, matrix sends,
level above/below, console link alive, relay has an iPad, every channel-strip switch above, and the meter graphic EQ bands, channel-strip values) is loaded the first time one is shown, and each button re-checks only when its
own value changes.
* Boolean (button colour): input muted / solo; aux send on; group assigned; buss muted / solo; matrix output muted /
solo; control group muted / solo; **matrix send on**; **graphic EQ in**; snapshot is current; fader level above/below
a threshold; aux send level above/below; console link alive; relay has an iPad; every channel-strip switch
(phantom, phase, pad, EQ in, compressor in, gate in, high-pass in, low-pass in, delay in, insert A/B in); and the meter
feedbacks below. feedbacks below.
* Value (use with a preset's local variable to put live numbers in button text): input fader, input pan, * Value (put live numbers in button text with a preset's local variable): input fader and pan; aux send level, On/Off
send level, send On/Off, buss / matrix / control group fader, current snapshot name, input meter value. and pan; buss / matrix / control group fader; **matrix send level**; **graphic EQ band gain**; trim, analog gain and
* Advanced bars: input fader, aux send level and buss fader draw a horizontal bar along the bottom of the filter frequency per input and buss (trim also per matrix output); current snapshot name; input meter value. Levels
button (-60 to +10 dB); the input meter draws a vertical bar at the right edge (green, amber, red). read `-inf` at or below -90 dB.
* Advanced bars: input fader, aux send level, buss / matrix output / control group fader, **matrix send level** (all
-60 to +10 dB) and **graphic EQ band gain** (-12 to +12 dB) draw a horizontal bar along the bottom of the button;
the input meter draws a vertical bar at the right edge (green, amber, red).
Not covered by a feedback: free-form *Send raw OSC*, and *Set name* (the name is the variable `input_N_name`). There is
no matrix-output solo action, only its feedback.
## Variables ## Variables
@@ -135,12 +149,15 @@ Presets are named with the live entity names and rebuilt when names load.
| Sends to *aux name* | One section per named aux buss: per input an **On/off** button (green when on, shows the level) and a **Level (knob)** (turn = send level, press = on/off, with a bar) | | Sends to *aux name* | One section per named aux buss: per input an **On/off** button (green when on, shows the level) and a **Level (knob)** (turn = send level, press = on/off, with a bar) |
| Group *group name* | One section per named group buss: per input an **Assign** toggle, green when assigned | | Group *group name* | One section per named group buss: per input an **Assign** toggle, green when assigned |
| Matrix, Control groups | Mute and fader knob per output / group | | Matrix, Control groups | Mute and fader knob per output / group |
| Matrix sends to *output name* | One section per matrix output: per matrix input an **On/off** button (green when on; shows `Matrix In N` and the level) and a **Level (knob)** (turn = send level, press = on/off, with a bar) |
| Graphic EQs | One **In/out** toggle per graphic EQ, green when in |
| Snapshots | **Fire** per snapshot (green when current), next, previous, current (display) | | Snapshots | **Fire** per snapshot (green when current), next, previous, current (display) |
| Input meters | Level, gate gain reduction and compressor gain reduction buttons per input | | Input meters | Level, gate gain reduction and compressor gain reduction buttons per input |
| Console | Reload, connection status, relay status | | Console | Reload, connection status, relay status |
Busses and control groups with no name are unused slots and get no presets. If the preset list would exceed Busses and control groups with no name are unused slots and get no presets. If the preset list would exceed
3000 entries or 4 MB, the per-input send level knobs are left out and a warning is logged. 3000 entries or 4 MB, the per-input gate / compressor reduction meter presets are left out first, then the
per-input aux send level knobs, and a warning is logged.
## Meters ## Meters
+21 -7
View File
@@ -29,13 +29,14 @@ export const opt = {
cg: (inst, id = 'cg', label = 'Control group') => opt.dropdown(id, label, inst.state.choices.cgs()), cg: (inst, id = 'cg', label = 'Control group') => opt.dropdown(id, label, inst.state.choices.cgs()),
geq: (inst, id = 'geq', label = 'Graphic EQ') => opt.dropdown(id, label, inst.state.choices.geqs()), geq: (inst, id = 'geq', label = 'Graphic EQ') => opt.dropdown(id, label, inst.state.choices.geqs()),
snapshot: (inst, id = 'snapshot', label = 'Snapshot') => opt.dropdown(id, label, inst.state.choices.snapshots(), { allowCustom: true, default: firstId(inst.state.choices.snapshots(), 0) }), snapshot: (inst, id = 'snapshot', label = 'Snapshot') => opt.dropdown(id, label, inst.state.choices.snapshots(), { allowCustom: true, default: firstId(inst.state.choices.snapshots(), 0) }),
band: (id = 'band', label = 'Band') => ({ type: 'number', id, label, default: 1, min: 1, max: LIMITS.geqBands, step: 1 }),
state: (label = 'State') => ({ type: 'dropdown', id: 'state', label, choices: STATE_CHOICES, default: 'toggle' }), state: (label = 'State') => ({ type: 'dropdown', id: 'state', label, choices: STATE_CHOICES, default: 'toggle' }),
level: (id = 'level', label = 'Level (dB)') => ({ level: (id = 'level', label = 'Level (dB)', min = LEVEL.min) => ({
type: 'number', type: 'number',
id, id,
label, label,
default: 0, default: 0,
min: LEVEL.min, min,
max: LEVEL.max, max: LEVEL.max,
step: 0.5, step: 0.5,
showMinAsNegativeInfinity: true, showMinAsNegativeInfinity: true,
@@ -46,6 +47,9 @@ export const opt = {
} }
/** Normalise a multidropdown / dropdown value to an array of numbers. */ /** Normalise a multidropdown / dropdown value to an array of numbers. */
/** 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.'
export const numbers = (v) => (Array.isArray(v) ? v : v === undefined || v === '' ? [] : [v]).map(Number).filter(Number.isFinite) 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' and/or 'matrix'. */ /** Which entity kinds get actions / feedbacks for a parameter: 'input', 'buss' and/or 'matrix'. */
@@ -130,7 +134,7 @@ export function getActionDefinitions(inst) {
}, },
input_fader_adjust: { input_fader_adjust: {
name: 'Input: adjust fader (relative)', name: 'Input: adjust fader (relative)',
description: 'For rotary encoders. From -inf the first step up lands at -60 dB.', description: `For rotary encoders. ${ADJUST_NOTE}`,
options: [opt.channel(inst), opt.delta()], options: [opt.channel(inst), opt.delta()],
callback: async ({ options }) => inst.adjustFader('input', n(options.channel), n(options.delta)), callback: async ({ options }) => inst.adjustFader('input', n(options.channel), n(options.delta)),
}, },
@@ -162,7 +166,7 @@ export function getActionDefinitions(inst) {
}, },
send_level_adjust: { send_level_adjust: {
name: 'Aux send: adjust level (relative)', name: 'Aux send: adjust level (relative)',
description: 'For rotary encoders. From -inf the first step up lands at -60 dB.', description: `For rotary encoders. ${ADJUST_NOTE}`,
options: [opt.channel(inst), opt.buss(inst, 'aux'), opt.delta()], options: [opt.channel(inst), opt.buss(inst, 'aux'), opt.delta()],
callback: async ({ options }) => inst.adjustSendLevel(n(options.channel), n(options.buss), n(options.delta)), callback: async ({ options }) => inst.adjustSendLevel(n(options.channel), n(options.buss), n(options.delta)),
}, },
@@ -199,6 +203,7 @@ export function getActionDefinitions(inst) {
}, },
buss_fader_adjust: { buss_fader_adjust: {
name: 'Buss: adjust fader (relative)', name: 'Buss: adjust fader (relative)',
description: `For rotary encoders. ${ADJUST_NOTE}`,
options: [opt.buss(inst), opt.delta()], options: [opt.buss(inst), opt.delta()],
callback: async ({ options }) => inst.adjustFader('buss', n(options.buss), n(options.delta)), callback: async ({ options }) => inst.adjustFader('buss', n(options.buss), n(options.delta)),
}, },
@@ -208,6 +213,11 @@ export function getActionDefinitions(inst) {
options: [opt.matrix(inst), opt.state()], options: [opt.matrix(inst), opt.state()],
callback: async ({ options }) => inst.setSwitch('matrix', 'mute', n(options.matrix), options.state), callback: async ({ options }) => inst.setSwitch('matrix', 'mute', n(options.matrix), options.state),
}, },
matrix_solo: {
name: 'Matrix output: solo',
options: [opt.matrix(inst), opt.state()],
callback: async ({ options }) => inst.setSwitch('matrix', 'solo', n(options.matrix), options.state),
},
matrix_fader_set: { matrix_fader_set: {
name: 'Matrix output: set fader', name: 'Matrix output: set fader',
options: [opt.matrix(inst), opt.level()], options: [opt.matrix(inst), opt.level()],
@@ -215,6 +225,7 @@ export function getActionDefinitions(inst) {
}, },
matrix_fader_adjust: { matrix_fader_adjust: {
name: 'Matrix output: adjust fader (relative)', name: 'Matrix output: adjust fader (relative)',
description: `For rotary encoders. ${ADJUST_NOTE}`,
options: [opt.matrix(inst), opt.delta()], options: [opt.matrix(inst), opt.delta()],
callback: async ({ options }) => inst.adjustFader('matrix', n(options.matrix), n(options.delta)), callback: async ({ options }) => inst.adjustFader('matrix', n(options.matrix), n(options.delta)),
}, },
@@ -225,11 +236,13 @@ export function getActionDefinitions(inst) {
}, },
matrix_send_level_set: { matrix_send_level_set: {
name: 'Matrix send: set level', name: 'Matrix send: set level',
options: [opt.matrixIn(inst), opt.matrix(inst, 'matrixOut'), opt.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)), callback: async ({ options }) => inst.setMatrixSendLevel(n(options.matrixIn), n(options.matrixOut), n(options.level)),
}, },
matrix_send_level_adjust: { matrix_send_level_adjust: {
name: 'Matrix send: adjust level (relative)', name: 'Matrix send: adjust level (relative)',
description: `For rotary encoders. ${ADJUST_NOTE}`,
options: [opt.matrixIn(inst), opt.matrix(inst, 'matrixOut'), opt.delta()], options: [opt.matrixIn(inst), opt.matrix(inst, 'matrixOut'), opt.delta()],
callback: async ({ options }) => inst.adjustMatrixSendLevel(n(options.matrixIn), n(options.matrixOut), n(options.delta)), callback: async ({ options }) => inst.adjustMatrixSendLevel(n(options.matrixIn), n(options.matrixOut), n(options.delta)),
}, },
@@ -251,6 +264,7 @@ export function getActionDefinitions(inst) {
}, },
cg_fader_adjust: { cg_fader_adjust: {
name: 'Control group: adjust fader (relative)', name: 'Control group: adjust fader (relative)',
description: `For rotary encoders. ${ADJUST_NOTE}`,
options: [opt.cg(inst), opt.delta()], options: [opt.cg(inst), opt.delta()],
callback: async ({ options }) => inst.adjustFader('cg', n(options.cg), n(options.delta)), callback: async ({ options }) => inst.adjustFader('cg', n(options.cg), n(options.delta)),
}, },
@@ -265,7 +279,7 @@ export function getActionDefinitions(inst) {
description: 'Band 1-32, gain in dB.', description: 'Band 1-32, gain in dB.',
options: [ options: [
opt.geq(inst), opt.geq(inst),
{ type: 'number', id: 'band', label: 'Band', default: 1, min: 1, max: LIMITS.geqBands, step: 1 }, opt.band(),
{ type: 'number', id: 'gain', label: 'Gain (dB)', default: 0, min: -12, max: 12, step: 0.5 }, { type: 'number', id: 'gain', label: 'Gain (dB)', default: 0, min: -12, max: 12, step: 0.5 },
], ],
callback: async ({ options }) => inst.setGeqBand(n(options.geq), n(options.band), n(options.gain)), callback: async ({ options }) => inst.setGeqBand(n(options.geq), n(options.band), n(options.gain)),
@@ -274,7 +288,7 @@ export function getActionDefinitions(inst) {
name: 'Graphic EQ: adjust band gain (relative)', name: 'Graphic EQ: adjust band gain (relative)',
options: [ options: [
opt.geq(inst), opt.geq(inst),
{ type: 'number', id: 'band', label: 'Band', default: 1, min: 1, max: LIMITS.geqBands, step: 1 }, opt.band(),
{ type: 'number', id: 'delta', label: 'Change (dB, for encoders)', default: 1, min: -12, max: 12, step: 0.5 }, { type: 'number', id: 'delta', label: 'Change (dB, for encoders)', default: 1, min: -12, max: 12, step: 0.5 },
], ],
callback: async ({ options }) => inst.adjustGeqBand(n(options.geq), n(options.band), n(options.delta)), callback: async ({ options }) => inst.adjustGeqBand(n(options.geq), n(options.band), n(options.delta)),
+77 -6
View File
@@ -1,5 +1,5 @@
import { combineRgb } from '@companion-module/base' import { combineRgb } from '@companion-module/base'
import { LEVEL, METERS, PARAMS, clamp, fmtDb, fmtPan, meterId } from '../protocol.js' import { GEQ_RANGE, LEVEL, METERS, PARAMS, clamp, fmtDb, fmtPan, fmtValue, meterId } from '../protocol.js'
import { horizontalBar, verticalMeter } from '../image.js' import { horizontalBar, verticalMeter } from '../image.js'
import { opt, paramEntities, paramEntityOption } from './actions.js' import { opt, paramEntities, paramEntityOption } from './actions.js'
@@ -18,6 +18,9 @@ const n = (v) => Number(v)
/** -60 dB .. +10 dB mapped to 0..1 for the level bars. */ /** -60 dB .. +10 dB mapped to 0..1 for the level bars. */
const levelFraction = (db) => clamp((db + 60) / (LEVEL.max + 60), 0, 1) const levelFraction = (db) => clamp((db + 60) / (LEVEL.max + 60), 0, 1)
/** GEQ band gain -12 .. +12 dB mapped to 0..1 for the band bar. */
const geqFraction = (db) => clamp((db - GEQ_RANGE.min) / (GEQ_RANGE.max - GEQ_RANGE.min), 0, 1)
const colorOption = { type: 'colorpicker', id: 'color', label: 'Bar colour', default: COLORS.bar, returnType: 'number' } const colorOption = { type: 'colorpicker', id: 'color', label: 'Bar colour', default: COLORS.bar, returnType: 'number' }
const KIND_VISIBLE = (kind) => `$(options:kind) == '${kind}'` const KIND_VISIBLE = (kind) => `$(options:kind) == '${kind}'`
@@ -30,6 +33,9 @@ const KIND_VISIBLE = (kind) => `$(options:kind) == '${kind}'`
export const watchKey = { export const watchKey = {
entity: (ent, num) => `${ent}:${num}`, entity: (ent, num) => `${ent}:${num}`,
send: (ch, buss) => `send:${ch}:${buss}`, send: (ch, buss) => `send:${ch}:${buss}`,
msend: (inN, outN) => `msend:${inN}:${outN}`,
geqOn: (n) => `geq:${n}:on`,
geqGain: (n, band) => `geq:${n}:gain:${band}`,
group: (ch, buss) => `group:${ch}:${buss}`, group: (ch, buss) => `group:${ch}:${buss}`,
meter: (id) => `meter:${id}`, meter: (id) => `meter:${id}`,
snapshot: 'snapshot', snapshot: 'snapshot',
@@ -107,11 +113,38 @@ export function getFeedbackDefinitions(inst) {
} }
const entityKey = (ent, id) => (o) => watchKey.entity(ent, n(o[id])) const entityKey = (ent, id) => (o) => watchKey.entity(ent, n(o[id]))
const sendKey = (o) => watchKey.send(n(o.channel), n(o.buss)) const sendKey = (o) => watchKey.send(n(o.channel), n(o.buss))
const msendKey = (o) => watchKey.msend(n(o.matrixIn), n(o.matrixOut))
const msendOf = (o) => {
inst.ensureMatrixSend(n(o.matrixIn), n(o.matrixOut))
return s.getMatrixSend(n(o.matrixIn), n(o.matrixOut))
}
const msendOptions = () => [opt.matrixIn(inst), opt.matrix(inst, 'matrixOut')]
const geqOnKey = (o) => watchKey.geqOn(n(o.geq))
const geqGainKey = (o) => watchKey.geqGain(n(o.geq), n(o.band))
const geqGainOf = (o) => {
inst.ensureGeqBand(n(o.geq), n(o.band))
return s.getGeq(n(o.geq)).gains?.get(n(o.band))
}
/** Boolean feedback for every switch in PARAMS, on the entity kinds that have it. */ /** Boolean feedback for every switch in PARAMS, on the entity kinds that have it. */
const paramFeedbacks = {} const paramFeedbacks = {}
for (const [param, p] of Object.entries(PARAMS)) { for (const [param, p] of Object.entries(PARAMS)) {
if (p.type !== 'switch') continue if (p.type === 'value') {
for (const ent of paramEntities(param)) {
const entOption = paramEntityOption(inst, ent)
paramFeedbacks[`${ent}_${param}_value`] = value(
`${{ input: 'Input', buss: 'Buss', matrix: 'Matrix output' }[ent]}: ${p.label} (${p.unit} text)`,
[entOption],
entityKey(ent, entOption.id),
({ options }) => {
const target = n(options[entOption.id])
inst.ensureParam(ent, target, param)
return fmtValue(s.getParam(ent, target, param), p.unit) || '--'
},
)
}
continue
}
for (const ent of paramEntities(param)) { for (const ent of paramEntities(param)) {
const style = param === 'phantom' ? RED : GREEN const style = param === 'phantom' ? RED : GREEN
const entOption = paramEntityOption(inst, ent) const entOption = paramEntityOption(inst, ent)
@@ -145,6 +178,13 @@ export function getFeedbackDefinitions(inst) {
buss_solo: bool('Buss: solo on', AMBER, [opt.buss(inst)], entityKey('buss', 'buss'), ({ options }) => s.getSolo('buss', n(options.buss)) === true), buss_solo: bool('Buss: solo on', AMBER, [opt.buss(inst)], entityKey('buss', 'buss'), ({ options }) => s.getSolo('buss', n(options.buss)) === true),
matrix_muted: bool('Matrix output: muted', RED, [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => s.getMute('matrix', n(options.matrix)) === true), matrix_muted: bool('Matrix output: muted', RED, [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => s.getMute('matrix', n(options.matrix)) === true),
cg_muted: bool('Control group: muted', RED, [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => s.getMute('cg', n(options.cg)) === true), cg_muted: bool('Control group: muted', RED, [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => s.getMute('cg', n(options.cg)) === true),
matrix_solo: bool('Matrix output: solo on', AMBER, [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => s.getSolo('matrix', n(options.matrix)) === true),
cg_solo: bool('Control group: solo on', AMBER, [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => s.getSolo('cg', n(options.cg)) === true),
matrix_send_on: bool('Matrix send: on', GREEN, msendOptions(), msendKey, ({ options }) => msendOf(options).on === true),
geq_on: bool('Graphic EQ: in', GREEN, [opt.geq(inst)], geqOnKey, ({ options }) => {
inst.ensureGeqOn(n(options.geq))
return s.getGeq(n(options.geq)).on === true
}),
snapshot_current: bool('Snapshot: is the current one', GREEN, [opt.snapshot(inst)], () => watchKey.snapshot, ({ options }) => s.snapshots.current === Math.trunc(n(options.snapshot))), snapshot_current: bool('Snapshot: is the current one', GREEN, [opt.snapshot(inst)], () => watchKey.snapshot, ({ options }) => s.snapshots.current === Math.trunc(n(options.snapshot))),
level_compare: bool( level_compare: bool(
'Fader level: above/below', 'Fader level: above/below',
@@ -213,6 +253,12 @@ export function getFeedbackDefinitions(inst) {
buss_fader_value: value('Buss: fader level (dB text)', [opt.buss(inst)], entityKey('buss', 'buss'), ({ options }) => fmtDb(s.getFader('buss', n(options.buss))) || '--'), buss_fader_value: value('Buss: fader level (dB text)', [opt.buss(inst)], entityKey('buss', 'buss'), ({ options }) => fmtDb(s.getFader('buss', n(options.buss))) || '--'),
matrix_fader_value: value('Matrix output: fader level (dB text)', [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => fmtDb(s.getFader('matrix', n(options.matrix))) || '--'), matrix_fader_value: value('Matrix output: fader level (dB text)', [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => fmtDb(s.getFader('matrix', n(options.matrix))) || '--'),
cg_fader_value: value('Control group: fader level (dB text)', [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => fmtDb(s.getFader('cg', n(options.cg))) || '--'), cg_fader_value: value('Control group: fader level (dB text)', [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => fmtDb(s.getFader('cg', n(options.cg))) || '--'),
send_pan_value: value('Aux send: pan (text)', [opt.channel(inst), opt.buss(inst, 'aux')], sendKey, ({ options }) => {
inst.ensureSendPan(n(options.channel), n(options.buss))
return fmtPan(s.getSend(n(options.channel), n(options.buss)).pan) || '--'
}),
matrix_send_level_value: value('Matrix send: level (dB text)', msendOptions(), msendKey, ({ options }) => fmtDb(msendOf(options).level) || '--'),
geq_band_value: value('Graphic EQ: band gain (dB text)', [opt.geq(inst), opt.band()], geqGainKey, ({ options }) => fmtDb(geqGainOf(options)) || '--'),
snapshot_name_value: value('Snapshot: current name', [], () => watchKey.snapshot, () => s.snapshotName(s.snapshots.current) ?? '--'), snapshot_name_value: value('Snapshot: current name', [], () => watchKey.snapshot, () => s.snapshotName(s.snapshots.current) ?? '--'),
input_meter_value: value( input_meter_value: value(
'Input meter: value', 'Input meter: value',
@@ -250,6 +296,22 @@ export function getFeedbackDefinitions(inst) {
const v = s.getFader('buss', n(fb.options.buss)) const v = s.getFader('buss', n(fb.options.buss))
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color }) return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
}), }),
matrix_fader_bar: bar('Matrix output: fader bar', [opt.matrix(inst)], entityKey('matrix', 'matrix'), (fb, image) => {
const v = s.getFader('matrix', n(fb.options.matrix))
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
}),
cg_fader_bar: bar('Control group: fader bar', [opt.cg(inst)], entityKey('cg', 'cg'), (fb, image) => {
const v = s.getFader('cg', n(fb.options.cg))
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
}),
matrix_send_level_bar: bar('Matrix send: level bar', msendOptions(), msendKey, (fb, image) => {
const v = msendOf(fb.options).level
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
}),
geq_band_bar: bar('Graphic EQ: band gain bar', [opt.geq(inst), opt.band()], geqGainKey, (fb, image) => {
const v = geqGainOf(fb.options)
return v === undefined ? {} : horizontalBar({ ...image, fraction: geqFraction(v), color: fb.options.color })
}),
input_meter_bar: { input_meter_bar: {
type: 'advanced', type: 'advanced',
name: 'Input meter: bar (right edge)', name: 'Input meter: bar (right edge)',
@@ -278,6 +340,10 @@ export function watchKeyForEvent(ev) {
return watchKey.send(ev.ch, ev.buss) return watchKey.send(ev.ch, ev.buss)
case 'group': case 'group':
return watchKey.group(ev.ch, ev.buss) return watchKey.group(ev.ch, ev.buss)
case 'msend':
return watchKey.msend(ev.input, ev.out)
case 'geq':
return ev.field === 'on' ? watchKey.geqOn(ev.n) : watchKey.geqGain(ev.n, ev.band)
case 'snapCurrent': case 'snapCurrent':
case 'snapName': case 'snapName':
return watchKey.snapshot return watchKey.snapshot
@@ -292,14 +358,14 @@ export function feedbackIdsForEvent(ev) {
case 'mute': case 'mute':
return { input: ['input_muted'], buss: ['buss_muted'], matrix: ['matrix_muted'], cg: ['cg_muted'] }[ev.ent] || [] return { input: ['input_muted'], buss: ['buss_muted'], matrix: ['matrix_muted'], cg: ['cg_muted'] }[ev.ent] || []
case 'solo': case 'solo':
return { input: ['input_solo'], buss: ['buss_solo'] }[ev.ent] || [] return { input: ['input_solo'], buss: ['buss_solo'], matrix: ['matrix_solo'], cg: ['cg_solo'] }[ev.ent] || []
case 'fader': case 'fader':
return ( return (
{ {
input: ['input_fader_value', 'input_fader_bar', 'level_compare'], input: ['input_fader_value', 'input_fader_bar', 'level_compare'],
buss: ['buss_fader_value', 'buss_fader_bar', 'level_compare'], buss: ['buss_fader_value', 'buss_fader_bar', 'level_compare'],
matrix: ['matrix_fader_value', 'level_compare'], matrix: ['matrix_fader_value', 'matrix_fader_bar', 'level_compare'],
cg: ['cg_fader_value', 'level_compare'], cg: ['cg_fader_value', 'cg_fader_bar', 'level_compare'],
}[ev.ent] || [] }[ev.ent] || []
) )
case 'pan': case 'pan':
@@ -307,11 +373,16 @@ export function feedbackIdsForEvent(ev) {
case 'send': case 'send':
if (ev.field === 'level') return ['send_level_value', 'send_level_bar', 'send_level_compare'] if (ev.field === 'level') return ['send_level_value', 'send_level_bar', 'send_level_compare']
if (ev.field === 'on') return ['send_on', 'send_on_value'] if (ev.field === 'on') return ['send_on', 'send_on_value']
if (ev.field === 'pan') return ['send_pan_value']
return [] return []
case 'msend':
return ev.field === 'level' ? ['matrix_send_level_value', 'matrix_send_level_bar'] : ['matrix_send_on']
case 'geq':
return ev.field === 'on' ? ['geq_on'] : ['geq_band_value', 'geq_band_bar']
case 'group': case 'group':
return ['group_assigned'] return ['group_assigned']
case 'param': case 'param':
return PARAMS[ev.param].type === 'switch' ? [`${ev.ent}_${ev.param}_state`] : [] return [`${ev.ent}_${ev.param}_${PARAMS[ev.param].type === 'switch' ? 'state' : 'value'}`]
case 'snapCurrent': case 'snapCurrent':
case 'snapName': case 'snapName':
return ['snapshot_current', 'snapshot_name_value'] return ['snapshot_current', 'snapshot_name_value']
+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 fb = (feedbackId, options, bg, color = WHITE) => ({ feedbackId, options, style: { bgcolor: bg, color } })
const act = (actionId, options) => ({ actionId, options }) 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 s = inst.state
const L = inst.label const L = inst.label
const presets = {} const presets = {}
@@ -69,6 +72,7 @@ function build(inst, withSendKnobs) {
['gate', 'input_gate_gr', 'Gate reduction'], ['gate', 'input_gate_gr', 'Gate reduction'],
['comp', 'input_comp_gr', 'Compressor reduction'], ['comp', 'input_comp_gr', 'Compressor reduction'],
]) { ]) {
if (meter !== 'level' && !grMeters) continue
presets[`${prefix}_${n}`] = simple( presets[`${prefix}_${n}`] = simple(
`${label}: ${tag}`, `${label}: ${tag}`,
nameVar('input', n), nameVar('input', n),
@@ -97,8 +101,7 @@ function build(inst, withSendKnobs) {
description: 'Meters subscribe on the console when the button first renders.', description: 'Meters subscribe on the console when the button first renders.',
definitions: [ definitions: [
group('meters_input', 'Level', meterIds.level), group('meters_input', 'Level', meterIds.level),
group('meters_gate', 'Gate gain reduction', meterIds.gate), ...(grMeters ? [group('meters_gate', 'Gate gain reduction', meterIds.gate), group('meters_comp', 'Compressor gain reduction', meterIds.comp)] : []),
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)] }) 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 // ---- Snapshots
{ {
const fireIds = [] 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 * Presets for the current state: { structure, presets, stats }. If the list would exceed the agreed limits the
* limits the per-input send level knobs (the largest group) are left out and stats.droppedSendKnobs is set. * 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) { export function getPresetDefinitions(inst, limits = { count: MAX_PRESETS, bytes: MAX_PRESET_BYTES }) {
let built = build(inst, true) const tiers = [
let count = Object.keys(built.presets).length { options: { grMeters: true, sendKnobs: true }, dropped: [] },
let bytes = JSON.stringify(built.presets).length { options: { grMeters: false, sendKnobs: true }, dropped: ['the gate / compressor reduction meter presets'] },
let droppedSendKnobs = false { options: { grMeters: false, sendKnobs: false }, dropped: ['the gate / compressor reduction meter presets', 'the per-input send level knobs'] },
if (count > MAX_PRESETS || bytes > MAX_PRESET_BYTES) { ]
built = build(inst, false) let built
droppedSendKnobs = true let count = 0
let bytes = 0
let dropped = []
for (const tier of tiers) {
built = build(inst, tier.options)
count = Object.keys(built.presets).length count = Object.keys(built.presets).length
bytes = JSON.stringify(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 } }
} }
+28 -10
View File
@@ -3,7 +3,7 @@ import { ConsoleLink } from './console-link.js'
import { Relay } from './relay.js' import { Relay } from './relay.js'
import { decode, describe, foldPattern } from './osc.js' import { decode, describe, foldPattern } from './osc.js'
import { State } from './state.js' import { State } from './state.js'
import { LEVEL, PARAMS, K, addr, adjustedLevel, clamp, indexProblem, meterId, meterSubscription, parse, query, sameValue } from './protocol.js' import { GEQ_RANGE, LEVEL, PARAMS, K, addr, adjustedLevel, clamp, indexProblem, meterId, meterSubscription, parse, query, sameValue } from './protocol.js'
import { getActionDefinitions } from './definitions/actions.js' import { getActionDefinitions } from './definitions/actions.js'
import { getFeedbackDefinitions, feedbackIdsForEvent, watchKey, watchKeyForEvent } from './definitions/feedbacks.js' import { getFeedbackDefinitions, feedbackIdsForEvent, watchKey, watchKeyForEvent } from './definitions/feedbacks.js'
import { getVariableDefinitions, getAllVariableValues, getEventVariableValues, describeChange } from './definitions/variables.js' import { getVariableDefinitions, getAllVariableValues, getEventVariableValues, describeChange } from './definitions/variables.js'
@@ -730,10 +730,27 @@ export default class DigicoS31Instance extends InstanceBase {
if (s.level === undefined || s.on === undefined) this.ensure(`send:${ch}:${buss}`, [addr.sendLevel(ch, buss), addr.sendOn(ch, buss)]) if (s.level === undefined || s.on === undefined) this.ensure(`send:${ch}:${buss}`, [addr.sendLevel(ch, buss), addr.sendOn(ch, buss)])
} }
ensureSendPan(ch, buss) {
if (this.state.getSend(ch, buss).pan === undefined) this.ensure(`pan:${ch}:${buss}`, [addr.sendPan(ch, buss)])
}
ensureGroup(ch, buss) { ensureGroup(ch, buss) {
if (this.state.getGroup(ch, buss) === undefined) this.ensure(`group:${ch}:${buss}`, [addr.groupAssign(ch, buss)]) if (this.state.getGroup(ch, buss) === undefined) this.ensure(`group:${ch}:${buss}`, [addr.groupAssign(ch, buss)])
} }
ensureMatrixSend(inN, outN) {
const m = this.state.getMatrixSend(inN, outN)
if (m.level === undefined || m.on === undefined) this.ensure(`msend:${inN}:${outN}`, [addr.matrixSendLevel(inN, outN), addr.matrixSendOn(inN, outN)])
}
ensureGeqOn(n) {
if (this.state.getGeq(n).on === undefined) this.ensure(`geq:${n}:on`, [addr.geqIn(n)])
}
ensureGeqBand(n, band) {
if (this.state.getGeq(n).gains?.get(Number(band)) === undefined) this.ensure(`geq:${n}:gain:${band}`, [addr.geqGain(n, band)])
}
ensureParam(ent, n, param) { ensureParam(ent, n, param) {
const address = addr.param(ent, n, this.sideOf(ent, n), param) const address = addr.param(ent, n, this.sideOf(ent, n), param)
if (address && this.state.getParam(ent, n, param) === undefined) this.ensure(`param:${ent}:${n}:${param}`, [address]) if (address && this.state.getParam(ent, n, param) === undefined) this.ensure(`param:${ent}:${n}:${param}`, [address])
@@ -758,21 +775,22 @@ export default class DigicoS31Instance extends InstanceBase {
return this.write(address, [on ? 1 : 0], on ? 1 : 0) return this.write(address, [on ? 1 : 0], on ? 1 : 0)
} }
writeLevel(address, dB) { /** Write a dB level, clamped to [lo, +10]; `lo` is -150 except for matrix sends (-90). */
writeLevel(address, dB, lo = LEVEL.min) {
if (!this.addressOk(address)) return false if (!this.addressOk(address)) return false
const v = clamp(Number(dB), LEVEL.min, LEVEL.max) const v = clamp(Number(dB), lo, LEVEL.max)
if (!Number.isFinite(v)) return false if (!Number.isFinite(v)) return false
return this.write(address, [v], v) return this.write(address, [v], v)
} }
async adjustLevel(address, key, get, delta) { async adjustLevel(address, key, get, delta, lo = LEVEL.min) {
if (!this.addressOk(address)) return false if (!this.addressOk(address)) return false
const cur = await this.current(key, get, address) const cur = await this.current(key, get, address)
if (cur === undefined) { if (cur === undefined) {
this.log('warn', `${address}: current level unknown (no reply from the console), not adjusted`) this.log('warn', `${address}: current level unknown (no reply from the console), not adjusted`)
return false return false
} }
return this.writeLevel(address, adjustedLevel(cur, delta)) return this.writeLevel(address, adjustedLevel(cur, delta, lo), lo)
} }
writePan(address, pan) { writePan(address, pan) {
@@ -857,11 +875,11 @@ export default class DigicoS31Instance extends InstanceBase {
} }
setMatrixSendLevel(inN, outN, dB) { setMatrixSendLevel(inN, outN, dB) {
return this.writeLevel(addr.matrixSendLevel(inN, outN), dB) return this.writeLevel(addr.matrixSendLevel(inN, outN), dB, LEVEL.matrixSendMin)
} }
adjustMatrixSendLevel(inN, outN, delta) { adjustMatrixSendLevel(inN, outN, delta) {
return this.adjustLevel(addr.matrixSendLevel(inN, outN), K.msend(inN, outN, 'level'), () => this.state.getMatrixSend(inN, outN).level, delta) return this.adjustLevel(addr.matrixSendLevel(inN, outN), K.msend(inN, outN, 'level'), () => this.state.getMatrixSend(inN, outN).level, delta, LEVEL.matrixSendMin)
} }
setGeqOn(n, mode) { setGeqOn(n, mode) {
@@ -871,7 +889,7 @@ export default class DigicoS31Instance extends InstanceBase {
setGeqBand(n, band, gain) { setGeqBand(n, band, gain) {
const address = addr.geqGain(n, band) const address = addr.geqGain(n, band)
if (!this.addressOk(address)) return false if (!this.addressOk(address)) return false
const v = clamp(Number(gain), -12, 12) const v = clamp(Number(gain), GEQ_RANGE.min, GEQ_RANGE.max)
if (!Number.isFinite(v)) return false if (!Number.isFinite(v)) return false
return this.write(address, [v], v) return this.write(address, [v], v)
} }
@@ -1153,8 +1171,8 @@ export default class DigicoS31Instance extends InstanceBase {
this.definedVariables = new Set(Object.keys(vars)) this.definedVariables = new Set(Object.keys(vars))
this.presetStats = stats this.presetStats = stats
const changedPresets = this.pushIfChanged('presets', { structure, presets }, () => this.setPresetDefinitions(structure, presets)) const changedPresets = this.pushIfChanged('presets', { structure, presets }, () => this.setPresetDefinitions(structure, presets))
if (changedPresets && stats.droppedSendKnobs) { if (changedPresets && stats.dropped.length) {
this.log('warn', `Too many presets (limit reached): the per-input send level knobs were left out (${stats.count} presets, ${stats.bytes} bytes)`) this.log('warn', `Too many presets (limit reached): left out ${stats.dropped.join(' and ')} (${stats.count} presets, ${stats.bytes} bytes)`)
} }
this.queueVariables(getAllVariableValues(this)) this.queueVariables(getAllVariableValues(this))
this.publishRelay() this.publishRelay()
+17 -2
View File
@@ -16,8 +16,15 @@ import { toBool, toNumber } from './osc.js'
/** Maxima probed on an S31; an S21 simply answers fewer. */ /** Maxima probed on an S31; an S21 simply answers fewer. */
export const LIMITS = { inputs: 60, busses: 25, matrix: 8, matrixIn: 10, cgs: 10, geqs: 16, geqBands: 32 } export const LIMITS = { inputs: 60, busses: 25, matrix: 8, matrixIn: 10, cgs: 10, geqs: 16, geqBands: 32 }
/** dB values. -150 is the console's "off" (-inf); anything at or below FLOOR displays as -inf. */ /**
export const LEVEL = { min: -150, max: 10, floor: -90, restart: -60 } * dB values. Faders and aux sends have -150 as their "off" (-inf); anything at or below `floor` displays as -inf.
* A matrix send's floor is -90 (verified on an S31: off sends read -90): its writes never go below `matrixSendMin`.
* A relative step up from -inf lands at `restart`.
*/
export const LEVEL = { min: -150, max: 10, floor: -90, restart: -60, matrixSendMin: -90 }
/** Graphic EQ band gain range in dB (assumed, the console reports plain dB floats). */
export const GEQ_RANGE = { min: -12, max: 12 }
/** Meter ids chosen by this module start here so they never collide with an iPad's (0-19, 200+, 300+). */ /** Meter ids chosen by this module start here so they never collide with an iPad's (0-19, 200+, 300+). */
export const METER_BASE_ID = 5000 export const METER_BASE_ID = 5000
@@ -254,6 +261,14 @@ export function fmtDb(v) {
return String(Object.is(r, -0) ? 0 : r) return String(Object.is(r, -0) ? 0 : r)
} }
/** A channel-strip value as text: whole Hz for frequencies, one decimal otherwise. */
export function fmtValue(v, unit) {
if (typeof v !== 'number' || !Number.isFinite(v)) return ''
if (unit === 'Hz') return String(Math.round(v))
const r = Math.round(v * 10) / 10
return String(Object.is(r, -0) ? 0 : r)
}
/** Pan 0..1 (0.5 centre) as "C", "L50", "R30". */ /** Pan 0..1 (0.5 centre) as "C", "L50", "R30". */
export function fmtPan(p) { export function fmtPan(p) {
if (typeof p !== 'number' || !Number.isFinite(p)) return '' if (typeof p !== 'number' || !Number.isFinite(p)) return ''
+111 -5
View File
@@ -23,6 +23,8 @@ import { State } from '../src/state.js'
import { LEVEL, PARAMS, adjustedLevel, fmtCue, fmtDb, fmtPan, indexProblem, meterId, meterIdForInput, parse } from '../src/protocol.js' import { LEVEL, PARAMS, adjustedLevel, fmtCue, fmtDb, fmtPan, indexProblem, meterId, meterIdForInput, parse } from '../src/protocol.js'
import { BYTES_PER_PIXEL, horizontalBar, verticalMeter, thicknessFor } from '../src/image.js' import { BYTES_PER_PIXEL, horizontalBar, verticalMeter, thicknessFor } from '../src/image.js'
import { MAX_PRESETS, MAX_PRESET_BYTES } from '../src/definitions/presets.js' import { MAX_PRESETS, MAX_PRESET_BYTES } from '../src/definitions/presets.js'
import { COLORS } from '../src/definitions/feedbacks.js'
const COLORS_GREEN = COLORS.green
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const BASE = Number(process.env.S31_TEST_PORT_BASE) || 18000 const BASE = Number(process.env.S31_TEST_PORT_BASE) || 18000
@@ -381,9 +383,12 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
check(entityOptions.length > 40 && entityOptions.every((o) => (o.type === 'dropdown' || o.type === 'multidropdown') && o.minChoicesForSearch === 0 && o.choices.every((c) => typeof c.id === 'number')), `all ${entityOptions.length} entity options are searchable dropdowns with numeric ids`) check(entityOptions.length > 40 && entityOptions.every((o) => (o.type === 'dropdown' || o.type === 'multidropdown') && o.minChoicesForSearch === 0 && o.choices.every((c) => typeof c.id === 'number')), `all ${entityOptions.length} entity options are searchable dropdowns with numeric ids`)
const levels = all.flatMap((d) => d.options).filter((o) => o.id === 'level') const levels = all.flatMap((d) => d.options).filter((o) => o.id === 'level')
const thresholds = all.flatMap((d) => d.options).filter((o) => o.id === 'threshold' && o.min === -150) const thresholds = all.flatMap((d) => d.options).filter((o) => o.id === 'threshold' && o.min === -150)
check(levels.length === 6 && levels.every((o) => o.type === 'number' && o.showMinAsNegativeInfinity === true && o.min === -150) && thresholds.every((o) => o.showMinAsNegativeInfinity === true), `level options use showMinAsNegativeInfinity (${levels.length} level + ${thresholds.length} threshold options)`) check(levels.length === 6 && levels.every((o) => o.type === 'number' && o.showMinAsNegativeInfinity === true && (o.min === -150 || o.min === -90)) && thresholds.every((o) => o.showMinAsNegativeInfinity === true), `level options use showMinAsNegativeInfinity (${levels.length} level + ${thresholds.length} threshold options)`)
check(ctx.actions.matrix_send_level_set.options[2].min === -90 && ctx.actions.matrix_send_level_set.options[2].showMinAsNegativeInfinity === true && ctx.actions.send_level_set.options[2].min === -150 && ctx.actions.input_fader_set.options[1].min === -150 && ctx.actions.cg_fader_set.options[1].min === -150, 'per-kind floor: a matrix send level goes down to -90, aux sends and faders to -150')
const adjusts = Object.entries(ctx.actions).filter(([id]) => /(fader|send_level)_adjust$/.test(id))
check(adjusts.length === 6 && adjusts.every(([, a]) => a.description.includes('From \u2212\u221e the first step up jumps to \u221260 dB')), `all ${adjusts.length} level adjust actions say that the first step up from -inf jumps to -60 dB`)
check(ctx.actions.input_mute.options[0].choices[0].label === '1 · KICK In' && ctx.actions.send_on.options[1].choices.every((c) => S.bussMode(c.id) === 'aux'), 'action dropdowns show the live labels; send actions list aux busses only') check(ctx.actions.input_mute.options[0].choices[0].label === '1 · KICK In' && ctx.actions.send_on.options[1].choices.every((c) => S.bussMode(c.id) === 'aux'), 'action dropdowns show the live labels; send actions list aux busses only')
const idsExpected = ['input_mute', 'input_solo', 'input_fader_set', 'input_fader_adjust', 'input_pan_set', 'input_pan_adjust', 'input_name_set', 'send_on', 'send_level_set', 'send_level_adjust', 'send_pan_set', 'send_pan_adjust', 'group_assign', 'buss_mute', 'buss_solo', 'buss_fader_set', 'buss_fader_adjust', 'matrix_mute', 'matrix_fader_set', 'matrix_fader_adjust', 'matrix_send_on', 'matrix_send_level_set', 'matrix_send_level_adjust', 'cg_mute', 'cg_solo', 'cg_fader_set', 'cg_fader_adjust', 'geq_band_set', 'geq_band_adjust', 'geq_on', 'snapshot_fire', 'snapshot_next', 'snapshot_prev', 'reload', 'raw_osc', 'relay_forget'] const idsExpected = ['input_mute', 'input_solo', 'input_fader_set', 'input_fader_adjust', 'input_pan_set', 'input_pan_adjust', 'input_name_set', 'send_on', 'send_level_set', 'send_level_adjust', 'send_pan_set', 'send_pan_adjust', 'group_assign', 'buss_mute', 'buss_solo', 'buss_fader_set', 'buss_fader_adjust', 'matrix_mute', 'matrix_solo', 'matrix_fader_set', 'matrix_fader_adjust', 'matrix_send_on', 'matrix_send_level_set', 'matrix_send_level_adjust', 'cg_mute', 'cg_solo', 'cg_fader_set', 'cg_fader_adjust', 'geq_band_set', 'geq_band_adjust', 'geq_on', 'snapshot_fire', 'snapshot_next', 'snapshot_prev', 'reload', 'raw_osc', 'relay_forget']
// per-channel switches and values, generated from the PARAMS table // per-channel switches and values, generated from the PARAMS table
for (const [param, p] of Object.entries(PARAMS)) { for (const [param, p] of Object.entries(PARAMS)) {
for (const ent of Object.keys(p.paths)) idsExpected.push(...(p.type === 'switch' ? [`${ent}_${param}`] : [`${ent}_${param}_set`, `${ent}_${param}_adjust`])) for (const ent of Object.keys(p.paths)) idsExpected.push(...(p.type === 'switch' ? [`${ent}_${param}`] : [`${ent}_${param}_set`, `${ent}_${param}_adjust`]))
@@ -391,8 +396,37 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
check(idsExpected.every((id) => ctx.actions[id]) && Object.keys(ctx.actions).length === idsExpected.length, `all ${idsExpected.length} designed actions exist (${Object.keys(ctx.actions).length} registered)`) check(idsExpected.every((id) => ctx.actions[id]) && Object.keys(ctx.actions).length === idsExpected.length, `all ${idsExpected.length} designed actions exist (${Object.keys(ctx.actions).length} registered)`)
check(!('macro_press' in ctx.actions), 'no first-class macro action (the console never answered a macro query)') check(!('macro_press' in ctx.actions), 'no first-class macro action (the console never answered a macro query)')
const fbExpected = ['input_muted', 'input_solo', 'send_on', 'group_assigned', 'buss_muted', 'buss_solo', 'matrix_muted', 'cg_muted', 'snapshot_current', 'level_compare', 'send_level_compare', 'connected', 'relay_clients', 'input_signal', 'input_peak', 'input_fader_value', 'input_pan_value', 'send_level_value', 'send_on_value', 'buss_fader_value', 'matrix_fader_value', 'cg_fader_value', 'snapshot_name_value', 'input_meter_value', 'input_fader_bar', 'send_level_bar', 'buss_fader_bar', 'input_meter_bar'] const fbExpected = ['input_muted', 'input_solo', 'send_on', 'group_assigned', 'buss_muted', 'buss_solo', 'matrix_muted', 'cg_muted', 'snapshot_current', 'level_compare', 'send_level_compare', 'connected', 'relay_clients', 'input_signal', 'input_peak', 'input_fader_value', 'input_pan_value', 'send_level_value', 'send_on_value', 'buss_fader_value', 'matrix_fader_value', 'cg_fader_value', 'snapshot_name_value', 'input_meter_value', 'input_fader_bar', 'send_level_bar', 'buss_fader_bar', 'input_meter_bar']
for (const [param, p] of Object.entries(PARAMS)) if (p.type === 'switch') for (const ent of Object.keys(p.paths)) fbExpected.push(`${ent}_${param}_state`) fbExpected.push('matrix_solo', 'cg_solo', 'matrix_send_on', 'geq_on', 'send_pan_value', 'matrix_send_level_value', 'geq_band_value', 'matrix_fader_bar', 'cg_fader_bar', 'matrix_send_level_bar', 'geq_band_bar')
for (const [param, p] of Object.entries(PARAMS)) for (const ent of Object.keys(p.paths)) fbExpected.push(`${ent}_${param}_${p.type === 'switch' ? 'state' : 'value'}`)
check(fbExpected.every((id) => ctx.feedbacks[id]) && Object.keys(ctx.feedbacks).length === fbExpected.length, `all ${fbExpected.length} designed feedbacks exist (${Object.keys(ctx.feedbacks).length} registered)`) check(fbExpected.every((id) => ctx.feedbacks[id]) && Object.keys(ctx.feedbacks).length === fbExpected.length, `all ${fbExpected.length} designed feedbacks exist (${Object.keys(ctx.feedbacks).length} registered)`)
// Feedback parity: every action that changes readable state has a feedback for that state
const PARITY = {
input_mute: ['input_muted'], input_solo: ['input_solo'], input_fader_set: ['input_fader_value', 'input_fader_bar'], input_fader_adjust: ['input_fader_value', 'input_fader_bar'],
input_pan_set: ['input_pan_value'], input_pan_adjust: ['input_pan_value'],
send_on: ['send_on', 'send_on_value'], send_level_set: ['send_level_value', 'send_level_bar'], send_level_adjust: ['send_level_value', 'send_level_bar'],
send_pan_set: ['send_pan_value'], send_pan_adjust: ['send_pan_value'], group_assign: ['group_assigned'],
buss_mute: ['buss_muted'], buss_solo: ['buss_solo'], buss_fader_set: ['buss_fader_value', 'buss_fader_bar'], buss_fader_adjust: ['buss_fader_value', 'buss_fader_bar'],
matrix_mute: ['matrix_muted'], matrix_solo: ['matrix_solo'], matrix_fader_set: ['matrix_fader_value', 'matrix_fader_bar'], matrix_fader_adjust: ['matrix_fader_value', 'matrix_fader_bar'],
matrix_send_on: ['matrix_send_on'], matrix_send_level_set: ['matrix_send_level_value', 'matrix_send_level_bar'], matrix_send_level_adjust: ['matrix_send_level_value', 'matrix_send_level_bar'],
cg_mute: ['cg_muted'], cg_solo: ['cg_solo'], cg_fader_set: ['cg_fader_value', 'cg_fader_bar'], cg_fader_adjust: ['cg_fader_value', 'cg_fader_bar'],
geq_on: ['geq_on'], geq_band_set: ['geq_band_value', 'geq_band_bar'], geq_band_adjust: ['geq_band_value', 'geq_band_bar'],
snapshot_fire: ['snapshot_current', 'snapshot_name_value'], snapshot_next: ['snapshot_current', 'snapshot_name_value'], snapshot_prev: ['snapshot_current', 'snapshot_name_value'],
reload: ['connected'], relay_forget: ['relay_clients'],
input_name_set: [], // the name is the variable input_N_name
raw_osc: [], // free-form
}
for (const [param, p] of Object.entries(PARAMS)) {
for (const ent of Object.keys(p.paths)) {
if (p.type === 'switch') PARITY[`${ent}_${param}`] = [`${ent}_${param}_state`]
else PARITY[`${ent}_${param}_set`] = PARITY[`${ent}_${param}_adjust`] = [`${ent}_${param}_value`]
}
}
const noFeedback = Object.keys(ctx.actions).filter((id) => !(id in PARITY))
const missing = Object.entries(PARITY).filter(([id, fbs]) => id in ctx.actions && fbs.some((f) => !ctx.feedbacks[f])).map(([id]) => id)
const levelActions = Object.keys(ctx.actions).filter((id) => /_(set|adjust)$/.test(id) && /(fader|level|pan|band|trim|gain|freq)/.test(id))
const lacksValue = levelActions.filter((id) => !(PARITY[id] || []).some((f) => /_value$/.test(f)))
const lacksBar = levelActions.filter((id) => /(fader|send_level)/.test(id) && !(PARITY[id] || []).some((f) => /_bar$/.test(f)))
check(noFeedback.length === 0 && missing.length === 0 && lacksValue.length === 0 && lacksBar.length === 0, `every action has its feedback (${Object.keys(ctx.actions).length} actions; no feedback listed: ${noFeedback.join(', ') || 'none'}; unknown feedback: ${missing.join(', ') || 'none'}; level without value: ${lacksValue.join(', ') || 'none'}; fader/send level without bar: ${lacksBar.join(', ') || 'none'})`)
check(Object.values(ctx.feedbacks).filter((f) => f.type === 'advanced').every((f) => Array.isArray(f.affectedProperties)), 'advanced feedbacks declare affectedProperties') check(Object.values(ctx.feedbacks).filter((f) => f.type === 'advanced').every((f) => Array.isArray(f.affectedProperties)), 'advanced feedbacks declare affectedProperties')
} }
@@ -442,10 +476,26 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
const knobs = ids.filter((id) => /_knob_/.test(id)) const knobs = ids.filter((id) => /_knob_/.test(id))
check(knobs.length > 100 && knobs.every((id) => P[id].steps[0].rotate_left?.length && P[id].steps[0].rotate_right?.length), `all ${knobs.length} knob presets have rotate_left and rotate_right`) check(knobs.length > 100 && knobs.every((id) => P[id].steps[0].rotate_left?.length && P[id].steps[0].rotate_right?.length), `all ${knobs.length} knob presets have rotate_left and rotate_right`)
const sections = ctx.presetStructure.map((s) => s.id) const sections = ctx.presetStructure.map((s) => s.id)
check(['inputs', 'busses', 'cgs', 'matrix', 'snapshots', 'console', 'meters'].every((s) => sections.includes(s)) && sections.some((s) => s.startsWith('sends_')) && sections.some((s) => s.startsWith('group_')), `preset sections cover inputs, busses, sends, groups, matrix, control groups, snapshots, meters, console (${sections.length} sections)`) check(['inputs', 'busses', 'cgs', 'matrix', 'snapshots', 'console', 'meters', 'geqs'].every((s) => sections.includes(s)) && sections.some((s) => s.startsWith('sends_')) && sections.some((s) => s.startsWith('group_')) && sections.some((s) => s.startsWith('msends_')), `preset sections cover inputs, busses, sends, groups, matrix, matrix sends, graphic EQs, control groups, snapshots, meters, console (${sections.length} sections)`)
// matrix sends: one section per matrix output, per matrix input an on/off button and a knob
const msendSections = ctx.presetStructure.filter((x) => x.id.startsWith('msends_'))
const msendOn = ids.filter((id) => id.startsWith('msend_on_'))
const msendKnob = ids.filter((id) => id.startsWith('msend_knob_'))
check(msendSections.length === 8 && msendSections[7].name === `Matrix sends to ${S.displayName('matrix', 8)}` && msendOn.length === 80 && msendKnob.length === 80, `a "Matrix sends to <output>" section per matrix output (${msendSections.length}: "${msendSections[7]?.name}") with ${msendOn.length} on/off and ${msendKnob.length} knob presets`)
const mOn = P.msend_on_1_8
const mKnob = P.msend_knob_1_8
check(mOn.style.text === 'Matrix In 1\\n$(local:level) dB' && mOn.localVariables[0].feedbackId === 'matrix_send_level_value' && JSON.stringify(mOn.localVariables[0].options) === JSON.stringify({ matrixIn: 1, matrixOut: 8 }) && mOn.steps[0].down[0].actionId === 'matrix_send_on' && mOn.steps[0].down[0].options.state === 'toggle' && mOn.feedbacks.some((f) => f.feedbackId === 'matrix_send_on' && f.style.bgcolor === COLORS_GREEN), 'matrix send on/off preset: toggle matrix_send_on, green when on, text from the level value feedback')
check(mKnob.steps[0].rotate_left[0].actionId === 'matrix_send_level_adjust' && mKnob.steps[0].rotate_left[0].options.delta === -1 && mKnob.steps[0].rotate_right[0].options.delta === 1 && mKnob.steps[0].down[0].actionId === 'matrix_send_on' && mKnob.feedbacks.some((f) => f.feedbackId === 'matrix_send_on') && mKnob.feedbacks.some((f) => f.feedbackId === 'matrix_send_level_bar'), 'matrix send knob preset: rotate = matrix_send_level_adjust -1/+1, press toggles on/off, on feedback and level bar')
check(!!P.geq_in_1 && P.geq_in_1.steps[0].down[0].actionId === 'geq_on' && P.geq_in_1.feedbacks[0].feedbackId === 'geq_on' && ids.filter((id) => id.startsWith('geq_in_')).length === 16, 'a Graphic EQs section with an in/out toggle and a geq_on feedback per graphic EQ')
const bytes = JSON.stringify(P).length const bytes = JSON.stringify(P).length
console.log(`info presets: ${ids.length} presets, ${bytes} bytes serialized, ${inst.presetStats?.droppedSendKnobs ? 'send knobs dropped' : 'nothing dropped'}`) console.log(`info presets: ${ids.length} presets, ${bytes} bytes serialized, ${inst.presetStats?.dropped?.length ? `dropped ${inst.presetStats.dropped.join(' and ')}` : 'nothing dropped'}`)
check(ids.length <= MAX_PRESETS && bytes <= MAX_PRESET_BYTES, `preset count ${ids.length} <= ${MAX_PRESETS} and size ${bytes} <= ${MAX_PRESET_BYTES} bytes`) check(ids.length <= MAX_PRESETS && bytes <= MAX_PRESET_BYTES, `preset count ${ids.length} <= ${MAX_PRESETS} and size ${bytes} <= ${MAX_PRESET_BYTES} bytes`)
// over the limit: the gate / compressor reduction meter presets go first, then the per-input send knobs
const { getPresetDefinitions } = await import('../src/definitions/presets.js')
const tier1 = getPresetDefinitions(inst, { count: 2900, bytes: MAX_PRESET_BYTES })
const tier2 = getPresetDefinitions(inst, { count: 2500, bytes: MAX_PRESET_BYTES })
check(tier1.stats.dropped.length === 1 && /reduction meter/.test(tier1.stats.dropped[0]) && tier1.stats.count === ids.length - 120 && !Object.keys(tier1.presets).some((id) => /_gr_/.test(id)) && Object.keys(tier1.presets).some((id) => id.startsWith('send_knob_')), `over the limit the 120 gate / compressor reduction meter presets are dropped first (${tier1.stats.count} left)`)
check(tier2.stats.dropped.length === 2 && !Object.keys(tier2.presets).some((id) => id.startsWith('send_knob_')) && tier2.stats.count === ids.length - 120 - 900, `still over: the 900 per-input send level knobs go next (${tier2.stats.count} left)`)
check(!/studio mon/i.test(JSON.stringify([P, ctx.presetStructure, ctx.variableDefs])) && !JSON.stringify(Object.values(ctx.actions).map((a) => a.name)).match(/studio mon/i), 'no site-specific wording in the generated definitions') check(!/studio mon/i.test(JSON.stringify([P, ctx.presetStructure, ctx.variableDefs])) && !JSON.stringify(Object.values(ctx.actions).map((a) => a.name)).match(/studio mon/i), 'no site-specific wording in the generated definitions')
} }
@@ -542,6 +592,10 @@ function expectWrite(list, address, tags, value, label) {
// matrix // matrix
r = await run('matrix_mute', { matrix: 2, state: 'toggle' }) r = await run('matrix_mute', { matrix: 2, state: 'toggle' })
check(writes(r).some((m) => m.address === '/Matrix_Outputs/2/mute' && m.tags === ',f'), 'matrix_mute toggle') check(writes(r).some((m) => m.address === '/Matrix_Outputs/2/mute' && m.tags === ',f'), 'matrix_mute toggle')
r = await run('matrix_solo', { matrix: 2, state: 'on' })
check(writes(r).some((m) => m.address === '/Matrix_Outputs/2/solo' && m.tags === ',f' && m.args[0] === 1), 'matrix_solo on (float 1)')
r = await run('matrix_solo', { matrix: 2, state: 'off' })
check(writes(r).some((m) => m.address === '/Matrix_Outputs/2/solo' && m.tags === ',f' && m.args[0] === 0), 'matrix_solo off (float 0)')
r = await run('matrix_fader_set', { matrix: 2, level: -20 }) r = await run('matrix_fader_set', { matrix: 2, level: -20 })
expectWrite(r, '/Matrix_Outputs/2/fader', ',f', -20, 'matrix_fader_set') expectWrite(r, '/Matrix_Outputs/2/fader', ',f', -20, 'matrix_fader_set')
r = await run('matrix_fader_adjust', { matrix: 2, delta: 4 }) r = await run('matrix_fader_adjust', { matrix: 2, delta: 4 })
@@ -553,6 +607,23 @@ function expectWrite(list, address, tags, value, label) {
r = await run('matrix_send_level_adjust', { matrixIn: 3, matrixOut: 2, delta: 1 }) r = await run('matrix_send_level_adjust', { matrixIn: 3, matrixOut: 2, delta: 1 })
check(r[0]?.address === '/Matrix_Inputs/3/Matrix_Send/2/send_level/?', 'matrix_send_level_adjust on an unknown level queries first') check(r[0]?.address === '/Matrix_Inputs/3/Matrix_Send/2/send_level/?', 'matrix_send_level_adjust on an unknown level queries first')
expectWrite(r, '/Matrix_Inputs/3/Matrix_Send/2/send_level', ',f', -60, 'matrix_send_level_adjust from the -90 floor lands at -60') expectWrite(r, '/Matrix_Inputs/3/Matrix_Send/2/send_level', ',f', -60, 'matrix_send_level_adjust from the -90 floor lands at -60')
// a matrix send floors at -90: stepping down from -inf stays there, a set below it is clamped, -150 is never written
r = await run('matrix_send_level_adjust', { matrixIn: 4, matrixOut: 2, delta: -1 })
check(r[0]?.address === '/Matrix_Inputs/4/Matrix_Send/2/send_level/?', 'matrix_send_level_adjust -1 on an unknown level queries first')
expectWrite(r, '/Matrix_Inputs/4/Matrix_Send/2/send_level', ',f', -90, 'matrix_send_level_adjust -1 from -90 stays at -90 (not -150)')
r = await run('matrix_send_level_set', { matrixIn: 4, matrixOut: 2, level: -150 })
expectWrite(r, '/Matrix_Inputs/4/Matrix_Send/2/send_level', ',f', -90, 'matrix_send_level_set -150 is clamped to -90')
r = await run('matrix_send_level_adjust', { matrixIn: 4, matrixOut: 2, delta: 1 })
expectWrite(r, '/Matrix_Inputs/4/Matrix_Send/2/send_level', ',f', -60, 'matrix_send_level_adjust +1 from -90 jumps to -60')
r = await run('matrix_send_level_adjust', { matrixIn: 4, matrixOut: 2, delta: -35 })
expectWrite(r, '/Matrix_Inputs/4/Matrix_Send/2/send_level', ',f', -90, 'matrix_send_level_adjust below -90 lands on -90')
check(!sent.some((m) => /^\/Matrix_Inputs\/\d+\/Matrix_Send\/\d+\/send_level$/.test(m.address) && m.args[0] === -150), 'no matrix send write ever carries -150')
r = await run('send_level_set', { channel: 6, buss: 3, level: -150 })
expectWrite(r, '/Input_Channels/6/Aux_Send/3/send_level', ',f', -150, 'an aux send still goes down to -150')
r = await run('send_level_adjust', { channel: 6, buss: 3, delta: -1 })
expectWrite(r, '/Input_Channels/6/Aux_Send/3/send_level', ',f', -150, 'aux send adjust -1 from -inf stays at -150')
r = await run('send_level_adjust', { channel: 6, buss: 3, delta: 1 })
expectWrite(r, '/Input_Channels/6/Aux_Send/3/send_level', ',f', -60, 'aux send adjust +1 from -inf jumps to -60')
// control groups (1-based) // control groups (1-based)
r = await run('cg_mute', { cg: 1, state: 'on' }) r = await run('cg_mute', { cg: 1, state: 'on' })
@@ -720,6 +791,21 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
check(S.getParam('input', 33, 'comp_in') === true && cb('input_comp_in_state', { channel: 33 }) === true, 'the replied switch value now drives the feedback (mock default: comp in)') check(S.getParam('input', 33, 'comp_in') === true && cb('input_comp_in_state', { channel: 33 }) === true, 'the replied switch value now drives the feedback (mock default: comp in)')
check(cb('input_fader_value', { channel: 5 }) === '-12.5' && cb('input_pan_value', { channel: 13 }) === 'R100' && cb('buss_fader_value', { buss: 24 }) === '-3' && cb('matrix_fader_value', { matrix: 2 }) === '-16' && cb('cg_fader_value', { cg: 2 }) === '-8', 'value feedbacks return dB / pan text') check(cb('input_fader_value', { channel: 5 }) === '-12.5' && cb('input_pan_value', { channel: 13 }) === 'R100' && cb('buss_fader_value', { buss: 24 }) === '-3' && cb('matrix_fader_value', { matrix: 2 }) === '-16' && cb('cg_fader_value', { cg: 2 }) === '-8', 'value feedbacks return dB / pan text')
check(cb('snapshot_name_value', {}) === 'Offering' || cb('snapshot_name_value', {}) === S.snapshotName(S.snapshots.current), 'snapshot_name_value follows the current snapshot') check(cb('snapshot_name_value', {}) === 'Offering' || cb('snapshot_name_value', {}) === S.snapshotName(S.snapshots.current), 'snapshot_name_value follows the current snapshot')
// matrix sends, solo, graphic EQs, channel-strip values and send pan
check(cb('matrix_send_on', { matrixIn: 2, matrixOut: 3 }) === true && cb('matrix_send_level_value', { matrixIn: 2, matrixOut: 3 }) === '-30' && cb('matrix_send_level_value', { matrixIn: 3, matrixOut: 2 }) === '-60', 'matrix_send_on and matrix_send_level_value follow the written matrix sends')
check(cb('matrix_send_level_value', { matrixIn: 5, matrixOut: 5 }) === '--' && cb('matrix_send_on', { matrixIn: 5, matrixOut: 5 }) === false, 'an unloaded matrix send reads "--" / false and is queued for loading')
check(await waitFor(() => sent.some((m) => m.address === '/Matrix_Inputs/5/Matrix_Send/5/send_level/?') && S.getMatrixSend(5, 5).level !== undefined, 2500), 'the unloaded matrix send is queried once and its state arrives')
check(cb('matrix_send_level_value', { matrixIn: 5, matrixOut: 5 }) === '-inf' && cb('matrix_send_on', { matrixIn: 5, matrixOut: 5 }) === false, 'a matrix send at its -90 floor reads "-inf"')
check(cb('matrix_solo', { matrix: 2 }) === false && cb('cg_solo', { cg: 10 }) === true && cb('cg_solo', { cg: 1 }) === false, 'matrix_solo / cg_solo follow the state')
inst.onConsoleMessage(decode(encode('/Matrix_Outputs/2/solo', [1.0])), { address: '127.0.0.1', port: P.mock })
check(cb('matrix_solo', { matrix: 2 }) === true, 'matrix_solo goes true when the console reports solo')
inst.onConsoleMessage(decode(encode('/Matrix_Outputs/2/solo', [0.0])), { address: '127.0.0.1', port: P.mock })
check(cb('geq_on', { geq: 1 }) === false && cb('geq_band_value', { geq: 1, band: 5 }) === '4', 'geq_on (switched off earlier) and geq_band_value (band 5 at +4 dB)')
check(cb('geq_on', { geq: 3 }) === false && cb('geq_band_value', { geq: 3, band: 7 }) === '--', 'an unloaded graphic EQ reads false / "--"')
check(await waitFor(() => S.getGeq(3).on !== undefined && S.getGeq(3).gains?.get(7) !== undefined, 2500) && cb('geq_on', { geq: 3 }) === true && cb('geq_band_value', { geq: 3, band: 7 }) === '0', 'the graphic EQ state loads lazily (mock default: in, flat)')
check(cb('send_pan_value', { channel: 2, buss: 3 }) === 'L80', 'send_pan_value (pan 0.1 -> L80)')
check(cb('input_trim_value', { channel: 14 }) === '-3' && cb('input_gain_value', { channel: 14 }) === '34.5' && cb('input_hpf_freq_value', { channel: 14 }) === '88' && cb('buss_trim_value', { buss: 1 }) === '-2' && cb('matrix_trim_value', { matrix: 2 }) === '1.5', 'channel-strip value feedbacks: trim, gain, filter frequency (whole Hz), buss and matrix trim')
check(cb('input_lpf_freq_value', { channel: 60 }) === '--' && (await waitFor(() => S.getParam('input', 60, 'lpf_freq') !== undefined, 2500)) && cb('input_lpf_freq_value', { channel: 60 }) === '12000', 'an unloaded channel-strip value reads "--" and loads lazily')
check(cb('send_level_value', { channel: 40, buss: 8 }) === '--' && cb('send_on', { channel: 40, buss: 8 }) === false, 'an unloaded send pair reads as unknown ("--" / false) and is queued for loading') check(cb('send_level_value', { channel: 40, buss: 8 }) === '--' && cb('send_on', { channel: 40, buss: 8 }) === false, 'an unloaded send pair reads as unknown ("--" / false) and is queued for loading')
check(await waitFor(() => sent.some((m) => m.address === '/Input_Channels/40/Aux_Send/8/send_level/?'), 2000), 'the unloaded pair is queried once its feedback runs') check(await waitFor(() => sent.some((m) => m.address === '/Input_Channels/40/Aux_Send/8/send_level/?'), 2000), 'the unloaded pair is queried once its feedback runs')
await sleep(120) await sleep(120)
@@ -736,6 +822,10 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
['input_fader_bar', { channel: 5 }, (-12.5 + 60) / 70], ['input_fader_bar', { channel: 5 }, (-12.5 + 60) / 70],
['buss_fader_bar', { buss: 24 }, (-3 + 60) / 70], ['buss_fader_bar', { buss: 24 }, (-3 + 60) / 70],
['send_level_bar', { channel: 2, buss: 3 }, (-7 + 60) / 70], ['send_level_bar', { channel: 2, buss: 3 }, (-7 + 60) / 70],
['matrix_send_level_bar', { matrixIn: 2, matrixOut: 3 }, (-30 + 60) / 70],
['matrix_fader_bar', { matrix: 2 }, (-16 + 60) / 70],
['cg_fader_bar', { cg: 2 }, (-8 + 60) / 70],
['geq_band_bar', { geq: 1, band: 5 }, (4 + 12) / 24],
]) { ]) {
const res = barOf(id, options) const res = barOf(id, options)
const raw = Buffer.from(res.imageBuffer || '', 'base64') const raw = Buffer.from(res.imageBuffer || '', 'base64')
@@ -1261,6 +1351,22 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
const ids2 = ctx.byId.flatMap(([, x]) => x) const ids2 = ctx.byId.flatMap(([, x]) => x)
check(ids2.includes('inst-S1') && !ids2.includes('inst-S2') && ids2.includes('inst-N'), 'a send change re-checks only that send pair; a snapshot change re-checks the snapshot feedbacks') check(ids2.includes('inst-S1') && !ids2.includes('inst-S2') && ids2.includes('inst-N'), 'a send change re-checks only that send pair; a snapshot change re-checks the snapshot feedbacks')
for (const f of [s1, s2, sn]) ctx.feedbacks[f.feedbackId].unsubscribe(f, {}) for (const f of [s1, s2, sn]) ctx.feedbacks[f.feedbackId].unsubscribe(f, {})
// matrix sends and graphic EQ bands are tracked per send pair / per band
const m1 = mk('matrix_send_on', { matrixIn: 2, matrixOut: 3 }, 'inst-M1')
const m2 = mk('matrix_send_level_value', { matrixIn: 3, matrixOut: 2 }, 'inst-M2')
const g1 = mk('geq_band_value', { geq: 1, band: 5 }, 'inst-G1')
const g2 = mk('geq_band_value', { geq: 1, band: 6 }, 'inst-G2')
const g3 = mk('geq_on', { geq: 1 }, 'inst-G3')
for (const f of [m1, m2, g1, g2, g3]) run1(f)
check(inst.watchers.get('msend:2:3')?.has('inst-M1') && inst.watchers.get('msend:3:2')?.has('inst-M2') && inst.watchers.get('geq:1:gain:5')?.has('inst-G1') && inst.watchers.get('geq:1:gain:6')?.has('inst-G2') && inst.watchers.get('geq:1:on')?.has('inst-G3'), 'matrix send pairs and graphic EQ bands register under msend:in:out and geq:n:gain:band keys')
await sleep(150)
ctx.byId.length = 0
inst.onConsoleMessage(decode(encode('/Matrix_Inputs/2/Matrix_Send/3/send_level', [-25])), { address: '127.0.0.1', port: P.mock })
inst.onConsoleMessage(decode(encode('/Graphic_EQ/1/geq_gain_5', [3.5])), { address: '127.0.0.1', port: P.mock })
await sleep(80)
const ids3 = ctx.byId.flatMap(([, x]) => x)
check(ids3.includes('inst-M1') && !ids3.includes('inst-M2') && ids3.includes('inst-G1') && !ids3.includes('inst-G2') && !ids3.includes('inst-G3'), 'a matrix send change re-checks only that send pair, a band change only that band')
for (const f of [m1, m2, g1, g2, g3]) ctx.feedbacks[f.feedbackId].unsubscribe(f, {})
// level_compare follows the kind option // level_compare follows the kind option
const lc = mk('level_compare', { kind: 'cg', cg: 2, comparison: 'above', threshold: -50 }, 'inst-L') const lc = mk('level_compare', { kind: 'cg', cg: 2, comparison: 'above', threshold: -50 }, 'inst-L')
run1(lc) run1(lc)
+1 -1
View File
@@ -75,7 +75,7 @@ inputs, `--auxes N` the busses (default 25), and the matrix (8 outputs, 10 input
| `/Input_Channels/N/Aux_Send/M/{send_level,send_on,send_pan}/?` | `f` (level dB -150..10, on 0/1, pan 0..1) | | `/Input_Channels/N/Aux_Send/M/{send_level,send_on,send_pan}/?` | `f` (level dB -150..10, on 0/1, pan 0..1) |
| `/Input_Channels/N/Aux_Send/M/send_pre-post/?` | `i` (captured as an int, default 0; settable 0..1, which value means "pre" is an assumption; the Companion module does not use it) | | `/Input_Channels/N/Aux_Send/M/send_pre-post/?` | `i` (captured as an int, default 0; settable 0..1, which value means "pre" is an assumption; the Companion module does not use it) |
| `/Input_Channels/N/Group_Send/M/group/?` | `f` 0/1 group assign. Default: inputs 1-8 and 18-21 on group 24. The wire type is the constant `SD_GROUP_ASSIGN_WIRE_TYPE` | | `/Input_Channels/N/Group_Send/M/group/?` | `f` 0/1 group assign. Default: inputs 1-8 and 18-21 on group 24. The wire type is the constant `SD_GROUP_ASSIGN_WIRE_TYPE` |
| `/Matrix_Inputs/N/Matrix_Send/M/{send_level,send_on}/?` | `f` (default -90 and 0, captured) | | `/Matrix_Inputs/N/Matrix_Send/M/{send_level,send_on}/?` | `f` (default -90 and 0, captured). A matrix send floors at -90 dB (verified on an S31): level writes are clamped to -90..10, never -150 as for faders and aux sends |
| `/Graphic_EQ/N/{geq_in,geq_gain_1..32}/?` | `f` | | `/Graphic_EQ/N/{geq_in,geq_gain_1..32}/?` | `f` |
| `/Input_Channels/N/{Channel_Input/{trim,phase,phantom,analog_gain,input_pad},Filters/{hi,lo}_filter_{in,freq},Channel_Delay/{delay_on,delay},EQ/eq_in,Dynamics/{comp_in,gate_in},Insert/insert_{A,B}_in}/?` | `f` (switches 0/1) | | `/Input_Channels/N/{Channel_Input/{trim,phase,phantom,analog_gain,input_pad},Filters/{hi,lo}_filter_{in,freq},Channel_Delay/{delay_on,delay},EQ/eq_in,Dynamics/{comp_in,gate_in},Insert/insert_{A,B}_in}/?` | `f` (switches 0/1) |
| `/{Aux_Outputs,Group_Outputs}/N/{Buss_Trim/{trim,phase},EQ/eq_in,Dynamics/{comp_in,gate_in},Channel_Delay/{delay_on,delay},Insert/insert_{A,B}_in}/?`, `/Matrix_Outputs/N/Buss_Trim/trim/?` | `f` | | `/{Aux_Outputs,Group_Outputs}/N/{Buss_Trim/{trim,phase},EQ/eq_in,Dynamics/{comp_in,gate_in},Channel_Delay/{delay_on,delay},Insert/insert_{A,B}_in}/?`, `/Matrix_Outputs/N/Buss_Trim/trim/?` | `f` |
+2 -1
View File
@@ -603,6 +603,7 @@ const SD_PROPS = {
prepost: { kind: 'int', min: 0, max: 1 }, // send_pre-post; captured as an int, range assumed prepost: { kind: 'int', min: 0, max: 1 }, // send_pre-post; captured as an int, range assumed
group: { kind: 'flag', wire: SD_GROUP_ASSIGN_WIRE_TYPE }, // Group_Send/M/group assign 0/1 group: { kind: 'flag', wire: SD_GROUP_ASSIGN_WIRE_TYPE }, // Group_Send/M/group assign 0/1
geqGain: { kind: 'f', min: -12, max: 12 }, geqGain: { kind: 'f', min: -12, max: 12 },
matrixLevel: { kind: 'f', min: -90, max: 10 }, // a matrix send floors at -90 dB (S31: off sends read -90), unlike -150 elsewhere
}; };
/** /**
* Per-channel switches and values answered on inputs and busses (Aux_Outputs and Group_Outputs), * Per-channel switches and values answered on inputs and busses (Aux_Outputs and Group_Outputs),
@@ -790,7 +791,7 @@ function sdResolve(address) {
const input = sdMatrixIn[Number(m[1])]; const input = sdMatrixIn[Number(m[1])];
const snd = input && input.sends[Number(m[2])]; const snd = input && input.sends[Number(m[2])];
const prop = m[3] === 'send_on' ? 'on' : 'level'; const prop = m[3] === 'send_on' ? 'on' : 'level';
return snd ? { target: snd, prop, spec: SD_PROPS[prop] } : null; return snd ? { target: snd, prop, spec: prop === 'level' ? SD_PROPS.matrixLevel : SD_PROPS.on } : null;
} }
m = SD_RE_GEQ.exec(address); m = SD_RE_GEQ.exec(address);
if (m) { if (m) {