diff --git a/companion/HELP.md b/companion/HELP.md index 2a6a703..6fd1257 100644 --- a/companion/HELP.md +++ b/companion/HELP.md @@ -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, 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 @@ -88,7 +91,9 @@ is not known yet. | 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 | | 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 | | Control groups | mute, solo, fader set/adjust | | 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 never verified. -**Fades.** Every *set level* action (input, buss, matrix output and control group faders, aux send 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 +**Fades.** Every *set level* action (input, group, aux master, any-buss, matrix output and control group faders, aux send +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 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 @@ -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 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 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 - 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 - 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 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 `geq_band_db`. * 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 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 | | --- | --- | --- | -| `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) | | `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 -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: 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 | -| 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) | | 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 | @@ -201,7 +211,7 @@ buttons. | Graphic EQs | One **In/out** toggle per graphic EQ, green when in | | Snapshots | **Fire** per snapshot (green when current), next, previous, current (display) | | Input meters | Level, gate gain reduction and compressor gain reduction buttons per input, 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 | 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 | | --- | --- | -| `input_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_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_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`, `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`, `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`, `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 busses, group busses (the address follows the buss's current aux / group side) and matrix outputs; they are level meters. diff --git a/src/definitions/actions.js b/src/definitions/actions.js index de414c6..24cd5a4 100644 --- a/src/definitions/actions.js +++ b/src/definitions/actions.js @@ -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() 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()), 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()), @@ -147,6 +149,36 @@ function quiet(inst, defs) { export function getActionDefinitions(inst) { const each = (list, fn) => Promise.all(numbers(list).map(fn)) 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, { ...getParamActions(inst), input_mute: { @@ -221,24 +253,27 @@ export function getActionDefinitions(inst) { 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: { - 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()], callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'mute', b, options.state))), }, buss_solo: { - name: 'Buss: solo', + name: 'Any buss (aux or group): solo', options: [opt.busses(inst), opt.state()], callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'solo', b, options.state))), }, buss_fader_set: { - name: 'Buss: set fader', + name: 'Any buss (aux or group): set fader', description: FADE_NOTE, options: [opt.buss(inst), opt.level(), opt.duration(), opt.curve()], callback: async ({ options }) => inst.setFader('buss', n(options.buss), n(options.level), ...fadeArgs(options)), }, buss_fader_adjust: { - name: 'Buss: adjust fader (relative)', + name: 'Any buss (aux or group): adjust fader (relative)', description: `For rotary encoders. ${ADJUST_NOTE}`, options: [opt.buss(inst), opt.delta()], callback: async ({ options }) => inst.adjustFader('buss', n(options.buss), n(options.delta)), diff --git a/src/definitions/feedbacks.js b/src/definitions/feedbacks.js index 0532a9e..a3c45c0 100644 --- a/src/definitions/feedbacks.js +++ b/src/definitions/feedbacks.js @@ -129,16 +129,30 @@ export function getFeedbackDefinitions(inst) { 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 outputMeters = (ent, id, entOption, who) => ({ - [`${ent}_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 }) => { + const outputMeters = (prefix, ent, id, entOption, who) => ({ + [`${prefix}_signal`]: bool(`${who} meter: signal present`, GREEN, [entOption(), signalOption], outputKey(ent, id), ({ options }) => levelSignal(outputMeterOf(ent, id, options), options)), + [`${prefix}_peak`]: bool(`${who} meter: peak hold (dBFS, approx.)`, RED, [entOption(), peakOption, holdOption], outputKey(ent, id), ({ options }) => { outputMeterOf(ent, 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)), - [`${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_value`]: value(`${who} meter: level text (dBFS, approx.)`, [entOption()], outputKey(ent, id), ({ options }) => fmtMeterLevel(outputMeterOf(ent, id, options)?.value)), + [`${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])) + /** + * 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 msendKey = (o) => watchKey.msend(n(o.matrixIn), n(o.matrixOut)) const msendOf = (o) => { @@ -201,8 +215,9 @@ export function getFeedbackDefinitions(inst) { inst.ensureGroup(n(options.channel), n(options.buss)) 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), - buss_solo: bool('Buss: solo on', AMBER, [opt.buss(inst)], entityKey('buss', 'buss'), ({ options }) => s.getSolo('buss', n(options.buss)) === true), + ...bussFeedbacks('buss', 'all', 'Any buss (aux or group)', 'Buss'), + ...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), 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), @@ -270,8 +285,10 @@ export function getFeedbackDefinitions(inst) { return !isGr(options) && levelPeak(meterId(n(options.channel), 'level'), options) }, ), - ...outputMeters('buss', 'buss', () => opt.buss(inst), 'Buss'), - ...outputMeters('matrix', 'matrix', () => opt.matrix(inst), 'Matrix output'), + ...outputMeters('buss', 'buss', 'buss', () => opt.buss(inst), 'Any buss (aux or group)'), + ...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_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 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))) || '--'), 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 }) => { @@ -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)) || '--'), // 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 }) => { @@ -328,7 +343,7 @@ export function getFeedbackDefinitions(inst) { 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 }) }), - 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)) return v === undefined ? {} : horizontalBar({ ...image, fraction: levelFraction(v), color: fb.options.color }) }), @@ -392,14 +407,14 @@ export function watchKeyForEvent(ev) { export function feedbackIdsForEvent(ev) { switch (ev.kind) { 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': - 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': return ( { 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'], cg: ['cg_fader_value', 'cg_fader_pos', 'cg_fader_bar', 'level_compare'], }[ev.ent] || [] diff --git a/src/definitions/presets.js b/src/definitions/presets.js index 1eb7713..2feae1e 100644 --- a/src/definitions/presets.js +++ b/src/definitions/presets.js @@ -19,6 +19,12 @@ 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 +/** 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 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 knobIds = [] - for (const n of s.named('buss')) { - 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_knob_${n}`] = layeredKnob({ - name: `Master fader (knob): ${tag}`, + for (const n of s.namedBusses(kind)) { + const tag = `${n} · ${s.displayName('buss', n)}` + 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[`${prefix}_knob_${n}`] = layeredKnob({ + name: `${title} fader (knob): ${tag}`, title: nameVar('buss', n), - press: [act('buss_mute', { busses: [n], state: 'toggle' })], - adjust: ['buss_fader_adjust', { buss: n }], - level: ['buss_fader_value', { buss: n }], - pos: ['buss_fader_pos', { buss: n }], - feedbacks: [tint('buss_muted', { buss: n }, MUTED_RED)], + press: [act(`${prefix}_mute`, { busses: [n], state: 'toggle' })], + adjust: [`${prefix}_fader_adjust`, { buss: n }], + level: [`${prefix}_fader_value`, { buss: n }], + pos: [`${prefix}_fader_pos`, { buss: n }], + feedbacks: [tint(`${prefix}_muted`, { buss: n }, MUTED_RED)], }) - muteIds.push(`buss_mute_${n}`) - knobIds.push(`buss_knob_${n}`) + muteIds.push(`${prefix}_mute_${n}`) + knobIds.push(`${prefix}_knob_${n}`) } 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 { - const bussIds = [] - for (const n of s.named('buss')) { - presets[`buss_meter_${n}`] = layeredMeter({ - name: `Buss meter: ${n} · ${s.displayName('buss', n)} (${s.bussMode(n)})`, - title: nameVar('buss', n), - posFeedback: ['buss_meter_pos', { buss: n }], - signal: ['buss_signal', { buss: n, thresholdDb: -50 }], - peak: ['buss_peak', { buss: n, peakDb: -1, hold: 2 }], - }) - bussIds.push(`buss_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)] }) + for (const { kind, prefix, meterSection, meterTitle } of BUSS_KINDS) { + const ids = [] + for (const n of s.namedBusses(kind)) { + presets[`${prefix}_meter_${n}`] = layeredMeter({ + name: `${meterTitle} meter: ${n} · ${s.displayName('buss', n)}`, + title: nameVar('buss', n), + posFeedback: [`${prefix}_meter_pos`, { buss: n }], + signal: [`${prefix}_signal`, { buss: n, thresholdDb: -50 }], + peak: [`${prefix}_peak`, { buss: n, peakDb: -1, hold: 2 }], + }) + ids.push(`${prefix}_meter_${n}`) + } + 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 = [] for (const n of s.numbers('matrix')) { diff --git a/src/state.js b/src/state.js index bd4319c..1e0fab8 100644 --- a/src/state.js +++ b/src/state.js @@ -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 bussSuffix = (n) => ` (${this.bussMode(n)})` const busses = entity('buss', bussSuffix) + const plainBusses = entity('buss') return { inputs: entity('input'), busses, - auxChoices: () => busses().filter((c) => this.bussMode(c.id) === 'aux'), - groupChoices: () => busses().filter((c) => this.bussMode(c.id) === 'group'), + // filtered by type, so the "(aux)" / "(group)" suffix would only repeat what the dropdown already says + auxChoices: () => plainBusses().filter((c) => this.bussMode(c.id) === 'aux'), + groupChoices: () => plainBusses().filter((c) => this.bussMode(c.id) === 'group'), matrix: entity('matrix'), matrixInputs: entity('minput'), cgs: entity('cg'), diff --git a/test/selftest.js b/test/selftest.js index f71fb8d..64d215e 100644 --- a/test/selftest.js +++ b/test/selftest.js @@ -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`) const levels = all.flatMap((d) => d.options).filter((o) => o.id === 'level') const thresholds = all.flatMap((d) => d.options).filter((o) => o.id === 'threshold' && o.min === -150) - check(levels.length === 6 && levels.every((o) => o.type === 'number' && o.showMinAsNegativeInfinity === true && (o.min === -150 || 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') 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') - 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 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`])) @@ -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(!('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'] + 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('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') @@ -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'], 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'], + 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 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`] } } - 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)) { 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))) + 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(', ')})`) const noFeedback = Object.keys(ctx.actions).filter((id) => !(id in PARITY)) const missing = Object.entries(PARITY).filter(([id, fbs]) => id in ctx.actions && fbs.some((f) => !ctx.feedbacks[f])).map(([id]) => id) const levelActions = Object.keys(ctx.actions).filter((id) => /_(set|adjust)$/.test(id) && /(fader|level|pan|band|trim|gain|freq)/.test(id)) const lacksValue = levelActions.filter((id) => !(PARITY[id] || []).some((f) => /_value$/.test(f))) - const lacksBar = levelActions.filter((id) => /(fader|send_level)/.test(id) && !(PARITY[id] || []).some((f) => /_bar$/.test(f))) + 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(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)) check(knobs.length > 100 && knobs.every((id) => P[id].steps[0].rotate_left?.length && P[id].steps[0].rotate_right?.length), `all ${knobs.length} knob presets have rotate_left and rotate_right`) const sections = ctx.presetStructure.map((s) => s.id) - check(['inputs', 'busses', 'cgs', 'matrix', 'snapshots', 'console', 'meters', '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 const msendSections = ctx.presetStructure.filter((x) => x.id.startsWith('msends_')) 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 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.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 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' @@ -1527,9 +1530,12 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb await sleep(inst.timing.rebuildDebounceMs + 400) 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})`) + 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 }) 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 const p1 = ctx.pushes.actions @@ -1618,6 +1624,22 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb run1(lc) check(inst.watchers.get('cg:2')?.has('inst-L'), 'level_compare registers under the entity kind it compares') 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 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') +// --------------------------------------------------------------------------- +// 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 // ---------------------------------------------------------------------------