Add Group and Aux master actions, feedbacks and preset sections

Groups were only reachable through the generic "Buss:" actions, whose
dropdown mixes all 25 busses, so users could not find group fader
control.

- New "Group:" and "Aux master:" mute, solo, set fader (with fade time)
  and adjust fader actions; their dropdowns list only busses of that
  type. Matching muted, solo, fader text/position, meter, signal and
  peak feedbacks.
- The old buss_* actions and feedbacks keep working for existing
  buttons and are now labelled "Any buss (aux or group)".
- Presets: "Busses" is split into "Groups" and "Aux masters", and the
  bus meters into "Group meters" and "Aux meters". A bus that changes
  type on the console moves section on the next rebuild.

npm test: 542 checks pass on Node 25 and on Companion's Node 22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 12:57:01 -04:00
co-authored by Claude Opus 5.5
parent 205f91e7d2
commit f81d581bf2
6 changed files with 263 additions and 75 deletions
+26 -16
View File
@@ -73,7 +73,10 @@ each is read the first time a button or action needs it.
The 25 busses are each an **aux** or a **group**, according to `/Console/Aux_Outputs/types` (1 = aux, The 25 busses are each an **aux** or a **group**, according to `/Console/Aux_Outputs/types` (1 = aux,
0 = group). Aux busses take sends (level, on, pan); group busses take input assigns. A buss's 0 = group). Aux busses take sends (level, on, pan); group busses take input assigns. A buss's
fader, mute and solo are on the Aux_Outputs or Group_Outputs side that matches its mode. fader, mute and solo are on the Aux_Outputs or Group_Outputs side that matches its mode. The module has
separate **Group** and **Aux master** actions, feedbacks and presets for them (their dropdowns list only busses of
that type); a buss that changes type on the console moves to the other dropdowns and preset sections when the
definitions are rebuilt.
## Actions ## Actions
@@ -88,7 +91,9 @@ is not known yet.
| Inputs | mute, solo (several inputs at once), fader set/adjust, pan set/adjust, set name | | Inputs | mute, solo (several inputs at once), fader set/adjust, pan set/adjust, set name |
| Aux sends | on/off, level set/adjust, pan set/adjust | | Aux sends | on/off, level set/adjust, pan set/adjust |
| Groups | assign input on/off | | Groups | assign input on/off |
| Busses | mute, solo (several), fader set/adjust | | Groups | **Group:** mute, solo (several), fader set (with fade) / adjust; input assign on/off is *Group: assign input* above |
| Aux masters | **Aux master:** mute, solo (several), fader set (with fade) / adjust (the master fader of an aux buss; the sends to it are under Aux sends) |
| Any buss | **Any buss (aux or group):** mute, solo (several), fader set / adjust, with one dropdown of all 25 busses. These are the original buss actions, kept so existing buttons keep working; use the Group and Aux master actions for new buttons |
| Matrix | output mute, fader set/adjust; matrix send on/off, level set/adjust | | Matrix | output mute, fader set/adjust; matrix send on/off, level set/adjust |
| Control groups | mute, solo, fader set/adjust | | Control groups | mute, solo, fader set/adjust |
| Graphic EQ | in/out, band gain set/adjust (bands 1-32) | | Graphic EQ | in/out, band gain set/adjust (bands 1-32) |
@@ -101,8 +106,8 @@ own naming `lo_filter` is the high-pass filter and `hi_filter` the low-pass filt
what they do. Delay times and the send pre/post fader switch are not offered: their units and meaning were what they do. Delay times and the send pre/post fader switch are not offered: their units and meaning were
never verified. never verified.
**Fades.** Every *set level* action (input, buss, matrix output and control group faders, aux send level, matrix send **Fades.** Every *set level* action (input, group, aux master, any-buss, matrix output and control group faders, aux send
level) has a **Fade time (ms, 0 = instant)**, 0 to 60000 ms. With 0 the level jumps to the target at once, as before; above 0 level, matrix send level) has a **Fade time (ms, 0 = instant)**, 0 to 60000 ms. With 0 the level jumps to the target at once, as before; above 0
the module moves the level there smoothly, starting from where it is now (it reads the level first when it is not known the module moves the level there smoothly, starting from where it is now (it reads the level first when it is not known
yet; if the console does not answer, the target is set at once and a warning is logged). A new level is written 20 times a yet; if the console does not answer, the target is set at once and a warning is logged). A new level is written 20 times a
second at most (every 50 ms, steps that would change the level by less than 0.05 dB are left out), and the last write is second at most (every 50 ms, steps that would change the level by less than 0.05 dB are left out), and the last write is
@@ -132,19 +137,23 @@ Every action that changes something the console reports has a feedback for it: a
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.
* Boolean (button colour): input muted / solo; aux send on; group assigned; buss muted / solo; matrix output muted / * Boolean (button colour): input muted / solo; aux send on; group assigned; group and aux master muted / solo (and the older any-buss versions); matrix output muted /
solo; control group muted / solo; **matrix send on**; **matrix send is off** (red like a mute; false while the state is not yet known); **graphic EQ in**; snapshot is current; fader level above/below solo; control group muted / solo; **matrix send on**; **matrix send is off** (red like a mute; false while the state is not yet known); **graphic EQ in**; snapshot is current; fader level above/below
a threshold; aux send level above/below; console link alive; relay has an iPad; a level fade is running; every channel-strip switch a threshold; aux send level above/below; console link alive; relay has an iPad; a level fade is running; 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; phase also (phantom, phase, pad, EQ in, compressor in, gate in, high-pass in, low-pass in, delay in, insert A/B in; phase also
per matrix input); and the meter feedbacks (signal present, peak hold) for inputs, busses and matrix outputs. per matrix input); and the meter feedbacks (signal present, peak hold) for inputs, group and aux master outputs, busses and matrix outputs.
* Value (put live numbers in button text with a preset's local variable): input fader and pan; aux send level, On/Off * 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 and pan; group / aux master / 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 and matrix input); current snapshot name; meter level filter frequency per input and buss (trim also per matrix output and matrix input); current snapshot name; meter level
text (see **Meters**). Levels read `-inf` at or below -90 dB. text (see **Meters**). Levels read `-inf` at or below -90 dB.
* Group and aux master feedbacks: `group_muted`, `group_solo`, `group_fader_value`, `group_fader_pos`, `group_meter_value`,
`group_meter_pos`, `group_signal`, `group_peak`, and the same eight with the prefix `aux_` for aux masters. They have the
same options and behave exactly like the older `buss_*` ones (which stay, named "Any buss (aux or group): ...") with
the dropdown limited to busses of that type. There are no image-bar versions of the new ones.
* Numeric, for gauges (see **Gauges** below): the `*_pos` position feedbacks, `input_pan_num` / `send_pan_num` and * Numeric, for gauges (see **Gauges** below): the `*_pos` position feedbacks, `input_pan_num` / `send_pan_num` and
`geq_band_db`. `geq_band_db`.
* Advanced image bars (**legacy image**, deprecated by Companion 5.1, kept so existing buttons keep working): input * 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 fader, aux send level, any-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 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. the right edge. Use a gauge on new buttons.
@@ -158,10 +167,10 @@ number you feed to a gauge through a button local variable:
| Feedback | Range | Meaning | | 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_fader_pos`, `group_fader_pos`, `aux_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) | | `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) | | `geq_band_db` | -12 to +12 | graphic EQ band gain in dB (unknown reads 0) |
| `input_meter_pos`, `buss_meter_pos`, `matrix_meter_pos` | 0-100 | meter position: levels over -60..0 dBFS (-inf = 0), gain reduction over 0..20 dB; the input meter has the *Meter* option level / gate / compressor reduction | | `input_meter_pos`, `group_meter_pos`, `aux_meter_pos`, `buss_meter_pos`, `matrix_meter_pos` | 0-100 | meter position: levels over -60..0 dBFS (-inf = 0), gain reduction over 0..20 dB; the input meter 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 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). -60 = 6 (straight lines in between), falls to about 1 just above -90 dB, and reads 0 at or below -90 dB (off).
@@ -193,7 +202,8 @@ buttons.
| 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 gauge along the bottom | | 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 | | Groups | Per named group buss: a **Mute** button (red when muted) and a **Fader (knob)** (turn = fader, press = mute, with a gauge) |
| Aux masters | The same for each named aux buss's master fader (the sends to an aux are in *Sends to aux name*) |
| 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) | | 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 (with a gauge) per output / group | | Matrix, Control groups | Mute and fader knob (with a gauge) per output / group |
@@ -201,7 +211,7 @@ buttons.
| 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, each with a vertical gauge at the right edge (green below 70, amber 70-90, red above 90) | | 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) |
| Buss meters, Matrix meters | One level meter button per named buss and per matrix output, same vertical gauge | | Group meters, Aux meters, Matrix meters | One level meter button per named group buss, per named aux buss and per matrix output, same vertical gauge |
| 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
@@ -222,10 +232,10 @@ step, not as a measurement.
| Feedback | What it shows | | Feedback | What it shows |
| --- | --- | | --- | --- |
| `input_meter_value`, `buss_meter_value`, `matrix_meter_value` | text: `-42` or `-inf` (dBFS, approx.); gain reduction meters: `3` (dB) | | `input_meter_value`, `group_meter_value`, `aux_meter_value`, `buss_meter_value`, `matrix_meter_value` | text: `-42` or `-inf` (dBFS, approx.); gain reduction meters: `3` (dB) |
| `input_meter_pos`, `buss_meter_pos`, `matrix_meter_pos` | gauge position 0-100: levels over -60..0 dBFS, gain reduction over 0..20 dB | | `input_meter_pos`, `group_meter_pos`, `aux_meter_pos`, `buss_meter_pos`, `matrix_meter_pos` | gauge position 0-100: levels over -60..0 dBFS, gain reduction over 0..20 dB |
| `input_signal`, `buss_signal`, `matrix_signal` | signal present: level above a dBFS threshold (option, default -50; gain reduction meters: any reduction) | | `input_signal`, `group_signal`, `aux_signal`, `buss_signal`, `matrix_signal` | signal present: level above a dBFS threshold (option, default -50; gain reduction meters: any reduction) |
| `input_peak`, `buss_peak`, `matrix_peak` | level at or above a dBFS value (option, default -1) held for the *Hold* time; not for gain reduction meters | | `input_peak`, `group_peak`, `aux_peak`, `buss_peak`, `matrix_peak` | level at or above a dBFS value (option, default -1) held for the *Hold* time; not for gain reduction meters |
Input meters are level (post-fader), gate gain reduction and compressor gain reduction. Output meters exist for aux Input meters are level (post-fader), gate gain reduction and compressor gain reduction. Output meters exist for aux
busses, group busses (the address follows the buss's current aux / group side) and matrix outputs; they are level meters. busses, group busses (the address follows the buss's current aux / group side) and matrix outputs; they are level meters.
+40 -5
View File
@@ -23,7 +23,9 @@ export const opt = {
const choices = kind === 'aux' ? inst.state.choices.auxChoices() : kind === 'group' ? inst.state.choices.groupChoices() : inst.state.choices.busses() const choices = kind === 'aux' ? inst.state.choices.auxChoices() : kind === 'group' ? inst.state.choices.groupChoices() : inst.state.choices.busses()
return opt.dropdown(id, label || (kind === 'aux' ? 'Aux buss' : kind === 'group' ? 'Group buss' : 'Buss'), choices) return opt.dropdown(id, label || (kind === 'aux' ? 'Aux buss' : kind === 'group' ? 'Group buss' : 'Buss'), choices)
}, },
busses: (inst, id = 'busses', label = 'Busses') => opt.multi(id, label, inst.state.choices.busses()), /** Several busses; `kind` 'aux' or 'group' lists only busses of that type (without the suffix), 'all' lists every buss. */
busses: (inst, id = 'busses', label = 'Busses', kind = 'all') =>
opt.multi(id, label, kind === 'aux' ? inst.state.choices.auxChoices() : kind === 'group' ? inst.state.choices.groupChoices() : inst.state.choices.busses()),
matrix: (inst, id = 'matrix', label = 'Matrix output') => opt.dropdown(id, label, inst.state.choices.matrix()), matrix: (inst, id = 'matrix', label = 'Matrix output') => opt.dropdown(id, label, inst.state.choices.matrix()),
matrixIn: (inst, id = 'matrixIn', label = 'Matrix input') => opt.dropdown(id, label, inst.state.choices.matrixInputs()), matrixIn: (inst, id = 'matrixIn', label = 'Matrix input') => opt.dropdown(id, label, inst.state.choices.matrixInputs()),
cg: (inst, id = 'cg', label = 'Control group') => opt.dropdown(id, label, inst.state.choices.cgs()), cg: (inst, id = 'cg', label = 'Control group') => opt.dropdown(id, label, inst.state.choices.cgs()),
@@ -147,6 +149,36 @@ function quiet(inst, defs) {
export function getActionDefinitions(inst) { export function getActionDefinitions(inst) {
const each = (list, fn) => Promise.all(numbers(list).map(fn)) const each = (list, fn) => Promise.all(numbers(list).map(fn))
const n = (v) => Number(v) const n = (v) => Number(v)
/**
* Mute, solo and fader actions for one type of buss (`prefix` 'group' or 'aux'). They are thin wrappers over the same
* buss operations as the buss_* actions (the address follows the buss's aux / group side), with the dropdowns
* filtered by type. The option ids are the same as the buss_* ones.
*/
const bussActions = (prefix, title, one, many) => ({
[`${prefix}_mute`]: {
name: `${title}: mute`,
description: 'Mute, unmute or toggle one or more.',
options: [opt.busses(inst, 'busses', many, prefix), opt.state()],
callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'mute', b, options.state))),
},
[`${prefix}_solo`]: {
name: `${title}: solo`,
options: [opt.busses(inst, 'busses', many, prefix), opt.state()],
callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'solo', b, options.state))),
},
[`${prefix}_fader_set`]: {
name: `${title}: set fader`,
description: FADE_NOTE,
options: [opt.buss(inst, prefix, 'buss', one), opt.level(), opt.duration(), opt.curve()],
callback: async ({ options }) => inst.setFader('buss', n(options.buss), n(options.level), ...fadeArgs(options)),
},
[`${prefix}_fader_adjust`]: {
name: `${title}: adjust fader (relative)`,
description: `For rotary encoders. ${ADJUST_NOTE}`,
options: [opt.buss(inst, prefix, 'buss', one), opt.delta()],
callback: async ({ options }) => inst.adjustFader('buss', n(options.buss), n(options.delta)),
},
})
return quiet(inst, { return quiet(inst, {
...getParamActions(inst), ...getParamActions(inst),
input_mute: { input_mute: {
@@ -221,24 +253,27 @@ export function getActionDefinitions(inst) {
callback: async ({ options }) => inst.setGroup(n(options.channel), n(options.buss), options.state), callback: async ({ options }) => inst.setGroup(n(options.channel), n(options.buss), options.state),
}, },
...bussActions('group', 'Group', 'Group', 'Groups'),
...bussActions('aux', 'Aux master', 'Aux master', 'Aux masters'),
buss_mute: { buss_mute: {
name: 'Buss: mute', name: 'Any buss (aux or group): mute',
description: 'Mute, unmute or toggle one or more busses, aux and group alike (one list of all busses).',
options: [opt.busses(inst), opt.state()], options: [opt.busses(inst), opt.state()],
callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'mute', b, options.state))), callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'mute', b, options.state))),
}, },
buss_solo: { buss_solo: {
name: 'Buss: solo', name: 'Any buss (aux or group): solo',
options: [opt.busses(inst), opt.state()], options: [opt.busses(inst), opt.state()],
callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'solo', b, options.state))), callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'solo', b, options.state))),
}, },
buss_fader_set: { buss_fader_set: {
name: 'Buss: set fader', name: 'Any buss (aux or group): set fader',
description: FADE_NOTE, description: FADE_NOTE,
options: [opt.buss(inst), opt.level(), opt.duration(), opt.curve()], options: [opt.buss(inst), opt.level(), opt.duration(), opt.curve()],
callback: async ({ options }) => inst.setFader('buss', n(options.buss), n(options.level), ...fadeArgs(options)), callback: async ({ options }) => inst.setFader('buss', n(options.buss), n(options.level), ...fadeArgs(options)),
}, },
buss_fader_adjust: { buss_fader_adjust: {
name: 'Buss: adjust fader (relative)', name: 'Any buss (aux or group): adjust fader (relative)',
description: `For rotary encoders. ${ADJUST_NOTE}`, description: `For rotary encoders. ${ADJUST_NOTE}`,
options: [opt.buss(inst), opt.delta()], options: [opt.buss(inst), opt.delta()],
callback: async ({ options }) => inst.adjustFader('buss', n(options.buss), n(options.delta)), callback: async ({ options }) => inst.adjustFader('buss', n(options.buss), n(options.delta)),
+30 -15
View File
@@ -129,16 +129,30 @@ export function getFeedbackDefinitions(inst) {
return at > 0 && Date.now() - at <= n(o.hold ?? holdOption.default) * 1000 return at > 0 && Date.now() - at <= n(o.hold ?? holdOption.default) * 1000
} }
const meterText = (m, gr) => (gr ? String(meterGrDb(m?.value)) : fmtMeterLevel(m?.value)) const meterText = (m, gr) => (gr ? String(meterGrDb(m?.value)) : fmtMeterLevel(m?.value))
const outputMeters = (ent, id, entOption, who) => ({ const outputMeters = (prefix, ent, id, entOption, who) => ({
[`${ent}_signal`]: bool(`${who} meter: signal present`, GREEN, [entOption(), signalOption], outputKey(ent, id), ({ options }) => levelSignal(outputMeterOf(ent, id, options), options)), [`${prefix}_signal`]: bool(`${who} meter: signal present`, GREEN, [entOption(), signalOption], outputKey(ent, id), ({ options }) => levelSignal(outputMeterOf(ent, id, options), options)),
[`${ent}_peak`]: bool(`${who} meter: peak hold (dBFS, approx.)`, RED, [entOption(), peakOption, holdOption], outputKey(ent, id), ({ options }) => { [`${prefix}_peak`]: bool(`${who} meter: peak hold (dBFS, approx.)`, RED, [entOption(), peakOption, holdOption], outputKey(ent, id), ({ options }) => {
outputMeterOf(ent, id, options) outputMeterOf(ent, id, options)
return levelPeak(outputMeterId(ent, n(options[id])), options) return levelPeak(outputMeterId(ent, n(options[id])), options)
}), }),
[`${ent}_meter_value`]: value(`${who} meter: level text (dBFS, approx.)`, [entOption()], outputKey(ent, id), ({ options }) => fmtMeterLevel(outputMeterOf(ent, id, options)?.value)), [`${prefix}_meter_value`]: value(`${who} meter: level text (dBFS, approx.)`, [entOption()], outputKey(ent, id), ({ options }) => fmtMeterLevel(outputMeterOf(ent, id, options)?.value)),
[`${ent}_meter_pos`]: value(`${who} meter: position (0-100 over -60..0 dBFS, for gauges)`, [entOption()], outputKey(ent, id), ({ options }) => meterPosition(outputMeterOf(ent, id, options)?.value, 'level')), [`${prefix}_meter_pos`]: value(`${who} meter: position (0-100 over -60..0 dBFS, for gauges)`, [entOption()], outputKey(ent, id), ({ options }) => meterPosition(outputMeterOf(ent, id, options)?.value, 'level')),
}) })
const entityKey = (ent, id) => (o) => watchKey.entity(ent, n(o[id])) const entityKey = (ent, id) => (o) => watchKey.entity(ent, n(o[id]))
/**
* Muted / solo / fader level text / fader position for a buss. `prefix` is the feedback id prefix: 'buss' lists every
* buss, 'group' and 'aux' only busses of that type (the dropdown is the only difference; the watch key is the buss's).
*/
const bussFeedbacks = (prefix, kind, title, label) => {
const one = () => opt.buss(inst, kind, 'buss', label)
const key = entityKey('buss', 'buss')
return {
[`${prefix}_muted`]: bool(`${title}: muted`, RED, [one()], key, ({ options }) => s.getMute('buss', n(options.buss)) === true),
[`${prefix}_solo`]: bool(`${title}: solo on`, AMBER, [one()], key, ({ options }) => s.getSolo('buss', n(options.buss)) === true),
[`${prefix}_fader_value`]: value(`${title}: fader level (dB text)`, [one()], key, ({ options }) => fmtDb(s.getFader('buss', n(options.buss))) || '--'),
[`${prefix}_fader_pos`]: value(`${title}: fader position (0-100, for gauges)`, [one()], key, ({ options }) => faderPosition(s.getFader('buss', n(options.buss)))),
}
}
const sendKey = (o) => watchKey.send(n(o.channel), n(o.buss)) const sendKey = (o) => watchKey.send(n(o.channel), n(o.buss))
const msendKey = (o) => watchKey.msend(n(o.matrixIn), n(o.matrixOut)) const msendKey = (o) => watchKey.msend(n(o.matrixIn), n(o.matrixOut))
const msendOf = (o) => { const msendOf = (o) => {
@@ -201,8 +215,9 @@ export function getFeedbackDefinitions(inst) {
inst.ensureGroup(n(options.channel), n(options.buss)) inst.ensureGroup(n(options.channel), n(options.buss))
return s.getGroup(n(options.channel), n(options.buss)) === true return s.getGroup(n(options.channel), n(options.buss)) === true
}), }),
buss_muted: bool('Buss: muted', RED, [opt.buss(inst)], entityKey('buss', 'buss'), ({ options }) => s.getMute('buss', n(options.buss)) === true), ...bussFeedbacks('buss', 'all', 'Any buss (aux or group)', 'Buss'),
buss_solo: bool('Buss: solo on', AMBER, [opt.buss(inst)], entityKey('buss', 'buss'), ({ options }) => s.getSolo('buss', n(options.buss)) === true), ...bussFeedbacks('group', 'group', 'Group', 'Group'),
...bussFeedbacks('aux', 'aux', 'Aux master', 'Aux master'),
matrix_muted: bool('Matrix output: muted', RED, [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => s.getMute('matrix', n(options.matrix)) === true), matrix_muted: bool('Matrix output: muted', RED, [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => s.getMute('matrix', n(options.matrix)) === true),
cg_muted: bool('Control group: muted', RED, [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => s.getMute('cg', n(options.cg)) === true), cg_muted: bool('Control group: muted', RED, [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => s.getMute('cg', n(options.cg)) === true),
matrix_solo: bool('Matrix output: solo on', AMBER, [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => s.getSolo('matrix', n(options.matrix)) === true), matrix_solo: bool('Matrix output: solo on', AMBER, [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => s.getSolo('matrix', n(options.matrix)) === true),
@@ -270,8 +285,10 @@ export function getFeedbackDefinitions(inst) {
return !isGr(options) && levelPeak(meterId(n(options.channel), 'level'), options) return !isGr(options) && levelPeak(meterId(n(options.channel), 'level'), options)
}, },
), ),
...outputMeters('buss', 'buss', () => opt.buss(inst), 'Buss'), ...outputMeters('buss', 'buss', 'buss', () => opt.buss(inst), 'Any buss (aux or group)'),
...outputMeters('matrix', 'matrix', () => opt.matrix(inst), 'Matrix output'), ...outputMeters('group', 'buss', 'buss', () => opt.buss(inst, 'group', 'buss', 'Group'), 'Group'),
...outputMeters('aux', 'buss', 'buss', () => opt.buss(inst, 'aux', 'buss', 'Aux master'), 'Aux master'),
...outputMeters('matrix', 'matrix', 'matrix', () => opt.matrix(inst), 'Matrix output'),
input_fader_value: value('Input: fader level (dB text)', [opt.channel(inst)], entityKey('input', 'channel'), ({ options }) => fmtDb(s.getFader('input', n(options.channel))) || '--'), input_fader_value: value('Input: fader level (dB text)', [opt.channel(inst)], entityKey('input', 'channel'), ({ options }) => fmtDb(s.getFader('input', n(options.channel))) || '--'),
input_pan_value: value('Input: pan (text)', [opt.channel(inst)], entityKey('input', 'channel'), ({ options }) => fmtPan(s.getPan(n(options.channel))) || '--'), input_pan_value: value('Input: pan (text)', [opt.channel(inst)], entityKey('input', 'channel'), ({ options }) => fmtPan(s.getPan(n(options.channel))) || '--'),
@@ -284,7 +301,6 @@ export function getFeedbackDefinitions(inst) {
const on = s.getSend(n(options.channel), n(options.buss)).on const on = s.getSend(n(options.channel), n(options.buss)).on
return on === undefined ? '--' : on ? 'On' : 'Off' return on === undefined ? '--' : on ? 'On' : 'Off'
}), }),
buss_fader_value: value('Buss: fader level (dB text)', [opt.buss(inst)], entityKey('buss', 'buss'), ({ options }) => fmtDb(s.getFader('buss', n(options.buss))) || '--'),
matrix_fader_value: value('Matrix output: fader level (dB text)', [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => fmtDb(s.getFader('matrix', n(options.matrix))) || '--'), matrix_fader_value: value('Matrix output: fader level (dB text)', [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => fmtDb(s.getFader('matrix', n(options.matrix))) || '--'),
cg_fader_value: value('Control group: fader level (dB text)', [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => fmtDb(s.getFader('cg', n(options.cg))) || '--'), cg_fader_value: value('Control group: fader level (dB text)', [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => fmtDb(s.getFader('cg', n(options.cg))) || '--'),
send_pan_value: value('Aux send: pan (text)', [opt.channel(inst), opt.buss(inst, 'aux')], sendKey, ({ options }) => { send_pan_value: value('Aux send: pan (text)', [opt.channel(inst), opt.buss(inst, 'aux')], sendKey, ({ options }) => {
@@ -295,7 +311,6 @@ export function getFeedbackDefinitions(inst) {
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) // 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)))), 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)))), 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)))), 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 }) => { send_level_pos: value('Aux send: level position (0-100, for gauges)', [opt.channel(inst), opt.buss(inst, 'aux')], sendKey, ({ options }) => {
@@ -328,7 +343,7 @@ export function getFeedbackDefinitions(inst) {
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 (legacy image)', [opt.buss(inst)], entityKey('buss', 'buss'), (fb, image) => { buss_fader_bar: bar('Any buss (aux or group): 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 })
}), }),
@@ -392,14 +407,14 @@ export function watchKeyForEvent(ev) {
export function feedbackIdsForEvent(ev) { export function feedbackIdsForEvent(ev) {
switch (ev.kind) { switch (ev.kind) {
case 'mute': case 'mute':
return { input: ['input_muted'], buss: ['buss_muted'], matrix: ['matrix_muted'], cg: ['cg_muted'] }[ev.ent] || [] return { input: ['input_muted'], buss: ['buss_muted', 'group_muted', 'aux_muted'], matrix: ['matrix_muted'], cg: ['cg_muted'] }[ev.ent] || []
case 'solo': case 'solo':
return { input: ['input_solo'], buss: ['buss_solo'], matrix: ['matrix_solo'], cg: ['cg_solo'] }[ev.ent] || [] return { input: ['input_solo'], buss: ['buss_solo', 'group_solo', 'aux_solo'], matrix: ['matrix_solo'], cg: ['cg_solo'] }[ev.ent] || []
case 'fader': case 'fader':
return ( return (
{ {
input: ['input_fader_value', 'input_fader_pos', 'input_fader_bar', 'level_compare'], input: ['input_fader_value', 'input_fader_pos', 'input_fader_bar', 'level_compare'],
buss: ['buss_fader_value', 'buss_fader_pos', 'buss_fader_bar', 'level_compare'], buss: ['buss_fader_value', 'buss_fader_pos', 'buss_fader_bar', 'group_fader_value', 'group_fader_pos', 'aux_fader_value', 'aux_fader_pos', 'level_compare'],
matrix: ['matrix_fader_value', 'matrix_fader_pos', 'matrix_fader_bar', 'level_compare'], matrix: ['matrix_fader_value', 'matrix_fader_pos', 'matrix_fader_bar', 'level_compare'],
cg: ['cg_fader_value', 'cg_fader_pos', 'cg_fader_bar', 'level_compare'], cg: ['cg_fader_value', 'cg_fader_pos', 'cg_fader_bar', 'level_compare'],
}[ev.ent] || [] }[ev.ent] || []
+36 -28
View File
@@ -19,6 +19,12 @@ const ON_GREEN = 0x00a050 // a send is on / an input is assigned
const MUTED_RED = 0xb01010 // muted, peak const MUTED_RED = 0xb01010 // muted, peak
const SIGNAL_BG = 0x00501e // signal present on a meter button const SIGNAL_BG = 0x00501e // signal present on a meter button
/** The two kinds of buss, each with its own actions, feedbacks and preset sections (the console's aux or group side of a buss). */
const BUSS_KINDS = [
{ kind: 'group', prefix: 'group', section: 'groups', meterSection: 'meters_groups', title: 'Group', meterTitle: 'Group' },
{ kind: 'aux', prefix: 'aux', section: 'aux_masters', meterSection: 'meters_aux', title: 'Aux master', meterTitle: 'Aux' },
]
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 })
@@ -209,27 +215,27 @@ const METER_TRACK_AMOUNT = 20
} }
} }
// ---- Busses // ---- Groups and aux masters: per named buss of that type, a mute button and a fader knob
{ for (const { kind, prefix, section, title } of BUSS_KINDS) {
const muteIds = [] const muteIds = []
const knobIds = [] const knobIds = []
for (const n of s.named('buss')) { for (const n of s.namedBusses(kind)) {
const tag = `${n} · ${s.displayName('buss', n)} (${s.bussMode(n)})` const tag = `${n} · ${s.displayName('buss', 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[`${prefix}_mute_${n}`] = simple(`${title} mute: ${tag}`, nameVar('buss', n), [act(`${prefix}_mute`, { busses: [n], state: 'toggle' })], [fb(`${prefix}_muted`, { buss: n }, RED)])
presets[`buss_knob_${n}`] = layeredKnob({ presets[`${prefix}_knob_${n}`] = layeredKnob({
name: `Master fader (knob): ${tag}`, name: `${title} fader (knob): ${tag}`,
title: nameVar('buss', n), title: nameVar('buss', n),
press: [act('buss_mute', { busses: [n], state: 'toggle' })], press: [act(`${prefix}_mute`, { busses: [n], state: 'toggle' })],
adjust: ['buss_fader_adjust', { buss: n }], adjust: [`${prefix}_fader_adjust`, { buss: n }],
level: ['buss_fader_value', { buss: n }], level: [`${prefix}_fader_value`, { buss: n }],
pos: ['buss_fader_pos', { buss: n }], pos: [`${prefix}_fader_pos`, { buss: n }],
feedbacks: [tint('buss_muted', { buss: n }, MUTED_RED)], feedbacks: [tint(`${prefix}_muted`, { buss: n }, MUTED_RED)],
}) })
muteIds.push(`buss_mute_${n}`) muteIds.push(`${prefix}_mute_${n}`)
knobIds.push(`buss_knob_${n}`) knobIds.push(`${prefix}_knob_${n}`)
} }
if (muteIds.length) { if (muteIds.length) {
structure.push({ id: 'busses', name: 'Busses', definitions: [group('busses_mute', 'Master mute', muteIds), group('busses_knob', 'Master fader (knob)', knobIds)] }) structure.push({ id: section, name: `${title}s`, definitions: [group(`${section}_mute`, 'Mute', muteIds), group(`${section}_knob`, 'Fader (knob)', knobIds)] })
} }
} }
@@ -362,19 +368,21 @@ const METER_TRACK_AMOUNT = 20
// ---- Output meters: one layered meter per buss and per matrix output // ---- Output meters: one layered meter per buss and per matrix output
{ {
const bussIds = [] for (const { kind, prefix, meterSection, meterTitle } of BUSS_KINDS) {
for (const n of s.named('buss')) { const ids = []
presets[`buss_meter_${n}`] = layeredMeter({ for (const n of s.namedBusses(kind)) {
name: `Buss meter: ${n} · ${s.displayName('buss', n)} (${s.bussMode(n)})`, presets[`${prefix}_meter_${n}`] = layeredMeter({
title: nameVar('buss', n), name: `${meterTitle} meter: ${n} · ${s.displayName('buss', n)}`,
posFeedback: ['buss_meter_pos', { buss: n }], title: nameVar('buss', n),
signal: ['buss_signal', { buss: n, thresholdDb: -50 }], posFeedback: [`${prefix}_meter_pos`, { buss: n }],
peak: ['buss_peak', { buss: n, peakDb: -1, hold: 2 }], signal: [`${prefix}_signal`, { buss: n, thresholdDb: -50 }],
}) peak: [`${prefix}_peak`, { buss: n, peakDb: -1, hold: 2 }],
bussIds.push(`buss_meter_${n}`) })
} ids.push(`${prefix}_meter_${n}`)
if (bussIds.length) { }
structure.push({ id: 'buss_meters', name: 'Buss meters', description: 'Meters subscribe on the console when the button first renders.', definitions: [group('buss_meters_level', 'Level', bussIds)] }) if (ids.length) {
structure.push({ id: meterSection, name: `${meterTitle} meters`, description: 'Meters subscribe on the console when the button first renders.', definitions: [group(`${meterSection}_level`, 'Level', ids)] })
}
} }
const matrixIds = [] const matrixIds = []
for (const n of s.numbers('matrix')) { for (const n of s.numbers('matrix')) {
+4 -2
View File
@@ -417,11 +417,13 @@ export class State {
const entity = (ent, suffix) => () => this.numbers(ent).map((n) => ({ id: n, label: `${n} · ${this.displayName(ent, n)}${suffix ? suffix(n) : ''}` })) const entity = (ent, suffix) => () => this.numbers(ent).map((n) => ({ id: n, label: `${n} · ${this.displayName(ent, n)}${suffix ? suffix(n) : ''}` }))
const bussSuffix = (n) => ` (${this.bussMode(n)})` const bussSuffix = (n) => ` (${this.bussMode(n)})`
const busses = entity('buss', bussSuffix) const busses = entity('buss', bussSuffix)
const plainBusses = entity('buss')
return { return {
inputs: entity('input'), inputs: entity('input'),
busses, busses,
auxChoices: () => busses().filter((c) => this.bussMode(c.id) === 'aux'), // filtered by type, so the "(aux)" / "(group)" suffix would only repeat what the dropdown already says
groupChoices: () => busses().filter((c) => this.bussMode(c.id) === 'group'), auxChoices: () => plainBusses().filter((c) => this.bussMode(c.id) === 'aux'),
groupChoices: () => plainBusses().filter((c) => this.bussMode(c.id) === 'group'),
matrix: entity('matrix'), matrix: entity('matrix'),
matrixInputs: entity('minput'), matrixInputs: entity('minput'),
cgs: entity('cg'), cgs: entity('cg'),
+127 -9
View File
@@ -425,12 +425,12 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
check(entityOptions.length > 40 && entityOptions.every((o) => (o.type === 'dropdown' || o.type === 'multidropdown') && o.minChoicesForSearch === 0 && o.choices.every((c) => typeof c.id === 'number')), `all ${entityOptions.length} entity options are searchable dropdowns with numeric ids`) check(entityOptions.length > 40 && entityOptions.every((o) => (o.type === 'dropdown' || o.type === 'multidropdown') && o.minChoicesForSearch === 0 && o.choices.every((c) => typeof c.id === 'number')), `all ${entityOptions.length} entity options are searchable dropdowns with numeric ids`)
const levels = all.flatMap((d) => d.options).filter((o) => o.id === 'level') const levels = all.flatMap((d) => d.options).filter((o) => o.id === 'level')
const thresholds = all.flatMap((d) => d.options).filter((o) => o.id === 'threshold' && o.min === -150) const thresholds = all.flatMap((d) => d.options).filter((o) => o.id === 'threshold' && o.min === -150)
check(levels.length === 6 && levels.every((o) => o.type === 'number' && o.showMinAsNegativeInfinity === true && (o.min === -150 || o.min === -90)) && thresholds.every((o) => o.showMinAsNegativeInfinity === true), `level options use showMinAsNegativeInfinity (${levels.length} level + ${thresholds.length} threshold options)`) check(levels.length === 8 && 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') 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)) 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(adjusts.length === 8 && adjusts.every(([, a]) => a.description.includes('From \u2212\u221e the first step up jumps to \u221260 dB')), `all ${adjusts.length} level adjust actions say that the first step up from -inf jumps to -60 dB`)
check(ctx.actions.input_mute.options[0].choices[0].label === '1 · KICK In' && ctx.actions.send_on.options[1].choices.every((c) => S.bussMode(c.id) === 'aux'), 'action dropdowns show the live labels; send actions list aux busses only') check(ctx.actions.input_mute.options[0].choices[0].label === '1 · KICK In' && ctx.actions.send_on.options[1].choices.every((c) => S.bussMode(c.id) === 'aux'), 'action dropdowns show the live labels; send actions list aux busses only')
const idsExpected = ['input_mute', 'input_solo', 'input_fader_set', 'input_fader_adjust', 'input_pan_set', 'input_pan_adjust', 'input_name_set', 'send_on', 'send_level_set', 'send_level_adjust', 'send_pan_set', 'send_pan_adjust', 'group_assign', 'buss_mute', 'buss_solo', 'buss_fader_set', 'buss_fader_adjust', 'matrix_mute', 'matrix_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', 'fade_stop'] 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', 'fade_stop', 'group_mute', 'group_solo', 'group_fader_set', 'group_fader_adjust', 'aux_mute', 'aux_solo', 'aux_fader_set', 'aux_fader_adjust']
// per-channel switches and values, generated from the PARAMS table // per-channel switches and values, generated from the PARAMS table
for (const [param, p] of Object.entries(PARAMS)) { for (const [param, p] of Object.entries(PARAMS)) {
for (const ent of Object.keys(p.paths)) idsExpected.push(...(p.type === 'switch' ? [`${ent}_${param}`] : [`${ent}_${param}_set`, `${ent}_${param}_adjust`])) for (const ent of Object.keys(p.paths)) idsExpected.push(...(p.type === 'switch' ? [`${ent}_${param}`] : [`${ent}_${param}_set`, `${ent}_${param}_adjust`]))
@@ -438,6 +438,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', 'fade_running', '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', 'fade_running', '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 k of ['group', 'aux']) fbExpected.push(`${k}_muted`, `${k}_solo`, `${k}_fader_value`, `${k}_fader_pos`, `${k}_meter_value`, `${k}_meter_pos`, `${k}_signal`, `${k}_peak`)
fbExpected.push('buss_signal', 'buss_peak', 'buss_meter_value', 'buss_meter_pos', 'matrix_signal', 'matrix_peak', 'matrix_meter_value', 'matrix_meter_pos') fbExpected.push('buss_signal', 'buss_peak', 'buss_meter_value', 'buss_meter_pos', 'matrix_signal', 'matrix_peak', 'matrix_meter_value', 'matrix_meter_pos')
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('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', 'matrix_send_off', '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', 'matrix_send_off', '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')
@@ -456,6 +457,8 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
geq_on: ['geq_on'], geq_band_set: ['geq_band_value', 'geq_band_bar'], geq_band_adjust: ['geq_band_value', 'geq_band_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'], 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'], fade_stop: ['fade_running'], reload: ['connected'], relay_forget: ['relay_clients'], fade_stop: ['fade_running'],
group_mute: ['group_muted'], group_solo: ['group_solo'], group_fader_set: ['group_fader_value'], group_fader_adjust: ['group_fader_value'],
aux_mute: ['aux_muted'], aux_solo: ['aux_solo'], aux_fader_set: ['aux_fader_value'], aux_fader_adjust: ['aux_fader_value'],
input_name_set: [], // the name is the variable input_N_name input_name_set: [], // the name is the variable input_N_name
raw_osc: [], // free-form raw_osc: [], // free-form
} }
@@ -465,18 +468,18 @@ 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' } const GAUGE = { input_fader: 'input_fader_pos', send_level: 'send_level_pos', buss_fader: 'buss_fader_pos', group_fader: 'group_fader_pos', aux_fader: 'aux_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)) { for (const id of Object.keys(PARITY)) {
const g = GAUGE[id.replace(/_(set|adjust)$/, '')] const g = GAUGE[id.replace(/_(set|adjust)$/, '')]
if (g && /_(set|adjust)$/.test(id)) PARITY[id] = [...PARITY[id], g] 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))) const noGauge = Object.keys(ctx.actions).filter((id) => /^(input_fader|send_level|buss_fader|group_fader|aux_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(', ')})`) 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))
const lacksValue = levelActions.filter((id) => !(PARITY[id] || []).some((f) => /_value$/.test(f))) 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))) const lacksBar = levelActions.filter((id) => /(fader|send_level)/.test(id) && !/^(group|aux)_/.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(noFeedback.length === 0 && missing.length === 0 && lacksValue.length === 0 && lacksBar.length === 0, `every action has its feedback (${Object.keys(ctx.actions).length} actions; no feedback listed: ${noFeedback.join(', ') || 'none'}; unknown feedback: ${missing.join(', ') || 'none'}; level without value: ${lacksValue.join(', ') || 'none'}; fader/send level without bar: ${lacksBar.join(', ') || 'none'})`)
check(Object.values(ctx.feedbacks).filter((f) => f.type === 'advanced').every((f) => Array.isArray(f.affectedProperties)), 'advanced feedbacks declare affectedProperties') check(Object.values(ctx.feedbacks).filter((f) => f.type === 'advanced').every((f) => Array.isArray(f.affectedProperties)), 'advanced feedbacks declare affectedProperties')
} }
@@ -529,7 +532,7 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
const knobs = ids.filter((id) => /_knob_/.test(id)) const knobs = ids.filter((id) => /_knob_/.test(id))
check(knobs.length > 100 && knobs.every((id) => P[id].steps[0].rotate_left?.length && P[id].steps[0].rotate_right?.length), `all ${knobs.length} knob presets have rotate_left and rotate_right`) check(knobs.length > 100 && knobs.every((id) => P[id].steps[0].rotate_left?.length && P[id].steps[0].rotate_right?.length), `all ${knobs.length} knob presets have rotate_left and rotate_right`)
const sections = ctx.presetStructure.map((s) => s.id) const sections = ctx.presetStructure.map((s) => s.id)
check(['inputs', 'busses', 'cgs', 'matrix', 'snapshots', 'console', 'meters', '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)`) check(['inputs', 'groups', 'aux_masters', '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 // 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 msendSections = ctx.presetStructure.filter((x) => x.id.startsWith('msends_'))
const msendOn = ids.filter((id) => id.startsWith('msend_on_')) const msendOn = ids.filter((id) => id.startsWith('msend_on_'))
@@ -553,7 +556,7 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
const layered = ids.filter((id) => P[id].type === 'layered') const layered = ids.filter((id) => P[id].type === 'layered')
const knobsLayered = layered.filter((id) => /_knob_/.test(id)) const knobsLayered = layered.filter((id) => /_knob_/.test(id))
const meterPresets = layered.filter((id) => /^input_(meter|gate_gr|comp_gr)_/.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.filter((id) => /^(buss|matrix)_meter_/.test(id)).length === 32 && layered.length === knobsLayered.length + 180 + 32 && !ids.some((id) => /_(mute|solo|on)_/.test(id) && P[id].type === 'layered'), `every knob (${knobsLayered.length}) and meter (${meterPresets.length} input + 32 buss / matrix output) preset is layered; plain toggles stay simple`) check(knobsLayered.length === ids.filter((id) => /_knob_/.test(id)).length && meterPresets.length === 180 && layered.filter((id) => /^(group|aux|matrix)_meter_/.test(id)).length === 32 && layered.length === knobsLayered.length + 180 + 32 && !ids.some((id) => /_(mute|solo|on)_/.test(id) && P[id].type === 'layered'), `every knob (${knobsLayered.length}) and meter (${meterPresets.length} input + 32 buss / matrix output) preset is layered; plain toggles stay simple`)
const ELEMENT_TYPES = new Set(['group', 'composite', 'text', 'image', 'box', 'line', 'circle', 'gauge']) 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', 'trackAmount'] } 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', 'trackAmount'] }
const isExpr = (v) => !!v && typeof v === 'object' && v.isExpression === true && typeof v.value === 'string' const isExpr = (v) => !!v && typeof v === 'object' && v.isExpression === true && typeof v.value === 'string'
@@ -1527,9 +1530,12 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
await sleep(inst.timing.rebuildDebounceMs + 400) await sleep(inst.timing.rebuildDebounceMs + 400)
const label3 = ctx.actions.buss_mute.options[0].choices.find((c) => c.id === 3)?.label const label3 = ctx.actions.buss_mute.options[0].choices.find((c) => c.id === 3)?.label
check(wasAux && label3 === '3 · Group 3 (group)' && ctx.pushes.actions === JSON.parse(p0).actions + 1 && ctx.pushes.presets === JSON.parse(p0).presets + 1, `a changed buss type rebuilds and pushes once (${label3})`) check(wasAux && label3 === '3 · Group 3 (group)' && ctx.pushes.actions === JSON.parse(p0).actions + 1 && ctx.pushes.presets === JSON.parse(p0).presets + 1, `a changed buss type rebuilds and pushes once (${label3})`)
const optIds = (id, o) => ctx.actions[id].options.find((x) => x.id === o).choices.map((c) => c.id)
check(optIds('group_fader_set', 'buss').includes(3) && !optIds('aux_fader_set', 'buss').includes(3) && optIds('group_mute', 'busses').includes(3) && !optIds('aux_mute', 'busses').includes(3) && ctx.actions.group_fader_set.options[0].choices.find((c) => c.id === 3).label === '3 · Group 3' && ctx.feedbacks.group_muted.options[0].choices.some((c) => c.id === 3) && !ctx.feedbacks.aux_muted.options[0].choices.some((c) => c.id === 3), 'the buss moved from the aux dropdowns to the group dropdowns (actions and feedbacks)')
check(!ctx.presets.aux_mute_3 && !ctx.presets.aux_knob_3 && !ctx.presets.aux_meter_3 && ctx.presetStructure.find((x) => x.id === 'aux_masters').definitions[0].presets.length === 14, 'and its aux master presets are gone from "Aux masters" (it has no group-side name, so no group presets either)')
inst.onConsoleMessage(decode(encode('/Console/Aux_Outputs/types', types.map((i) => ({ i })))), { address: '127.0.0.1', port: P.mock }) inst.onConsoleMessage(decode(encode('/Console/Aux_Outputs/types', types.map((i) => ({ i })))), { address: '127.0.0.1', port: P.mock })
await sleep(inst.timing.rebuildDebounceMs + 400) await sleep(inst.timing.rebuildDebounceMs + 400)
check(ctx.actions.buss_mute.options[0].choices.find((c) => c.id === 3)?.label === '3 · TRACKS (aux)', 'and back again') check(ctx.actions.buss_mute.options[0].choices.find((c) => c.id === 3)?.label === '3 · TRACKS (aux)' && optIds('aux_fader_set', 'buss').includes(3) && !optIds('group_fader_set', 'buss').includes(3) && !!ctx.presets.aux_mute_3 && ctx.presetStructure.find((x) => x.id === 'aux_masters').definitions[0].presets.includes('aux_mute_3'), 'and back again (dropdowns and the "Aux masters" presets)')
// a changed snapshot list rebuilds // a changed snapshot list rebuilds
const p1 = ctx.pushes.actions const p1 = ctx.pushes.actions
@@ -1618,6 +1624,22 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
run1(lc) run1(lc)
check(inst.watchers.get('cg:2')?.has('inst-L'), 'level_compare registers under the entity kind it compares') check(inst.watchers.get('cg:2')?.has('inst-L'), 'level_compare registers under the entity kind it compares')
ctx.feedbacks.level_compare.unsubscribe(lc, {}) ctx.feedbacks.level_compare.unsubscribe(lc, {})
// the group and aux master feedbacks watch the buss's own key, like the buss_* ones
const bm = mk('buss_muted', { buss: 24 }, 'inst-BM')
const gm = mk('group_muted', { buss: 24 }, 'inst-GM')
const gf = mk('group_fader_pos', { buss: 24 }, 'inst-GF')
const gx = mk('group_fader_value', { buss: 25 }, 'inst-GX')
const ax = mk('aux_fader_value', { buss: 3 }, 'inst-AX')
const gp = mk('group_signal', { buss: 24, thresholdDb: -50 }, 'inst-GP')
for (const f of [bm, gm, gf, gx, ax, gp]) run1(f)
check(['inst-BM', 'inst-GM', 'inst-GF'].every((id) => inst.watchers.get('buss:24')?.has(id)) && inst.watchers.get('buss:25')?.has('inst-GX') && inst.watchers.get('buss:3')?.has('inst-AX') && inst.watchers.get(`meter:${outputMeterId('buss', 24)}`)?.has('inst-GP'), 'group_* and aux_* feedbacks register under the buss key (meters under the buss meter id)')
await sleep(150)
ctx.byId.length = 0
inst.onConsoleMessage(decode(encode('/Group_Outputs/24/fader', [-12.25])), { address: '127.0.0.1', port: P.mock })
await sleep(80)
const idsG = ctx.byId.flatMap(([, x]) => x)
check(idsG.includes('inst-BM') && idsG.includes('inst-GM') && idsG.includes('inst-GF') && !idsG.includes('inst-GX') && !idsG.includes('inst-AX') && !ctx.checks.some(([, t]) => /^(group|aux|buss)_/.test(String(t))), 'a group fader change re-checks only the instances watching that buss, by id and with no per-type check')
for (const f of [bm, gm, gf, gx, ax, gp]) ctx.feedbacks[f.feedbackId].unsubscribe(f, {})
// every option referenced by an isVisibleExpression must not become an expression // every option referenced by an isVisibleExpression must not become an expression
const bad = [] const bad = []
@@ -1873,6 +1895,102 @@ const idle = () => waitFor(() => inst.fades.size === 0, 3000)
} }
check(inst.fades.size === 0 && inst.fadeStarts.size === 0, 'no fade or fade start is left over after the fade tests') check(inst.fades.size === 0 && inst.fadeStarts.size === 0, 'no fade or fade start is left over after the fade tests')
// ---------------------------------------------------------------------------
// 16c. Group and aux master actions, feedbacks and presets
// ---------------------------------------------------------------------------
{
const GROUPS = [9, 10, 11, 12, 13, 18, 23, 24, 25]
const allIds = S.choices.busses().map((c) => c.id)
const AUXES = allIds.filter((id) => !GROUPS.includes(id))
const ids = (opts, id) => opts.find((o) => o.id === id).choices.map((c) => c.id)
const labels = (opts, id) => opts.find((o) => o.id === id).choices.map((c) => c.label)
check(S.choices.groupChoices().map((c) => c.id).join() === GROUPS.join() && AUXES.length === 16, `the mock has ${GROUPS.length} group busses and ${AUXES.length} aux busses`)
let ok = true
for (const [prefix, want] of [['group', GROUPS], ['aux', AUXES]]) {
for (const a of ['fader_set', 'fader_adjust']) ok = ok && ids(ctx.actions[`${prefix}_${a}`].options, 'buss').join() === want.join()
for (const a of ['mute', 'solo']) ok = ok && ids(ctx.actions[`${prefix}_${a}`].options, 'busses').join() === want.join() && ctx.actions[`${prefix}_${a}`].options[0].type === 'multidropdown'
for (const f of ['muted', 'solo', 'fader_value', 'fader_pos', 'meter_value', 'meter_pos', 'signal', 'peak']) ok = ok && ids(ctx.feedbacks[`${prefix}_${f}`].options, 'buss').join() === want.join()
}
check(ok, 'every group_* dropdown (actions and feedbacks) lists exactly the nine group busses, every aux_* one exactly the sixteen aux busses')
const gl = labels(ctx.actions.group_fader_set.options, 'buss')
const al = labels(ctx.actions.aux_fader_set.options, 'buss')
check(gl.includes('24 · Studio') && al.includes('4 · CLK-GD-MD') && !gl.concat(al, labels(ctx.actions.group_mute.options, 'busses'), labels(ctx.actions.aux_mute.options, 'busses')).some((l) => /\((aux|group)\)/.test(l)), 'their labels carry no "(group)" / "(aux)" suffix')
check(ids(ctx.actions.buss_fader_set.options, 'buss').length === 25 && labels(ctx.actions.buss_fader_set.options, 'buss').includes('24 · Studio (group)') && ids(ctx.actions.buss_mute.options, 'busses').length === 25, 'the buss_* dropdowns still list all 25 busses with the suffix')
check(ctx.actions.group_fader_set.options.map((o) => o.id).join() === 'buss,level,duration,curve' && ctx.actions.aux_fader_set.options.map((o) => o.id).join() === 'buss,level,duration,curve' && ctx.actions.group_fader_adjust.options.map((o) => o.id).join() === 'buss,delta' && ctx.actions.aux_mute.options.map((o) => o.id).join() === 'busses,state', 'option ids are the same as the buss_* ones (fade options on the set actions)')
check(ctx.actions.group_mute.name === 'Group: mute' && ctx.actions.group_solo.name === 'Group: solo' && ctx.actions.group_fader_set.name === 'Group: set fader' && ctx.actions.group_fader_adjust.name === 'Group: adjust fader (relative)' && ctx.actions.aux_mute.name === 'Aux master: mute' && ctx.actions.aux_solo.name === 'Aux master: solo' && ctx.actions.aux_fader_set.name === 'Aux master: set fader' && ctx.actions.aux_fader_adjust.name === 'Aux master: adjust fader (relative)' && ctx.actions.group_assign.name === 'Group: assign input on/off', 'action names: "Group: ..." and "Aux master: ..." (group_assign keeps its name)')
check(/fade time above 0/.test(ctx.actions.group_fader_set.description) && /fade time above 0/.test(ctx.actions.aux_fader_set.description), 'the two set actions describe the fade')
// the old ids stay, with names that sort apart
const oldActions = ['buss_mute', 'buss_solo', 'buss_fader_set', 'buss_fader_adjust']
const oldFeedbacks = ['buss_muted', 'buss_solo', 'buss_fader_value', 'buss_fader_pos', 'buss_fader_bar', 'buss_signal', 'buss_peak', 'buss_meter_value', 'buss_meter_pos']
check(oldActions.every((id) => ctx.actions[id]?.name.startsWith('Any buss (aux or group): ')) && oldFeedbacks.every((id) => ctx.feedbacks[id]?.name.startsWith('Any buss (aux or group)')), `the old buss_* ids still exist and their names start with "Any buss (aux or group)" (${oldActions.length} actions, ${oldFeedbacks.length} feedbacks)`)
check(ctx.feedbacks.group_muted.name === 'Group: muted' && ctx.feedbacks.aux_fader_pos.name === 'Aux master: fader position (0-100, for gauges)' && ctx.feedbacks.group_peak.name.startsWith('Group meter: ') && ctx.feedbacks.aux_signal.name.startsWith('Aux master meter: ') && !('group_fader_bar' in ctx.feedbacks) && !('aux_fader_bar' in ctx.feedbacks), 'feedback names, and no legacy image-bar variants for group / aux masters')
// writes: the group side for a group buss, the aux side for an aux
let r = await run('group_fader_set', { buss: 24, level: -7 })
expectWrite(r, '/Group_Outputs/24/fader', ',f', -7, 'group_fader_set')
r = await run('aux_fader_set', { buss: 1, level: -9 })
expectWrite(r, '/Aux_Outputs/1/fader', ',f', -9, 'aux_fader_set')
r = await run('group_mute', { busses: [9, 24], state: 'on' })
expectWrite(r, '/Group_Outputs/9/mute', ',f', 1, 'group_mute (first)')
expectWrite(r, '/Group_Outputs/24/mute', ',f', 1, 'group_mute (second)')
r = await run('aux_mute', { busses: [3, 4], state: 'on' })
expectWrite(r, '/Aux_Outputs/3/mute', ',f', 1, 'aux_mute (first)')
expectWrite(r, '/Aux_Outputs/4/mute', ',f', 1, 'aux_mute (second)')
r = await run('group_solo', { busses: [24], state: 'on' })
expectWrite(r, '/Group_Outputs/24/solo', ',f', 1, 'group_solo')
r = await run('aux_solo', { busses: [3], state: 'off' })
expectWrite(r, '/Aux_Outputs/3/solo', ',f', 0, 'aux_solo off')
r = await run('group_mute', { busses: [9], state: 'toggle' })
expectWrite(r, '/Group_Outputs/9/mute', ',f', 0, 'group_mute toggle (from muted)')
await run('group_fader_set', { buss: 23, level: -20 })
r = await run('group_fader_adjust', { buss: 23, delta: 3 })
expectWrite(r, '/Group_Outputs/23/fader', ',f', -17, 'group_fader_adjust')
await run('aux_fader_set', { buss: 2, level: -20 })
r = await run('aux_fader_adjust', { buss: 2, delta: -2.5 })
expectWrite(r, '/Aux_Outputs/2/fader', ',f', -22.5, 'aux_fader_adjust')
// feedbacks read the same state as the buss_* ones
check(cb('group_muted', { buss: 24 }) === true && cb('buss_muted', { buss: 24 }) === true && cb('aux_muted', { buss: 3 }) === true && cb('aux_muted', { buss: 1 }) === false && cb('group_solo', { buss: 24 }) === true && cb('aux_solo', { buss: 3 }) === false && cb('group_fader_value', { buss: 24 }) === '-7' && cb('aux_fader_value', { buss: 1 }) === '-9' && near(cb('group_fader_pos', { buss: 24 }), cb('buss_fader_pos', { buss: 24 })) && near(cb('aux_fader_pos', { buss: 2 }), faderPosition(-22.5)), 'group_* / aux_* feedbacks follow the buss state (muted, solo, fader text and position)')
// fades through the same engine
const mark = sent.length
await go('group_fader_set', { buss: 25, level: -40 })
const m2 = sent.length
await go('group_fader_set', { buss: 25, level: -10, duration: 300, curve: 'db' })
check(inst.fades.size === 1, 'a group fader fade is running')
check(await idle(), 'the group fade finishes')
const gw = writesTo('/Group_Outputs/25/fader', m2).map((x) => x.db)
check(gw.length === 6 && gw.at(-1) === -10 && gw.every((v, k) => near(v, -40 + 5 * (k + 1), 0.01)), `group_fader_set fades on /Group_Outputs/25/fader (${gw.map((v) => Math.round(v * 100) / 100).join(', ')})`)
await go('aux_fader_set', { buss: 5, level: -40 })
const m3 = sent.length
await go('aux_fader_set', { buss: 5, level: -10, duration: 200, curve: 'db' })
check(await idle(), 'the aux master fade finishes')
const aw = writesTo('/Aux_Outputs/5/fader', m3).map((x) => x.db)
check(aw.length === 4 && aw.at(-1) === -10 && !sent.slice(m3).some((m) => m.address.startsWith('/Group_Outputs/5/')), `aux_fader_set fades on /Aux_Outputs/5/fader (${aw.map((v) => Math.round(v * 100) / 100).join(', ')})`)
void mark
// presets: Groups and Aux masters sections, group / aux meter sections, no "Busses" section
const P2 = ctx.presets
const secs = ctx.presetStructure
const sec = (id) => secs.find((x) => x.id === id)
const idsOf = (section) => section.definitions.flatMap((d) => d.presets)
const namedGroups = S.namedBusses('group')
const namedAux = S.namedBusses('aux')
check(namedGroups.join() === GROUPS.join() && namedAux.length === 15 && sec('groups')?.name === 'Groups' && sec('aux_masters')?.name === 'Aux masters' && !sec('busses') && !sec('buss_meters'), `sections "Groups" (${namedGroups.length} named group busses) and "Aux masters" (${namedAux.length} named aux busses) replace "Busses"`)
check(idsOf(sec('groups')).length === 2 * namedGroups.length && idsOf(sec('aux_masters')).length === 2 * namedAux.length && sec('groups').definitions.map((d) => d.name).join() === 'Mute,Fader (knob)' && sec('aux_masters').definitions.map((d) => d.name).join() === 'Mute,Fader (knob)', 'each has a Mute and a Fader (knob) preset per named buss of that type')
const gMute = P2.group_mute_24
const gKnob = P2.group_knob_24
const aMute = P2.aux_mute_4
const aKnob = P2.aux_knob_4
check(gMute.name === 'Group mute: 24 · Studio' && gMute.steps[0].down[0].actionId === 'group_mute' && JSON.stringify(gMute.steps[0].down[0].options) === JSON.stringify({ busses: [24], state: 'toggle' }) && gMute.feedbacks[0].feedbackId === 'group_muted' && gMute.style.text === '$(S31:buss_24_name)', 'group mute preset uses group_mute / group_muted')
check(gKnob.type === 'layered' && gKnob.name === 'Group fader (knob): 24 · Studio' && gKnob.steps[0].rotate_right[0].actionId === 'group_fader_adjust' && gKnob.steps[0].down[0].actionId === 'group_mute' && gKnob.feedbacks[0].feedbackId === 'group_muted' && gKnob.localVariables.map((v) => `${v.variableName}=${v.feedbackId}`).join() === 'level=group_fader_value,pos=group_fader_pos', 'group fader knob preset uses group_fader_adjust / group_fader_value / group_fader_pos / group_muted')
check(aMute.name === 'Aux master mute: 4 · CLK-GD-MD' && aMute.steps[0].down[0].actionId === 'aux_mute' && aMute.feedbacks[0].feedbackId === 'aux_muted' && aKnob.steps[0].rotate_left[0].actionId === 'aux_fader_adjust' && aKnob.localVariables.map((v) => v.feedbackId).join() === 'aux_fader_value,aux_fader_pos' && aKnob.feedbacks[0].feedbackId === 'aux_muted', 'aux master mute and knob presets use aux_*')
check(!Object.keys(P2).some((id) => /^buss_(mute|knob|meter)_/.test(id)) && !JSON.stringify(P2).includes('"buss_') , 'no preset refers to a buss_* action or feedback any more')
const gMeters = sec('meters_groups')
const aMeters = sec('meters_aux')
check(gMeters?.name === 'Group meters' && aMeters?.name === 'Aux meters' && idsOf(gMeters).length === 9 && idsOf(aMeters).length === 15 && P2.group_meter_24.localVariables.some((v) => v.feedbackId === 'group_meter_pos') && P2.group_meter_24.feedbacks.map((f) => f.feedbackId).join() === 'group_signal,group_peak' && P2.aux_meter_4.feedbacks.map((f) => f.feedbackId).join() === 'aux_signal,aux_peak' && !sec('buss_meters'), 'group meter and aux meter sections (9 and 15 meters) use group_* / aux_* meter feedbacks')
check(idsOf(gMeters).concat(idsOf(aMeters)).every((id) => P2[id].type === 'layered'), 'the group and aux meter presets are layered gauges')
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 17. A slow console (every answer and echo 150 ms late): bind failures, encoder clicks, lazy loads // 17. A slow console (every answer and echo 150 ms late): bind failures, encoder clicks, lazy loads
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------