Use Companion's native Gauge element for level displays
Companion 5.1 marks image-buffer feedbacks as deprecated, and its layered buttons have a Gauge element that follows a number directly. So: - New numeric value feedbacks for gauges: fader/level position 0-100 for inputs, busses, matrix outputs, control groups, aux sends and matrix sends (one shared fader taper: 0 dB at 75 %, -inf at 0), raw pan 0..1 for a symmetric gauge, GEQ band dB, and input meter position. - Every knob and meter preset is now a layered preset: background box, channel name, live dB text and a Gauge bound to $(local:pos); on/off and mute tint the background. Meters use a vertical green/amber/red gauge. Text uses size 100 with shrink-to-fit (14 rendered tiny on 5.1) and the level gauge is white so it stays visible on a green background. - The old image-bar feedbacks keep working and are labelled "(legacy image)"; HELP.md explains how to add a gauge to your own button. - A sleeping iPad no longer logs a relay send error every few seconds: one warning per iPad per 10 minutes. Checked on the booth Companion 5.1: the layered knob previews show live names, levels and gauge fills. npm test: 392 checks pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+39
-10
@@ -109,7 +109,7 @@ snapshot's list position (0 = the first); verify it once on your console before
|
|||||||
## Feedbacks
|
## Feedbacks
|
||||||
|
|
||||||
Every action that changes something the console reports has a feedback for it: a boolean for every switch, a value
|
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,
|
(text for a button's local variable) and a numeric value for a gauge 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
|
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.
|
own value changes.
|
||||||
|
|
||||||
@@ -122,13 +122,40 @@ own value changes.
|
|||||||
and pan; buss / matrix / control group fader; **matrix send level**; **graphic EQ band gain**; trim, analog gain and
|
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
|
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.
|
read `-inf` at or below -90 dB.
|
||||||
* Advanced bars: input fader, aux send level, buss / matrix output / control group fader, **matrix send level** (all
|
* Numeric, for gauges (see **Gauges** below): the `*_pos` position feedbacks, `input_pan_num` / `send_pan_num` and
|
||||||
-60 to +10 dB) and **graphic EQ band gain** (-12 to +12 dB) draw a horizontal bar along the bottom of the button;
|
`geq_band_db`.
|
||||||
the input meter draws a vertical bar at the right edge (green, amber, red).
|
* Advanced image bars (**legacy image**, deprecated by Companion 5.1, kept so existing buttons keep working): input
|
||||||
|
fader, aux send level, buss / matrix output / control group fader, matrix send level (-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. Use a gauge on new buttons.
|
||||||
|
|
||||||
Not covered by a feedback: free-form *Send raw OSC*, and *Set name* (the name is the variable `input_N_name`). There is
|
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.
|
no matrix-output solo action, only its feedback.
|
||||||
|
|
||||||
|
## Gauges
|
||||||
|
|
||||||
|
Companion 5.x has a native **Gauge** element, and every knob and meter preset uses one. Each position feedback is a
|
||||||
|
number you feed to a gauge through a button local variable:
|
||||||
|
|
||||||
|
| Feedback | Range | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `input_fader_pos`, `buss_fader_pos`, `matrix_fader_pos`, `cg_fader_pos`, `send_level_pos`, `matrix_send_level_pos` | 0-100 | fader / send position on a fader taper; unknown reads 0 |
|
||||||
|
| `input_pan_num`, `send_pan_num` | 0-1 | the console's pan, 0.5 = centre (unknown reads 0.5) |
|
||||||
|
| `geq_band_db` | -12 to +12 | graphic EQ band gain in dB (unknown reads 0) |
|
||||||
|
| `input_meter_pos` | 0-100 | input meter (0-127 scaled); has the *Meter* option level / gate / compressor reduction |
|
||||||
|
|
||||||
|
The fader taper maps +10 dB = 100, +5 = 87.5, 0 = 75, -5 = 65, -10 = 55, -20 = 40, -30 = 28, -40 = 18, -50 = 11 and
|
||||||
|
-60 = 6 (straight lines in between), falls to about 1 just above -90 dB, and reads 0 at or below -90 dB (off).
|
||||||
|
|
||||||
|
To add a gauge to your own button in Companion 5.1:
|
||||||
|
|
||||||
|
1. **Local Variables** tab: add a feedback variable, name it `pos`, and pick the position feedback (for example
|
||||||
|
`input_fader_pos` with its input).
|
||||||
|
2. **Style** tab: **+**, then **Gauge**. Set *Value* to the expression `$(local:pos)`, *Min* 0, *Max* 100
|
||||||
|
(fader and meter positions).
|
||||||
|
3. For pan use `input_pan_num` / `send_pan_num` with *Min* 0, *Max* 1, **Symmetric** on and *Origin* 0.5. For a graphic EQ
|
||||||
|
band use `geq_band_db` with *Min* -12, *Max* 12, symmetric, *Origin* 0.
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
`input_N_name|fader|mute|pan`, `buss_N_name|mode|stereo|fader|mute`, `matrix_N_name|fader|mute`,
|
`input_N_name|fader|mute|pan`, `buss_N_name|mode|stereo|fader|mute`, `matrix_N_name|fader|mute`,
|
||||||
@@ -138,21 +165,23 @@ no matrix-output solo action, only its feedback.
|
|||||||
|
|
||||||
## Presets
|
## Presets
|
||||||
|
|
||||||
Presets are named with the live entity names and rebuilt when names load.
|
Presets are named with the live entity names and rebuilt when names load. The knob and meter presets are *layered*
|
||||||
|
(black background, name, dB text, gauge) and a muted / on state tints their background; the plain toggles are simple
|
||||||
|
buttons.
|
||||||
|
|
||||||
| Preset group | What the buttons do |
|
| Preset group | What the buttons do |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Inputs: Mute | One button per input; red when muted |
|
| Inputs: Mute | One button per input; red when muted |
|
||||||
| Inputs: Fader (knob) | Rotary: turn = fader -/+1 dB, press = mute; shows name and dB, with a level bar |
|
| Inputs: Fader (knob) | Rotary: turn = fader -/+1 dB, press = mute; shows name and dB with a gauge along the bottom |
|
||||||
| Inputs: Solo | One button per input |
|
| Inputs: Solo | One button per input |
|
||||||
| Busses | Master mute, and master fader knob (turn = fader, press = mute) per named buss |
|
| Busses | Master mute, and master fader knob (turn = fader, press = mute) per named buss |
|
||||||
| 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 gauge) |
|
||||||
| 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 (with a gauge) 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) |
|
| 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 gauge) |
|
||||||
| Graphic EQs | One **In/out** toggle per graphic EQ, green when in |
|
| 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, each with a vertical gauge at the right edge (green below 70, amber 70-90, red above 90) |
|
||||||
| 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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { combineRgb } from '@companion-module/base'
|
import { combineRgb } from '@companion-module/base'
|
||||||
import { GEQ_RANGE, LEVEL, METERS, PARAMS, clamp, fmtDb, fmtPan, fmtValue, meterId } from '../protocol.js'
|
import { GEQ_RANGE, LEVEL, METERS, PARAMS, clamp, faderPosition, 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'
|
||||||
|
|
||||||
@@ -259,6 +259,24 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
}),
|
}),
|
||||||
matrix_send_level_value: value('Matrix send: level (dB text)', msendOptions(), msendKey, ({ options }) => fmtDb(msendOf(options).level) || '--'),
|
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)) || '--'),
|
geq_band_value: value('Graphic EQ: band gain (dB text)', [opt.geq(inst), opt.band()], geqGainKey, ({ options }) => fmtDb(geqGainOf(options)) || '--'),
|
||||||
|
// Numbers for Companion's native Gauge element (use as a button local variable, see the Gauges section of the help)
|
||||||
|
input_fader_pos: value('Input: fader position (0-100, for gauges)', [opt.channel(inst)], entityKey('input', 'channel'), ({ options }) => faderPosition(s.getFader('input', n(options.channel)))),
|
||||||
|
buss_fader_pos: value('Buss: fader position (0-100, for gauges)', [opt.buss(inst)], entityKey('buss', 'buss'), ({ options }) => faderPosition(s.getFader('buss', n(options.buss)))),
|
||||||
|
matrix_fader_pos: value('Matrix output: fader position (0-100, for gauges)', [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => faderPosition(s.getFader('matrix', n(options.matrix)))),
|
||||||
|
cg_fader_pos: value('Control group: fader position (0-100, for gauges)', [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => faderPosition(s.getFader('cg', n(options.cg)))),
|
||||||
|
send_level_pos: value('Aux send: level position (0-100, for gauges)', [opt.channel(inst), opt.buss(inst, 'aux')], sendKey, ({ options }) => {
|
||||||
|
inst.ensureSend(n(options.channel), n(options.buss))
|
||||||
|
return faderPosition(s.getSend(n(options.channel), n(options.buss)).level)
|
||||||
|
}),
|
||||||
|
matrix_send_level_pos: value('Matrix send: level position (0-100, for gauges)', msendOptions(), msendKey, ({ options }) => faderPosition(msendOf(options).level)),
|
||||||
|
// pan is the raw 0..1 value (0.5 = centre) for a symmetric gauge with origin 0.5; unknown reads as centre
|
||||||
|
input_pan_num: value('Input: pan (0-1, 0.5 = centre, for gauges)', [opt.channel(inst)], entityKey('input', 'channel'), ({ options }) => s.getPan(n(options.channel)) ?? 0.5),
|
||||||
|
send_pan_num: value('Aux send: pan (0-1, 0.5 = centre, for gauges)', [opt.channel(inst), opt.buss(inst, 'aux')], sendKey, ({ options }) => {
|
||||||
|
inst.ensureSendPan(n(options.channel), n(options.buss))
|
||||||
|
return s.getSend(n(options.channel), n(options.buss)).pan ?? 0.5
|
||||||
|
}),
|
||||||
|
geq_band_db: value('Graphic EQ: band gain (dB number -12..12, for gauges)', [opt.geq(inst), opt.band()], geqGainKey, ({ options }) => geqGainOf(options) ?? 0),
|
||||||
|
input_meter_pos: value('Input meter: position (0-100, for gauges)', [opt.channel(inst), meterOption], meterKey, ({ options }) => ((meterOf(options)?.value ?? 0) / 127) * 100),
|
||||||
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',
|
||||||
@@ -283,38 +301,38 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
input_fader_bar: bar('Input: fader bar', [opt.channel(inst)], entityKey('input', 'channel'), (fb, image) => {
|
input_fader_bar: bar('Input: fader bar (legacy image)', [opt.channel(inst)], entityKey('input', 'channel'), (fb, image) => {
|
||||||
const v = s.getFader('input', n(fb.options.channel))
|
const v = s.getFader('input', n(fb.options.channel))
|
||||||
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
|
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
|
||||||
}),
|
}),
|
||||||
send_level_bar: bar('Aux send: level bar', [opt.channel(inst), opt.buss(inst, 'aux')], sendKey, (fb, image) => {
|
send_level_bar: bar('Aux send: level bar (legacy image)', [opt.channel(inst), opt.buss(inst, 'aux')], sendKey, (fb, image) => {
|
||||||
inst.ensureSend(n(fb.options.channel), n(fb.options.buss))
|
inst.ensureSend(n(fb.options.channel), n(fb.options.buss))
|
||||||
const v = s.getSend(n(fb.options.channel), n(fb.options.buss)).level
|
const v = s.getSend(n(fb.options.channel), n(fb.options.buss)).level
|
||||||
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
|
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
|
||||||
}),
|
}),
|
||||||
buss_fader_bar: bar('Buss: fader bar', [opt.buss(inst)], entityKey('buss', 'buss'), (fb, image) => {
|
buss_fader_bar: bar('Buss: fader bar (legacy image)', [opt.buss(inst)], entityKey('buss', 'buss'), (fb, image) => {
|
||||||
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) => {
|
matrix_fader_bar: bar('Matrix output: fader bar (legacy image)', [opt.matrix(inst)], entityKey('matrix', 'matrix'), (fb, image) => {
|
||||||
const v = s.getFader('matrix', n(fb.options.matrix))
|
const v = s.getFader('matrix', n(fb.options.matrix))
|
||||||
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
|
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) => {
|
cg_fader_bar: bar('Control group: fader bar (legacy image)', [opt.cg(inst)], entityKey('cg', 'cg'), (fb, image) => {
|
||||||
const v = s.getFader('cg', n(fb.options.cg))
|
const v = s.getFader('cg', n(fb.options.cg))
|
||||||
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_send_level_bar: bar('Matrix send: level bar', msendOptions(), msendKey, (fb, image) => {
|
matrix_send_level_bar: bar('Matrix send: level bar (legacy image)', msendOptions(), msendKey, (fb, image) => {
|
||||||
const v = msendOf(fb.options).level
|
const v = msendOf(fb.options).level
|
||||||
return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color })
|
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) => {
|
geq_band_bar: bar('Graphic EQ: band gain bar (legacy image)', [opt.geq(inst), opt.band()], geqGainKey, (fb, image) => {
|
||||||
const v = geqGainOf(fb.options)
|
const v = geqGainOf(fb.options)
|
||||||
return v === undefined ? {} : horizontalBar({ ...image, fraction: geqFraction(v), color: fb.options.color })
|
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 at the right edge (legacy image)',
|
||||||
affectedProperties: ['imageBuffer'],
|
affectedProperties: ['imageBuffer'],
|
||||||
options: [opt.channel(inst), meterOption],
|
options: [opt.channel(inst), meterOption],
|
||||||
callback: tracked(meterKey, (fb) => {
|
callback: tracked(meterKey, (fb) => {
|
||||||
@@ -362,23 +380,23 @@ export function feedbackIdsForEvent(ev) {
|
|||||||
case 'fader':
|
case 'fader':
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
input: ['input_fader_value', 'input_fader_bar', 'level_compare'],
|
input: ['input_fader_value', 'input_fader_pos', 'input_fader_bar', 'level_compare'],
|
||||||
buss: ['buss_fader_value', 'buss_fader_bar', 'level_compare'],
|
buss: ['buss_fader_value', 'buss_fader_pos', 'buss_fader_bar', 'level_compare'],
|
||||||
matrix: ['matrix_fader_value', 'matrix_fader_bar', 'level_compare'],
|
matrix: ['matrix_fader_value', 'matrix_fader_pos', 'matrix_fader_bar', 'level_compare'],
|
||||||
cg: ['cg_fader_value', 'cg_fader_bar', 'level_compare'],
|
cg: ['cg_fader_value', 'cg_fader_pos', 'cg_fader_bar', 'level_compare'],
|
||||||
}[ev.ent] || []
|
}[ev.ent] || []
|
||||||
)
|
)
|
||||||
case 'pan':
|
case 'pan':
|
||||||
return ['input_pan_value']
|
return ['input_pan_value', 'input_pan_num']
|
||||||
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_pos', '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']
|
if (ev.field === 'pan') return ['send_pan_value', 'send_pan_num']
|
||||||
return []
|
return []
|
||||||
case 'msend':
|
case 'msend':
|
||||||
return ev.field === 'level' ? ['matrix_send_level_value', 'matrix_send_level_bar'] : ['matrix_send_on']
|
return ev.field === 'level' ? ['matrix_send_level_value', 'matrix_send_level_pos', 'matrix_send_level_bar'] : ['matrix_send_on']
|
||||||
case 'geq':
|
case 'geq':
|
||||||
return ev.field === 'on' ? ['geq_on'] : ['geq_band_value', 'geq_band_bar']
|
return ev.field === 'on' ? ['geq_on'] : ['geq_band_value', 'geq_band_db', 'geq_band_bar']
|
||||||
case 'group':
|
case 'group':
|
||||||
return ['group_assigned']
|
return ['group_assigned']
|
||||||
case 'param':
|
case 'param':
|
||||||
|
|||||||
+139
-75
@@ -4,7 +4,19 @@ import { COLORS } from './feedbacks.js'
|
|||||||
export const MAX_PRESETS = 3000
|
export const MAX_PRESETS = 3000
|
||||||
export const MAX_PRESET_BYTES = 4 * 1024 * 1024
|
export const MAX_PRESET_BYTES = 4 * 1024 * 1024
|
||||||
|
|
||||||
const { white: WHITE, dark: DARK, red: RED, green: GREEN, amber: AMBER, bar: BAR } = COLORS
|
const { white: WHITE, dark: DARK, red: RED, green: GREEN, amber: AMBER } = COLORS
|
||||||
|
|
||||||
|
// Layered presets (native gauges). Colours are 0xRRGGBB numbers.
|
||||||
|
const BLACK = 0x000000
|
||||||
|
const TEXT = 0xffffff
|
||||||
|
const GAUGE_GREEN = 0x00b050
|
||||||
|
/** Level gauge fill: white, so it stays visible on a green (send on) or red (muted) background. */
|
||||||
|
const GAUGE_LEVEL = 0xffffff
|
||||||
|
const METER_AMBER = 0xf0a000
|
||||||
|
const METER_RED = 0xe00000
|
||||||
|
const ON_GREEN = 0x00a050 // a send is on / an input is assigned
|
||||||
|
const MUTED_RED = 0xb01010 // muted, peak
|
||||||
|
const SIGNAL_BG = 0x00501e // signal present on a meter button
|
||||||
|
|
||||||
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 })
|
||||||
@@ -29,21 +41,82 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
feedbacks,
|
feedbacks,
|
||||||
...extra,
|
...extra,
|
||||||
})
|
})
|
||||||
/** Rotary preset: press = `press`, turning = `adjust` with ∓1 (the action's delta option). */
|
/** A rotary step: press = `press`, turning = `adjust` with the action's delta option set to -1 / +1. */
|
||||||
const knob = (name, text, press, adjustActionId, adjustOptions, localVariables, feedbacks) => ({
|
const knobSteps = (press, adjustActionId, adjustOptions) => [
|
||||||
type: 'simple',
|
{
|
||||||
|
down: press,
|
||||||
|
up: [],
|
||||||
|
rotate_left: [act(adjustActionId, { ...adjustOptions, delta: -1 })],
|
||||||
|
rotate_right: [act(adjustActionId, { ...adjustOptions, delta: 1 })],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const text = (id, x, y, width, height, value) => ({ type: 'text', id, x, y, width, height, text: value, fontsize: 100, fontsizeAllowShrink: true, color: TEXT, halign: 'center', valign: 'center' })
|
||||||
|
/** Boolean feedback on a layered preset: it colours the background box. */
|
||||||
|
const tint = (feedbackId, options, color) => ({ feedbackId, options, styleOverrides: [{ elementId: 'bg', elementProperty: 'color', override: color }] })
|
||||||
|
/**
|
||||||
|
* Layered rotary preset with a native Companion gauge along the bottom: black background, the entity name on top, the
|
||||||
|
* dB text below it, and a gauge fed by the `pos` local variable. `level` and `pos` are the text and the numeric value
|
||||||
|
* feedbacks for the same thing.
|
||||||
|
*/
|
||||||
|
const layeredKnob = ({ name, title, press, adjust: [adjustActionId, adjustOptions], level: [levelId, levelOptions], pos: [posId, posOptions], feedbacks }) => ({
|
||||||
|
type: 'layered',
|
||||||
name,
|
name,
|
||||||
style: { text, size: 'auto', color: WHITE, bgcolor: DARK },
|
elements: [
|
||||||
steps: [
|
{ type: 'box', id: 'bg', x: 0, y: 0, width: 100, height: 100, color: BLACK },
|
||||||
|
text('title', 0, 0, 100, 55, title),
|
||||||
|
text('level', 0, 52, 100, 32, '$(local:level) dB'),
|
||||||
{
|
{
|
||||||
down: press,
|
type: 'gauge',
|
||||||
up: [],
|
id: 'gauge',
|
||||||
rotate_left: [act(adjustActionId, { ...adjustOptions, delta: -1 })],
|
x: 0,
|
||||||
rotate_right: [act(adjustActionId, { ...adjustOptions, delta: 1 })],
|
y: 84,
|
||||||
|
width: 100,
|
||||||
|
height: 16,
|
||||||
|
value: { isExpression: true, value: '$(local:pos)' },
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
orientation: 'horizontal',
|
||||||
|
trackStyle: 'dimmed',
|
||||||
|
fillEnabled: true,
|
||||||
|
multiColour: false,
|
||||||
|
stops: [{ value: 0, color: GAUGE_LEVEL, gradient: false }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
steps: knobSteps(press, adjustActionId, adjustOptions),
|
||||||
feedbacks,
|
feedbacks,
|
||||||
localVariables,
|
localVariables: [localFeedback('level', levelId, levelOptions), localFeedback('pos', posId, posOptions)],
|
||||||
|
})
|
||||||
|
/** Layered input meter: name on the left, a vertical gauge (green / amber / red zones) at the right edge. */
|
||||||
|
const layeredMeter = ({ name, title, options }) => ({
|
||||||
|
type: 'layered',
|
||||||
|
name,
|
||||||
|
elements: [
|
||||||
|
{ type: 'box', id: 'bg', x: 0, y: 0, width: 100, height: 100, color: BLACK },
|
||||||
|
text('title', 0, 0, 86, 100, title),
|
||||||
|
{
|
||||||
|
type: 'gauge',
|
||||||
|
id: 'gauge',
|
||||||
|
x: 86,
|
||||||
|
y: 0,
|
||||||
|
width: 14,
|
||||||
|
height: 100,
|
||||||
|
value: { isExpression: true, value: '$(local:pos)' },
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
orientation: 'vertical',
|
||||||
|
trackStyle: 'dimmed',
|
||||||
|
fillEnabled: true,
|
||||||
|
multiColour: true,
|
||||||
|
stops: [
|
||||||
|
{ value: 0, color: GAUGE_GREEN, gradient: false },
|
||||||
|
{ value: 70, color: METER_AMBER, gradient: false },
|
||||||
|
{ value: 90, color: METER_RED, gradient: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
steps: [{ down: [], up: [] }],
|
||||||
|
feedbacks: [tint('input_signal', { ...options, threshold: 5 }, SIGNAL_BG), tint('input_peak', { ...options, hold: 2 }, MUTED_RED)],
|
||||||
|
localVariables: [localFeedback('pos', 'input_meter_pos', options)],
|
||||||
})
|
})
|
||||||
const group = (id, name, ids) => ({ id, type: 'simple', name, presets: ids })
|
const group = (id, name, ids) => ({ id, type: 'simple', name, presets: ids })
|
||||||
|
|
||||||
@@ -57,15 +130,15 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
const name = s.displayName('input', n)
|
const name = s.displayName('input', n)
|
||||||
const tag = `${n} · ${name}`
|
const tag = `${n} · ${name}`
|
||||||
presets[`input_mute_${n}`] = simple(`Mute: ${tag}`, nameVar('input', n), [act('input_mute', { channels: [n], state: 'toggle' })], [fb('input_muted', { channel: n }, RED)])
|
presets[`input_mute_${n}`] = simple(`Mute: ${tag}`, nameVar('input', n), [act('input_mute', { channels: [n], state: 'toggle' })], [fb('input_muted', { channel: n }, RED)])
|
||||||
presets[`input_knob_${n}`] = knob(
|
presets[`input_knob_${n}`] = layeredKnob({
|
||||||
`Fader knob: ${tag}`,
|
name: `Fader knob: ${tag}`,
|
||||||
`${nameVar('input', n)}\\n$(local:level) dB`,
|
title: nameVar('input', n),
|
||||||
[act('input_mute', { channels: [n], state: 'toggle' })],
|
press: [act('input_mute', { channels: [n], state: 'toggle' })],
|
||||||
'input_fader_adjust',
|
adjust: ['input_fader_adjust', { channel: n }],
|
||||||
{ channel: n },
|
level: ['input_fader_value', { channel: n }],
|
||||||
[localFeedback('level', 'input_fader_value', { channel: n })],
|
pos: ['input_fader_pos', { channel: n }],
|
||||||
[fb('input_muted', { channel: n }, RED), { feedbackId: 'input_fader_bar', options: { channel: n, color: BAR } }],
|
feedbacks: [tint('input_muted', { channel: n }, MUTED_RED)],
|
||||||
)
|
})
|
||||||
presets[`input_solo_${n}`] = simple(`Solo: ${tag}`, `${nameVar('input', n)}\\nSOLO`, [act('input_solo', { channels: [n], state: 'toggle' })], [fb('input_solo', { channel: n }, AMBER, COLORS.black)])
|
presets[`input_solo_${n}`] = simple(`Solo: ${tag}`, `${nameVar('input', n)}\\nSOLO`, [act('input_solo', { channels: [n], state: 'toggle' })], [fb('input_solo', { channel: n }, AMBER, COLORS.black)])
|
||||||
for (const [meter, prefix, label] of [
|
for (const [meter, prefix, label] of [
|
||||||
['level', 'input_meter', 'Meter'],
|
['level', 'input_meter', 'Meter'],
|
||||||
@@ -73,16 +146,7 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
['comp', 'input_comp_gr', 'Compressor reduction'],
|
['comp', 'input_comp_gr', 'Compressor reduction'],
|
||||||
]) {
|
]) {
|
||||||
if (meter !== 'level' && !grMeters) continue
|
if (meter !== 'level' && !grMeters) continue
|
||||||
presets[`${prefix}_${n}`] = simple(
|
presets[`${prefix}_${n}`] = layeredMeter({ name: `${label}: ${tag}`, title: nameVar('input', n), options: { channel: n, meter } })
|
||||||
`${label}: ${tag}`,
|
|
||||||
nameVar('input', n),
|
|
||||||
[],
|
|
||||||
[
|
|
||||||
{ feedbackId: 'input_meter_bar', options: { channel: n, meter } },
|
|
||||||
fb('input_signal', { channel: n, meter, threshold: 5 }, GREEN),
|
|
||||||
fb('input_peak', { channel: n, meter, hold: 2 }, RED),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
meterIds[meter].push(`${prefix}_${n}`)
|
meterIds[meter].push(`${prefix}_${n}`)
|
||||||
}
|
}
|
||||||
muteIds.push(`input_mute_${n}`)
|
muteIds.push(`input_mute_${n}`)
|
||||||
@@ -114,15 +178,15 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
for (const n of s.named('buss')) {
|
for (const n of s.named('buss')) {
|
||||||
const tag = `${n} · ${s.displayName('buss', n)} (${s.bussMode(n)})`
|
const tag = `${n} · ${s.displayName('buss', n)} (${s.bussMode(n)})`
|
||||||
presets[`buss_mute_${n}`] = simple(`Master mute: ${tag}`, nameVar('buss', n), [act('buss_mute', { busses: [n], state: 'toggle' })], [fb('buss_muted', { buss: n }, RED)])
|
presets[`buss_mute_${n}`] = simple(`Master mute: ${tag}`, nameVar('buss', n), [act('buss_mute', { busses: [n], state: 'toggle' })], [fb('buss_muted', { buss: n }, RED)])
|
||||||
presets[`buss_knob_${n}`] = knob(
|
presets[`buss_knob_${n}`] = layeredKnob({
|
||||||
`Master fader (knob): ${tag}`,
|
name: `Master fader (knob): ${tag}`,
|
||||||
`${nameVar('buss', n)}\\n$(local:level) dB`,
|
title: nameVar('buss', n),
|
||||||
[act('buss_mute', { busses: [n], state: 'toggle' })],
|
press: [act('buss_mute', { busses: [n], state: 'toggle' })],
|
||||||
'buss_fader_adjust',
|
adjust: ['buss_fader_adjust', { buss: n }],
|
||||||
{ buss: n },
|
level: ['buss_fader_value', { buss: n }],
|
||||||
[localFeedback('level', 'buss_fader_value', { buss: n })],
|
pos: ['buss_fader_pos', { buss: n }],
|
||||||
[fb('buss_muted', { buss: n }, RED), { feedbackId: 'buss_fader_bar', options: { buss: n, color: BAR } }],
|
feedbacks: [tint('buss_muted', { buss: n }, MUTED_RED)],
|
||||||
)
|
})
|
||||||
muteIds.push(`buss_mute_${n}`)
|
muteIds.push(`buss_mute_${n}`)
|
||||||
knobIds.push(`buss_knob_${n}`)
|
knobIds.push(`buss_knob_${n}`)
|
||||||
}
|
}
|
||||||
@@ -145,15 +209,15 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
presets[`send_on_${ch}_${b}`] = simple(`Send on/off: ${tag}`, text, [act('send_on', { channel: ch, buss: b, state: 'toggle' })], [fb('send_on', { channel: ch, buss: b }, GREEN)], { localVariables: lv })
|
presets[`send_on_${ch}_${b}`] = simple(`Send on/off: ${tag}`, text, [act('send_on', { channel: ch, buss: b, state: 'toggle' })], [fb('send_on', { channel: ch, buss: b }, GREEN)], { localVariables: lv })
|
||||||
onIds.push(`send_on_${ch}_${b}`)
|
onIds.push(`send_on_${ch}_${b}`)
|
||||||
if (withSendKnobs) {
|
if (withSendKnobs) {
|
||||||
presets[`send_knob_${ch}_${b}`] = knob(
|
presets[`send_knob_${ch}_${b}`] = layeredKnob({
|
||||||
`Send level (knob): ${tag}`,
|
name: `Send level (knob): ${tag}`,
|
||||||
text,
|
title: nameVar('input', ch),
|
||||||
[act('send_on', { channel: ch, buss: b, state: 'toggle' })],
|
press: [act('send_on', { channel: ch, buss: b, state: 'toggle' })],
|
||||||
'send_level_adjust',
|
adjust: ['send_level_adjust', { channel: ch, buss: b }],
|
||||||
{ channel: ch, buss: b },
|
level: ['send_level_value', { channel: ch, buss: b }],
|
||||||
lv,
|
pos: ['send_level_pos', { channel: ch, buss: b }],
|
||||||
[fb('send_on', { channel: ch, buss: b }, GREEN), { feedbackId: 'send_level_bar', options: { channel: ch, buss: b, color: BAR } }],
|
feedbacks: [tint('send_on', { channel: ch, buss: b }, ON_GREEN)],
|
||||||
)
|
})
|
||||||
knobIds.push(`send_knob_${ch}_${b}`)
|
knobIds.push(`send_knob_${ch}_${b}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,15 +249,15 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
for (const n of s.named('matrix')) {
|
for (const n of s.named('matrix')) {
|
||||||
const tag = `${n} · ${s.displayName('matrix', n)}`
|
const tag = `${n} · ${s.displayName('matrix', n)}`
|
||||||
presets[`matrix_mute_${n}`] = simple(`Mute: ${tag}`, nameVar('matrix', n), [act('matrix_mute', { matrix: n, state: 'toggle' })], [fb('matrix_muted', { matrix: n }, RED)])
|
presets[`matrix_mute_${n}`] = simple(`Mute: ${tag}`, nameVar('matrix', n), [act('matrix_mute', { matrix: n, state: 'toggle' })], [fb('matrix_muted', { matrix: n }, RED)])
|
||||||
presets[`matrix_knob_${n}`] = knob(
|
presets[`matrix_knob_${n}`] = layeredKnob({
|
||||||
`Fader (knob): ${tag}`,
|
name: `Fader (knob): ${tag}`,
|
||||||
`${nameVar('matrix', n)}\\n$(local:level) dB`,
|
title: nameVar('matrix', n),
|
||||||
[act('matrix_mute', { matrix: n, state: 'toggle' })],
|
press: [act('matrix_mute', { matrix: n, state: 'toggle' })],
|
||||||
'matrix_fader_adjust',
|
adjust: ['matrix_fader_adjust', { matrix: n }],
|
||||||
{ matrix: n },
|
level: ['matrix_fader_value', { matrix: n }],
|
||||||
[localFeedback('level', 'matrix_fader_value', { matrix: n })],
|
pos: ['matrix_fader_pos', { matrix: n }],
|
||||||
[fb('matrix_muted', { matrix: n }, RED)],
|
feedbacks: [tint('matrix_muted', { matrix: n }, MUTED_RED)],
|
||||||
)
|
})
|
||||||
muteIds.push(`matrix_mute_${n}`)
|
muteIds.push(`matrix_mute_${n}`)
|
||||||
knobIds.push(`matrix_knob_${n}`)
|
knobIds.push(`matrix_knob_${n}`)
|
||||||
}
|
}
|
||||||
@@ -205,15 +269,15 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
for (const n of s.named('cg')) {
|
for (const n of s.named('cg')) {
|
||||||
const tag = `${n} · ${s.displayName('cg', n)}`
|
const tag = `${n} · ${s.displayName('cg', n)}`
|
||||||
presets[`cg_mute_${n}`] = simple(`Mute: ${tag}`, nameVar('cg', n), [act('cg_mute', { cg: n, state: 'toggle' })], [fb('cg_muted', { cg: n }, RED)])
|
presets[`cg_mute_${n}`] = simple(`Mute: ${tag}`, nameVar('cg', n), [act('cg_mute', { cg: n, state: 'toggle' })], [fb('cg_muted', { cg: n }, RED)])
|
||||||
presets[`cg_knob_${n}`] = knob(
|
presets[`cg_knob_${n}`] = layeredKnob({
|
||||||
`Fader (knob): ${tag}`,
|
name: `Fader (knob): ${tag}`,
|
||||||
`${nameVar('cg', n)}\\n$(local:level) dB`,
|
title: nameVar('cg', n),
|
||||||
[act('cg_mute', { cg: n, state: 'toggle' })],
|
press: [act('cg_mute', { cg: n, state: 'toggle' })],
|
||||||
'cg_fader_adjust',
|
adjust: ['cg_fader_adjust', { cg: n }],
|
||||||
{ cg: n },
|
level: ['cg_fader_value', { cg: n }],
|
||||||
[localFeedback('level', 'cg_fader_value', { cg: n })],
|
pos: ['cg_fader_pos', { cg: n }],
|
||||||
[fb('cg_muted', { cg: n }, RED)],
|
feedbacks: [tint('cg_muted', { cg: n }, MUTED_RED)],
|
||||||
)
|
})
|
||||||
muteIds.push(`cg_mute_${n}`)
|
muteIds.push(`cg_mute_${n}`)
|
||||||
knobIds.push(`cg_knob_${n}`)
|
knobIds.push(`cg_knob_${n}`)
|
||||||
}
|
}
|
||||||
@@ -231,15 +295,15 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
const options = { matrixIn: inN, matrixOut: out }
|
const options = { matrixIn: inN, matrixOut: out }
|
||||||
const lv = [localFeedback('level', 'matrix_send_level_value', options)]
|
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_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(
|
presets[`msend_knob_${inN}_${out}`] = layeredKnob({
|
||||||
`Matrix send level (knob): ${tag}`,
|
name: `Matrix send level (knob): ${tag}`,
|
||||||
text,
|
title: `Matrix In ${inN}`,
|
||||||
[act('matrix_send_on', { ...options, state: 'toggle' })],
|
press: [act('matrix_send_on', { ...options, state: 'toggle' })],
|
||||||
'matrix_send_level_adjust',
|
adjust: ['matrix_send_level_adjust', options],
|
||||||
options,
|
level: ['matrix_send_level_value', options],
|
||||||
lv,
|
pos: ['matrix_send_level_pos', options],
|
||||||
[fb('matrix_send_on', options, GREEN), { feedbackId: 'matrix_send_level_bar', options: { ...options, color: BAR } }],
|
feedbacks: [tint('matrix_send_on', options, ON_GREEN)],
|
||||||
)
|
})
|
||||||
onIds.push(`msend_on_${inN}_${out}`)
|
onIds.push(`msend_on_${inN}_${out}`)
|
||||||
knobIds.push(`msend_knob_${inN}_${out}`)
|
knobIds.push(`msend_knob_${inN}_${out}`)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -367,6 +367,18 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
if (last !== undefined && now - last < this.timing.errorLogMs) return false
|
if (last !== undefined && now - last < this.timing.errorLogMs) return false
|
||||||
if (this.errorLog.size > 200) this.errorLog.clear()
|
if (this.errorLog.size > 200) this.errorLog.clear()
|
||||||
this.errorLog.set(key, now)
|
this.errorLog.set(key, now)
|
||||||
|
// An iPad that went to sleep makes every fan-out fail with "host down/unreachable" until the relay forgets it
|
||||||
|
// (up to the idle timeout): say so once per iPad as a warning instead of an error every few seconds.
|
||||||
|
const asleep = source === 'Relay socket' && err.dest && ['EHOSTDOWN', 'EHOSTUNREACH', 'ENETUNREACH'].includes(err.code)
|
||||||
|
if (asleep) {
|
||||||
|
const ip = String(err.dest).split(':')[0]
|
||||||
|
const sleepKey = `asleep|${ip}`
|
||||||
|
const lastSleep = this.errorLog.get(sleepKey)
|
||||||
|
if (lastSleep !== undefined && now - lastSleep < 10 * 60 * 1000) return false
|
||||||
|
this.errorLog.set(sleepKey, now)
|
||||||
|
this.log('warn', `Relay: iPad ${ip} is not reachable (asleep or off the network); it stays on the list until it sends again or the idle timeout passes`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
this.log('error', `${source}: ${err.message}${err.dest ? ` (${err.dest})` : ''}`)
|
this.log('error', `${source}: ${err.message}${err.dest ? ` (${err.dest})` : ''}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,37 @@ export function fmtPan(p) {
|
|||||||
return pct < 0 ? `L${-pct}` : `R${pct}`
|
return pct < 0 ? `L${-pct}` : `R${pct}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fader taper for gauges: dB -> position 0..100, piecewise linear through these anchor points. */
|
||||||
|
const FADER_TAPER = [
|
||||||
|
[10, 100],
|
||||||
|
[5, 87.5],
|
||||||
|
[0, 75],
|
||||||
|
[-5, 65],
|
||||||
|
[-10, 55],
|
||||||
|
[-20, 40],
|
||||||
|
[-30, 28],
|
||||||
|
[-40, 18],
|
||||||
|
[-50, 11],
|
||||||
|
[-60, 6],
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position 0..100 of a level on a fader-like gauge (the shared taper above). Anything at or below `floor`, the
|
||||||
|
* level the kind treats as off (-90 dB for every kind in this module), is 0; just above it the position is about 1
|
||||||
|
* (a sliver stays visible for a send that is on), rising through -60 dB = 6 up to +10 dB = 100. Pass -150 as `floor`
|
||||||
|
* for a kind whose off level is -150. Non-numbers give 0.
|
||||||
|
*/
|
||||||
|
export function faderPosition(dB, floor = LEVEL.floor) {
|
||||||
|
if (typeof dB !== 'number' || !Number.isFinite(dB) || dB <= floor) return 0
|
||||||
|
if (dB >= FADER_TAPER[0][0]) return 100
|
||||||
|
let prev = FADER_TAPER[0]
|
||||||
|
for (const point of FADER_TAPER.slice(1)) {
|
||||||
|
if (dB >= point[0]) return point[1] + ((dB - point[0]) / (prev[0] - point[0])) * (prev[1] - point[1])
|
||||||
|
prev = point
|
||||||
|
}
|
||||||
|
return 1 + ((dB - floor) / (prev[0] - floor)) * (prev[1] - 1)
|
||||||
|
}
|
||||||
|
|
||||||
/** Snapshot cue number as the surface shows it: raw 100 -> "1", 250 -> "2.5". */
|
/** Snapshot cue number as the surface shows it: raw 100 -> "1", 250 -> "2.5". */
|
||||||
export function fmtCue(raw) {
|
export function fmtCue(raw) {
|
||||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) return ''
|
if (typeof raw !== 'number' || !Number.isFinite(raw)) return ''
|
||||||
|
|||||||
+117
-8
@@ -20,7 +20,7 @@ import { ConsoleLink } from '../src/console-link.js'
|
|||||||
import { Relay } from '../src/relay.js'
|
import { Relay } from '../src/relay.js'
|
||||||
import { encode, decode, foldPattern } from '../src/osc.js'
|
import { encode, decode, foldPattern } from '../src/osc.js'
|
||||||
import { State } from '../src/state.js'
|
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, faderPosition, 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'
|
import { COLORS } from '../src/definitions/feedbacks.js'
|
||||||
@@ -246,6 +246,26 @@ check(typeof Instance === 'function' && Array.isArray(mainMod.UpgradeScripts), '
|
|||||||
check(parse('/Control_Groups/3/name', ['KEYS']).n === 3 && parse('/Snapshots/name', [3, 400, 0, 'X']).cue === 400 && parse('/Meters/values', [5001, 10, 5002, 20]).pairs.length === 2, 'parse: control group name, snapshot name, meters')
|
check(parse('/Control_Groups/3/name', ['KEYS']).n === 3 && parse('/Snapshots/name', [3, 400, 0, 'X']).cue === 400 && parse('/Meters/values', [5001, 10, 5002, 20]).pairs.length === 2, 'parse: control group name, snapshot name, meters')
|
||||||
check(parse('/Console/Aux_Outputs', [25]).field === 'busses' && parse('/Nonexistent/thing', [1]) === null, 'parse: counts, unknown address -> null')
|
check(parse('/Console/Aux_Outputs', [25]).field === 'busses' && parse('/Nonexistent/thing', [1]) === null, 'parse: counts, unknown address -> null')
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
// the fader taper used by every position feedback
|
||||||
|
const anchors = [[10, 100], [5, 87.5], [0, 75], [-5, 65], [-10, 55], [-20, 40], [-30, 28], [-40, 18], [-50, 11], [-60, 6]]
|
||||||
|
check(anchors.every(([db, pos]) => near(faderPosition(db), pos, 1e-9)) && faderPosition(25) === 100, 'faderPosition hits every anchor: +10 = 100, +5 = 87.5, 0 = 75, -5 = 65, -10 = 55, -20 = 40, -30 = 28, -40 = 18, -50 = 11, -60 = 6')
|
||||||
|
check(near(faderPosition(-7.5), 60) && near(faderPosition(2.5), 81.25) && near(faderPosition(-15), 47.5) && near(faderPosition(-75), 3.5), 'faderPosition interpolates linearly between the anchors')
|
||||||
|
check(faderPosition(-90) === 0 && faderPosition(-120) === 0 && faderPosition(-150) === 0 && near(faderPosition(-89.99), 1, 0.01), 'at or below the -90 floor the position is 0, just above it about 1')
|
||||||
|
check(faderPosition(-150, -150) === 0 && near(faderPosition(-149.99, -150), 1, 0.01) && near(faderPosition(-60, -150), 6) && faderPosition(-100, -150) > 1, 'with a -150 floor the taper runs down to -150')
|
||||||
|
check(faderPosition(NaN) === 0 && faderPosition(undefined) === 0 && faderPosition('x') === 0 && faderPosition(null) === 0, 'non-numbers read 0')
|
||||||
|
let monotone = true
|
||||||
|
for (const floor of [-90, -150]) {
|
||||||
|
let last = -1
|
||||||
|
for (let db = floor; db <= 12; db += 0.25) {
|
||||||
|
const v = faderPosition(db, floor)
|
||||||
|
if (v < last || v < 0 || v > 100) monotone = false
|
||||||
|
last = v
|
||||||
|
}
|
||||||
|
for (let db = floor + 0.25; db < 10; db += 0.25) if (!(faderPosition(db, floor) < faderPosition(db + 0.25, floor))) monotone = false
|
||||||
|
}
|
||||||
|
check(monotone, 'faderPosition is monotonic (strictly rising up to +10) and stays within 0..100')
|
||||||
|
}
|
||||||
{
|
{
|
||||||
const h = horizontalBar({ width: 72, height: 58, fraction: 0.5, color: 0x00c85a })
|
const h = horizontalBar({ width: 72, height: 58, fraction: 0.5, color: 0x00c85a })
|
||||||
const raw = Buffer.from(h.imageBuffer, 'base64')
|
const raw = Buffer.from(h.imageBuffer, 'base64')
|
||||||
@@ -396,6 +416,7 @@ 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']
|
||||||
|
fbExpected.push('input_fader_pos', 'buss_fader_pos', 'matrix_fader_pos', 'cg_fader_pos', 'send_level_pos', 'matrix_send_level_pos', 'input_pan_num', 'send_pan_num', 'geq_band_db', 'input_meter_pos')
|
||||||
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')
|
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'}`)
|
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)`)
|
||||||
@@ -421,6 +442,13 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
else PARITY[`${ent}_${param}_set`] = PARITY[`${ent}_${param}_adjust`] = [`${ent}_${param}_value`]
|
else PARITY[`${ent}_${param}_set`] = PARITY[`${ent}_${param}_adjust`] = [`${ent}_${param}_value`]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const GAUGE = { input_fader: 'input_fader_pos', send_level: 'send_level_pos', buss_fader: 'buss_fader_pos', matrix_fader: 'matrix_fader_pos', matrix_send_level: 'matrix_send_level_pos', cg_fader: 'cg_fader_pos', input_pan: 'input_pan_num', send_pan: 'send_pan_num', geq_band: 'geq_band_db' }
|
||||||
|
for (const id of Object.keys(PARITY)) {
|
||||||
|
const g = GAUGE[id.replace(/_(set|adjust)$/, '')]
|
||||||
|
if (g && /_(set|adjust)$/.test(id)) PARITY[id] = [...PARITY[id], g]
|
||||||
|
}
|
||||||
|
const noGauge = Object.keys(ctx.actions).filter((id) => /^(input_fader|send_level|buss_fader|matrix_fader|matrix_send_level|cg_fader|input_pan|send_pan|geq_band)_(set|adjust)$/.test(id) && !(PARITY[id] || []).some((f) => /_(pos|num|db)$/.test(f)))
|
||||||
|
check(noGauge.length === 0 && Object.values(GAUGE).every((f) => ctx.feedbacks[f]?.type === 'value'), `every level and pan action has a numeric value feedback for gauges (${Object.values(GAUGE).join(', ')})`)
|
||||||
const noFeedback = Object.keys(ctx.actions).filter((id) => !(id in PARITY))
|
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 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 levelActions = Object.keys(ctx.actions).filter((id) => /_(set|adjust)$/.test(id) && /(fader|level|pan|band|trim|gain|freq)/.test(id))
|
||||||
@@ -447,7 +475,7 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
if (optionKeys(def) !== Object.keys(a.options).sort().join()) problems.push(`${id}: ${a.actionId} options ${Object.keys(a.options)} vs ${optionKeys(def)}`)
|
if (optionKeys(def) !== Object.keys(a.options).sort().join()) problems.push(`${id}: ${a.actionId} options ${Object.keys(a.options)} vs ${optionKeys(def)}`)
|
||||||
}
|
}
|
||||||
for (const [id, p] of Object.entries(P)) {
|
for (const [id, p] of Object.entries(P)) {
|
||||||
if (p.type !== 'simple') problems.push(`${id}: type ${p.type}`)
|
if (p.type !== 'simple' && p.type !== 'layered') problems.push(`${id}: type ${p.type}`)
|
||||||
for (const step of p.steps) {
|
for (const step of p.steps) {
|
||||||
for (const key of ['down', 'up', 'rotate_left', 'rotate_right']) for (const a of step[key] || []) checkAction(id, a)
|
for (const key of ['down', 'up', 'rotate_left', 'rotate_right']) for (const a of step[key] || []) checkAction(id, a)
|
||||||
if ((step.rotate_left && !step.rotate_right) || (!step.rotate_left && step.rotate_right)) problems.push(`${id}: only one rotate array`)
|
if ((step.rotate_left && !step.rotate_right) || (!step.rotate_left && step.rotate_right)) problems.push(`${id}: only one rotate array`)
|
||||||
@@ -457,8 +485,9 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
if (!def) problems.push(`${id}: unknown feedback ${f.feedbackId}`)
|
if (!def) problems.push(`${id}: unknown feedback ${f.feedbackId}`)
|
||||||
else {
|
else {
|
||||||
if (optionKeys(def) !== Object.keys(f.options).sort().join()) problems.push(`${id}: ${f.feedbackId} options ${Object.keys(f.options)} vs ${optionKeys(def)}`)
|
if (optionKeys(def) !== Object.keys(f.options).sort().join()) problems.push(`${id}: ${f.feedbackId} options ${Object.keys(f.options)} vs ${optionKeys(def)}`)
|
||||||
if (def.type === 'boolean' && !f.style) problems.push(`${id}: boolean feedback ${f.feedbackId} without style`)
|
if (p.type === 'simple' && def.type === 'boolean' && !f.style) problems.push(`${id}: boolean feedback ${f.feedbackId} without style`)
|
||||||
if (def.type === 'advanced' && f.style) problems.push(`${id}: advanced feedback ${f.feedbackId} with style`)
|
if (p.type === 'simple' && def.type === 'advanced' && f.style) problems.push(`${id}: advanced feedback ${f.feedbackId} with style`)
|
||||||
|
if (p.type === 'layered' && (def.type !== 'boolean' || f.style || !Array.isArray(f.styleOverrides) || !f.styleOverrides.length)) problems.push(`${id}: layered feedback ${f.feedbackId} must be boolean with styleOverrides`)
|
||||||
if (def.type === 'value') problems.push(`${id}: value feedback ${f.feedbackId} used as a button feedback`)
|
if (def.type === 'value') problems.push(`${id}: value feedback ${f.feedbackId} used as a button feedback`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,8 +498,9 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
if (lv.variableType !== 'feedback' || !def || def.type !== 'value') problems.push(`${id}: local variable ${lv.variableName} is not backed by a value feedback`)
|
if (lv.variableType !== 'feedback' || !def || def.type !== 'value') problems.push(`${id}: local variable ${lv.variableName} is not backed by a value feedback`)
|
||||||
else if (optionKeys(def) !== Object.keys(lv.options).sort().join()) problems.push(`${id}: local ${lv.feedbackId} options`)
|
else if (optionKeys(def) !== Object.keys(lv.options).sort().join()) problems.push(`${id}: local ${lv.feedbackId} options`)
|
||||||
}
|
}
|
||||||
for (const m of p.style.text.matchAll(/\$\(local:(\w+)\)/g)) if (!locals.has(m[1])) problems.push(`${id}: text uses undeclared local ${m[1]}`)
|
const used = p.type === 'layered' ? JSON.stringify(p.elements) : p.style.text
|
||||||
for (const m of p.style.text.matchAll(/\$\(S31:(\w+)\)/g)) if (!(m[1] in ctx.variableDefs)) problems.push(`${id}: text uses undefined variable ${m[1]}`)
|
for (const m of used.matchAll(/\$\(local:(\w+)\)/g)) if (!locals.has(m[1])) problems.push(`${id}: uses undeclared local ${m[1]}`)
|
||||||
|
for (const m of used.matchAll(/\$\(S31:(\w+)\)/g)) if (!(m[1] in ctx.variableDefs)) problems.push(`${id}: uses undefined variable ${m[1]}`)
|
||||||
}
|
}
|
||||||
check(problems.length === 0, `preset actions, feedbacks, options and local variables are valid (${problems.length ? problems.slice(0, 5).join('; ') : 'no problems'})`)
|
check(problems.length === 0, `preset actions, feedbacks, options and local variables are valid (${problems.length ? problems.slice(0, 5).join('; ') : 'no problems'})`)
|
||||||
const knobs = ids.filter((id) => /_knob_/.test(id))
|
const knobs = ids.filter((id) => /_knob_/.test(id))
|
||||||
@@ -485,8 +515,50 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
const mOn = P.msend_on_1_8
|
const mOn = P.msend_on_1_8
|
||||||
const mKnob = P.msend_knob_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(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(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.type === 'layered' && mKnob.feedbacks[0].feedbackId === 'matrix_send_on' && mKnob.feedbacks[0].styleOverrides[0].elementId === 'bg' && mKnob.localVariables.map((v) => `${v.variableName}=${v.feedbackId}`).join() === 'level=matrix_send_level_value,pos=matrix_send_level_pos', 'matrix send knob preset (layered): rotate = matrix_send_level_adjust -1/+1, press toggles on/off, on tints the background, level and gauge position local variables')
|
||||||
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')
|
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')
|
||||||
|
// layered presets: element types and required fields from the Companion graphics definitions
|
||||||
|
const layered = ids.filter((id) => P[id].type === 'layered')
|
||||||
|
const knobsLayered = layered.filter((id) => /_knob_/.test(id))
|
||||||
|
const meterPresets = layered.filter((id) => /^input_(meter|gate_gr|comp_gr)_/.test(id))
|
||||||
|
check(knobsLayered.length === ids.filter((id) => /_knob_/.test(id)).length && meterPresets.length === 180 && layered.length === knobsLayered.length + 180 && !ids.some((id) => /_(mute|solo|on)_/.test(id) && P[id].type === 'layered'), `every knob (${knobsLayered.length}) and meter (${meterPresets.length}) preset is layered; plain toggles stay simple`)
|
||||||
|
const ELEMENT_TYPES = new Set(['group', 'composite', 'text', 'image', 'box', 'line', 'circle', 'gauge'])
|
||||||
|
const LAYERED_PROPS = { box: ['id', 'type', 'x', 'y', 'width', 'height', 'color'], text: ['id', 'type', 'x', 'y', 'width', 'height', 'text', 'fontsize', 'fontsizeAllowShrink', 'color', 'halign', 'valign'], gauge: ['id', 'type', 'x', 'y', 'width', 'height', 'value', 'min', 'max', 'orientation', 'trackStyle', 'fillEnabled', 'multiColour', 'stops'] }
|
||||||
|
const isExpr = (v) => !!v && typeof v === 'object' && v.isExpression === true && typeof v.value === 'string'
|
||||||
|
const lp = []
|
||||||
|
for (const id of layered) {
|
||||||
|
const p = P[id]
|
||||||
|
if (!Array.isArray(p.elements) || !p.elements.length || typeof p.name !== 'string' || !Array.isArray(p.steps) || !Array.isArray(p.feedbacks) || p.style !== undefined) lp.push(`${id}: shape`)
|
||||||
|
const elementIds = new Set()
|
||||||
|
for (const e of p.elements) {
|
||||||
|
if (!ELEMENT_TYPES.has(e.type) || typeof e.id !== 'string' || elementIds.has(e.id)) lp.push(`${id}: element ${e.type}/${e.id}`)
|
||||||
|
elementIds.add(e.id)
|
||||||
|
if (!LAYERED_PROPS[e.type]) lp.push(`${id}: unexpected element type ${e.type}`)
|
||||||
|
else for (const k of Object.keys(e)) if (!LAYERED_PROPS[e.type].includes(k)) lp.push(`${id}: ${e.type} ${e.id} has unknown property ${k}`)
|
||||||
|
for (const k of ['x', 'y', 'width', 'height']) if (typeof e[k] !== 'number' || e[k] < 0 || e[k] > 100) lp.push(`${id}: ${e.id}.${k}`)
|
||||||
|
if (e.type === 'text' && (typeof e.text !== 'string' || typeof e.fontsize !== 'number')) lp.push(`${id}: text ${e.id}`)
|
||||||
|
if (e.type === 'box' && typeof e.color !== 'number') lp.push(`${id}: box color`)
|
||||||
|
if (e.type === 'gauge') {
|
||||||
|
const okStops = Array.isArray(e.stops) && e.stops.length > 0 && e.stops.every((st) => typeof st.value === 'number' && typeof st.color === 'number' && typeof st.gradient === 'boolean')
|
||||||
|
if (!isExpr(e.value) || e.value.value !== '$(local:pos)' || e.min !== 0 || e.max !== 100 || !['horizontal', 'vertical', 'ring'].includes(e.orientation) || !['transparent', 'dimmed'].includes(e.trackStyle) || !okStops) lp.push(`${id}: gauge`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const f of p.feedbacks) for (const o of f.styleOverrides || []) {
|
||||||
|
const el = p.elements.find((e) => e.id === o.elementId)
|
||||||
|
if (!el || !(o.elementProperty in el) || typeof o.override !== 'number') lp.push(`${id}: styleOverride ${o.elementId}.${o.elementProperty}`)
|
||||||
|
}
|
||||||
|
const posVar = (p.localVariables || []).find((v) => v.variableName === 'pos')
|
||||||
|
if (!posVar || !/_(pos)$/.test(posVar.feedbackId) || ctx.feedbacks[posVar.feedbackId]?.type !== 'value') lp.push(`${id}: pos local variable`)
|
||||||
|
}
|
||||||
|
check(lp.length === 0, `layered presets are valid: element types and fields, unique element ids, style overrides name existing elements, every $(local:x) is declared and backed by a value feedback (${lp.length ? lp.slice(0, 4).join('; ') : `${layered.length} presets, no problems`})`)
|
||||||
|
const iK = P.input_knob_3
|
||||||
|
check(iK.elements.map((e) => `${e.type}:${e.id}`).join() === 'box:bg,text:title,text:level,gauge:gauge' && iK.elements[1].text === `$(S31:input_3_name)` && iK.elements[2].text === '$(local:level) dB' && iK.elements[3].orientation === 'horizontal' && iK.elements[3].y === 84 && iK.elements[3].height === 16 && iK.elements[3].stops.length === 1 && iK.elements[3].stops[0].color === 0xffffff && iK.elements[1].fontsize === 100 && iK.elements[1].fontsizeAllowShrink === true && iK.elements[3].trackStyle === 'dimmed' && iK.feedbacks[0].feedbackId === 'input_muted' && iK.feedbacks[0].styleOverrides[0].override === 0xb01010 && iK.localVariables.map((v) => `${v.variableName}=${v.feedbackId}`).join() === 'level=input_fader_value,pos=input_fader_pos', 'input knob: black box, name text, "$(local:level) dB", a white horizontal bottom gauge fed by $(local:pos), text size 100 with shrink, muted tints the background red')
|
||||||
|
const sK = P.send_knob_3_24 || P[ids.find((id) => id.startsWith('send_knob_'))]
|
||||||
|
check(sK.feedbacks[0].styleOverrides[0].override === 0x00a050 && sK.localVariables[1].feedbackId === 'send_level_pos', 'aux send knob: send on tints the background green, gauge from send_level_pos')
|
||||||
|
const mt = P.input_meter_7
|
||||||
|
const g = mt.elements.find((e) => e.id === 'gauge')
|
||||||
|
check(g.orientation === 'vertical' && g.x === 86 && g.width === 14 && g.multiColour === true && g.stops.map((st) => `${st.value}:${st.color.toString(16)}`).join() === '0:b050,70:f0a000,90:e00000' && mt.localVariables[0].feedbackId === 'input_meter_pos' && mt.feedbacks.map((f) => f.feedbackId).join() === 'input_signal,input_peak', 'meter preset: vertical gauge on the right edge, green below 70, amber 70-90, red above 90, signal and peak tint the background')
|
||||||
|
check(Object.entries(ctx.feedbacks).filter(([id]) => /_bar$/.test(id)).every(([, f]) => /\(legacy image\)$/.test(f.name)) && Object.keys(ctx.feedbacks).filter((id) => /_bar$/.test(id)).length === 8, 'the 8 advanced image bar feedbacks keep their ids and are named "(legacy image)"')
|
||||||
const bytes = JSON.stringify(P).length
|
const bytes = JSON.stringify(P).length
|
||||||
console.log(`info presets: ${ids.length} presets, ${bytes} bytes serialized, ${inst.presetStats?.dropped?.length ? `dropped ${inst.presetStats.dropped.join(' and ')}` : '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`)
|
||||||
@@ -804,6 +876,23 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
check(cb('geq_on', { geq: 3 }) === false && cb('geq_band_value', { geq: 3, band: 7 }) === '--', 'an unloaded graphic EQ reads false / "--"')
|
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(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('send_pan_value', { channel: 2, buss: 3 }) === 'L80', 'send_pan_value (pan 0.1 -> L80)')
|
||||||
|
|
||||||
|
// numeric position feedbacks for native gauges: known state gives the taper position, unknown gives 0 (and starts the lazy load)
|
||||||
|
check(near(cb('input_fader_pos', { channel: 5 }), 51.25) && near(cb('buss_fader_pos', { buss: 24 }), 69) && near(cb('matrix_fader_pos', { matrix: 2 }), 46) && near(cb('cg_fader_pos', { cg: 2 }), 59), 'input / buss / matrix / control group fader position follow the fader taper (-12.5 dB -> 51.25, -3 -> 69, -16 -> 46, -8 -> 59)')
|
||||||
|
check(near(cb('send_level_pos', { channel: 2, buss: 3 }), 61) && near(cb('matrix_send_level_pos', { matrixIn: 2, matrixOut: 3 }), 28) && near(cb('matrix_send_level_pos', { matrixIn: 3, matrixOut: 2 }), 6), 'aux send and matrix send position (-7 dB -> 61, -30 -> 28, -60 -> 6)')
|
||||||
|
check(cb('input_pan_num', { channel: 13 }) === 1 && near(cb('send_pan_num', { channel: 2, buss: 3 }), 0.1) && cb('geq_band_db', { geq: 1, band: 5 }) === 4, 'pan is the raw 0..1 number and the graphic EQ band is the dB number')
|
||||||
|
check(cb('input_fader_pos', { channel: 99 }) === 0 && cb('buss_fader_pos', { buss: 99 }) === 0 && cb('matrix_fader_pos', { matrix: 99 }) === 0 && cb('cg_fader_pos', { cg: 99 }) === 0 && cb('input_pan_num', { channel: 99 }) === 0.5, 'unknown fader state reads 0 (pan reads the 0.5 centre)')
|
||||||
|
const lazy = [
|
||||||
|
['send_level_pos', { channel: 45, buss: 9 }, '/Input_Channels/45/Aux_Send/9/send_level/?', 0],
|
||||||
|
['matrix_send_level_pos', { matrixIn: 7, matrixOut: 6 }, '/Matrix_Inputs/7/Matrix_Send/6/send_level/?', 0],
|
||||||
|
['send_pan_num', { channel: 45, buss: 9 }, '/Input_Channels/45/Aux_Send/9/send_pan/?', 0.5],
|
||||||
|
['geq_band_db', { geq: 4, band: 9 }, '/Graphic_EQ/4/geq_gain_9/?', 0],
|
||||||
|
]
|
||||||
|
const firsts = lazy.map(([id, options]) => cb(id, options))
|
||||||
|
check(lazy.every(([, , , want], i) => firsts[i] === want), `unloaded send / matrix send / send pan / GEQ band read ${firsts.join(', ')} on first sight`)
|
||||||
|
check(await waitFor(() => lazy.every(([, , address]) => sent.some((m) => m.address === address)), 2500), 'and each starts its lazy load')
|
||||||
|
await sleep(150)
|
||||||
|
check(typeof cb('send_level_pos', { channel: 45, buss: 9 }) === 'number' && S.getSend(45, 9).level !== undefined && S.getMatrixSend(7, 6).level === -90 && cb('matrix_send_level_pos', { matrixIn: 7, matrixOut: 6 }) === 0 && typeof cb('send_pan_num', { channel: 45, buss: 9 }) === 'number' && cb('geq_band_db', { geq: 4, band: 9 }) === 0, 'once the state arrives the position feedbacks return the real values (a matrix send at -90 stays 0)')
|
||||||
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_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('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')
|
||||||
@@ -867,6 +956,10 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
// deterministic values injected as if from the console
|
// deterministic values injected as if from the console
|
||||||
const inject = (value) => inst.onConsoleMessage(decode(encode('/Meters/values', [{ i: id7 }, { i: value }])), { address: '127.0.0.1', port: P.mock })
|
const inject = (value) => inst.onConsoleMessage(decode(encode('/Meters/values', [{ i: id7 }, { i: value }])), { address: '127.0.0.1', port: P.mock })
|
||||||
inject(127)
|
inject(127)
|
||||||
|
check(cb('input_meter_pos', { channel: 7, meter: 'level' }) === 100, 'input_meter_pos is 100 at full scale (127)')
|
||||||
|
inject(64)
|
||||||
|
check(near(cb('input_meter_pos', { channel: 7 }), (64 / 127) * 100) && cb('input_meter_pos', { channel: 7, meter: 'comp' }) >= 0, 'input_meter_pos is 0..100 from 0..127 and takes the level / gate / comp option')
|
||||||
|
inject(127)
|
||||||
check(cb('input_meter_value', { channel: 7, unit: 'raw' }) === 127 && cb('input_meter_value', { channel: 7, unit: 'percent' }) === 100, 'input_meter_value raw and percent')
|
check(cb('input_meter_value', { channel: 7, unit: 'raw' }) === 127 && cb('input_meter_value', { channel: 7, unit: 'percent' }) === 100, 'input_meter_value raw and percent')
|
||||||
check(cb('input_signal', { channel: 7, threshold: 5 }) === true && cb('input_peak', { channel: 7, hold: 2 }) === true, 'input_signal and input_peak at full scale')
|
check(cb('input_signal', { channel: 7, threshold: 5 }) === true && cb('input_peak', { channel: 7, hold: 2 }) === true, 'input_signal and input_peak at full scale')
|
||||||
inject(3)
|
inject(3)
|
||||||
@@ -897,7 +990,7 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
const meterChecks = ctx.byId
|
const meterChecks = ctx.byId
|
||||||
check(meterChecks.length > 0 && meterChecks.length <= Math.ceil((spanMs / 1000) * 10) + 1, `meter feedback checks throttled to <= 10 Hz (${meterChecks.length} checks in ${spanMs} ms for 100 messages)`)
|
check(meterChecks.length > 0 && meterChecks.length <= Math.ceil((spanMs / 1000) * 10) + 1, `meter feedback checks throttled to <= 10 Hz (${meterChecks.length} checks in ${spanMs} ms for 100 messages)`)
|
||||||
check(meterChecks.every(([, ids]) => ids.every((x) => watching7.has(x))) && !meterChecks.some(([, ids]) => ids.includes(gateInstance)), 'a meter update re-checks only the instances watching the changed meter id')
|
check(meterChecks.every(([, ids]) => ids.every((x) => watching7.has(x))) && !meterChecks.some(([, ids]) => ids.includes(gateInstance)), 'a meter update re-checks only the instances watching the changed meter id')
|
||||||
check(ctx.checks.length === 0, 'meter updates never fall back to per-type checks')
|
check(!ctx.checks.some(([, types]) => types.some((t) => /meter|signal|peak/.test(t))), `meter updates never fall back to per-type checks (per-type checks in that window: ${JSON.stringify(ctx.checks.map(([, t]) => t))}, from the keepalive tick)`)
|
||||||
const rebuildsBefore = ctx.rebuilds
|
const rebuildsBefore = ctx.rebuilds
|
||||||
await sleep(200)
|
await sleep(200)
|
||||||
check(ctx.rebuilds === rebuildsBefore, 'meter traffic never rebuilds the definitions')
|
check(ctx.rebuilds === rebuildsBefore, 'meter traffic never rebuilds the definitions')
|
||||||
@@ -1544,6 +1637,22 @@ mockB.kill('SIGTERM')
|
|||||||
check(off.length === 0, `every address sent during the whole run is answered by the real console, a documented trigger node or a meter subscription${off.length ? `: ${off.join(', ')}` : ''}`)
|
check(off.length === 0, `every address sent during the whole run is answered by the real console, a documented trigger node or a meter subscription${off.length ? `: ${off.join(', ')}` : ''}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 11. A sleeping iPad is reported once as a warning, not as an error on every fan-out
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
const before = logs.length
|
||||||
|
const asleep = (code) => Object.assign(new Error(`send ${code} 192.0.2.50:9100`), { code, dest: '192.0.2.50:9100' })
|
||||||
|
inst.reportError('Relay socket', asleep('EHOSTDOWN'))
|
||||||
|
inst.reportError('Relay socket', asleep('EHOSTUNREACH'))
|
||||||
|
inst.reportError('Relay socket', asleep('EHOSTDOWN'))
|
||||||
|
const fresh = logs.slice(before).filter((l) => /192\.0\.2\.50/.test(l.message))
|
||||||
|
check(fresh.length === 1 && fresh[0].level === 'warn' && /not reachable/.test(fresh[0].message), `a sleeping iPad logs one warning (${fresh.map((l) => l.level).join(',') || 'none'})`)
|
||||||
|
const other = Object.assign(new Error('send EPERM 192.0.2.51:9100'), { code: 'EPERM', dest: '192.0.2.51:9100' })
|
||||||
|
inst.reportError('Relay socket', other)
|
||||||
|
check(logs.slice(before).some((l) => l.level === 'error' && /EPERM/.test(l.message)), 'other relay send errors are still logged as errors')
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Shutdown
|
// Shutdown
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user