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:
+27
-10
@@ -75,8 +75,10 @@ fader, mute and solo are on the Aux_Outputs or Group_Outputs side that matches i
|
||||
## Actions
|
||||
|
||||
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.
|
||||
Toggles read the current state first when it is not known yet.
|
||||
relative **adjust** version for rotary encoders. A relative step cannot start from -inf: from −∞ the first
|
||||
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 |
|
||||
| --- | --- |
|
||||
@@ -106,14 +108,26 @@ snapshot's list position (0 = the first); verify it once on your console before
|
||||
|
||||
## Feedbacks
|
||||
|
||||
* Boolean (button colour): input muted / solo, aux send on, group assigned, buss muted /
|
||||
solo, matrix muted, control group muted, snapshot is current, fader level above/below a threshold, send
|
||||
level above/below, console link alive, relay has an iPad, every channel-strip switch above, and the meter
|
||||
Every action that changes something the console reports has a feedback for it: a boolean for every switch, a value
|
||||
(text for a button's local variable) and a bar for every level. The state a feedback needs (sends, matrix sends,
|
||||
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.
|
||||
* Value (use with a preset's local variable to put live numbers in button text): input fader, input pan,
|
||||
send level, send On/Off, buss / matrix / control group fader, current snapshot name, input meter value.
|
||||
* Advanced bars: input fader, aux send level and buss fader draw a horizontal bar along the bottom of the
|
||||
button (-60 to +10 dB); the input meter draws a vertical bar at the right edge (green, amber, red).
|
||||
* Value (put live numbers in button text with a preset's local variable): input fader and pan; aux send level, On/Off
|
||||
and pan; buss / matrix / control group fader; **matrix send level**; **graphic EQ band gain**; trim, analog gain and
|
||||
filter frequency per input and buss (trim also per matrix output); current snapshot name; input meter value. Levels
|
||||
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
|
||||
|
||||
@@ -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) |
|
||||
| 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 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) |
|
||||
| Input meters | Level, gate gain reduction and compressor gain reduction buttons per input |
|
||||
| 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
|
||||
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
|
||||
|
||||
|
||||
@@ -29,13 +29,14 @@ export const opt = {
|
||||
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()),
|
||||
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' }),
|
||||
level: (id = 'level', label = 'Level (dB)') => ({
|
||||
level: (id = 'level', label = 'Level (dB)', min = LEVEL.min) => ({
|
||||
type: 'number',
|
||||
id,
|
||||
label,
|
||||
default: 0,
|
||||
min: LEVEL.min,
|
||||
min,
|
||||
max: LEVEL.max,
|
||||
step: 0.5,
|
||||
showMinAsNegativeInfinity: true,
|
||||
@@ -46,6 +47,9 @@ export const opt = {
|
||||
}
|
||||
|
||||
/** 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)
|
||||
|
||||
/** 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: {
|
||||
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()],
|
||||
callback: async ({ options }) => inst.adjustFader('input', n(options.channel), n(options.delta)),
|
||||
},
|
||||
@@ -162,7 +166,7 @@ export function getActionDefinitions(inst) {
|
||||
},
|
||||
send_level_adjust: {
|
||||
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()],
|
||||
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: {
|
||||
name: 'Buss: adjust fader (relative)',
|
||||
description: `For rotary encoders. ${ADJUST_NOTE}`,
|
||||
options: [opt.buss(inst), opt.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()],
|
||||
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: {
|
||||
name: 'Matrix output: set fader',
|
||||
options: [opt.matrix(inst), opt.level()],
|
||||
@@ -215,6 +225,7 @@ export function getActionDefinitions(inst) {
|
||||
},
|
||||
matrix_fader_adjust: {
|
||||
name: 'Matrix output: adjust fader (relative)',
|
||||
description: `For rotary encoders. ${ADJUST_NOTE}`,
|
||||
options: [opt.matrix(inst), opt.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: {
|
||||
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)),
|
||||
},
|
||||
matrix_send_level_adjust: {
|
||||
name: 'Matrix send: adjust level (relative)',
|
||||
description: `For rotary encoders. ${ADJUST_NOTE}`,
|
||||
options: [opt.matrixIn(inst), opt.matrix(inst, 'matrixOut'), opt.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: {
|
||||
name: 'Control group: adjust fader (relative)',
|
||||
description: `For rotary encoders. ${ADJUST_NOTE}`,
|
||||
options: [opt.cg(inst), opt.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.',
|
||||
options: [
|
||||
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 },
|
||||
],
|
||||
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)',
|
||||
options: [
|
||||
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 },
|
||||
],
|
||||
callback: async ({ options }) => inst.adjustGeqBand(n(options.geq), n(options.band), n(options.delta)),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { 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. */
|
||||
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 KIND_VISIBLE = (kind) => `$(options:kind) == '${kind}'`
|
||||
@@ -30,6 +33,9 @@ const KIND_VISIBLE = (kind) => `$(options:kind) == '${kind}'`
|
||||
export const watchKey = {
|
||||
entity: (ent, num) => `${ent}:${num}`,
|
||||
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}`,
|
||||
meter: (id) => `meter:${id}`,
|
||||
snapshot: 'snapshot',
|
||||
@@ -107,11 +113,38 @@ export function getFeedbackDefinitions(inst) {
|
||||
}
|
||||
const entityKey = (ent, id) => (o) => watchKey.entity(ent, n(o[id]))
|
||||
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. */
|
||||
const paramFeedbacks = {}
|
||||
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)) {
|
||||
const style = param === 'phantom' ? RED : GREEN
|
||||
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),
|
||||
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),
|
||||
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))),
|
||||
level_compare: bool(
|
||||
'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))) || '--'),
|
||||
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))) || '--'),
|
||||
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) ?? '--'),
|
||||
input_meter_value: value(
|
||||
'Input meter: value',
|
||||
@@ -250,6 +296,22 @@ export function getFeedbackDefinitions(inst) {
|
||||
const v = s.getFader('buss', n(fb.options.buss))
|
||||
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: {
|
||||
type: 'advanced',
|
||||
name: 'Input meter: bar (right edge)',
|
||||
@@ -278,6 +340,10 @@ export function watchKeyForEvent(ev) {
|
||||
return watchKey.send(ev.ch, ev.buss)
|
||||
case 'group':
|
||||
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 'snapName':
|
||||
return watchKey.snapshot
|
||||
@@ -292,14 +358,14 @@ export function feedbackIdsForEvent(ev) {
|
||||
case 'mute':
|
||||
return { input: ['input_muted'], buss: ['buss_muted'], matrix: ['matrix_muted'], cg: ['cg_muted'] }[ev.ent] || []
|
||||
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':
|
||||
return (
|
||||
{
|
||||
input: ['input_fader_value', 'input_fader_bar', 'level_compare'],
|
||||
buss: ['buss_fader_value', 'buss_fader_bar', 'level_compare'],
|
||||
matrix: ['matrix_fader_value', 'level_compare'],
|
||||
cg: ['cg_fader_value', 'level_compare'],
|
||||
matrix: ['matrix_fader_value', 'matrix_fader_bar', 'level_compare'],
|
||||
cg: ['cg_fader_value', 'cg_fader_bar', 'level_compare'],
|
||||
}[ev.ent] || []
|
||||
)
|
||||
case 'pan':
|
||||
@@ -307,11 +373,16 @@ export function feedbackIdsForEvent(ev) {
|
||||
case 'send':
|
||||
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 === 'pan') return ['send_pan_value']
|
||||
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':
|
||||
return ['group_assigned']
|
||||
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 'snapName':
|
||||
return ['snapshot_current', 'snapshot_name_value']
|
||||
|
||||
+67
-15
@@ -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 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 L = inst.label
|
||||
const presets = {}
|
||||
@@ -69,6 +72,7 @@ function build(inst, withSendKnobs) {
|
||||
['gate', 'input_gate_gr', 'Gate reduction'],
|
||||
['comp', 'input_comp_gr', 'Compressor reduction'],
|
||||
]) {
|
||||
if (meter !== 'level' && !grMeters) continue
|
||||
presets[`${prefix}_${n}`] = simple(
|
||||
`${label}: ${tag}`,
|
||||
nameVar('input', n),
|
||||
@@ -97,8 +101,7 @@ function build(inst, withSendKnobs) {
|
||||
description: 'Meters subscribe on the console when the button first renders.',
|
||||
definitions: [
|
||||
group('meters_input', 'Level', meterIds.level),
|
||||
group('meters_gate', 'Gate gain reduction', meterIds.gate),
|
||||
group('meters_comp', 'Compressor gain reduction', meterIds.comp),
|
||||
...(grMeters ? [group('meters_gate', 'Gate gain reduction', meterIds.gate), 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)] })
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
{
|
||||
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
|
||||
* limits the per-input send level knobs (the largest group) are left out and stats.droppedSendKnobs is set.
|
||||
* Presets for the current state: { structure, presets, stats }. If the list would exceed the agreed limits the
|
||||
* 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) {
|
||||
let built = build(inst, true)
|
||||
let count = Object.keys(built.presets).length
|
||||
let bytes = JSON.stringify(built.presets).length
|
||||
let droppedSendKnobs = false
|
||||
if (count > MAX_PRESETS || bytes > MAX_PRESET_BYTES) {
|
||||
built = build(inst, false)
|
||||
droppedSendKnobs = true
|
||||
export function getPresetDefinitions(inst, limits = { count: MAX_PRESETS, bytes: MAX_PRESET_BYTES }) {
|
||||
const tiers = [
|
||||
{ options: { grMeters: true, sendKnobs: true }, dropped: [] },
|
||||
{ options: { grMeters: false, sendKnobs: true }, dropped: ['the gate / compressor reduction meter presets'] },
|
||||
{ options: { grMeters: false, sendKnobs: false }, dropped: ['the gate / compressor reduction meter presets', 'the per-input send level knobs'] },
|
||||
]
|
||||
let built
|
||||
let count = 0
|
||||
let bytes = 0
|
||||
let dropped = []
|
||||
for (const tier of tiers) {
|
||||
built = build(inst, tier.options)
|
||||
count = Object.keys(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
@@ -3,7 +3,7 @@ import { ConsoleLink } from './console-link.js'
|
||||
import { Relay } from './relay.js'
|
||||
import { decode, describe, foldPattern } from './osc.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 { getFeedbackDefinitions, feedbackIdsForEvent, watchKey, watchKeyForEvent } from './definitions/feedbacks.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)])
|
||||
}
|
||||
|
||||
ensureSendPan(ch, buss) {
|
||||
if (this.state.getSend(ch, buss).pan === undefined) this.ensure(`pan:${ch}:${buss}`, [addr.sendPan(ch, buss)])
|
||||
}
|
||||
|
||||
ensureGroup(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) {
|
||||
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])
|
||||
@@ -758,21 +775,22 @@ export default class DigicoS31Instance extends InstanceBase {
|
||||
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
|
||||
const v = clamp(Number(dB), LEVEL.min, LEVEL.max)
|
||||
const v = clamp(Number(dB), lo, LEVEL.max)
|
||||
if (!Number.isFinite(v)) return false
|
||||
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
|
||||
const cur = await this.current(key, get, address)
|
||||
if (cur === undefined) {
|
||||
this.log('warn', `${address}: current level unknown (no reply from the console), not adjusted`)
|
||||
return false
|
||||
}
|
||||
return this.writeLevel(address, adjustedLevel(cur, delta))
|
||||
return this.writeLevel(address, adjustedLevel(cur, delta, lo), lo)
|
||||
}
|
||||
|
||||
writePan(address, pan) {
|
||||
@@ -857,11 +875,11 @@ export default class DigicoS31Instance extends InstanceBase {
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
@@ -871,7 +889,7 @@ export default class DigicoS31Instance extends InstanceBase {
|
||||
setGeqBand(n, band, gain) {
|
||||
const address = addr.geqGain(n, band)
|
||||
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
|
||||
return this.write(address, [v], v)
|
||||
}
|
||||
@@ -1153,8 +1171,8 @@ export default class DigicoS31Instance extends InstanceBase {
|
||||
this.definedVariables = new Set(Object.keys(vars))
|
||||
this.presetStats = stats
|
||||
const changedPresets = this.pushIfChanged('presets', { structure, presets }, () => this.setPresetDefinitions(structure, presets))
|
||||
if (changedPresets && stats.droppedSendKnobs) {
|
||||
this.log('warn', `Too many presets (limit reached): the per-input send level knobs were left out (${stats.count} presets, ${stats.bytes} bytes)`)
|
||||
if (changedPresets && stats.dropped.length) {
|
||||
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.publishRelay()
|
||||
|
||||
+17
-2
@@ -16,8 +16,15 @@ import { toBool, toNumber } from './osc.js'
|
||||
/** 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 }
|
||||
|
||||
/** 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+). */
|
||||
export const METER_BASE_ID = 5000
|
||||
@@ -254,6 +261,14 @@ export function fmtDb(v) {
|
||||
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". */
|
||||
export function fmtPan(p) {
|
||||
if (typeof p !== 'number' || !Number.isFinite(p)) return ''
|
||||
|
||||
+111
-5
@@ -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 { BYTES_PER_PIXEL, horizontalBar, verticalMeter, thicknessFor } from '../src/image.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 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`)
|
||||
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)
|
||||
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')
|
||||
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
|
||||
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`]))
|
||||
@@ -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(!('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']
|
||||
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)`)
|
||||
// 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')
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
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)
|
||||
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
|
||||
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`)
|
||||
// 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')
|
||||
}
|
||||
|
||||
@@ -542,6 +592,10 @@ function expectWrite(list, address, tags, value, label) {
|
||||
// matrix
|
||||
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')
|
||||
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 })
|
||||
expectWrite(r, '/Matrix_Outputs/2/fader', ',f', -20, 'matrix_fader_set')
|
||||
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 })
|
||||
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')
|
||||
// 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)
|
||||
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(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')
|
||||
// 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(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)
|
||||
@@ -736,6 +822,10 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
||||
['input_fader_bar', { channel: 5 }, (-12.5 + 60) / 70],
|
||||
['buss_fader_bar', { buss: 24 }, (-3 + 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 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)
|
||||
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, {})
|
||||
// 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
|
||||
const lc = mk('level_compare', { kind: 'cg', cg: 2, comparison: 'above', threshold: -50 }, 'inst-L')
|
||||
run1(lc)
|
||||
|
||||
+1
-1
@@ -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_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` |
|
||||
| `/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` |
|
||||
| `/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` |
|
||||
|
||||
+2
-1
@@ -603,6 +603,7 @@ const SD_PROPS = {
|
||||
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
|
||||
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),
|
||||
@@ -790,7 +791,7 @@ function sdResolve(address) {
|
||||
const input = sdMatrixIn[Number(m[1])];
|
||||
const snd = input && input.sends[Number(m[2])];
|
||||
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);
|
||||
if (m) {
|
||||
|
||||
Reference in New Issue
Block a user