Name matrix inputs, fix the meter scale, add output meters
Found on the live S31 (read-only, plus meter subscriptions):
- Matrix inputs are named at /Matrix_Inputs/N/Channel_Input/name (an
address missing from DiGiCo's 2014 list). Their names now appear in
every matrix-send dropdown, preset and the new matrix_in_N_name
variables, falling back to "Matrix In N" when empty. Matrix inputs also
get trim and phase.
- Console names are space-padded where the surface wraps them; every name
is now trimmed and its whitespace runs collapsed ("LEAD V RESI").
- Meter values are inverted: 127 is silence and lower is louder, about
1 dB per step below full scale (silent channels read 127, the room mic
36-48). The module treated 127 as peak. Level meters now read dBFS
(approximate), gauges span -60..0 dBFS, signal and peak thresholds are
in dBFS; gain reduction reads dB of reduction, 127 meaning none.
- Bus and matrix outputs have meters (/Aux_Outputs, /Group_Outputs and
/Matrix_Outputs/N/Output/meter): new signal, peak, value and gauge
feedbacks and "Buss meters" / "Matrix meters" presets.
3002 presets, 2.7 MB. npm test: 425 checks pass on Node 25 and on
Companion's Node 22. Deployed and checked on the booth Companion 5.1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+44
-23
@@ -6,7 +6,7 @@ This connection does two things:
|
|||||||
matrix outputs, control groups, graphic EQs, snapshots) and builds its dropdowns, variables and
|
matrix outputs, control groups, graphic EQs, snapshots) and builds its dropdowns, variables and
|
||||||
presets from the live names. Faders, mutes, solos, pans, aux sends, group assigns, matrix sends,
|
presets from the live names. Faders, mutes, solos, pans, aux sends, group assigns, matrix sends,
|
||||||
control groups, graphic EQs, per-channel switches (phantom, EQ in, filters, inserts ...) and
|
control groups, graphic EQs, per-channel switches (phantom, EQ in, filters, inserts ...) and
|
||||||
snapshots are covered, with feedbacks, variables, rotary-encoder presets and input meters.
|
snapshots are covered, with feedbacks, variables, rotary-encoder presets and input and output meters.
|
||||||
2. **Relays the DiGiCo iPad app.** An S-Series console talks to exactly one iPad at a time. Companion
|
2. **Relays the DiGiCo iPad app.** An S-Series console talks to exactly one iPad at a time. Companion
|
||||||
takes that slot and the iPad apps connect to this computer instead; everything an app sends is
|
takes that slot and the iPad apps connect to this computer instead; everything an app sends is
|
||||||
forwarded to the console unchanged, and everything the console sends is copied to every app, so
|
forwarded to the console unchanged, and everything the console sends is copied to every app, so
|
||||||
@@ -60,10 +60,13 @@ and anything they change reaches this module too, so feedbacks follow moves made
|
|||||||
|
|
||||||
On every connect (and on the **Reload** action, and automatically after the console was silent for
|
On every connect (and on the **Reload** action, and automatically after the console was silent for
|
||||||
20 seconds and comes back) the module asks for the console name, the matrix, control group and graphic EQ
|
20 seconds and comes back) the module asks for the console name, the matrix, control group and graphic EQ
|
||||||
counts, the buss types and modes, every name, the session name, the snapshot list, and the fader, mute,
|
counts, the buss types and modes, every name (inputs, busses, matrix inputs and outputs, control groups, graphic EQs), the session name, the snapshot list, and the fader, mute,
|
||||||
solo and pan of everything that answered. The console does not answer the input and buss counts, so an
|
solo and pan of everything that answered. The console does not answer the input and buss counts, so an
|
||||||
entity exists when its **name** answered; entities that never answer are hidden from dropdowns, variables
|
entity exists when its **name** answered; entities that never answer are hidden from dropdowns, variables
|
||||||
and presets. Dropdown entries read `1 · KICK In`, `24 · Monitors (group)`, `3 · Message · cue 4`.
|
and presets. Dropdown entries read `1 · KICK In`, `24 · Monitors (group)`, `3 · Message · cue 4`. The console pads names
|
||||||
|
with trailing spaces and wraps long ones with runs of spaces; every name is trimmed and its whitespace collapsed to single
|
||||||
|
spaces when it arrives, so dropdowns, variables and presets show `Studio TB`, not `Studio TB `. A matrix input with
|
||||||
|
no name reads `Matrix In N`.
|
||||||
When a name, a buss mode or the snapshot list changes, the definitions are rebuilt a moment later.
|
When a name, a buss mode or the snapshot list changes, the definitions are rebuilt a moment later.
|
||||||
Sends, group assigns, matrix sends, graphic EQ gains and the per-channel switches are **not** bulk-loaded:
|
Sends, group assigns, matrix sends, graphic EQ gains and the per-channel switches are **not** bulk-loaded:
|
||||||
each is read the first time a button or action needs it.
|
each is read the first time a button or action needs it.
|
||||||
@@ -89,7 +92,7 @@ is not known yet.
|
|||||||
| 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) |
|
||||||
| Channel strip | per input and per buss: phantom, phase, pad, EQ in, compressor in, gate in, high-pass in, low-pass in, delay in, insert A/B in (on/off); trim (-40 to +40 dB), analog gain (0 to 60 dB), filter frequencies (20 to 20000 Hz) (set/adjust; trim also on matrix outputs) |
|
| Channel strip | per input and per buss: phantom, phase, pad, EQ in, compressor in, gate in, high-pass in, low-pass in, delay in, insert A/B in (on/off); trim (-40 to +40 dB), analog gain (0 to 60 dB), filter frequencies (20 to 20000 Hz) (set/adjust; trim also on matrix outputs and matrix inputs, phase also on matrix inputs) |
|
||||||
| Snapshots | fire (pick from the list or type an index), next, previous |
|
| Snapshots | fire (pick from the list or type an index), next, previous |
|
||||||
| Console | reload, raw OSC, relay: forget all iPads |
|
| Console | reload, raw OSC, relay: forget all iPads |
|
||||||
|
|
||||||
@@ -116,12 +119,12 @@ 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; buss muted / solo; matrix output muted /
|
||||||
solo; control group muted / solo; **matrix send on**; **graphic EQ in**; snapshot is current; fader level above/below
|
solo; control group muted / solo; **matrix send on**; **graphic EQ in**; snapshot is current; fader level above/below
|
||||||
a threshold; aux send level above/below; console link alive; relay has an iPad; every channel-strip switch
|
a threshold; aux send level above/below; console link alive; relay has an iPad; every channel-strip switch
|
||||||
(phantom, phase, pad, EQ in, compressor in, gate in, high-pass in, low-pass in, delay in, insert A/B in); and the meter
|
(phantom, phase, pad, EQ in, compressor in, gate in, high-pass in, low-pass in, delay in, insert A/B in; phase also
|
||||||
feedbacks below.
|
per matrix input); and the meter feedbacks (signal present, peak hold) for inputs, 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; buss / matrix / control group fader; **matrix send level**; **graphic EQ band gain**; trim, analog gain and
|
||||||
filter frequency per input and buss (trim also per matrix output); current snapshot name; input meter value. Levels
|
filter frequency per input and buss (trim also per matrix output and matrix input); current snapshot name; meter level
|
||||||
read `-inf` at or below -90 dB.
|
text (see **Meters**). Levels read `-inf` at or below -90 dB.
|
||||||
* 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
|
||||||
@@ -142,7 +145,7 @@ number you feed to a gauge through a button local variable:
|
|||||||
| `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`, `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` | 0-100 | input meter (0-127 scaled); has the *Meter* option level / gate / compressor reduction |
|
| `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 |
|
||||||
|
|
||||||
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).
|
||||||
@@ -158,7 +161,7 @@ To add a gauge to your own button in Companion 5.1:
|
|||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
`input_N_name|fader|mute|pan`, `buss_N_name|mode|stereo|fader|mute`, `matrix_N_name|fader|mute`,
|
`input_N_name|fader|mute|pan`, `buss_N_name|mode|stereo|fader|mute`, `matrix_N_name|fader|mute`, `matrix_in_N_name`,
|
||||||
`cg_N_name|fader|mute` (only for entities that exist), `snapshot_index|name|cue|count`, `console_name`,
|
`cg_N_name|fader|mute` (only for entities that exist), `snapshot_index|name|cue|count`, `console_name`,
|
||||||
`session_name`, `connected`, `relay_clients`, `relay_client_ips` and `last_change`. Use them as
|
`session_name`, `connected`, `relay_clients`, `relay_client_ips` and `last_change`. Use them as
|
||||||
`$(label:input_1_name)` with the connection's label. There are no per-send variables: use the value feedbacks.
|
`$(label:input_1_name)` with the connection's label. There are no per-send variables: use the value feedbacks.
|
||||||
@@ -178,25 +181,42 @@ buttons.
|
|||||||
| 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 |
|
||||||
| Matrix sends to *output name* | One section per matrix output: per matrix input an **On/off** button (green when on; shows `Matrix In N` and the level) and a **Level (knob)** (turn = send level, press = on/off, with a gauge) |
|
| Matrix sends to *output name* | One section per matrix output: per matrix input an **On/off** button (green when on; shows the matrix input's name and the level) and a **Level (knob)** (turn = send level, press = on/off, with a gauge) |
|
||||||
| Graphic EQs | One **In/out** toggle per graphic EQ, green when in |
|
| Graphic EQs | One **In/out** toggle per graphic EQ, green when in |
|
||||||
| Snapshots | **Fire** per snapshot (green when current), next, previous, current (display) |
|
| Snapshots | **Fire** per snapshot (green when current), next, previous, current (display) |
|
||||||
| Input meters | Level, gate gain reduction and compressor gain reduction buttons per input, 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 |
|
||||||
| Console | Reload, connection status, relay status |
|
| Console | Reload, connection status, relay status |
|
||||||
|
|
||||||
Busses and control groups with no name are unused slots and get no presets. If the preset list would exceed
|
Busses and control groups with no name are unused slots and get no presets. If the preset list would exceed
|
||||||
3000 entries or 4 MB, the per-input gate / compressor reduction meter presets are left out first, then the
|
3200 entries or 4 MB, the per-input gate / compressor reduction meter presets are left out first, then the
|
||||||
per-input aux send level knobs, and a warning is logged.
|
per-input aux send level knobs, and a warning is logged.
|
||||||
|
|
||||||
## Meters
|
## Meters
|
||||||
|
|
||||||
Input meters are subscribed on the console the first time a meter feedback renders (ids start at 5000 so
|
Meters are subscribed on the console the first time a meter feedback renders (ids start at 5000, so they never collide
|
||||||
they never collide with an iPad's). The console then streams values 0-127 several times a second; the
|
with an iPad's: input meters 5000-5260, buss meters 5400 + N, matrix output meters 5450 + N). The console then streams
|
||||||
module re-checks the meter feedbacks at most 10 times a second. Level is the post-fader input meter; gate and
|
integer values 0-127 several times a second, only for the ones that changed, and the module re-checks the meter
|
||||||
compressor gain reduction are the two reduction meters. There is no known buss meter address, so busses
|
feedbacks at most 10 times a second, and only the buttons showing the meter that moved.
|
||||||
have no meters. The console keeps one subscription table: an iPad connecting clears it (`/Meters/clear`),
|
|
||||||
and the module re-subscribes half a second after it sees that pass through the relay. Two iPads watching
|
**The scale is inverted and approximate.** A level meter value is about the **dB below full scale**: 0 = full scale,
|
||||||
different banks both receive the mixed stream. The module never sends `/Meters/clear` itself.
|
42 = about -42 dBFS, **127 = silence (-inf)**. A gain reduction meter value is about the **dB of reduction** (0-12 is
|
||||||
|
typical, 127 = inactive). It has not been calibrated against a reference tone, so treat the numbers as roughly 1 dB per
|
||||||
|
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 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.
|
||||||
|
Control groups and matrix inputs have no meter. The console keeps one subscription table: an iPad connecting clears it
|
||||||
|
(`/Meters/clear`), and the module re-subscribes half a second after it sees that pass through the relay (and after a
|
||||||
|
reconnect). Two iPads watching different banks both receive the mixed stream. The module never sends `/Meters/clear`
|
||||||
|
itself.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@@ -224,17 +244,18 @@ Levels, sends and pans are floats too. Numbers are 1-based, snapshot indexes 0-b
|
|||||||
| `/Console/Aux_Outputs/types` | 25 x i | 1 = aux, 0 = group |
|
| `/Console/Aux_Outputs/types` | 25 x i | 1 = aux, 0 = group |
|
||||||
| `/Console/{Aux_Outputs,Group_Outputs,Input_Channels}/modes` | i per buss / input | 2 = stereo, 1 = mono |
|
| `/Console/{Aux_Outputs,Group_Outputs,Input_Channels}/modes` | i per buss / input | 2 = stereo, 1 = mono |
|
||||||
| `/Console/Session/Filename` | s | |
|
| `/Console/Session/Filename` | s | |
|
||||||
| `/Input_Channels/N/Channel_Input/name`, `/Aux_Outputs/N/Buss_Trim/name`, `/Group_Outputs/N/Buss_Trim/name`, `/Matrix_Outputs/N/Buss_Trim/name`, `/Control_Groups/N/name`, `/Graphic_EQ/N/name` | s | matrix inputs have no name address |
|
| `/Input_Channels/N/Channel_Input/name`, `/Matrix_Inputs/N/Channel_Input/name`, `/Aux_Outputs/N/Buss_Trim/name`, `/Group_Outputs/N/Buss_Trim/name`, `/Matrix_Outputs/N/Buss_Trim/name`, `/Control_Groups/N/name`, `/Graphic_EQ/N/name` | s | names arrive padded; the module cleans them |
|
||||||
| `/{Input_Channels,Aux_Outputs,Group_Outputs,Matrix_Outputs,Control_Groups}/N/{fader,mute,solo}` | f | fader in dB, -150 = -inf |
|
| `/{Input_Channels,Aux_Outputs,Group_Outputs,Matrix_Outputs,Control_Groups}/N/{fader,mute,solo}` | f | fader in dB, -150 = -inf |
|
||||||
| `/Input_Channels/N/Panner/pan` | f | 0..1, 0.5 = centre |
|
| `/Input_Channels/N/Panner/pan` | f | 0..1, 0.5 = centre |
|
||||||
| `/Input_Channels/N/Aux_Send/M/{send_level,send_on,send_pan}` | f | |
|
| `/Input_Channels/N/Aux_Send/M/{send_level,send_on,send_pan}` | f | |
|
||||||
| `/Input_Channels/N/Group_Send/M/group` | f | |
|
| `/Input_Channels/N/Group_Send/M/group` | f | |
|
||||||
| `/Matrix_Inputs/N/Matrix_Send/M/{send_level,send_on}` | f | |
|
| `/Matrix_Inputs/N/Matrix_Send/M/{send_level,send_on}` | f | a matrix send floors at -90 dB |
|
||||||
|
| `/Matrix_Inputs/N/Channel_Input/{trim,phase}` | f | |
|
||||||
| `/Graphic_EQ/N/{geq_in,geq_gain_1..32}` | f | |
|
| `/Graphic_EQ/N/{geq_in,geq_gain_1..32}` | f | |
|
||||||
| `/Input_Channels/N/{Channel_Input,Filters,Channel_Delay,EQ,Dynamics,Insert}/...` and the buss equivalents | f | the channel-strip switches and values above |
|
| `/Input_Channels/N/{Channel_Input,Filters,Channel_Delay,EQ,Dynamics,Insert}/...` and the buss equivalents | f | the channel-strip switches and values above |
|
||||||
| `/Snapshots/Current_Snapshot`, `/Snapshots/count` | i | |
|
| `/Snapshots/Current_Snapshot`, `/Snapshots/count` | i | |
|
||||||
| `/Snapshots/names` | one `/Snapshots/name` `iiis` per snapshot: index, cue x 100, ?, name | |
|
| `/Snapshots/names` | one `/Snapshots/name` `iiis` per snapshot: index, cue x 100, ?, name | |
|
||||||
| `/Snapshots/Fire_Snapshot_number`, `/Snapshots/Fire_Next_Snapshot`, `/Snapshots/Fire_Prev_Snapshot` | none | write-only int |
|
| `/Snapshots/Fire_Snapshot_number`, `/Snapshots/Fire_Next_Snapshot`, `/Snapshots/Fire_Prev_Snapshot` | none | write-only int |
|
||||||
| `/Input_Channels/N/Channel_Input/post_meter`, `/Input_Channels/N/Dynamics/GR_meter` | none | send `,ii [id, kind]` (kind 1 level and gate, 4 compressor) to subscribe |
|
| `/Input_Channels/N/Channel_Input/post_meter`, `/Input_Channels/N/Dynamics/GR_meter`, `/{Aux_Outputs,Group_Outputs,Matrix_Outputs}/N/Output/meter` | none | send `,ii [id, kind]` (kind 1 level and gate and outputs, 4 compressor) to subscribe |
|
||||||
| `/Meters/values` | `ii...` | pushed pairs `[id, value]`, value 0..127 |
|
| `/Meters/values` | `ii...` | pushed pairs `[id, value]`, value 0..127: inverted scale, 127 = silence / inactive |
|
||||||
| `/Meters/clear` | none | clears every subscription on the console; sent by iPad apps, never by this module |
|
| `/Meters/clear` | none | clears every subscription on the console; sent by iPad apps, never by this module |
|
||||||
|
|||||||
@@ -52,11 +52,14 @@ const ADJUST_NOTE = 'From −∞ the first step up jumps to −60 dB.'
|
|||||||
|
|
||||||
export const numbers = (v) => (Array.isArray(v) ? v : v === undefined || v === '' ? [] : [v]).map(Number).filter(Number.isFinite)
|
export const numbers = (v) => (Array.isArray(v) ? v : v === undefined || v === '' ? [] : [v]).map(Number).filter(Number.isFinite)
|
||||||
|
|
||||||
/** Which entity kinds get actions / feedbacks for a parameter: 'input', 'buss' and/or 'matrix'. */
|
/** Which entity kinds get actions / feedbacks for a parameter: 'input', 'buss', 'matrix' and/or 'minput' (matrix input). */
|
||||||
export const paramEntities = (param) => Object.keys(PARAMS[param].paths)
|
export const paramEntities = (param) => Object.keys(PARAMS[param].paths)
|
||||||
|
|
||||||
/** Entity dropdown for a parameter's entity kind. */
|
/** Entity dropdown for a parameter's entity kind. */
|
||||||
export const paramEntityOption = (inst, ent) => (ent === 'input' ? opt.channel(inst) : ent === 'matrix' ? opt.matrix(inst) : opt.buss(inst, 'all'))
|
export const paramEntityOption = (inst, ent) => (ent === 'input' ? opt.channel(inst) : ent === 'matrix' ? opt.matrix(inst) : ent === 'minput' ? opt.matrixIn(inst) : opt.buss(inst, 'all'))
|
||||||
|
|
||||||
|
/** Display name of an entity kind in action and feedback names. */
|
||||||
|
export const ENTITY_TITLE = { input: 'Input', buss: 'Buss', matrix: 'Matrix output', minput: 'Matrix input' }
|
||||||
|
|
||||||
/** Switch toggles and value set/adjust actions for the lazily loaded per-channel parameters (PARAMS). */
|
/** Switch toggles and value set/adjust actions for the lazily loaded per-channel parameters (PARAMS). */
|
||||||
function getParamActions(inst) {
|
function getParamActions(inst) {
|
||||||
@@ -64,7 +67,7 @@ function getParamActions(inst) {
|
|||||||
for (const [param, p] of Object.entries(PARAMS)) {
|
for (const [param, p] of Object.entries(PARAMS)) {
|
||||||
for (const ent of paramEntities(param)) {
|
for (const ent of paramEntities(param)) {
|
||||||
const entOpt = paramEntityOption(inst, ent)
|
const entOpt = paramEntityOption(inst, ent)
|
||||||
const who = { input: 'Input', buss: 'Buss', matrix: 'Matrix output' }[ent]
|
const who = ENTITY_TITLE[ent]
|
||||||
const target = (options) => Number(options[entOpt.id])
|
const target = (options) => Number(options[entOpt.id])
|
||||||
if (p.type === 'switch') {
|
if (p.type === 'switch') {
|
||||||
out[`${ent}_${param}`] = {
|
out[`${ent}_${param}`] = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { combineRgb } from '@companion-module/base'
|
import { combineRgb } from '@companion-module/base'
|
||||||
import { GEQ_RANGE, LEVEL, METERS, PARAMS, clamp, faderPosition, fmtDb, fmtPan, fmtValue, meterId } from '../protocol.js'
|
import { GEQ_RANGE, LEVEL, METERS, PARAMS, clamp, faderPosition, fmtDb, fmtMeterLevel, fmtPan, fmtValue, meterGrDb, meterId, meterLevelDb, meterPosition, outputMeterId } from '../protocol.js'
|
||||||
import { horizontalBar, verticalMeter } from '../image.js'
|
import { horizontalBar, verticalMeter } from '../image.js'
|
||||||
import { opt, paramEntities, paramEntityOption } from './actions.js'
|
import { ENTITY_TITLE, opt, paramEntities, paramEntityOption } from './actions.js'
|
||||||
|
|
||||||
export const COLORS = {
|
export const COLORS = {
|
||||||
white: combineRgb(255, 255, 255),
|
white: combineRgb(255, 255, 255),
|
||||||
@@ -111,6 +111,32 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
inst.subscribeInputMeter(ch, meter)
|
inst.subscribeInputMeter(ch, meter)
|
||||||
return s.getMeter(meterId(ch, meter))
|
return s.getMeter(meterId(ch, meter))
|
||||||
}
|
}
|
||||||
|
const isGr = (o) => meterName(o) !== 'level'
|
||||||
|
/** Output meters (busses, matrix outputs) are level meters; the console subscription follows a buss's aux / group side. */
|
||||||
|
const outputKey = (ent, id) => (o) => watchKey.meter(outputMeterId(ent, n(o[id])))
|
||||||
|
const outputMeterOf = (ent, id, options) => {
|
||||||
|
inst.subscribeOutputMeter(ent, n(options[id]))
|
||||||
|
return s.getMeter(outputMeterId(ent, n(options[id])))
|
||||||
|
}
|
||||||
|
// the meter scale is inverted and approximate: level value v = about dB below full scale (127 = silence), gain reduction v = dB
|
||||||
|
const signalOption = { type: 'number', id: 'thresholdDb', label: 'Signal above (dBFS, approx.)', default: -50, min: -80, max: 0, step: 1 }
|
||||||
|
const peakOption = { type: 'number', id: 'peakDb', label: 'Peak at or above (dBFS, approx.)', default: -1, min: -20, max: 0, step: 1 }
|
||||||
|
const holdOption = { type: 'number', id: 'hold', label: 'Hold (seconds)', default: 2, min: 0, max: 10, step: 0.5 }
|
||||||
|
const levelSignal = (m, o) => meterLevelDb(m?.value) > n(o.thresholdDb ?? signalOption.default)
|
||||||
|
const levelPeak = (id, o) => {
|
||||||
|
const at = s.meterPeakAt(id, n(o.peakDb ?? peakOption.default))
|
||||||
|
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 }) => {
|
||||||
|
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')),
|
||||||
|
})
|
||||||
const entityKey = (ent, id) => (o) => watchKey.entity(ent, n(o[id]))
|
const entityKey = (ent, id) => (o) => watchKey.entity(ent, n(o[id]))
|
||||||
const sendKey = (o) => watchKey.send(n(o.channel), n(o.buss))
|
const sendKey = (o) => watchKey.send(n(o.channel), n(o.buss))
|
||||||
const msendKey = (o) => watchKey.msend(n(o.matrixIn), n(o.matrixOut))
|
const msendKey = (o) => watchKey.msend(n(o.matrixIn), n(o.matrixOut))
|
||||||
@@ -133,7 +159,7 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
for (const ent of paramEntities(param)) {
|
for (const ent of paramEntities(param)) {
|
||||||
const entOption = paramEntityOption(inst, ent)
|
const entOption = paramEntityOption(inst, ent)
|
||||||
paramFeedbacks[`${ent}_${param}_value`] = value(
|
paramFeedbacks[`${ent}_${param}_value`] = value(
|
||||||
`${{ input: 'Input', buss: 'Buss', matrix: 'Matrix output' }[ent]}: ${p.label} (${p.unit} text)`,
|
`${ENTITY_TITLE[ent]}: ${p.label} (${p.unit} text)`,
|
||||||
[entOption],
|
[entOption],
|
||||||
entityKey(ent, entOption.id),
|
entityKey(ent, entOption.id),
|
||||||
({ options }) => {
|
({ options }) => {
|
||||||
@@ -149,7 +175,7 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
const style = param === 'phantom' ? RED : GREEN
|
const style = param === 'phantom' ? RED : GREEN
|
||||||
const entOption = paramEntityOption(inst, ent)
|
const entOption = paramEntityOption(inst, ent)
|
||||||
paramFeedbacks[`${ent}_${param}_state`] = bool(
|
paramFeedbacks[`${ent}_${param}_state`] = bool(
|
||||||
`${{ input: 'Input', buss: 'Buss', matrix: 'Matrix output' }[ent]}: ${p.label} is on`,
|
`${ENTITY_TITLE[ent]}: ${p.label} is on`,
|
||||||
style,
|
style,
|
||||||
[entOption],
|
[entOption],
|
||||||
entityKey(ent, entOption.id),
|
entityKey(ent, entOption.id),
|
||||||
@@ -222,22 +248,27 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
relay_clients: bool('Relay has at least one iPad', GREEN, [], () => watchKey.relay, () => (inst.relay?.clientCount || 0) > 0),
|
relay_clients: bool('Relay has at least one iPad', GREEN, [], () => watchKey.relay, () => (inst.relay?.clientCount || 0) > 0),
|
||||||
|
|
||||||
input_signal: bool(
|
input_signal: bool(
|
||||||
'Input meter: signal present',
|
'Input meter: signal present (level above a dBFS threshold; gain reduction meters: any reduction)',
|
||||||
GREEN,
|
GREEN,
|
||||||
[opt.channel(inst), meterOption, { type: 'number', id: 'threshold', label: 'Signal above (0-127)', default: 5, min: 0, max: 127, step: 1 }],
|
[opt.channel(inst), meterOption, signalOption],
|
||||||
meterKey,
|
|
||||||
({ options }) => (meterOf(options)?.value ?? 0) > n(options.threshold),
|
|
||||||
),
|
|
||||||
input_peak: bool(
|
|
||||||
'Input meter: peak (full scale) hold',
|
|
||||||
RED,
|
|
||||||
[opt.channel(inst), meterOption, { type: 'number', id: 'hold', label: 'Hold (seconds)', default: 2, min: 0, max: 10, step: 0.5 }],
|
|
||||||
meterKey,
|
meterKey,
|
||||||
({ options }) => {
|
({ options }) => {
|
||||||
const m = meterOf(options)
|
const m = meterOf(options)
|
||||||
return !!m && m.peakAt > 0 && Date.now() - m.peakAt <= n(options.hold) * 1000
|
return isGr(options) ? meterGrDb(m?.value) > 0 : levelSignal(m, options)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
input_peak: bool(
|
||||||
|
'Input meter: peak hold (level at or above a dBFS threshold; not for gain reduction meters)',
|
||||||
|
RED,
|
||||||
|
[opt.channel(inst), meterOption, peakOption, holdOption],
|
||||||
|
meterKey,
|
||||||
|
({ options }) => {
|
||||||
|
meterOf(options)
|
||||||
|
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'),
|
||||||
|
|
||||||
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))) || '--'),
|
||||||
@@ -276,29 +307,13 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
return s.getSend(n(options.channel), n(options.buss)).pan ?? 0.5
|
return s.getSend(n(options.channel), n(options.buss)).pan ?? 0.5
|
||||||
}),
|
}),
|
||||||
geq_band_db: value('Graphic EQ: band gain (dB number -12..12, for gauges)', [opt.geq(inst), opt.band()], geqGainKey, ({ options }) => geqGainOf(options) ?? 0),
|
geq_band_db: value('Graphic EQ: band gain (dB number -12..12, for gauges)', [opt.geq(inst), opt.band()], geqGainKey, ({ options }) => geqGainOf(options) ?? 0),
|
||||||
input_meter_pos: value('Input meter: position (0-100, for gauges)', [opt.channel(inst), meterOption], meterKey, ({ options }) => ((meterOf(options)?.value ?? 0) / 127) * 100),
|
input_meter_pos: value('Input meter: position (0-100, for gauges; level over -60..0 dBFS, gain reduction over 0..20 dB)', [opt.channel(inst), meterOption], meterKey, ({ options }) => meterPosition(meterOf(options)?.value, isGr(options) ? 'gr' : 'level')),
|
||||||
snapshot_name_value: value('Snapshot: current name', [], () => watchKey.snapshot, () => s.snapshotName(s.snapshots.current) ?? '--'),
|
snapshot_name_value: value('Snapshot: current name', [], () => watchKey.snapshot, () => s.snapshotName(s.snapshots.current) ?? '--'),
|
||||||
input_meter_value: value(
|
input_meter_value: value(
|
||||||
'Input meter: value',
|
'Input meter: value text (level in dBFS, approx., -inf = silence; gain reduction in dB)',
|
||||||
[
|
[opt.channel(inst), meterOption],
|
||||||
opt.channel(inst),
|
|
||||||
meterOption,
|
|
||||||
{
|
|
||||||
type: 'dropdown',
|
|
||||||
id: 'unit',
|
|
||||||
label: 'Unit',
|
|
||||||
default: 'raw',
|
|
||||||
choices: [
|
|
||||||
{ id: 'raw', label: 'Raw (0-127)' },
|
|
||||||
{ id: 'percent', label: 'Percent (0-100)' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
meterKey,
|
meterKey,
|
||||||
({ options }) => {
|
({ options }) => meterText(meterOf(options), isGr(options)),
|
||||||
const v = meterOf(options)?.value ?? 0
|
|
||||||
return options.unit === 'percent' ? Math.round((v / 127) * 100) : v
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
input_fader_bar: bar('Input: fader bar (legacy image)', [opt.channel(inst)], entityKey('input', 'channel'), (fb, image) => {
|
input_fader_bar: bar('Input: fader bar (legacy image)', [opt.channel(inst)], entityKey('input', 'channel'), (fb, image) => {
|
||||||
@@ -338,7 +353,7 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
callback: tracked(meterKey, (fb) => {
|
callback: tracked(meterKey, (fb) => {
|
||||||
const m = meterOf(fb.options)
|
const m = meterOf(fb.options)
|
||||||
if (!fb.image) return {}
|
if (!fb.image) return {}
|
||||||
return verticalMeter({ ...fb.image, fraction: (m?.value ?? 0) / 127 })
|
return verticalMeter({ ...fb.image, fraction: meterPosition(m?.value, isGr(fb.options) ? 'gr' : 'level') / 100 })
|
||||||
}),
|
}),
|
||||||
unsubscribe,
|
unsubscribe,
|
||||||
},
|
},
|
||||||
|
|||||||
+54
-10
@@ -1,7 +1,8 @@
|
|||||||
import { COLORS } from './feedbacks.js'
|
import { COLORS } from './feedbacks.js'
|
||||||
|
import { VAR_PREFIX } from './variables.js'
|
||||||
|
|
||||||
/** Hard limits agreed for the preset list (Companion keeps all of it in memory and sends it to the web UI). */
|
/** Hard limits agreed for the preset list (Companion keeps all of it in memory and sends it to the web UI). */
|
||||||
export const MAX_PRESETS = 3000
|
export const MAX_PRESETS = 3200
|
||||||
export const MAX_PRESET_BYTES = 4 * 1024 * 1024
|
export const MAX_PRESET_BYTES = 4 * 1024 * 1024
|
||||||
|
|
||||||
const { white: WHITE, dark: DARK, red: RED, green: GREEN, amber: AMBER } = COLORS
|
const { white: WHITE, dark: DARK, red: RED, green: GREEN, amber: AMBER } = COLORS
|
||||||
@@ -30,7 +31,7 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
const L = inst.label
|
const L = inst.label
|
||||||
const presets = {}
|
const presets = {}
|
||||||
const structure = []
|
const structure = []
|
||||||
const nameVar = (ent, n) => `$(${L}:${ent}_${n}_name)`
|
const nameVar = (ent, n) => `$(${L}:${VAR_PREFIX[ent]}_${n}_name)`
|
||||||
const localFeedback = (variableName, feedbackId, options) => ({ variableName, variableType: 'feedback', feedbackId, options })
|
const localFeedback = (variableName, feedbackId, options) => ({ variableName, variableType: 'feedback', feedbackId, options })
|
||||||
|
|
||||||
const simple = (name, text, down, feedbacks = [], extra = {}) => ({
|
const simple = (name, text, down, feedbacks = [], extra = {}) => ({
|
||||||
@@ -86,8 +87,11 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
feedbacks,
|
feedbacks,
|
||||||
localVariables: [localFeedback('level', levelId, levelOptions), localFeedback('pos', posId, posOptions)],
|
localVariables: [localFeedback('level', levelId, levelOptions), localFeedback('pos', posId, posOptions)],
|
||||||
})
|
})
|
||||||
/** Layered input meter: name on the left, a vertical gauge (green / amber / red zones) at the right edge. */
|
/**
|
||||||
const layeredMeter = ({ name, title, options }) => ({
|
* Layered meter: name on the left, a vertical gauge (green / amber / red zones) at the right edge; the position comes
|
||||||
|
* from `posFeedback`. A signal feedback tints the background dark green and, for level meters, a peak feedback red.
|
||||||
|
*/
|
||||||
|
const layeredMeter = ({ name, title, posFeedback, signal, peak }) => ({
|
||||||
type: 'layered',
|
type: 'layered',
|
||||||
name,
|
name,
|
||||||
elements: [
|
elements: [
|
||||||
@@ -115,8 +119,8 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
steps: [{ down: [], up: [] }],
|
steps: [{ down: [], up: [] }],
|
||||||
feedbacks: [tint('input_signal', { ...options, threshold: 5 }, SIGNAL_BG), tint('input_peak', { ...options, hold: 2 }, MUTED_RED)],
|
feedbacks: [tint(signal[0], signal[1], SIGNAL_BG), ...(peak ? [tint(peak[0], peak[1], MUTED_RED)] : [])],
|
||||||
localVariables: [localFeedback('pos', 'input_meter_pos', options)],
|
localVariables: [localFeedback('pos', posFeedback[0], posFeedback[1])],
|
||||||
})
|
})
|
||||||
const group = (id, name, ids) => ({ id, type: 'simple', name, presets: ids })
|
const group = (id, name, ids) => ({ id, type: 'simple', name, presets: ids })
|
||||||
|
|
||||||
@@ -146,7 +150,15 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
['comp', 'input_comp_gr', 'Compressor reduction'],
|
['comp', 'input_comp_gr', 'Compressor reduction'],
|
||||||
]) {
|
]) {
|
||||||
if (meter !== 'level' && !grMeters) continue
|
if (meter !== 'level' && !grMeters) continue
|
||||||
presets[`${prefix}_${n}`] = layeredMeter({ name: `${label}: ${tag}`, title: nameVar('input', n), options: { channel: n, meter } })
|
const options = { channel: n, meter }
|
||||||
|
presets[`${prefix}_${n}`] = layeredMeter({
|
||||||
|
name: `${label}: ${tag}`,
|
||||||
|
title: nameVar('input', n),
|
||||||
|
posFeedback: ['input_meter_pos', options],
|
||||||
|
signal: ['input_signal', { ...options, thresholdDb: -50 }],
|
||||||
|
// a gain reduction meter has no peak
|
||||||
|
peak: meter === 'level' ? ['input_peak', { ...options, peakDb: -1, hold: 2 }] : null,
|
||||||
|
})
|
||||||
meterIds[meter].push(`${prefix}_${n}`)
|
meterIds[meter].push(`${prefix}_${n}`)
|
||||||
}
|
}
|
||||||
muteIds.push(`input_mute_${n}`)
|
muteIds.push(`input_mute_${n}`)
|
||||||
@@ -290,14 +302,14 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
const onIds = []
|
const onIds = []
|
||||||
const knobIds = []
|
const knobIds = []
|
||||||
for (const inN of s.numbers('minput')) {
|
for (const inN of s.numbers('minput')) {
|
||||||
const tag = `Matrix In ${inN} → ${out} · ${oname}`
|
const tag = `${s.displayName('minput', inN)} → ${out} · ${oname}`
|
||||||
const text = `Matrix In ${inN}\\n$(local:level) dB`
|
const text = `${nameVar('minput', inN)}\\n$(local:level) dB`
|
||||||
const options = { matrixIn: inN, matrixOut: out }
|
const options = { matrixIn: inN, matrixOut: out }
|
||||||
const lv = [localFeedback('level', 'matrix_send_level_value', options)]
|
const lv = [localFeedback('level', 'matrix_send_level_value', options)]
|
||||||
presets[`msend_on_${inN}_${out}`] = simple(`Matrix send on/off: ${tag}`, text, [act('matrix_send_on', { ...options, state: 'toggle' })], [fb('matrix_send_on', options, GREEN)], { localVariables: lv })
|
presets[`msend_on_${inN}_${out}`] = simple(`Matrix send on/off: ${tag}`, text, [act('matrix_send_on', { ...options, state: 'toggle' })], [fb('matrix_send_on', options, GREEN)], { localVariables: lv })
|
||||||
presets[`msend_knob_${inN}_${out}`] = layeredKnob({
|
presets[`msend_knob_${inN}_${out}`] = layeredKnob({
|
||||||
name: `Matrix send level (knob): ${tag}`,
|
name: `Matrix send level (knob): ${tag}`,
|
||||||
title: `Matrix In ${inN}`,
|
title: nameVar('minput', inN),
|
||||||
press: [act('matrix_send_on', { ...options, state: 'toggle' })],
|
press: [act('matrix_send_on', { ...options, state: 'toggle' })],
|
||||||
adjust: ['matrix_send_level_adjust', options],
|
adjust: ['matrix_send_level_adjust', options],
|
||||||
level: ['matrix_send_level_value', options],
|
level: ['matrix_send_level_value', options],
|
||||||
@@ -316,6 +328,38 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 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)] })
|
||||||
|
}
|
||||||
|
const matrixIds = []
|
||||||
|
for (const n of s.numbers('matrix')) {
|
||||||
|
presets[`matrix_meter_${n}`] = layeredMeter({
|
||||||
|
name: `Matrix meter: ${n} · ${s.displayName('matrix', n)}`,
|
||||||
|
title: nameVar('matrix', n),
|
||||||
|
posFeedback: ['matrix_meter_pos', { matrix: n }],
|
||||||
|
signal: ['matrix_signal', { matrix: n, thresholdDb: -50 }],
|
||||||
|
peak: ['matrix_peak', { matrix: n, peakDb: -1, hold: 2 }],
|
||||||
|
})
|
||||||
|
matrixIds.push(`matrix_meter_${n}`)
|
||||||
|
}
|
||||||
|
if (matrixIds.length) {
|
||||||
|
structure.push({ id: 'matrix_meters', name: 'Matrix meters', description: 'Meters subscribe on the console when the button first renders.', definitions: [group('matrix_meters_level', 'Level', matrixIds)] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Graphic EQs: in / out
|
// ---- Graphic EQs: in / out
|
||||||
{
|
{
|
||||||
const ids = []
|
const ids = []
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ const ENTITY_VARS = {
|
|||||||
buss: ['name', 'mode', 'stereo', 'fader', 'mute'],
|
buss: ['name', 'mode', 'stereo', 'fader', 'mute'],
|
||||||
matrix: ['name', 'fader', 'mute'],
|
matrix: ['name', 'fader', 'mute'],
|
||||||
cg: ['name', 'fader', 'mute'],
|
cg: ['name', 'fader', 'mute'],
|
||||||
|
minput: ['name'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Variable id prefix per entity kind (matrix inputs are matrix_in_N_...). */
|
||||||
|
export const VAR_PREFIX = { input: 'input', buss: 'buss', matrix: 'matrix', cg: 'cg', minput: 'matrix_in' }
|
||||||
|
|
||||||
const VAR_LABEL = {
|
const VAR_LABEL = {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
fader: 'fader level (dB)',
|
fader: 'fader level (dB)',
|
||||||
@@ -16,7 +20,7 @@ const VAR_LABEL = {
|
|||||||
stereo: 'Stereo/Mono',
|
stereo: 'Stereo/Mono',
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENT_LABEL = { input: 'Input', buss: 'Buss', matrix: 'Matrix output', cg: 'Control group' }
|
const ENT_LABEL = { input: 'Input', buss: 'Buss', matrix: 'Matrix output', cg: 'Control group', minput: 'Matrix input' }
|
||||||
|
|
||||||
/** Variables exist only for entities the console reported (all possible ones until the first load finishes). */
|
/** Variables exist only for entities the console reported (all possible ones until the first load finishes). */
|
||||||
export function getVariableDefinitions(inst) {
|
export function getVariableDefinitions(inst) {
|
||||||
@@ -34,7 +38,7 @@ export function getVariableDefinitions(inst) {
|
|||||||
}
|
}
|
||||||
for (const ent of Object.keys(ENTITY_VARS)) {
|
for (const ent of Object.keys(ENTITY_VARS)) {
|
||||||
for (const n of inst.state.numbers(ent)) {
|
for (const n of inst.state.numbers(ent)) {
|
||||||
for (const field of ENTITY_VARS[ent]) defs[`${ent}_${n}_${field}`] = { name: `${ENT_LABEL[ent]} ${n} ${VAR_LABEL[field]}` }
|
for (const field of ENTITY_VARS[ent]) defs[`${VAR_PREFIX[ent]}_${n}_${field}`] = { name: `${ENT_LABEL[ent]} ${n} ${VAR_LABEL[field]}` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defs
|
return defs
|
||||||
@@ -45,7 +49,8 @@ const muteText = (m) => (m === undefined ? '' : m ? 'Muted' : 'Unmuted')
|
|||||||
/** All current values of one entity, keyed by variable id. */
|
/** All current values of one entity, keyed by variable id. */
|
||||||
export function entityVariableValues(inst, ent, n) {
|
export function entityVariableValues(inst, ent, n) {
|
||||||
const s = inst.state
|
const s = inst.state
|
||||||
const p = `${ent}_${n}_`
|
const p = `${VAR_PREFIX[ent]}_${n}_`
|
||||||
|
if (ent === 'minput') return { [`${p}name`]: s.displayName(ent, n) }
|
||||||
const out = { [`${p}name`]: s.displayName(ent, n), [`${p}fader`]: fmtDb(s.getFader(ent, n)), [`${p}mute`]: muteText(s.getMute(ent, n)) }
|
const out = { [`${p}name`]: s.displayName(ent, n), [`${p}fader`]: fmtDb(s.getFader(ent, n)), [`${p}mute`]: muteText(s.getMute(ent, n)) }
|
||||||
if (ent === 'input') out[`${p}pan`] = fmtPan(s.getPan(n))
|
if (ent === 'input') out[`${p}pan`] = fmtPan(s.getPan(n))
|
||||||
if (ent === 'buss') {
|
if (ent === 'buss') {
|
||||||
|
|||||||
+24
-8
@@ -3,7 +3,7 @@ import { ConsoleLink } from './console-link.js'
|
|||||||
import { Relay } from './relay.js'
|
import { Relay } from './relay.js'
|
||||||
import { decode, describe, foldPattern } from './osc.js'
|
import { decode, describe, foldPattern } from './osc.js'
|
||||||
import { State } from './state.js'
|
import { State } from './state.js'
|
||||||
import { GEQ_RANGE, LEVEL, PARAMS, K, addr, adjustedLevel, clamp, indexProblem, meterId, meterSubscription, parse, query, sameValue } from './protocol.js'
|
import { GEQ_RANGE, LEVEL, PARAMS, K, addr, adjustedLevel, clamp, indexProblem, meterId, meterSubscription, outputMeterId, outputMeterSubscription, parse, query, sameValue } from './protocol.js'
|
||||||
import { getActionDefinitions } from './definitions/actions.js'
|
import { getActionDefinitions } from './definitions/actions.js'
|
||||||
import { getFeedbackDefinitions, feedbackIdsForEvent, watchKey, watchKeyForEvent } from './definitions/feedbacks.js'
|
import { getFeedbackDefinitions, feedbackIdsForEvent, watchKey, watchKeyForEvent } from './definitions/feedbacks.js'
|
||||||
import { getVariableDefinitions, getAllVariableValues, getEventVariableValues, describeChange } from './definitions/variables.js'
|
import { getVariableDefinitions, getAllVariableValues, getEventVariableValues, describeChange } from './definitions/variables.js'
|
||||||
@@ -216,7 +216,7 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
this.lastDefs = {} // JSON of what was last pushed, per kind of definition
|
this.lastDefs = {} // JSON of what was last pushed, per kind of definition
|
||||||
this.definedVariables = new Set()
|
this.definedVariables = new Set()
|
||||||
this.presetStats = null
|
this.presetStats = null
|
||||||
this.meterWanted = new Map() // meter id -> { n, meter }
|
this.meterWanted = new Map() // meter id -> { ent: 'input' | 'buss' | 'matrix', n, meter }
|
||||||
this.meterDirtyIds = new Set()
|
this.meterDirtyIds = new Set()
|
||||||
this.peakUntil = new Map() // meter id -> time until which its peak hold is re-checked
|
this.peakUntil = new Map() // meter id -> time until which its peak hold is re-checked
|
||||||
this.meterTimer = null
|
this.meterTimer = null
|
||||||
@@ -1048,7 +1048,7 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
const s = this.state.summary()
|
const s = this.state.summary()
|
||||||
this.log(
|
this.log(
|
||||||
'info',
|
'info',
|
||||||
`load complete: ${s.inputs} inputs, ${s.busses} busses (${s.aux} aux and ${s.group} group named), ${s.matrix} matrix outputs, ` +
|
`load complete: ${s.inputs} inputs, ${s.busses} busses (${s.aux} aux and ${s.group} group named), ${s.matrix} matrix outputs, ${s.matrixIn} matrix inputs, ` +
|
||||||
`${s.cgs} control groups, ${s.geqs} graphic EQs, ${s.snapshots} snapshots`,
|
`${s.cgs} control groups, ${s.geqs} graphic EQs, ${s.snapshots} snapshots`,
|
||||||
)
|
)
|
||||||
if (!this.connectedFlag || s.total === 0) this.reloadOnReconnect = true
|
if (!this.connectedFlag || s.total === 0) this.reloadOnReconnect = true
|
||||||
@@ -1121,7 +1121,8 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
for (const [id, value] of ev.pairs) {
|
for (const [id, value] of ev.pairs) {
|
||||||
if (!this.watchers.has(watchKey.meter(id))) continue
|
if (!this.watchers.has(watchKey.meter(id))) continue
|
||||||
this.meterDirtyIds.add(id)
|
this.meterDirtyIds.add(id)
|
||||||
if (value >= 127) this.peakUntil.set(id, now + this.timing.peakWatchMs)
|
// within 20 dB of full scale on a level meter: a peak hold may be running (gain reduction meters have none)
|
||||||
|
if (value <= 20 && this.meterWanted.get(id)?.meter === 'level') this.peakUntil.set(id, now + this.timing.peakWatchMs)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1138,6 +1139,7 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
onStructural(ev) {
|
onStructural(ev) {
|
||||||
if (ev.kind === 'bussTypes' && this.state.loaded) {
|
if (ev.kind === 'bussTypes' && this.state.loaded) {
|
||||||
for (const n of this.state.existing('buss')) this.enqueue(this.state.bussValueQueries(n))
|
for (const n of this.state.existing('buss')) this.enqueue(this.state.bussValueQueries(n))
|
||||||
|
this.resubscribeMeters() // a buss meter lives on the buss's aux or group side
|
||||||
}
|
}
|
||||||
this.scheduleRebuild()
|
this.scheduleRebuild()
|
||||||
}
|
}
|
||||||
@@ -1225,20 +1227,34 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
|
|
||||||
// ---------- meters ----------
|
// ---------- meters ----------
|
||||||
|
|
||||||
|
/** The [address, args] that subscribes one wanted meter; a buss meter follows the buss's current aux / group side. */
|
||||||
|
meterSubscriptionFor({ ent, n, meter }) {
|
||||||
|
if (ent === 'input') return meterSubscription(n, meter)
|
||||||
|
return outputMeterSubscription(ent, n, this.sideOf(ent, n))
|
||||||
|
}
|
||||||
|
|
||||||
/** Subscribe to an input meter ('level', 'gate' or 'comp' gain reduction) the first time a meter feedback for it runs. */
|
/** Subscribe to an input meter ('level', 'gate' or 'comp' gain reduction) the first time a meter feedback for it runs. */
|
||||||
subscribeInputMeter(n, meter = 'level') {
|
subscribeInputMeter(n, meter = 'level') {
|
||||||
const id = meterId(n, meter)
|
this.subscribeMeter(meterId(n, meter), { ent: 'input', n, meter })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subscribe to a buss ('buss') or matrix output ('matrix') level meter, lazily like the input meters. */
|
||||||
|
subscribeOutputMeter(ent, n) {
|
||||||
|
this.subscribeMeter(outputMeterId(ent, n), { ent, n, meter: 'level' })
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeMeter(id, entry) {
|
||||||
if (this.meterWanted.has(id)) return
|
if (this.meterWanted.has(id)) return
|
||||||
const sub = meterSubscription(n, meter)
|
const sub = this.meterSubscriptionFor(entry)
|
||||||
if (!this.addressOk(sub[0])) return
|
if (!this.addressOk(sub[0])) return
|
||||||
this.meterWanted.set(id, { n, meter })
|
this.meterWanted.set(id, entry)
|
||||||
this.enqueue([sub])
|
this.enqueue([sub])
|
||||||
this.ensureMeterTimer()
|
this.ensureMeterTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
resubscribeMeters() {
|
resubscribeMeters() {
|
||||||
if (!this.meterWanted.size) return
|
if (!this.meterWanted.size) return
|
||||||
this.enqueue([...this.meterWanted.values()].map(({ n, meter }) => meterSubscription(n, meter)))
|
this.enqueue([...this.meterWanted.values()].map((entry) => this.meterSubscriptionFor(entry)))
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleMeterResubscribe() {
|
scheduleMeterResubscribe() {
|
||||||
|
|||||||
+57
-16
@@ -30,8 +30,13 @@ export const GEQ_RANGE = { min: -12, max: 12 }
|
|||||||
export const METER_BASE_ID = 5000
|
export const METER_BASE_ID = 5000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Input meters, subscribed by sending `<address> ,ii [id, kind]`. The console then streams
|
* Meters are subscribed by sending `<address> ,ii [id, kind]`; the console then streams /Meters/values [id, value, ...]
|
||||||
* /Meters/values [id, value, ...] (0..127, only changed ids). There is no known buss meter address.
|
* (integers 0..127, only changed ids). The scale is inverted and approximate (verified on an S31, not calibrated against
|
||||||
|
* a reference tone): a level meter value is about the dB below full scale, so 127 = silence / -inf and 0 = full scale;
|
||||||
|
* a gain reduction meter value is about the dB of reduction, 127 = inactive / no data.
|
||||||
|
*
|
||||||
|
* Input meters: level (post-fader), gate and compressor gain reduction. Output meters (aux, group and matrix outputs)
|
||||||
|
* are level meters. Control groups and matrix inputs have no meter address.
|
||||||
*/
|
*/
|
||||||
export const METERS = {
|
export const METERS = {
|
||||||
level: { label: 'Level (post-fader)', offset: 0, kind: 1, path: 'Channel_Input/post_meter' },
|
level: { label: 'Level (post-fader)', offset: 0, kind: 1, path: 'Channel_Input/post_meter' },
|
||||||
@@ -39,20 +44,52 @@ export const METERS = {
|
|||||||
comp: { label: 'Compressor gain reduction', offset: 200, kind: 4, path: 'Dynamics/GR_meter' },
|
comp: { label: 'Compressor gain reduction', offset: 200, kind: 4, path: 'Dynamics/GR_meter' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Output meter ids: busses 5400 + N, matrix outputs 5450 + N (the address follows a buss's aux / group side). */
|
||||||
|
export const OUTPUT_METER_BASE = { buss: 5400, matrix: 5450 }
|
||||||
|
|
||||||
export function meterId(n, meter = 'level') {
|
export function meterId(n, meter = 'level') {
|
||||||
return METER_BASE_ID + (METERS[meter] || METERS.level).offset + Number(n)
|
return METER_BASE_ID + (METERS[meter] || METERS.level).offset + Number(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const meterIdForInput = (n) => meterId(n, 'level')
|
export const meterIdForInput = (n) => meterId(n, 'level')
|
||||||
|
|
||||||
|
export const outputMeterId = (ent, n) => OUTPUT_METER_BASE[ent] + Number(n)
|
||||||
|
|
||||||
/** [address, args] that subscribes this module to one input meter. */
|
/** [address, args] that subscribes this module to one input meter. */
|
||||||
export function meterSubscription(n, meter = 'level') {
|
export function meterSubscription(n, meter = 'level') {
|
||||||
const m = METERS[meter] || METERS.level
|
const m = METERS[meter] || METERS.level
|
||||||
return [`/Input_Channels/${n}/${m.path}`, [{ i: meterId(n, meter) }, { i: m.kind }]]
|
return [`/Input_Channels/${n}/${m.path}`, [{ i: meterId(n, meter) }, { i: m.kind }]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** [address, args] for an output meter: ent 'buss' (side 'aux' | 'group') or 'matrix'. */
|
||||||
|
export function outputMeterSubscription(ent, n, side = 'aux') {
|
||||||
|
const tree = ent === 'matrix' ? 'Matrix_Outputs' : side === 'group' ? 'Group_Outputs' : 'Aux_Outputs'
|
||||||
|
return [`/${tree}/${n}/Output/meter`, [{ i: outputMeterId(ent, n) }, { i: 1 }]]
|
||||||
|
}
|
||||||
|
|
||||||
|
const meterNumber = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 127)
|
||||||
|
|
||||||
|
/** Level meter value -> dB below full scale (approximate): 127 (or no data) = -Infinity, 0 = 0 dBFS. */
|
||||||
|
export const meterLevelDb = (v) => (meterNumber(v) >= 127 ? -Infinity : -meterNumber(v) || 0)
|
||||||
|
|
||||||
|
/** Gain reduction meter value -> dB of reduction (approximate): 127 (or no data) means none, 0. */
|
||||||
|
export const meterGrDb = (v) => (meterNumber(v) >= 127 ? 0 : meterNumber(v))
|
||||||
|
|
||||||
|
/** Gauge position 0..100: levels over -60..0 dBFS (-inf = 0), gain reduction over 0..20 dB. */
|
||||||
|
export function meterPosition(v, kind = 'level') {
|
||||||
|
if (kind === 'level') return clamp(((meterLevelDb(v) + 60) / 60) * 100, 0, 100)
|
||||||
|
return clamp((meterGrDb(v) / 20) * 100, 0, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Level meter text: "-42" or "-inf" (dBFS, approximate). */
|
||||||
|
export const fmtMeterLevel = (v) => (meterNumber(v) >= 127 ? '-inf' : String(-meterNumber(v) || 0))
|
||||||
|
|
||||||
|
/** A name as shown anywhere: whitespace runs (the surface pads and wraps names) collapse to one space, ends trimmed. */
|
||||||
|
export const cleanName = (name) => String(name ?? '').replace(/\s+/g, ' ').trim()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-channel switches and values the console answers on inputs and busses (trim also on matrix outputs). They are loaded lazily
|
* Per-channel switches and values the console answers on inputs and busses (trim also on matrix outputs; trim and phase
|
||||||
|
* also on matrix inputs). They are loaded lazily
|
||||||
* (the first time an action or feedback needs one), never bulk-loaded. `paths` is per entity kind;
|
* (the first time an action or feedback needs one), never bulk-loaded. `paths` is per entity kind;
|
||||||
* a buss uses the Aux_Outputs or Group_Outputs tree according to its mode.
|
* a buss uses the Aux_Outputs or Group_Outputs tree according to its mode.
|
||||||
*
|
*
|
||||||
@@ -63,7 +100,7 @@ export function meterSubscription(n, meter = 'level') {
|
|||||||
*/
|
*/
|
||||||
export const PARAMS = {
|
export const PARAMS = {
|
||||||
phantom: { type: 'switch', label: 'Phantom power (+48 V)', paths: { input: 'Channel_Input/phantom' } },
|
phantom: { type: 'switch', label: 'Phantom power (+48 V)', paths: { input: 'Channel_Input/phantom' } },
|
||||||
phase: { type: 'switch', label: 'Phase invert', paths: { input: 'Channel_Input/phase', buss: 'Buss_Trim/phase' } },
|
phase: { type: 'switch', label: 'Phase invert', paths: { input: 'Channel_Input/phase', buss: 'Buss_Trim/phase', minput: 'Channel_Input/phase' } },
|
||||||
pad: { type: 'switch', label: 'Input pad', paths: { input: 'Channel_Input/input_pad' } },
|
pad: { type: 'switch', label: 'Input pad', paths: { input: 'Channel_Input/input_pad' } },
|
||||||
eq_in: { type: 'switch', label: 'EQ in', paths: { input: 'EQ/eq_in', buss: 'EQ/eq_in' } },
|
eq_in: { type: 'switch', label: 'EQ in', paths: { input: 'EQ/eq_in', buss: 'EQ/eq_in' } },
|
||||||
comp_in: { type: 'switch', label: 'Compressor in', paths: { input: 'Dynamics/comp_in', buss: 'Dynamics/comp_in' } },
|
comp_in: { type: 'switch', label: 'Compressor in', paths: { input: 'Dynamics/comp_in', buss: 'Dynamics/comp_in' } },
|
||||||
@@ -74,13 +111,13 @@ export const PARAMS = {
|
|||||||
insert_a: { type: 'switch', label: 'Insert A in', paths: { input: 'Insert/insert_A_in', buss: 'Insert/insert_A_in' } },
|
insert_a: { type: 'switch', label: 'Insert A in', paths: { input: 'Insert/insert_A_in', buss: 'Insert/insert_A_in' } },
|
||||||
insert_b: { type: 'switch', label: 'Insert B in', paths: { input: 'Insert/insert_B_in', buss: 'Insert/insert_B_in' } },
|
insert_b: { type: 'switch', label: 'Insert B in', paths: { input: 'Insert/insert_B_in', buss: 'Insert/insert_B_in' } },
|
||||||
|
|
||||||
trim: { type: 'value', label: 'Trim', unit: 'dB', min: -40, max: 40, step: 0.5, delta: 1, adjust: 'linear', paths: { input: 'Channel_Input/trim', buss: 'Buss_Trim/trim', matrix: 'Buss_Trim/trim' } },
|
trim: { type: 'value', label: 'Trim', unit: 'dB', min: -40, max: 40, step: 0.5, delta: 1, adjust: 'linear', paths: { input: 'Channel_Input/trim', buss: 'Buss_Trim/trim', matrix: 'Buss_Trim/trim', minput: 'Channel_Input/trim' } },
|
||||||
gain: { type: 'value', label: 'Analog gain', unit: 'dB', min: 0, max: 60, step: 0.5, delta: 1, adjust: 'linear', paths: { input: 'Channel_Input/analog_gain' } },
|
gain: { type: 'value', label: 'Analog gain', unit: 'dB', min: 0, max: 60, step: 0.5, delta: 1, adjust: 'linear', paths: { input: 'Channel_Input/analog_gain' } },
|
||||||
hpf_freq: { type: 'value', label: 'High-pass filter frequency', unit: 'Hz', min: 20, max: 20000, step: 1, delta: 5, adjust: 'percent', paths: { input: 'Filters/lo_filter_freq' } },
|
hpf_freq: { type: 'value', label: 'High-pass filter frequency', unit: 'Hz', min: 20, max: 20000, step: 1, delta: 5, adjust: 'percent', paths: { input: 'Filters/lo_filter_freq' } },
|
||||||
lpf_freq: { type: 'value', label: 'Low-pass filter frequency', unit: 'Hz', min: 20, max: 20000, step: 1, delta: 5, adjust: 'percent', paths: { input: 'Filters/hi_filter_freq' } },
|
lpf_freq: { type: 'value', label: 'Low-pass filter frequency', unit: 'Hz', min: 20, max: 20000, step: 1, delta: 5, adjust: 'percent', paths: { input: 'Filters/hi_filter_freq' } },
|
||||||
}
|
}
|
||||||
|
|
||||||
const PARAM_BY_PATH = { input: {}, buss: {}, matrix: {} }
|
const PARAM_BY_PATH = { input: {}, buss: {}, matrix: {}, minput: {} }
|
||||||
for (const [param, p] of Object.entries(PARAMS)) for (const [ent, path] of Object.entries(p.paths)) PARAM_BY_PATH[ent][path] = param
|
for (const [param, p] of Object.entries(PARAMS)) for (const [ent, path] of Object.entries(p.paths)) PARAM_BY_PATH[ent][path] = param
|
||||||
|
|
||||||
export const COUNT_FIELDS = {
|
export const COUNT_FIELDS = {
|
||||||
@@ -95,7 +132,7 @@ export const COUNT_FIELDS = {
|
|||||||
Multis: 'multis',
|
Multis: 'multis',
|
||||||
}
|
}
|
||||||
|
|
||||||
const TREE = { input: 'Input_Channels', aux: 'Aux_Outputs', group: 'Group_Outputs', matrix: 'Matrix_Outputs', cg: 'Control_Groups' }
|
const TREE = { input: 'Input_Channels', minput: 'Matrix_Inputs', aux: 'Aux_Outputs', group: 'Group_Outputs', matrix: 'Matrix_Outputs', cg: 'Control_Groups' }
|
||||||
|
|
||||||
/** ent: 'input' | 'buss' | 'matrix' | 'cg'; side matters for 'buss' only ('aux' | 'group'). */
|
/** ent: 'input' | 'buss' | 'matrix' | 'cg'; side matters for 'buss' only ('aux' | 'group'). */
|
||||||
function tree(ent, side) {
|
function tree(ent, side) {
|
||||||
@@ -114,6 +151,7 @@ export const addr = {
|
|||||||
|
|
||||||
name(ent, n, side) {
|
name(ent, n, side) {
|
||||||
if (ent === 'input') return `/Input_Channels/${n}/Channel_Input/name`
|
if (ent === 'input') return `/Input_Channels/${n}/Channel_Input/name`
|
||||||
|
if (ent === 'minput') return `/Matrix_Inputs/${n}/Channel_Input/name`
|
||||||
if (ent === 'buss') return `/${tree(ent, side)}/${n}/Buss_Trim/name`
|
if (ent === 'buss') return `/${tree(ent, side)}/${n}/Buss_Trim/name`
|
||||||
if (ent === 'matrix') return `/Matrix_Outputs/${n}/Buss_Trim/name`
|
if (ent === 'matrix') return `/Matrix_Outputs/${n}/Buss_Trim/name`
|
||||||
if (ent === 'cg') return `/Control_Groups/${n}/name`
|
if (ent === 'cg') return `/Control_Groups/${n}/name`
|
||||||
@@ -168,16 +206,16 @@ const num = (v) => toNumber(v)
|
|||||||
const str = (v) => (v === undefined || v === null ? '' : String(v))
|
const str = (v) => (v === undefined || v === null ? '' : String(v))
|
||||||
|
|
||||||
const RE_COUNT = /^\/Console\/(Input_Channels|Aux_Outputs|Group_Outputs|Control_Groups|Matrix_Outputs|Matrix_Inputs|Graphic_EQ|Talkback_Outputs|Multis)$/
|
const RE_COUNT = /^\/Console\/(Input_Channels|Aux_Outputs|Group_Outputs|Control_Groups|Matrix_Outputs|Matrix_Inputs|Graphic_EQ|Talkback_Outputs|Multis)$/
|
||||||
const RE_NAME = /^\/(?:Input_Channels\/(\d+)\/Channel_Input|(Aux_Outputs|Group_Outputs|Matrix_Outputs)\/(\d+)\/Buss_Trim|(Control_Groups|Graphic_EQ)\/(\d+))\/name$/
|
const RE_NAME = /^\/(?:(Input_Channels|Matrix_Inputs)\/(\d+)\/Channel_Input|(Aux_Outputs|Group_Outputs|Matrix_Outputs)\/(\d+)\/Buss_Trim|(Control_Groups|Graphic_EQ)\/(\d+))\/name$/
|
||||||
const RE_LEVEL = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs|Control_Groups)\/(\d+)\/(fader|mute|solo)$/
|
const RE_LEVEL = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs|Control_Groups)\/(\d+)\/(fader|mute|solo)$/
|
||||||
const RE_PAN = /^\/Input_Channels\/(\d+)\/Panner\/pan$/
|
const RE_PAN = /^\/Input_Channels\/(\d+)\/Panner\/pan$/
|
||||||
const RE_AUX_SEND = /^\/Input_Channels\/(\d+)\/Aux_Send\/(\d+)\/(send_level|send_on|send_pan)$/
|
const RE_AUX_SEND = /^\/Input_Channels\/(\d+)\/Aux_Send\/(\d+)\/(send_level|send_on|send_pan)$/
|
||||||
const RE_GROUP_SEND = /^\/Input_Channels\/(\d+)\/Group_Send\/(\d+)\/group$/
|
const RE_GROUP_SEND = /^\/Input_Channels\/(\d+)\/Group_Send\/(\d+)\/group$/
|
||||||
const RE_MATRIX_SEND = /^\/Matrix_Inputs\/(\d+)\/Matrix_Send\/(\d+)\/(send_level|send_on)$/
|
const RE_MATRIX_SEND = /^\/Matrix_Inputs\/(\d+)\/Matrix_Send\/(\d+)\/(send_level|send_on)$/
|
||||||
const RE_GEQ = /^\/Graphic_EQ\/(\d+)\/(geq_in|geq_gain_(\d+))$/
|
const RE_GEQ = /^\/Graphic_EQ\/(\d+)\/(geq_in|geq_gain_(\d+))$/
|
||||||
const RE_PARAM = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs)\/(\d+)\/((?:Channel_Input|Filters|Channel_Delay|EQ|Dynamics|Insert|Buss_Trim)\/[\w-]+)$/
|
const RE_PARAM = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs|Matrix_Inputs)\/(\d+)\/((?:Channel_Input|Filters|Channel_Delay|EQ|Dynamics|Insert|Buss_Trim)\/[\w-]+)$/
|
||||||
|
|
||||||
const ENT_BY_TREE = { Input_Channels: ['input'], Aux_Outputs: ['buss', 'aux'], Group_Outputs: ['buss', 'group'], Matrix_Outputs: ['matrix'], Control_Groups: ['cg'], Graphic_EQ: ['geq'] }
|
const ENT_BY_TREE = { Input_Channels: ['input'], Matrix_Inputs: ['minput'], Aux_Outputs: ['buss', 'aux'], Group_Outputs: ['buss', 'group'], Matrix_Outputs: ['matrix'], Control_Groups: ['cg'], Graphic_EQ: ['geq'] }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn an inbound message into a normalised event, or null when it is not something the
|
* Turn an inbound message into a normalised event, or null when it is not something the
|
||||||
@@ -196,13 +234,16 @@ export function parse(address, args = []) {
|
|||||||
if (address === addr.inputModes) return { kind: 'inputModes', values: args.map(num) }
|
if (address === addr.inputModes) return { kind: 'inputModes', values: args.map(num) }
|
||||||
|
|
||||||
if ((m = RE_NAME.exec(address))) {
|
if ((m = RE_NAME.exec(address))) {
|
||||||
if (m[1]) return { kind: 'name', ent: 'input', n: +m[1], value: str(args[0]), key: K.value('input', +m[1], 'name') }
|
if (m[1]) {
|
||||||
if (m[2]) {
|
const ent = m[1] === 'Matrix_Inputs' ? 'minput' : 'input'
|
||||||
const [ent, side] = ENT_BY_TREE[m[2]]
|
return { kind: 'name', ent, n: +m[2], value: str(args[0]), key: K.value(ent, +m[2], 'name') }
|
||||||
return { kind: 'name', ent, side, n: +m[3], value: str(args[0]), key: K.value(ent, +m[3], 'name', side) }
|
|
||||||
}
|
}
|
||||||
const [ent] = ENT_BY_TREE[m[4]]
|
if (m[3]) {
|
||||||
return { kind: 'name', ent, n: +m[5], value: str(args[0]), key: K.value(ent, +m[5], 'name') }
|
const [ent, side] = ENT_BY_TREE[m[3]]
|
||||||
|
return { kind: 'name', ent, side, n: +m[4], value: str(args[0]), key: K.value(ent, +m[4], 'name', side) }
|
||||||
|
}
|
||||||
|
const [ent] = ENT_BY_TREE[m[5]]
|
||||||
|
return { kind: 'name', ent, n: +m[6], value: str(args[0]), key: K.value(ent, +m[6], 'name') }
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((m = RE_LEVEL.exec(address))) {
|
if ((m = RE_LEVEL.exec(address))) {
|
||||||
|
|||||||
+28
-10
@@ -3,8 +3,7 @@
|
|||||||
* choice builders and the query plans used to load it.
|
* choice builders and the query plans used to load it.
|
||||||
*
|
*
|
||||||
* Entities: input, buss (aux and group are two views of the same 25 busses, named and levelled
|
* Entities: input, buss (aux and group are two views of the same 25 busses, named and levelled
|
||||||
* on the side matching the buss mode), matrix (matrix outputs), minput (matrix inputs, no
|
* on the side matching the buss mode), matrix (matrix outputs), minput (matrix inputs), cg (control groups), geq (graphic EQs). All numbers are 1-based.
|
||||||
* name address), cg (control groups), geq (graphic EQs). All numbers are 1-based.
|
|
||||||
*
|
*
|
||||||
* applyMessage(address, args) is the single entry point for console traffic (and for the
|
* applyMessage(address, args) is the single entry point for console traffic (and for the
|
||||||
* module's own optimistic writes): it returns the parsed event annotated with
|
* module's own optimistic writes): it returns the parsed event annotated with
|
||||||
@@ -13,7 +12,7 @@
|
|||||||
* stereo/mono flags alone never are)
|
* stereo/mono flags alone never are)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LIMITS, K, addr, query, parse, fmtCue } from './protocol.js'
|
import { LIMITS, K, addr, query, parse, fmtCue, cleanName } from './protocol.js'
|
||||||
|
|
||||||
const EPS = 1e-4
|
const EPS = 1e-4
|
||||||
|
|
||||||
@@ -25,6 +24,10 @@ function setField(obj, field, value) {
|
|||||||
return old !== value
|
return old !== value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampInt(v, lo, hi) {
|
||||||
|
return Math.min(hi, Math.max(lo, v))
|
||||||
|
}
|
||||||
|
|
||||||
function rec(map, key) {
|
function rec(map, key) {
|
||||||
let r = map.get(key)
|
let r = map.get(key)
|
||||||
if (!r) {
|
if (!r) {
|
||||||
@@ -77,6 +80,9 @@ export class Buss {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Peak tracking keeps, per meter, the last time a reading was at most k steps below full scale, for k = 0..LOUD_STEPS. */
|
||||||
|
const LOUD_STEPS = 20
|
||||||
|
|
||||||
const FALLBACK_NAME = { input: 'Input', matrix: 'Matrix', cg: 'CG', geq: 'Graphic EQ', minput: 'Matrix In' }
|
const FALLBACK_NAME = { input: 'Input', matrix: 'Matrix', cg: 'CG', geq: 'Graphic EQ', minput: 'Matrix In' }
|
||||||
|
|
||||||
export class State {
|
export class State {
|
||||||
@@ -93,6 +99,7 @@ export class State {
|
|||||||
this.inputs = new Map()
|
this.inputs = new Map()
|
||||||
this.busses = new Map()
|
this.busses = new Map()
|
||||||
this.matrix = new Map()
|
this.matrix = new Map()
|
||||||
|
this.minputs = new Map() // matrix inputs: { name }
|
||||||
this.cgs = new Map()
|
this.cgs = new Map()
|
||||||
this.geqs = new Map()
|
this.geqs = new Map()
|
||||||
this.sends = new Map() // "ch:buss" -> { level, on, pan }
|
this.sends = new Map() // "ch:buss" -> { level, on, pan }
|
||||||
@@ -100,7 +107,7 @@ export class State {
|
|||||||
this.matrixSends = new Map() // "in:out" -> { level, on }
|
this.matrixSends = new Map() // "in:out" -> { level, on }
|
||||||
this.params = new Map() // K.param(...) -> boolean | number (per-channel switches and values, loaded lazily)
|
this.params = new Map() // K.param(...) -> boolean | number (per-channel switches and values, loaded lazily)
|
||||||
this.snapshots = { current: undefined, count: undefined, list: new Map() } // index -> { name, cue }
|
this.snapshots = { current: undefined, count: undefined, list: new Map() } // index -> { name, cue }
|
||||||
this.meters = new Map() // meter id -> { value, at, peakAt }
|
this.meters = new Map() // meter id -> { value, at, loud } (loud[k]: last time a reading was k steps below full scale or louder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- inbound ----------
|
// ---------- inbound ----------
|
||||||
@@ -112,6 +119,7 @@ export class State {
|
|||||||
ev.structural = false
|
ev.structural = false
|
||||||
switch (ev.kind) {
|
switch (ev.kind) {
|
||||||
case 'consoleName':
|
case 'consoleName':
|
||||||
|
ev.value = cleanName(ev.value)
|
||||||
ev.changed = setField(this.console, 'name', ev.value)
|
ev.changed = setField(this.console, 'name', ev.value)
|
||||||
break
|
break
|
||||||
case 'session':
|
case 'session':
|
||||||
@@ -143,6 +151,7 @@ export class State {
|
|||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'name':
|
case 'name':
|
||||||
|
ev.value = cleanName(ev.value) // the surface pads and wraps names: trim and collapse whitespace once, here
|
||||||
ev.changed = this.#applyName(ev)
|
ev.changed = this.#applyName(ev)
|
||||||
ev.structural = ev.changed
|
ev.structural = ev.changed
|
||||||
break
|
break
|
||||||
@@ -189,6 +198,7 @@ export class State {
|
|||||||
this.snapshots.count = ev.value
|
this.snapshots.count = ev.value
|
||||||
break
|
break
|
||||||
case 'snapName': {
|
case 'snapName': {
|
||||||
|
ev.name = cleanName(ev.name)
|
||||||
const old = this.snapshots.list.get(ev.index)
|
const old = this.snapshots.list.get(ev.index)
|
||||||
this.snapshots.list.set(ev.index, { name: ev.name, cue: ev.cue })
|
this.snapshots.list.set(ev.index, { name: ev.name, cue: ev.cue })
|
||||||
ev.changed = !old || old.name !== ev.name || old.cue !== ev.cue
|
ev.changed = !old || old.name !== ev.name || old.cue !== ev.cue
|
||||||
@@ -198,10 +208,10 @@ export class State {
|
|||||||
case 'meters': {
|
case 'meters': {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
for (const [id, value] of ev.pairs) {
|
for (const [id, value] of ev.pairs) {
|
||||||
const m = this.meters.get(id) || { value: 0, at: 0, peakAt: 0 }
|
const m = this.meters.get(id) || { value: 127, at: 0, loud: new Float64Array(LOUD_STEPS + 1) }
|
||||||
m.value = value
|
m.value = value
|
||||||
m.at = now
|
m.at = now
|
||||||
if (value >= 127) m.peakAt = now
|
for (let k = Math.max(0, value); k <= LOUD_STEPS; k++) m.loud[k] = now
|
||||||
this.meters.set(id, m)
|
this.meters.set(id, m)
|
||||||
}
|
}
|
||||||
ev.changed = ev.pairs.length > 0
|
ev.changed = ev.pairs.length > 0
|
||||||
@@ -254,6 +264,8 @@ export class State {
|
|||||||
return this.inputs
|
return this.inputs
|
||||||
case 'matrix':
|
case 'matrix':
|
||||||
return this.matrix
|
return this.matrix
|
||||||
|
case 'minput':
|
||||||
|
return this.minputs
|
||||||
case 'cg':
|
case 'cg':
|
||||||
return this.cgs
|
return this.cgs
|
||||||
case 'geq':
|
case 'geq':
|
||||||
@@ -286,7 +298,7 @@ export class State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An entity exists when its name answered (matrix inputs have no name: they exist while matrix outputs do). */
|
/** An entity exists when its name answered. */
|
||||||
has(ent, n) {
|
has(ent, n) {
|
||||||
if (!(n >= 1 && n <= this.limit(ent))) return false
|
if (!(n >= 1 && n <= this.limit(ent))) return false
|
||||||
switch (ent) {
|
switch (ent) {
|
||||||
@@ -294,8 +306,6 @@ export class State {
|
|||||||
const b = this.busses.get(n)
|
const b = this.busses.get(n)
|
||||||
return !!b && (b.auxName !== undefined || b.groupName !== undefined)
|
return !!b && (b.auxName !== undefined || b.groupName !== undefined)
|
||||||
}
|
}
|
||||||
case 'minput':
|
|
||||||
return this.limit('matrix') > 0 && (this.counts.matrixIn !== undefined || this.matrix.size > 0)
|
|
||||||
default:
|
default:
|
||||||
return this.#map(ent).get(n)?.name !== undefined
|
return this.#map(ent).get(n)?.name !== undefined
|
||||||
}
|
}
|
||||||
@@ -319,7 +329,6 @@ export class State {
|
|||||||
/** The name as the console reports it (may be ""), or undefined while unknown. */
|
/** The name as the console reports it (may be ""), or undefined while unknown. */
|
||||||
rawName(ent, n) {
|
rawName(ent, n) {
|
||||||
if (ent === 'buss') return this.busses.get(n)?.name
|
if (ent === 'buss') return this.busses.get(n)?.name
|
||||||
if (ent === 'minput') return undefined
|
|
||||||
return this.#map(ent).get(n)?.name
|
return this.#map(ent).get(n)?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +400,13 @@ export class State {
|
|||||||
return this.meters.get(id)
|
return this.meters.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Last time meter `id` read at or above `thresholdDb` dBFS (0 = never; thresholds from -20 to 0 are tracked). */
|
||||||
|
meterPeakAt(id, thresholdDb) {
|
||||||
|
const m = this.meters.get(id)
|
||||||
|
if (!m || !Number.isFinite(thresholdDb)) return 0
|
||||||
|
return m.loud[clampInt(Math.floor(-thresholdDb), 0, LOUD_STEPS)]
|
||||||
|
}
|
||||||
|
|
||||||
snapshotName(index) {
|
snapshotName(index) {
|
||||||
return this.snapshots.list.get(index)?.name
|
return this.snapshots.list.get(index)?.name
|
||||||
}
|
}
|
||||||
@@ -458,6 +474,7 @@ export class State {
|
|||||||
if (want('buss', n, 'group')) q.push(query(addr.name('buss', n, 'group')))
|
if (want('buss', n, 'group')) q.push(query(addr.name('buss', n, 'group')))
|
||||||
}
|
}
|
||||||
for (let n = 1; n <= this.limit('matrix'); n++) if (want('matrix', n)) q.push(query(addr.name('matrix', n)))
|
for (let n = 1; n <= this.limit('matrix'); n++) if (want('matrix', n)) q.push(query(addr.name('matrix', n)))
|
||||||
|
for (let n = 1; n <= this.limit('minput'); n++) if (want('minput', n)) q.push(query(addr.name('minput', n)))
|
||||||
for (let n = 1; n <= this.limit('cg'); n++) if (want('cg', n)) q.push(query(addr.name('cg', n)))
|
for (let n = 1; n <= this.limit('cg'); n++) if (want('cg', n)) q.push(query(addr.name('cg', n)))
|
||||||
for (let n = 1; n <= this.limit('geq'); n++) if (want('geq', n)) q.push(query(addr.name('geq', n)))
|
for (let n = 1; n <= this.limit('geq'); n++) if (want('geq', n)) q.push(query(addr.name('geq', n)))
|
||||||
return q
|
return q
|
||||||
@@ -493,6 +510,7 @@ export class State {
|
|||||||
aux: this.namedBusses('aux').length,
|
aux: this.namedBusses('aux').length,
|
||||||
group: this.namedBusses('group').length,
|
group: this.namedBusses('group').length,
|
||||||
matrix: found('matrix'),
|
matrix: found('matrix'),
|
||||||
|
matrixIn: found('minput'),
|
||||||
cgs: found('cg'),
|
cgs: found('cg'),
|
||||||
geqs: found('geq'),
|
geqs: found('geq'),
|
||||||
snapshots: this.snapshots.list.size,
|
snapshots: this.snapshots.list.size,
|
||||||
|
|||||||
+115
-23
@@ -20,7 +20,7 @@ import { ConsoleLink } from '../src/console-link.js'
|
|||||||
import { Relay } from '../src/relay.js'
|
import { Relay } from '../src/relay.js'
|
||||||
import { encode, decode, foldPattern } from '../src/osc.js'
|
import { encode, decode, foldPattern } from '../src/osc.js'
|
||||||
import { State } from '../src/state.js'
|
import { State } from '../src/state.js'
|
||||||
import { LEVEL, PARAMS, adjustedLevel, faderPosition, fmtCue, fmtDb, fmtPan, indexProblem, meterId, meterIdForInput, parse } from '../src/protocol.js'
|
import { LEVEL, PARAMS, adjustedLevel, cleanName, faderPosition, fmtCue, fmtDb, fmtMeterLevel, fmtPan, indexProblem, meterGrDb, meterId, meterIdForInput, meterLevelDb, meterPosition, outputMeterId, parse } from '../src/protocol.js'
|
||||||
import { BYTES_PER_PIXEL, horizontalBar, verticalMeter, thicknessFor } from '../src/image.js'
|
import { BYTES_PER_PIXEL, horizontalBar, verticalMeter, thicknessFor } from '../src/image.js'
|
||||||
import { MAX_PRESETS, MAX_PRESET_BYTES } from '../src/definitions/presets.js'
|
import { MAX_PRESETS, MAX_PRESET_BYTES } from '../src/definitions/presets.js'
|
||||||
import { COLORS } from '../src/definitions/feedbacks.js'
|
import { COLORS } from '../src/definitions/feedbacks.js'
|
||||||
@@ -298,6 +298,22 @@ check(typeof Instance === 'function' && Array.isArray(mainMod.UpgradeScripts), '
|
|||||||
s.applyMessage('/Console/Aux_Outputs/types', [1, 1, 1, 1, 0, 1])
|
s.applyMessage('/Console/Aux_Outputs/types', [1, 1, 1, 1, 0, 1])
|
||||||
check(s.bussMode(5) === 'group' && s.bussMode(6) === 'aux', 'the types array (1 = aux, 0 = group) overrides the name heuristic')
|
check(s.bussMode(5) === 'group' && s.bussMode(6) === 'aux', 'the types array (1 = aux, 0 = group) overrides the name heuristic')
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
// names arrive padded and wrapped: trimmed, whitespace runs collapsed, once, in the state
|
||||||
|
const s = new State()
|
||||||
|
check(cleanName(' LEAD V RESI \t') === 'LEAD V RESI' && cleanName('') === '' && cleanName(' ') === '' && cleanName(undefined) === '', 'cleanName trims and collapses whitespace runs')
|
||||||
|
const e1 = s.applyMessage('/Input_Channels/3/Channel_Input/name', ['MD RESI '])
|
||||||
|
s.applyMessage('/Matrix_Inputs/8/Channel_Input/name', ['Dante In 63 '])
|
||||||
|
s.applyMessage('/Control_Groups/2/name', [' MC & SPEAKER'])
|
||||||
|
s.applyMessage('/Snapshots/name', [0, 100, 0, ' Pre Service '])
|
||||||
|
s.applyMessage('/Console/Name', ['S31 AB '])
|
||||||
|
s.loaded = true
|
||||||
|
check(s.rawName('input', 3) === 'MD RESI' && e1.value === 'MD RESI' && e1.changed === true && s.rawName('minput', 8) === 'Dante In 63' && s.rawName('cg', 2) === 'MC & SPEAKER' && s.snapshotName(0) === 'Pre Service' && s.console.name === 'S31 AB', 'input, matrix input, control group, snapshot and console names are cleaned when they arrive')
|
||||||
|
check(s.applyMessage('/Input_Channels/3/Channel_Input/name', ['MD RESI']).changed === false && s.applyMessage('/Input_Channels/3/Channel_Input/name', ['MD RESI']).structural === false, 'a name that only differs in padding is no change (no rebuild)')
|
||||||
|
check(s.choices.inputs()[0].label === '3 · MD RESI' && s.choices.matrixInputs()[0].label === '8 · Dante In 63' && s.displayName('minput', 8) === 'Dante In 63' && s.displayName('minput', 3) === 'Matrix In 3', 'dropdown labels use the cleaned names; an unnamed matrix input reads "Matrix In N"')
|
||||||
|
s.applyMessage('/Matrix_Inputs/4/Channel_Input/name', [' '])
|
||||||
|
check(s.has('minput', 4) && s.rawName('minput', 4) === '' && s.displayName('minput', 4) === 'Matrix In 4', 'a blank matrix input name still exists and falls back to "Matrix In N"')
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 3. Mock console, the real instance, a fake iPad
|
// 3. Mock console, the real instance, a fake iPad
|
||||||
@@ -361,9 +377,12 @@ check(S.busses.get(1).stereo === true && S.busses.get(4).stereo === false, 'buss
|
|||||||
check(S.cgs.get(1)?.name === 'DRUMS' && S.cgs.get(10)?.name === '' && S.cgs.get(0) === undefined && S.choices.cgs()[0].id === 1 && S.choices.cgs()[0].label === '1 · DRUMS', 'control groups are 1-based (1 = DRUMS, 10 = last, no 0)')
|
check(S.cgs.get(1)?.name === 'DRUMS' && S.cgs.get(10)?.name === '' && S.cgs.get(0) === undefined && S.choices.cgs()[0].id === 1 && S.choices.cgs()[0].label === '1 · DRUMS', 'control groups are 1-based (1 = DRUMS, 10 = last, no 0)')
|
||||||
check(!sent.some((m) => /Control_Groups\/0\//.test(m.address)), 'the module never addresses control group 0')
|
check(!sent.some((m) => /Control_Groups\/0\//.test(m.address)), 'the module never addresses control group 0')
|
||||||
check(!sent.slice(0, loadSent).some((m) => /^\/Console\/(Input_Channels|Aux_Outputs|Group_Outputs)\/\?$/.test(m.address)), 'the unanswered Input_Channels / Aux_Outputs / Group_Outputs count queries are never sent')
|
check(!sent.slice(0, loadSent).some((m) => /^\/Console\/(Input_Channels|Aux_Outputs|Group_Outputs)\/\?$/.test(m.address)), 'the unanswered Input_Channels / Aux_Outputs / Group_Outputs count queries are never sent')
|
||||||
check(S.choices.matrixInputs()[0].label === '1 · Matrix In 1', `matrix inputs (no name address) are labelled "Matrix In N" (${S.choices.matrixInputs()[0].label})`)
|
const mIn = S.choices.matrixInputs()
|
||||||
|
check(mIn.length === 10 && mIn[0].label === '1 · TB' && mIn[1].label === '2 · Studio L' && mIn[6].label === '7 · Studio TB' && mIn[7].label === '8 · Dante In 63' && mIn[9].label === '10 · Matrix In 10', `matrix inputs have names: dropdown labels "N · name", padding and runs of spaces cleaned, "Matrix In N" when empty (${mIn.map((c) => c.label).join(' | ')})`)
|
||||||
|
check(bussChoices.find((c) => c.id === 17)?.label === '17 · Studio TB (aux)' && S.rawName('buss', 17) === 'Studio TB', 'a padded buss name ("Studio TB") shows with single spaces in the dropdown')
|
||||||
|
check(sent.slice(0, loadSent).some((m) => m.address === '/Matrix_Inputs/10/Channel_Input/name/?') && !sent.some((m) => /^\/Matrix_Inputs\/\d+\/(Buss_Trim\/name|name)\/\?$/.test(m.address)), 'matrix input names are queried at /Matrix_Inputs/N/Channel_Input/name')
|
||||||
check(!sent.slice(0, loadSent).some((m) => /\/(Filters|Channel_Delay|Insert|Dynamics|EQ)\/|phantom|input_pad|analog_gain|\/trim|\/phase|\/Aux_Send\//.test(m.address)), 'channel parameters and sends are not bulk-loaded at connect')
|
check(!sent.slice(0, loadSent).some((m) => /\/(Filters|Channel_Delay|Insert|Dynamics|EQ)\/|phantom|input_pad|analog_gain|\/trim|\/phase|\/Aux_Send\//.test(m.address)), 'channel parameters and sends are not bulk-loaded at connect')
|
||||||
check(S.matrix.get(1)?.name === 'MAINS L' && S.has('minput', 1) && S.choices.matrixInputs().length === 10, 'matrix outputs named; matrix inputs (no name address) listed from the count')
|
check(S.matrix.get(1)?.name === 'MAINS L' && S.has('minput', 1) && S.minputs.get(10).name === '' && S.numbers('minput').length === 10, 'matrix outputs and matrix inputs are named (input 10 has an empty name and still exists)')
|
||||||
check(S.inputs.get(7).stereo === true && S.inputs.get(1).stereo === false, 'input stereo flags come from /Console/Input_Channels/modes')
|
check(S.inputs.get(7).stereo === true && S.inputs.get(1).stereo === false, 'input stereo flags come from /Console/Input_Channels/modes')
|
||||||
check(S.console.name === 'S31 AB' && S.console.session === 'Mock Session.session', 'console and session name loaded')
|
check(S.console.name === 'S31 AB' && S.console.session === 'Mock Session.session', 'console and session name loaded')
|
||||||
|
|
||||||
@@ -375,6 +394,8 @@ check(S.console.name === 'S31 AB' && S.console.session === 'Mock Session.session
|
|||||||
/^\/Console\/Aux_Outputs\/types$/, // design: seen in a live iPad session, not probed in the catalog
|
/^\/Console\/Aux_Outputs\/types$/, // design: seen in a live iPad session, not probed in the catalog
|
||||||
/^\/Snapshots\/(Fire_Snapshot_number|Fire_Next_Snapshot|Fire_Prev_Snapshot)$/, // write-only trigger nodes
|
/^\/Snapshots\/(Fire_Snapshot_number|Fire_Next_Snapshot|Fire_Prev_Snapshot)$/, // write-only trigger nodes
|
||||||
/^\/Input_Channels\/N\/(Channel_Input\/post_meter|Dynamics\/GR_meter)$/, // meter subscriptions captured from an iPad session
|
/^\/Input_Channels\/N\/(Channel_Input\/post_meter|Dynamics\/GR_meter)$/, // meter subscriptions captured from an iPad session
|
||||||
|
/^\/(Aux_Outputs|Group_Outputs|Matrix_Outputs)\/N\/Output\/meter$/, // output meter subscriptions (iPad app, confirmed live)
|
||||||
|
/^\/Matrix_Inputs\/N\/Channel_Input\/(name|trim|phase)$/, // matrix input names and params, verified live after the catalog was written
|
||||||
]
|
]
|
||||||
const offenders = new Set()
|
const offenders = new Set()
|
||||||
const check1 = (a) => {
|
const check1 = (a) => {
|
||||||
@@ -390,6 +411,7 @@ check(S.console.name === 'S31 AB' && S.console.session === 'Mock Session.session
|
|||||||
// Variables
|
// Variables
|
||||||
await sleep(80)
|
await sleep(80)
|
||||||
const V = ctx.variables
|
const V = ctx.variables
|
||||||
|
check(V.matrix_in_1_name === 'TB' && V.matrix_in_7_name === 'Studio TB' && V.matrix_in_10_name === 'Matrix In 10' && 'matrix_in_10_name' in ctx.variableDefs, 'matrix input name variables matrix_in_N_name: cleaned, with the Matrix In N fallback')
|
||||||
check(V.input_1_name === 'KICK In' && V.buss_24_name === 'Studio' && V.buss_24_mode === 'Group' && V.buss_1_stereo === 'Stereo' && V.cg_1_name === 'DRUMS' && V.matrix_1_name === 'MAINS L', 'variables carry live names and modes')
|
check(V.input_1_name === 'KICK In' && V.buss_24_name === 'Studio' && V.buss_24_mode === 'Group' && V.buss_1_stereo === 'Stereo' && V.cg_1_name === 'DRUMS' && V.matrix_1_name === 'MAINS L', 'variables carry live names and modes')
|
||||||
check(V.snapshot_name === 'Worship 2' && V.snapshot_index === 3 && V.snapshot_cue === '4' && V.snapshot_count === 8 && V.console_name === 'S31 AB' && V.session_name === 'Mock Session.session' && V.connected === 'Yes', 'snapshot / console / connection variables')
|
check(V.snapshot_name === 'Worship 2' && V.snapshot_index === 3 && V.snapshot_cue === '4' && V.snapshot_count === 8 && V.console_name === 'S31 AB' && V.session_name === 'Mock Session.session' && V.connected === 'Yes', 'snapshot / console / connection variables')
|
||||||
check(typeof V.input_1_fader === 'string' && V.input_1_fader !== '' && ['Muted', 'Unmuted'].includes(V.input_1_mute) && V.input_1_pan === 'C', 'input fader / mute / pan variables are filled')
|
check(typeof V.input_1_fader === 'string' && V.input_1_fader !== '' && ['Muted', 'Unmuted'].includes(V.input_1_mute) && V.input_1_pan === 'C', 'input fader / mute / pan variables are filled')
|
||||||
@@ -416,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', 'input_signal', 'input_peak', 'input_fader_value', 'input_pan_value', 'send_level_value', 'send_on_value', 'buss_fader_value', 'matrix_fader_value', 'cg_fader_value', 'snapshot_name_value', 'input_meter_value', 'input_fader_bar', 'send_level_bar', 'buss_fader_bar', 'input_meter_bar']
|
const fbExpected = ['input_muted', 'input_solo', 'send_on', 'group_assigned', 'buss_muted', 'buss_solo', 'matrix_muted', 'cg_muted', 'snapshot_current', 'level_compare', 'send_level_compare', 'connected', 'relay_clients', 'input_signal', 'input_peak', 'input_fader_value', 'input_pan_value', 'send_level_value', 'send_on_value', 'buss_fader_value', 'matrix_fader_value', 'cg_fader_value', 'snapshot_name_value', 'input_meter_value', 'input_fader_bar', 'send_level_bar', 'buss_fader_bar', 'input_meter_bar']
|
||||||
|
fbExpected.push('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', 'geq_on', 'send_pan_value', 'matrix_send_level_value', 'geq_band_value', 'matrix_fader_bar', 'cg_fader_bar', 'matrix_send_level_bar', 'geq_band_bar')
|
fbExpected.push('matrix_solo', 'cg_solo', 'matrix_send_on', 'geq_on', 'send_pan_value', 'matrix_send_level_value', 'geq_band_value', 'matrix_fader_bar', 'cg_fader_bar', 'matrix_send_level_bar', 'geq_band_bar')
|
||||||
for (const [param, p] of Object.entries(PARAMS)) for (const ent of Object.keys(p.paths)) fbExpected.push(`${ent}_${param}_${p.type === 'switch' ? 'state' : 'value'}`)
|
for (const [param, p] of Object.entries(PARAMS)) for (const ent of Object.keys(p.paths)) fbExpected.push(`${ent}_${param}_${p.type === 'switch' ? 'state' : 'value'}`)
|
||||||
@@ -514,14 +537,14 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
check(msendSections.length === 8 && msendSections[7].name === `Matrix sends to ${S.displayName('matrix', 8)}` && msendOn.length === 80 && msendKnob.length === 80, `a "Matrix sends to <output>" section per matrix output (${msendSections.length}: "${msendSections[7]?.name}") with ${msendOn.length} on/off and ${msendKnob.length} knob presets`)
|
check(msendSections.length === 8 && msendSections[7].name === `Matrix sends to ${S.displayName('matrix', 8)}` && msendOn.length === 80 && msendKnob.length === 80, `a "Matrix sends to <output>" section per matrix output (${msendSections.length}: "${msendSections[7]?.name}") with ${msendOn.length} on/off and ${msendKnob.length} knob presets`)
|
||||||
const mOn = P.msend_on_1_8
|
const mOn = P.msend_on_1_8
|
||||||
const mKnob = P.msend_knob_1_8
|
const mKnob = P.msend_knob_1_8
|
||||||
check(mOn.style.text === 'Matrix In 1\\n$(local:level) dB' && mOn.localVariables[0].feedbackId === 'matrix_send_level_value' && JSON.stringify(mOn.localVariables[0].options) === JSON.stringify({ matrixIn: 1, matrixOut: 8 }) && mOn.steps[0].down[0].actionId === 'matrix_send_on' && mOn.steps[0].down[0].options.state === 'toggle' && mOn.feedbacks.some((f) => f.feedbackId === 'matrix_send_on' && f.style.bgcolor === COLORS_GREEN), 'matrix send on/off preset: toggle matrix_send_on, green when on, text from the level value feedback')
|
check(mOn.style.text === '$(S31:matrix_in_1_name)\\n$(local:level) dB' && mOn.name === 'Matrix send on/off: TB → 8 · Studio' && mOn.localVariables[0].feedbackId === 'matrix_send_level_value' && JSON.stringify(mOn.localVariables[0].options) === JSON.stringify({ matrixIn: 1, matrixOut: 8 }) && mOn.steps[0].down[0].actionId === 'matrix_send_on' && mOn.steps[0].down[0].options.state === 'toggle' && mOn.feedbacks.some((f) => f.feedbackId === 'matrix_send_on' && f.style.bgcolor === COLORS_GREEN), 'matrix send on/off preset: toggle matrix_send_on, green when on, text from the level value feedback')
|
||||||
check(mKnob.steps[0].rotate_left[0].actionId === 'matrix_send_level_adjust' && mKnob.steps[0].rotate_left[0].options.delta === -1 && mKnob.steps[0].rotate_right[0].options.delta === 1 && mKnob.steps[0].down[0].actionId === 'matrix_send_on' && mKnob.type === 'layered' && mKnob.feedbacks[0].feedbackId === 'matrix_send_on' && mKnob.feedbacks[0].styleOverrides[0].elementId === 'bg' && mKnob.localVariables.map((v) => `${v.variableName}=${v.feedbackId}`).join() === 'level=matrix_send_level_value,pos=matrix_send_level_pos', 'matrix send knob preset (layered): rotate = matrix_send_level_adjust -1/+1, press toggles on/off, on tints the background, level and gauge position local variables')
|
check(mKnob.steps[0].rotate_left[0].actionId === 'matrix_send_level_adjust' && mKnob.steps[0].rotate_left[0].options.delta === -1 && mKnob.steps[0].rotate_right[0].options.delta === 1 && mKnob.steps[0].down[0].actionId === 'matrix_send_on' && mKnob.type === 'layered' && mKnob.feedbacks[0].feedbackId === 'matrix_send_on' && mKnob.feedbacks[0].styleOverrides[0].elementId === 'bg' && mKnob.localVariables.map((v) => `${v.variableName}=${v.feedbackId}`).join() === 'level=matrix_send_level_value,pos=matrix_send_level_pos', 'matrix send knob preset (layered): rotate = matrix_send_level_adjust -1/+1, press toggles on/off, on tints the background, level and gauge position local variables')
|
||||||
check(!!P.geq_in_1 && P.geq_in_1.steps[0].down[0].actionId === 'geq_on' && P.geq_in_1.feedbacks[0].feedbackId === 'geq_on' && ids.filter((id) => id.startsWith('geq_in_')).length === 16, 'a Graphic EQs section with an in/out toggle and a geq_on feedback per graphic EQ')
|
check(!!P.geq_in_1 && P.geq_in_1.steps[0].down[0].actionId === 'geq_on' && P.geq_in_1.feedbacks[0].feedbackId === 'geq_on' && ids.filter((id) => id.startsWith('geq_in_')).length === 16, 'a Graphic EQs section with an in/out toggle and a geq_on feedback per graphic EQ')
|
||||||
// layered presets: element types and required fields from the Companion graphics definitions
|
// layered presets: element types and required fields from the Companion graphics definitions
|
||||||
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.length === knobsLayered.length + 180 && !ids.some((id) => /_(mute|solo|on)_/.test(id) && P[id].type === 'layered'), `every knob (${knobsLayered.length}) and meter (${meterPresets.length}) preset is layered; plain toggles stay simple`)
|
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`)
|
||||||
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'] }
|
const LAYERED_PROPS = { box: ['id', 'type', 'x', 'y', 'width', 'height', 'color'], text: ['id', 'type', 'x', 'y', 'width', 'height', 'text', 'fontsize', 'fontsizeAllowShrink', 'color', 'halign', 'valign'], gauge: ['id', 'type', 'x', 'y', 'width', 'height', 'value', 'min', 'max', 'orientation', 'trackStyle', 'fillEnabled', 'multiColour', 'stops'] }
|
||||||
const isExpr = (v) => !!v && typeof v === 'object' && v.isExpression === true && typeof v.value === 'string'
|
const isExpr = (v) => !!v && typeof v === 'object' && v.isExpression === true && typeof v.value === 'string'
|
||||||
@@ -756,6 +779,13 @@ function expectWrite(list, address, tags, value, label) {
|
|||||||
expectWrite(r, '/Group_Outputs/24/Buss_Trim/trim', ',f', 1, 'buss_trim_adjust from the replied 0')
|
expectWrite(r, '/Group_Outputs/24/Buss_Trim/trim', ',f', 1, 'buss_trim_adjust from the replied 0')
|
||||||
r = await run('matrix_trim_set', { matrix: 2, value: 1.5 })
|
r = await run('matrix_trim_set', { matrix: 2, value: 1.5 })
|
||||||
expectWrite(r, '/Matrix_Outputs/2/Buss_Trim/trim', ',f', 1.5, 'matrix_trim_set')
|
expectWrite(r, '/Matrix_Outputs/2/Buss_Trim/trim', ',f', 1.5, 'matrix_trim_set')
|
||||||
|
r = await run('minput_trim_set', { matrixIn: 3, value: -2 })
|
||||||
|
expectWrite(r, '/Matrix_Inputs/3/Channel_Input/trim', ',f', -2, 'minput_trim_set (matrix input trim, dB)')
|
||||||
|
r = await run('minput_trim_adjust', { matrixIn: 3, delta: 0.5 })
|
||||||
|
expectWrite(r, '/Matrix_Inputs/3/Channel_Input/trim', ',f', -1.5, 'minput_trim_adjust')
|
||||||
|
r = await run('minput_phase', { matrixIn: 3, state: 'toggle' })
|
||||||
|
check(r[0]?.address === '/Matrix_Inputs/3/Channel_Input/phase/?', 'minput_phase toggle on an unloaded switch queries first')
|
||||||
|
expectWrite(r, '/Matrix_Inputs/3/Channel_Input/phase', ',f', 1, 'minput_phase toggle writes a float 1 (the replied state was 0)')
|
||||||
r = await run('buss_eq_in', { buss: 3, state: 'toggle' })
|
r = await run('buss_eq_in', { buss: 3, state: 'toggle' })
|
||||||
check(r[0]?.address === '/Aux_Outputs/3/EQ/eq_in/?' && writes(r).some((m) => m.address === '/Aux_Outputs/3/EQ/eq_in' && m.tags === ',f'), 'buss_eq_in toggle on an unloaded aux buss queries first')
|
check(r[0]?.address === '/Aux_Outputs/3/EQ/eq_in/?' && writes(r).some((m) => m.address === '/Aux_Outputs/3/EQ/eq_in' && m.tags === ',f'), 'buss_eq_in toggle on an unloaded aux buss queries first')
|
||||||
r = await run('buss_insert_a', { buss: 9, state: 'on' })
|
r = await run('buss_insert_a', { buss: 9, state: 'on' })
|
||||||
@@ -894,6 +924,7 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
await sleep(150)
|
await sleep(150)
|
||||||
check(typeof cb('send_level_pos', { channel: 45, buss: 9 }) === 'number' && S.getSend(45, 9).level !== undefined && S.getMatrixSend(7, 6).level === -90 && cb('matrix_send_level_pos', { matrixIn: 7, matrixOut: 6 }) === 0 && typeof cb('send_pan_num', { channel: 45, buss: 9 }) === 'number' && cb('geq_band_db', { geq: 4, band: 9 }) === 0, 'once the state arrives the position feedbacks return the real values (a matrix send at -90 stays 0)')
|
check(typeof cb('send_level_pos', { channel: 45, buss: 9 }) === 'number' && S.getSend(45, 9).level !== undefined && S.getMatrixSend(7, 6).level === -90 && cb('matrix_send_level_pos', { matrixIn: 7, matrixOut: 6 }) === 0 && typeof cb('send_pan_num', { channel: 45, buss: 9 }) === 'number' && cb('geq_band_db', { geq: 4, band: 9 }) === 0, 'once the state arrives the position feedbacks return the real values (a matrix send at -90 stays 0)')
|
||||||
check(cb('input_trim_value', { channel: 14 }) === '-3' && cb('input_gain_value', { channel: 14 }) === '34.5' && cb('input_hpf_freq_value', { channel: 14 }) === '88' && cb('buss_trim_value', { buss: 1 }) === '-2' && cb('matrix_trim_value', { matrix: 2 }) === '1.5', 'channel-strip value feedbacks: trim, gain, filter frequency (whole Hz), buss and matrix trim')
|
check(cb('input_trim_value', { channel: 14 }) === '-3' && cb('input_gain_value', { channel: 14 }) === '34.5' && cb('input_hpf_freq_value', { channel: 14 }) === '88' && cb('buss_trim_value', { buss: 1 }) === '-2' && cb('matrix_trim_value', { matrix: 2 }) === '1.5', 'channel-strip value feedbacks: trim, gain, filter frequency (whole Hz), buss and matrix trim')
|
||||||
|
check(cb('minput_trim_value', { matrixIn: 3 }) === '-1.5' && cb('minput_phase_state', { matrixIn: 3 }) === true && cb('minput_phase_state', { matrixIn: 4 }) === false, 'matrix input trim text and phase feedback follow the writes')
|
||||||
check(cb('input_lpf_freq_value', { channel: 60 }) === '--' && (await waitFor(() => S.getParam('input', 60, 'lpf_freq') !== undefined, 2500)) && cb('input_lpf_freq_value', { channel: 60 }) === '12000', 'an unloaded channel-strip value reads "--" and loads lazily')
|
check(cb('input_lpf_freq_value', { channel: 60 }) === '--' && (await waitFor(() => S.getParam('input', 60, 'lpf_freq') !== undefined, 2500)) && cb('input_lpf_freq_value', { channel: 60 }) === '12000', 'an unloaded channel-strip value reads "--" and loads lazily')
|
||||||
check(cb('send_level_value', { channel: 40, buss: 8 }) === '--' && cb('send_on', { channel: 40, buss: 8 }) === false, 'an unloaded send pair reads as unknown ("--" / false) and is queued for loading')
|
check(cb('send_level_value', { channel: 40, buss: 8 }) === '--' && cb('send_on', { channel: 40, buss: 8 }) === false, 'an unloaded send pair reads as unknown ("--" / false) and is queued for loading')
|
||||||
check(await waitFor(() => sent.some((m) => m.address === '/Input_Channels/40/Aux_Send/8/send_level/?'), 2000), 'the unloaded pair is queried once its feedback runs')
|
check(await waitFor(() => sent.some((m) => m.address === '/Input_Channels/40/Aux_Send/8/send_level/?'), 2000), 'the unloaded pair is queried once its feedback runs')
|
||||||
@@ -926,53 +957,98 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 7. Meters
|
// 7. Meters (scale: a level value is about the dB below full scale, 127 = silence; gain reduction is dB, 127 = inactive)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
{
|
{
|
||||||
const id7 = meterIdForInput(7)
|
const id7 = meterIdForInput(7)
|
||||||
check(id7 >= 5000 && meterIdForInput(60) === 5060, `meter ids start at 5000 (input 7 -> ${id7})`)
|
check(id7 >= 5000 && meterIdForInput(60) === 5060 && outputMeterId('buss', 3) === 5403 && outputMeterId('matrix', 2) === 5452, `meter ids start at 5000 (input 7 -> ${id7}); output meters use 5400 + buss and 5450 + matrix output`)
|
||||||
|
// the scale helpers
|
||||||
|
check(meterLevelDb(42) === -42 && meterLevelDb(0) === 0 && meterLevelDb(127) === -Infinity && meterLevelDb(undefined) === -Infinity && meterGrDb(3) === 3 && meterGrDb(127) === 0 && meterGrDb(undefined) === 0, 'meterLevelDb: 127 = -inf, v = -v dBFS; meterGrDb: 127 = no reduction, v = v dB')
|
||||||
|
check(fmtMeterLevel(42) === '-42' && fmtMeterLevel(127) === '-inf' && fmtMeterLevel(0) === '0' && meterPosition(127) === 0 && meterPosition(60) === 0 && near(meterPosition(42), 30) && meterPosition(0) === 100 && near(meterPosition(6, 'gr'), 30) && meterPosition(127, 'gr') === 0 && meterPosition(30, 'gr') === 100, 'meter text and positions: -42 -> "-42" and 30 %, 0 -> 100 %, 127 -> "-inf" and 0 %, GR 6 dB -> 30 %')
|
||||||
const mark = sent.length
|
const mark = sent.length
|
||||||
cb('input_meter_value', { channel: 7, unit: 'raw' })
|
cb('input_meter_value', { channel: 7 })
|
||||||
await sleep(80)
|
await sleep(80)
|
||||||
const sub = sent.slice(mark).find((m) => m.address === '/Input_Channels/7/Channel_Input/post_meter')
|
const sub = sent.slice(mark).find((m) => m.address === '/Input_Channels/7/Channel_Input/post_meter')
|
||||||
const subWire = sub && decodeArgs(sub)
|
const subWire = sub && decodeArgs(sub)
|
||||||
check(!!subWire && subWire.tags === ',ii' && subWire.args[0] === id7 && subWire.args[1] === 1, `the first meter feedback subscribes lazily: ${JSON.stringify(subWire)}`)
|
check(!!subWire && subWire.tags === ',ii' && subWire.args[0] === id7 && subWire.args[1] === 1, `the first meter feedback subscribes lazily: ${JSON.stringify(subWire)}`)
|
||||||
cb('input_meter_value', { channel: 7, unit: 'raw' })
|
cb('input_meter_value', { channel: 7 })
|
||||||
check(sent.slice(mark).filter((m) => m.address === '/Input_Channels/7/Channel_Input/post_meter').length === 1, 'a meter is subscribed only once')
|
check(sent.slice(mark).filter((m) => m.address === '/Input_Channels/7/Channel_Input/post_meter').length === 1, 'a meter is subscribed only once')
|
||||||
check(await waitFor(() => S.getMeter(id7), 3000), 'the console streams /Meters/values for the subscribed id')
|
check(await waitFor(() => S.getMeter(id7), 3000), 'the console streams /Meters/values for the subscribed id')
|
||||||
|
check(S.getMeter(id7).value === 127 && cb('input_meter_value', { channel: 7 }) === '-inf' && cb('input_meter_pos', { channel: 7 }) === 0 && cb('input_signal', { channel: 7 }) === false && cb('input_peak', { channel: 7 }) === false, 'a silent channel (127) reads -inf, position 0, no signal, no peak')
|
||||||
|
// a live channel streams 20-60 (the mock has inputs 1-3 live)
|
||||||
|
cb('input_meter_value', { channel: 2 })
|
||||||
|
check(await waitFor(() => S.getMeter(meterIdForInput(2)), 3000) && S.getMeter(meterIdForInput(2)).value >= 20 && S.getMeter(meterIdForInput(2)).value <= 60 && /^-(2\d|[3-5]\d|60)$/.test(cb('input_meter_value', { channel: 2 })) && cb('input_signal', { channel: 2, thresholdDb: -70 }) === true, 'a live channel streams 20-60: the text reads -20 .. -60 dBFS and signal is present')
|
||||||
{
|
{
|
||||||
const m2 = sent.length
|
const m2 = sent.length
|
||||||
cb('input_meter_value', { channel: 7, meter: 'gate', unit: 'raw' })
|
cb('input_meter_value', { channel: 7, meter: 'gate' })
|
||||||
cb('input_peak', { channel: 7, meter: 'comp', hold: 1 })
|
cb('input_meter_pos', { channel: 7, meter: 'comp' })
|
||||||
await sleep(80)
|
await sleep(80)
|
||||||
const gate = sent.slice(m2).find((m) => m.address === '/Input_Channels/7/Dynamics/GR_meter' && m.args[1].i === 1)
|
const gate = sent.slice(m2).find((m) => m.address === '/Input_Channels/7/Dynamics/GR_meter' && m.args[1].i === 1)
|
||||||
const comp = sent.slice(m2).find((m) => m.address === '/Input_Channels/7/Dynamics/GR_meter' && m.args[1].i === 4)
|
const comp = sent.slice(m2).find((m) => m.address === '/Input_Channels/7/Dynamics/GR_meter' && m.args[1].i === 4)
|
||||||
check(gate?.args[0].i === meterId(7, 'gate') && comp?.args[0].i === meterId(7, 'comp') && meterId(7, 'gate') >= 5000 && meterId(7, 'comp') !== meterId(7, 'gate'), `gate and compressor gain-reduction meters subscribe with kinds 1 and 4 (ids ${meterId(7, 'gate')} / ${meterId(7, 'comp')})`)
|
check(gate?.args[0].i === meterId(7, 'gate') && comp?.args[0].i === meterId(7, 'comp') && meterId(7, 'gate') >= 5000 && meterId(7, 'comp') !== meterId(7, 'gate'), `gate and compressor gain-reduction meters subscribe with kinds 1 and 4 (ids ${meterId(7, 'gate')} / ${meterId(7, 'comp')})`)
|
||||||
check(await waitFor(() => S.getMeter(meterId(7, 'gate')) && S.getMeter(meterId(7, 'comp')), 3000), 'gain-reduction meters stream too')
|
check(await waitFor(() => S.getMeter(meterId(7, 'gate')) && S.getMeter(meterId(7, 'comp')), 3000), 'gain-reduction meters stream too')
|
||||||
|
const g = S.getMeter(meterId(7, 'gate')).value
|
||||||
|
check(g === 127 || (g >= 0 && g <= 12), `gain reduction streams 0-12 dB, or 127 when inactive (${g})`)
|
||||||
}
|
}
|
||||||
const m = S.getMeter(id7)
|
const m = S.getMeter(id7)
|
||||||
check(m.value >= 0 && m.value <= 127, `meter values are 0..127 (${m.value})`)
|
check(m.value >= 0 && m.value <= 127, `meter values are 0..127 (${m.value})`)
|
||||||
|
|
||||||
// deterministic values injected as if from the console
|
// deterministic values injected as if from the console
|
||||||
const inject = (value) => inst.onConsoleMessage(decode(encode('/Meters/values', [{ i: id7 }, { i: value }])), { address: '127.0.0.1', port: P.mock })
|
const inject = (value, id = id7) => inst.onConsoleMessage(decode(encode('/Meters/values', [{ i: id }, { i: value }])), { address: '127.0.0.1', port: P.mock })
|
||||||
inject(127)
|
inject(127)
|
||||||
check(cb('input_meter_pos', { channel: 7, meter: 'level' }) === 100, 'input_meter_pos is 100 at full scale (127)')
|
check(cb('input_meter_value', { channel: 7 }) === '-inf' && cb('input_meter_pos', { channel: 7 }) === 0 && cb('input_signal', { channel: 7 }) === false && cb('input_peak', { channel: 7 }) === false, 'silence (127): -inf, position 0 %, no signal, no peak')
|
||||||
inject(64)
|
inject(0)
|
||||||
check(near(cb('input_meter_pos', { channel: 7 }), (64 / 127) * 100) && cb('input_meter_pos', { channel: 7, meter: 'comp' }) >= 0, 'input_meter_pos is 0..100 from 0..127 and takes the level / gate / comp option')
|
check(cb('input_meter_value', { channel: 7 }) === '0' && cb('input_meter_pos', { channel: 7 }) === 100 && cb('input_signal', { channel: 7 }) === true && cb('input_peak', { channel: 7, hold: 2 }) === true, 'full scale (0): 0 dBFS, position 100 %, signal and peak')
|
||||||
inject(127)
|
inject(42)
|
||||||
check(cb('input_meter_value', { channel: 7, unit: 'raw' }) === 127 && cb('input_meter_value', { channel: 7, unit: 'percent' }) === 100, 'input_meter_value raw and percent')
|
check(cb('input_meter_value', { channel: 7 }) === '-42' && near(cb('input_meter_pos', { channel: 7 }), 30), 'a value of 42 reads "-42" and sits at 30 % of the -60..0 scale')
|
||||||
check(cb('input_signal', { channel: 7, threshold: 5 }) === true && cb('input_peak', { channel: 7, hold: 2 }) === true, 'input_signal and input_peak at full scale')
|
check(cb('input_signal', { channel: 7, thresholdDb: -50 }) === true && cb('input_signal', { channel: 7, thresholdDb: -40 }) === false, 'signal is a dBFS threshold: -42 is above -50 and below -40')
|
||||||
inject(3)
|
check(cb('input_peak', { channel: 7, hold: 2 }) === true, 'the peak from a moment ago is still held after the level fell back')
|
||||||
check(cb('input_signal', { channel: 7, threshold: 5 }) === false && cb('input_peak', { channel: 7, hold: 2 }) === true, 'signal drops below the threshold while the peak is still held')
|
|
||||||
await sleep(30)
|
await sleep(30)
|
||||||
check(cb('input_peak', { channel: 7, hold: 0 }) === false, 'the peak hold expires')
|
check(cb('input_peak', { channel: 7, hold: 0 }) === false, 'the peak hold expires')
|
||||||
|
// the peak threshold is an option: 4 steps below full scale is a peak at -5 dBFS but not at -1
|
||||||
|
const id8 = meterIdForInput(8)
|
||||||
|
cb('input_meter_value', { channel: 8 })
|
||||||
|
await sleep(60)
|
||||||
|
inject(4, id8)
|
||||||
|
check(cb('input_peak', { channel: 8, peakDb: -5, hold: 2 }) === true && cb('input_peak', { channel: 8, peakDb: -1, hold: 2 }) === false && cb('input_peak', { channel: 8, hold: 2 }) === false, 'peak at or above a dBFS option: a reading of 4 peaks at -5 dBFS, not at the default -1')
|
||||||
|
// gain reduction meters
|
||||||
|
const gateId = meterId(7, 'gate')
|
||||||
|
inject(6, gateId)
|
||||||
|
check(cb('input_meter_value', { channel: 7, meter: 'gate' }) === '6' && near(cb('input_meter_pos', { channel: 7, meter: 'gate' }), 30) && cb('input_signal', { channel: 7, meter: 'gate' }) === true && cb('input_peak', { channel: 7, meter: 'gate' }) === false, 'gain reduction 6 dB reads "6", sits at 30 % of 0..20 dB and counts as signal; there is no peak')
|
||||||
|
inject(127, gateId)
|
||||||
|
check(cb('input_meter_value', { channel: 7, meter: 'gate' }) === '0' && cb('input_meter_pos', { channel: 7, meter: 'gate' }) === 0 && cb('input_signal', { channel: 7, meter: 'gate' }) === false, 'gain reduction 127 (inactive) reads "0", 0 % and no signal')
|
||||||
inject(64)
|
inject(64)
|
||||||
const bar = cb('input_meter_bar', { channel: 7 }, { width: 72, height: 72 })
|
const bar = cb('input_meter_bar', { channel: 7 }, { width: 72, height: 72 })
|
||||||
check(Buffer.from(bar.imageBuffer, 'base64').length === thicknessFor(72) * 72 * 4 && bar.imageBufferPosition.x === 72 - thicknessFor(72), 'input_meter_bar returns a right-edge strip of the right size')
|
check(Buffer.from(bar.imageBuffer, 'base64').length === thicknessFor(72) * 72 * 4 && bar.imageBufferPosition.x === 72 - thicknessFor(72), 'input_meter_bar (legacy) returns a right-edge strip of the right size')
|
||||||
|
|
||||||
|
// output meters: buss (the address follows the aux / group side) and matrix output
|
||||||
|
{
|
||||||
|
const m3 = sent.length
|
||||||
|
cb('buss_meter_value', { buss: 3 })
|
||||||
|
cb('buss_meter_pos', { buss: 9 })
|
||||||
|
cb('matrix_meter_value', { matrix: 2 })
|
||||||
|
cb('buss_meter_value', { buss: 3 })
|
||||||
|
await sleep(100)
|
||||||
|
const subs = sent.slice(m3).filter((x) => /\/Output\/meter$/.test(x.address)).map((x) => `${x.address} ${JSON.stringify(decodeArgs(x).args)}`)
|
||||||
|
check(subs.join('|') === '/Aux_Outputs/3/Output/meter [5403,1]|/Group_Outputs/9/Output/meter [5409,1]|/Matrix_Outputs/2/Output/meter [5452,1]', `output meters subscribe lazily and once: aux buss 3, group buss 9 (Group_Outputs side), matrix output 2 (${subs.join(' | ')})`)
|
||||||
|
check(await waitFor(() => S.getMeter(5403) && S.getMeter(5409) && S.getMeter(5452), 3000) && S.getMeter(5403).value === 127, 'output meters stream; a silent output reads 127')
|
||||||
|
cb('buss_meter_value', { buss: 1 })
|
||||||
|
check(await waitFor(() => S.getMeter(5401), 3000) && S.getMeter(5401).value >= 20 && S.getMeter(5401).value <= 60 && /^-(2\d|[3-5]\d|60)$/.test(cb('buss_meter_value', { buss: 1 })), 'a live output (aux buss 1) reads -20 .. -60 dBFS')
|
||||||
|
inject(30, 5403)
|
||||||
|
check(cb('buss_meter_value', { buss: 3 }) === '-30' && near(cb('buss_meter_pos', { buss: 3 }), 50) && cb('buss_signal', { buss: 3 }) === true && cb('buss_peak', { buss: 3 }) === false, 'buss meter: 30 -> "-30", 50 %, signal, no peak')
|
||||||
|
inject(1, 5403)
|
||||||
|
check(cb('buss_peak', { buss: 3 }) === true && cb('buss_signal', { buss: 3, thresholdDb: -0.5 }) === false, 'buss meter: a reading of 1 (-1 dBFS) peaks; signal needs to exceed its threshold')
|
||||||
|
inject(127, 5403)
|
||||||
|
check(cb('buss_meter_value', { buss: 3 }) === '-inf' && cb('buss_meter_pos', { buss: 3 }) === 0 && cb('buss_signal', { buss: 3 }) === false, 'buss meter: 127 is -inf, 0 %, no signal')
|
||||||
|
inject(50, 5452)
|
||||||
|
check(cb('matrix_meter_value', { matrix: 2 }) === '-50' && near(cb('matrix_meter_pos', { matrix: 2 }), (10 / 60) * 100) && cb('matrix_signal', { matrix: 2, thresholdDb: -60 }) === true && cb('matrix_signal', { matrix: 2 }) === false && cb('matrix_peak', { matrix: 2 }) === false, 'matrix output meter: 50 -> "-50", position over -60..0, signal against the dBFS option')
|
||||||
|
inject(0, 5452)
|
||||||
|
check(cb('matrix_peak', { matrix: 2 }) === true && cb('matrix_meter_pos', { matrix: 2 }) === 100, 'matrix output meter: 0 is full scale, peak and 100 %')
|
||||||
|
}
|
||||||
|
|
||||||
// throttling: a flood of meter messages produces at most ~10 feedback checks per second, by instance id
|
// throttling: a flood of meter messages produces at most ~10 feedback checks per second, by instance id
|
||||||
const watching7 = inst.watchers.get('meter:' + id7)
|
const watching7 = inst.watchers.get('meter:' + id7)
|
||||||
const gateInstance = `fb-input_meter_value-${JSON.stringify({ channel: 7, meter: 'gate', unit: 'raw' })}`
|
const gateInstance = `fb-input_meter_value-${JSON.stringify({ channel: 7, meter: 'gate' })}`
|
||||||
check(!!watching7 && watching7.size >= 4 && !watching7.has(gateInstance) && inst.watchers.get('meter:' + meterId(7, 'gate'))?.has(gateInstance), `meter feedback instances are tracked per meter id (${watching7?.size} watch meter ${id7}; the gate-reduction one watches its own id)`)
|
check(!!watching7 && watching7.size >= 4 && !watching7.has(gateInstance) && inst.watchers.get('meter:' + meterId(7, 'gate'))?.has(gateInstance), `meter feedback instances are tracked per meter id (${watching7?.size} watch meter ${id7}; the gate-reduction one watches its own id)`)
|
||||||
// stop the mock's own meter stream (a raw /Meters/clear straight to the console) so only injected values move
|
// stop the mock's own meter stream (a raw /Meters/clear straight to the console) so only injected values move
|
||||||
const raw = dgram.createSocket('udp4')
|
const raw = dgram.createSocket('udp4')
|
||||||
@@ -989,7 +1065,9 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
const spanMs = Date.now() - t0
|
const spanMs = Date.now() - t0
|
||||||
const meterChecks = ctx.byId
|
const meterChecks = ctx.byId
|
||||||
check(meterChecks.length > 0 && meterChecks.length <= Math.ceil((spanMs / 1000) * 10) + 1, `meter feedback checks throttled to <= 10 Hz (${meterChecks.length} checks in ${spanMs} ms for 100 messages)`)
|
check(meterChecks.length > 0 && meterChecks.length <= Math.ceil((spanMs / 1000) * 10) + 1, `meter feedback checks throttled to <= 10 Hz (${meterChecks.length} checks in ${spanMs} ms for 100 messages)`)
|
||||||
check(meterChecks.every(([, ids]) => ids.every((x) => watching7.has(x))) && !meterChecks.some(([, ids]) => ids.includes(gateInstance)), 'a meter update re-checks only the instances watching the changed meter id')
|
// level meters with a peak hold still running (a live channel near full scale, the injected ones) are re-checked too; only meter instances ever are
|
||||||
|
const meterInstances = new Set([...inst.watchers].filter(([key]) => key.startsWith('meter:')).flatMap(([, ids]) => [...ids]))
|
||||||
|
check(meterChecks.every(([, ids]) => ids.every((x) => meterInstances.has(x))) && !meterChecks.some(([, ids]) => ids.includes(gateInstance)), 'a meter update re-checks only meter instances: those watching the changed id (and meters with a peak hold running), never the gain reduction one that did not move')
|
||||||
check(!ctx.checks.some(([, types]) => types.some((t) => /meter|signal|peak/.test(t))), `meter updates never fall back to per-type checks (per-type checks in that window: ${JSON.stringify(ctx.checks.map(([, t]) => t))}, from the keepalive tick)`)
|
check(!ctx.checks.some(([, types]) => types.some((t) => /meter|signal|peak/.test(t))), `meter updates never fall back to per-type checks (per-type checks in that window: ${JSON.stringify(ctx.checks.map(([, t]) => t))}, from the keepalive tick)`)
|
||||||
const rebuildsBefore = ctx.rebuilds
|
const rebuildsBefore = ctx.rebuilds
|
||||||
await sleep(200)
|
await sleep(200)
|
||||||
@@ -1030,6 +1108,9 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
check(await waitFor(() => sent.slice(mark).some((m) => m.address === '/Input_Channels/7/Channel_Input/post_meter'), 2000), 'after the iPad sent /Meters/clear the module re-sent its meter subscription')
|
check(await waitFor(() => sent.slice(mark).some((m) => m.address === '/Input_Channels/7/Channel_Input/post_meter'), 2000), 'after the iPad sent /Meters/clear the module re-sent its meter subscription')
|
||||||
const resub = sent.slice(mark).find((m) => m.address === '/Input_Channels/7/Channel_Input/post_meter')
|
const resub = sent.slice(mark).find((m) => m.address === '/Input_Channels/7/Channel_Input/post_meter')
|
||||||
check(decodeArgs(resub).args[0] === meterIdForInput(7), `resubscribed with the same id (${meterIdForInput(7)})`)
|
check(decodeArgs(resub).args[0] === meterIdForInput(7), `resubscribed with the same id (${meterIdForInput(7)})`)
|
||||||
|
await sleep(200)
|
||||||
|
const reOut = sent.slice(mark).filter((m) => /\/Output\/meter$/.test(m.address)).map((m) => `${m.address} ${decodeArgs(m).args[0]}`)
|
||||||
|
check(['/Aux_Outputs/3/Output/meter 5403', '/Group_Outputs/9/Output/meter 5409', '/Matrix_Outputs/2/Output/meter 5452', '/Aux_Outputs/1/Output/meter 5401'].every((x) => reOut.includes(x)), `the output meters are re-subscribed too after /Meters/clear (${reOut.length} output subscriptions re-sent)`)
|
||||||
check(await waitFor(() => (S.getMeter(meterIdForInput(7))?.at ?? 0) > t0 + 250, 3000), 'meter values flow again after the resubscribe')
|
check(await waitFor(() => (S.getMeter(meterIdForInput(7))?.at ?? 0) > t0 + 250, 3000), 'meter values flow again after the resubscribe')
|
||||||
check(!sent.some((m) => m.address === '/Meters/clear'), 'still never sent /Meters/clear ourselves')
|
check(!sent.some((m) => m.address === '/Meters/clear'), 'still never sent /Meters/clear ourselves')
|
||||||
|
|
||||||
@@ -1400,6 +1481,17 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
inst.onConsoleMessage(decode(encode('/Snapshots/name', [{ i: 8 }, { i: 900 }, { i: 0 }, 'Extra'])), { address: '127.0.0.1', port: P.mock })
|
inst.onConsoleMessage(decode(encode('/Snapshots/name', [{ i: 8 }, { i: 900 }, { i: 0 }, 'Extra'])), { address: '127.0.0.1', port: P.mock })
|
||||||
await sleep(inst.timing.rebuildDebounceMs + 300)
|
await sleep(inst.timing.rebuildDebounceMs + 300)
|
||||||
check(ctx.pushes.actions === p1 + 1 && ctx.actions.snapshot_fire.options[0].choices.some((c) => c.id === 8 && /Extra/.test(c.label)), 'a new snapshot in the list rebuilds the definitions')
|
check(ctx.pushes.actions === p1 + 1 && ctx.actions.snapshot_fire.options[0].choices.some((c) => c.id === 8 && /Extra/.test(c.label)), 'a new snapshot in the list rebuilds the definitions')
|
||||||
|
|
||||||
|
// a matrix input name pushed by the console rebuilds the matrix send dropdowns, preset names and titles
|
||||||
|
const pm = { actions: ctx.pushes.actions, presets: ctx.pushes.presets }
|
||||||
|
inst.onConsoleMessage(decode(encode('/Matrix_Inputs/2/Channel_Input/name', [' New Name '])), { address: '127.0.0.1', port: P.mock })
|
||||||
|
await sleep(inst.timing.rebuildDebounceMs + 400)
|
||||||
|
const knob2 = ctx.presets.msend_knob_2_8
|
||||||
|
check(S.minputs.get(2).name === 'New Name' && ctx.pushes.actions === pm.actions + 1 && ctx.pushes.presets === pm.presets + 1, 'a matrix input name change rebuilds the definitions once')
|
||||||
|
check(ctx.actions.matrix_send_on.options[0].choices.find((c) => c.id === 2)?.label === '2 · New Name' && ctx.actions.minput_trim_set.options[0].choices.find((c) => c.id === 2)?.label === '2 · New Name' && ctx.feedbacks.matrix_send_on.options[0].choices.find((c) => c.id === 2)?.label === '2 · New Name', 'the matrix send and matrix input param dropdowns show the new name, cleaned')
|
||||||
|
check(ctx.variables.matrix_in_2_name === 'New Name' && knob2.name === `Matrix send level (knob): New Name → 8 · ${S.displayName('matrix', 8)}` && ctx.presets.msend_on_2_8.name.includes('New Name') && knob2.elements[1].text === '$(S31:matrix_in_2_name)' && ctx.presets.msend_on_2_8.style.text.startsWith('$(S31:matrix_in_2_name)'), 'the matrix send preset names and titles carry the new name (titles read the matrix_in_N_name variable)')
|
||||||
|
inst.onConsoleMessage(decode(encode('/Matrix_Inputs/2/Channel_Input/name', ['Studio L'])), { address: '127.0.0.1', port: P.mock })
|
||||||
|
await sleep(inst.timing.rebuildDebounceMs + 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+11
-7
@@ -68,7 +68,7 @@ inputs, `--auxes N` the busses (default 25), and the matrix (8 outputs, 10 input
|
|||||||
| `/Console/{Input_Channels,Aux_Outputs,Group_Outputs}/?` | **silent**, like the real console. All nine counts are pushed once, ~200 ms after the first datagram it receives |
|
| `/Console/{Input_Channels,Aux_Outputs,Group_Outputs}/?` | **silent**, like the real console. All nine counts are pushed once, ~200 ms after the first datagram it receives |
|
||||||
| `/Console/Aux_Outputs/types/?` | one `i` per buss, 1 = aux, 0 = group (a buss named only on the Group side is a group) |
|
| `/Console/Aux_Outputs/types/?` | one `i` per buss, 1 = aux, 0 = group (a buss named only on the Group side is a group) |
|
||||||
| `/Console/{Aux_Outputs,Group_Outputs}/modes/?`, `/Console/Input_Channels/modes/?` | one `i` per buss / input (2 = stereo, 1 = mono, captured) |
|
| `/Console/{Aux_Outputs,Group_Outputs}/modes/?`, `/Console/Input_Channels/modes/?` | one `i` per buss / input (2 = stereo, 1 = mono, captured) |
|
||||||
| `/Input_Channels/N/Channel_Input/name/?`, `/Aux_Outputs/N/Buss_Trim/name/?`, `/Group_Outputs/N/Buss_Trim/name/?`, `/Matrix_Outputs/N/Buss_Trim/name/?`, `/Control_Groups/N/name/?`, `/Graphic_EQ/N/name/?` | `s`. Only input names accept a write here (not verified on the real console); the others are read-only. The 25 busses appear under both Aux_Outputs and Group_Outputs; a buss reports its name on the side matching its mode and `""` on the other. Matrix inputs have no name address |
|
| `/Input_Channels/N/Channel_Input/name/?`, `/Matrix_Inputs/N/Channel_Input/name/?`, `/Aux_Outputs/N/Buss_Trim/name/?`, `/Group_Outputs/N/Buss_Trim/name/?`, `/Matrix_Outputs/N/Buss_Trim/name/?`, `/Control_Groups/N/name/?`, `/Graphic_EQ/N/name/?` | `s`. Only input names accept a write here (not verified on the real console); the others are read-only. The 25 busses appear under both Aux_Outputs and Group_Outputs; a buss reports its name on the side matching its mode and `""` on the other. Names are padded with trailing spaces and runs of spaces, as the surface reports them (matrix input names are read-only here) |
|
||||||
| `/{Input_Channels,Aux_Outputs,Group_Outputs,Matrix_Outputs,Control_Groups}/N/fader/?` | `f`, dB, -150 = -inf, max +10 |
|
| `/{Input_Channels,Aux_Outputs,Group_Outputs,Matrix_Outputs,Control_Groups}/N/fader/?` | `f`, dB, -150 = -inf, max +10 |
|
||||||
| `/{...}/N/mute/?`, `/{...}/N/solo/?` | `f` 0/1 (a float, not an int); a SET must be a float too, see below |
|
| `/{...}/N/mute/?`, `/{...}/N/solo/?` | `f` 0/1 (a float, not an int); a SET must be a float too, see below |
|
||||||
| `/Input_Channels/N/Panner/pan/?` | `f`, 0..1, 0.5 = centre |
|
| `/Input_Channels/N/Panner/pan/?` | `f`, 0..1, 0.5 = centre |
|
||||||
@@ -78,17 +78,21 @@ inputs, `--auxes N` the busses (default 25), and the matrix (8 outputs, 10 input
|
|||||||
| `/Matrix_Inputs/N/Matrix_Send/M/{send_level,send_on}/?` | `f` (default -90 and 0, captured). A matrix send floors at -90 dB (verified on an S31): level writes are clamped to -90..10, never -150 as for faders and aux sends |
|
| `/Matrix_Inputs/N/Matrix_Send/M/{send_level,send_on}/?` | `f` (default -90 and 0, captured). A matrix send floors at -90 dB (verified on an S31): level writes are clamped to -90..10, never -150 as for faders and aux sends |
|
||||||
| `/Graphic_EQ/N/{geq_in,geq_gain_1..32}/?` | `f` |
|
| `/Graphic_EQ/N/{geq_in,geq_gain_1..32}/?` | `f` |
|
||||||
| `/Input_Channels/N/{Channel_Input/{trim,phase,phantom,analog_gain,input_pad},Filters/{hi,lo}_filter_{in,freq},Channel_Delay/{delay_on,delay},EQ/eq_in,Dynamics/{comp_in,gate_in},Insert/insert_{A,B}_in}/?` | `f` (switches 0/1) |
|
| `/Input_Channels/N/{Channel_Input/{trim,phase,phantom,analog_gain,input_pad},Filters/{hi,lo}_filter_{in,freq},Channel_Delay/{delay_on,delay},EQ/eq_in,Dynamics/{comp_in,gate_in},Insert/insert_{A,B}_in}/?` | `f` (switches 0/1) |
|
||||||
| `/{Aux_Outputs,Group_Outputs}/N/{Buss_Trim/{trim,phase},EQ/eq_in,Dynamics/{comp_in,gate_in},Channel_Delay/{delay_on,delay},Insert/insert_{A,B}_in}/?`, `/Matrix_Outputs/N/Buss_Trim/trim/?` | `f` |
|
| `/{Aux_Outputs,Group_Outputs}/N/{Buss_Trim/{trim,phase},EQ/eq_in,Dynamics/{comp_in,gate_in},Channel_Delay/{delay_on,delay},Insert/insert_{A,B}_in}/?`, `/Matrix_Outputs/N/Buss_Trim/trim/?`, `/Matrix_Inputs/N/Channel_Input/{trim,phase}/?` | `f` |
|
||||||
| `/Snapshots/Current_Snapshot/?`, `/Snapshots/count/?` | `i` |
|
| `/Snapshots/Current_Snapshot/?`, `/Snapshots/count/?` | `i` |
|
||||||
| `/Snapshots/names/?` | one `/Snapshots/name ,iiis [index, cue*100, 0, "name"]` per snapshot. `/Snapshots/name/?` without an argument stays silent (unreliable on the real console) |
|
| `/Snapshots/names/?` | one `/Snapshots/name ,iiis [index, cue*100, 0, "name"]` per snapshot. `/Snapshots/name/?` without an argument stays silent (unreliable on the real console) |
|
||||||
| `/Snapshots/Fire_Snapshot_number [i]`, `/Snapshots/Fire_Next_Snapshot`, `/Snapshots/Fire_Prev_Snapshot` | write-only triggers; the mock moves its current snapshot and pushes `/Snapshots/Current_Snapshot` |
|
| `/Snapshots/Fire_Snapshot_number [i]`, `/Snapshots/Fire_Next_Snapshot`, `/Snapshots/Fire_Prev_Snapshot` | write-only triggers; the mock moves its current snapshot and pushes `/Snapshots/Current_Snapshot` |
|
||||||
| `/Macros/Buttons/press [i]` | logged only (no macro query is answered) |
|
| `/Macros/Buttons/press [i]` | logged only (no macro query is answered) |
|
||||||
|
|
||||||
**Meters.** Subscribe with `/Input_Channels/N/Channel_Input/post_meter ,ii [id, 1]` (post-fader level) or
|
**Meters.** Subscribe with `/Input_Channels/N/Channel_Input/post_meter ,ii [id, 1]` (post-fader level),
|
||||||
`/Input_Channels/N/Dynamics/GR_meter ,ii [id, 1|4]` (gate / compressor gain reduction), where the client
|
`/Input_Channels/N/Dynamics/GR_meter ,ii [id, 1|4]` (gate / compressor gain reduction) or
|
||||||
chooses `id`. Every subscribed id then gets a random value 0..127 about five times a second, as
|
`/{Aux_Outputs,Group_Outputs,Matrix_Outputs}/N/Output/meter ,ii [id, 1]` (output level), where the client chooses `id`.
|
||||||
`/Meters/values ,ii... [id, v, id, v, ...]` (only ids whose value changed). `/Meters/clear` drops every
|
The scale is inverted, as on the real console (verified on an S31, approximate): a level meter reads about the dB
|
||||||
subscription, like the real console.
|
below full scale, so **silence is 127** and the mock's live channels (inputs 1-3, aux 1, group 9, matrix output 1) read
|
||||||
|
20-60; a gain reduction meter reads the dB of reduction, 0-12, now and then 127 (inactive). Every subscribed id gets
|
||||||
|
its value about five times a second, as `/Meters/values ,ii... [id, v, id, v, ...]` (only ids whose value changed, so a
|
||||||
|
silent channel reports 127 once). `/Meters/clear` drops every subscription, like the real console. Control groups and
|
||||||
|
matrix inputs have no meter address.
|
||||||
|
|
||||||
A 0/1 switch (mute, solo, send on, group assign, matrix send on, `geq_in` and every channel-strip switch) is both
|
A 0/1 switch (mute, solo, send on, group assign, matrix send on, `geq_in` and every channel-strip switch) is both
|
||||||
reported **and written as a float** 0.0 / 1.0, verified on a real S31 on 2026-10-15 (the 2014 DiGiCo list says
|
reported **and written as a float** 0.0 / 1.0, verified on a real S31 on 2026-10-15 (the 2014 DiGiCo list says
|
||||||
|
|||||||
+39
-14
@@ -584,6 +584,8 @@ const SD_GROUP_NAMES = [
|
|||||||
'', '', '', '', '', '', '', '', 'BAND', 'VOCALS', 'MISC', 'DRUMS', 'DRUM CRUSH', '', '', '', '', 'Broadcast mix',
|
'', '', '', '', '', '', '', '', 'BAND', 'VOCALS', 'MISC', 'DRUMS', 'DRUM CRUSH', '', '', '', '', 'Broadcast mix',
|
||||||
'', '', '', '', 'CAFE', 'Studio', 'Master',
|
'', '', '', '', 'CAFE', 'Studio', 'Master',
|
||||||
];
|
];
|
||||||
|
/** Matrix input names as the console reports them: padded with trailing spaces, with runs of spaces inside (captured). */
|
||||||
|
const SD_MATRIX_IN_NAMES = ['TB ', 'Studio L', 'Studio R', 'Master L', 'Master R', 'GUIDE ', 'Studio TB ', 'Dante In 63 ', 'Dante In 64 ', ''];
|
||||||
const SD_MATRIX_NAMES = ['MAINS L', 'MAINS R', 'SUB L', 'SUB R', 'ProP LR', 'Matrix 6', 'Matrix 7', 'Studio'];
|
const SD_MATRIX_NAMES = ['MAINS L', 'MAINS R', 'SUB L', 'SUB R', 'ProP LR', 'Matrix 6', 'Matrix 7', 'Studio'];
|
||||||
const SD_CG_NAMES = ['DRUMS', 'GTR', 'KEYS', 'TRKS', 'VOCALS', 'FX', '', 'ProP', 'MC & SPEAKER', ''];
|
const SD_CG_NAMES = ['DRUMS', 'GTR', 'KEYS', 'TRKS', 'VOCALS', 'FX', '', 'ProP', 'MC & SPEAKER', ''];
|
||||||
/** Snapshot list: index n has cue number (n+1)*100 (reported as iiis [index, cue*100, 0, name]). */
|
/** Snapshot list: index n has cue number (n+1)*100 (reported as iiis [index, cue*100, 0, name]). */
|
||||||
@@ -644,6 +646,10 @@ const SD_PARAM_SPECS = {
|
|||||||
matrix: {
|
matrix: {
|
||||||
'Buss_Trim/trim': { spec: sdValue(-40, 40), def: 0 },
|
'Buss_Trim/trim': { spec: sdValue(-40, 40), def: 0 },
|
||||||
},
|
},
|
||||||
|
minput: {
|
||||||
|
'Channel_Input/trim': { spec: sdValue(-40, 40), def: 0 },
|
||||||
|
'Channel_Input/phase': { spec: SD_SWITCH, def: 0 },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const SD_SEND_PROPS = { send_level: 'level', send_on: 'on', send_pan: 'pan', 'send_pre-post': 'prepost' };
|
const SD_SEND_PROPS = { send_level: 'level', send_on: 'on', send_pan: 'pan', 'send_pre-post': 'prepost' };
|
||||||
|
|
||||||
@@ -655,11 +661,13 @@ const sdInputs = [null]; // 1-based: { name, fader, mute, solo, pan, sends:[null
|
|||||||
const sdAuxes = [null]; // 1-based: { name, fader, mute, solo }
|
const sdAuxes = [null]; // 1-based: { name, fader, mute, solo }
|
||||||
const sdGroups = [null]; // 1-based: { name, fader, mute, solo }
|
const sdGroups = [null]; // 1-based: { name, fader, mute, solo }
|
||||||
const sdMatrix = [null]; // 1-based matrix outputs: { name, fader, mute, solo }
|
const sdMatrix = [null]; // 1-based matrix outputs: { name, fader, mute, solo }
|
||||||
const sdMatrixIn = [null]; // 1-based matrix inputs: { sends:[null,{level,on}...] }
|
const sdMatrixIn = [null]; // 1-based matrix inputs: { name, sends:[null,{level,on}...] }
|
||||||
const sdCgs = [null]; // 1-based control groups: { name, fader, mute, solo }
|
const sdCgs = [null]; // 1-based control groups: { name, fader, mute, solo }
|
||||||
const sdGeqs = [null]; // 1-based graphic EQs: { name, on, gains:[null, ...] }
|
const sdGeqs = [null]; // 1-based graphic EQs: { name, on, gains:[null, ...] }
|
||||||
let sdSnapshot = 3; // /Snapshots/Current_Snapshot
|
let sdSnapshot = 3; // /Snapshots/Current_Snapshot
|
||||||
const sdMeterSubs = new Map(); // meter id -> last value sent
|
/** meter id -> { last, kind: 'level' | 'gr', live }: what was last sent and what to send. Silence is 127; live channels read 20-60. */
|
||||||
|
const sdMeterSubs = new Map();
|
||||||
|
const SD_LIVE_INPUTS = 3; // inputs 1-3 carry signal; aux 1, group 9 and matrix output 1 do too
|
||||||
let sdMeterTimer = null;
|
let sdMeterTimer = null;
|
||||||
const SD_METER_INTERVAL_MS = 200; // ~5 Hz
|
const SD_METER_INTERVAL_MS = 200; // ~5 Hz
|
||||||
|
|
||||||
@@ -705,7 +713,7 @@ function buildStateSd() {
|
|||||||
for (let k = 0; k < SD_MATRIX_IN; k++) {
|
for (let k = 0; k < SD_MATRIX_IN; k++) {
|
||||||
const sends = [null];
|
const sends = [null];
|
||||||
for (let m = 1; m <= SD_MATRIX_OUT; m++) sends.push({ level: -90, on: 0 }); // captured: -90 / 0
|
for (let m = 1; m <= SD_MATRIX_OUT; m++) sends.push({ level: -90, on: 0 }); // captured: -90 / 0
|
||||||
sdMatrixIn.push({ sends });
|
sdMatrixIn.push({ name: SD_MATRIX_IN_NAMES[k], sends });
|
||||||
}
|
}
|
||||||
for (let k = 0; k < SD_CGS; k++) sdCgs.push({ name: SD_CG_NAMES[k], fader: tidy16(-20 + rng() * 20), mute: 0, solo: 0 });
|
for (let k = 0; k < SD_CGS; k++) sdCgs.push({ name: SD_CG_NAMES[k], fader: tidy16(-20 + rng() * 20), mute: 0, solo: 0 });
|
||||||
for (let k = 1; k <= SD_GEQS; k++) {
|
for (let k = 1; k <= SD_GEQS; k++) {
|
||||||
@@ -738,8 +746,9 @@ const SD_RE_MATRIX_SEND = /^\/Matrix_Inputs\/(\d+)\/Matrix_Send\/(\d+)\/(send_le
|
|||||||
const SD_RE_CG_NAME = /^\/Control_Groups\/(\d+)\/name$/;
|
const SD_RE_CG_NAME = /^\/Control_Groups\/(\d+)\/name$/;
|
||||||
const SD_RE_CG_PROP = /^\/Control_Groups\/(\d+)\/(fader|mute|solo)$/;
|
const SD_RE_CG_PROP = /^\/Control_Groups\/(\d+)\/(fader|mute|solo)$/;
|
||||||
const SD_RE_GEQ = /^\/Graphic_EQ\/(\d+)\/(name|geq_in|geq_gain_(\d+))$/;
|
const SD_RE_GEQ = /^\/Graphic_EQ\/(\d+)\/(name|geq_in|geq_gain_(\d+))$/;
|
||||||
const SD_RE_PARAM = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs)\/(\d+)\/((?:Channel_Input|Filters|Channel_Delay|EQ|Dynamics|Insert|Buss_Trim)\/\w+)$/;
|
const SD_RE_MINPUT_NAME = /^\/Matrix_Inputs\/(\d+)\/Channel_Input\/name$/;
|
||||||
const SD_RE_METER_SUB = /^\/Input_Channels\/(\d+)\/(?:Channel_Input\/post_meter|Dynamics\/GR_meter)$/;
|
const SD_RE_PARAM = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs|Matrix_Inputs)\/(\d+)\/((?:Channel_Input|Filters|Channel_Delay|EQ|Dynamics|Insert|Buss_Trim)\/\w+)$/;
|
||||||
|
const SD_RE_METER_SUB = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs)\/(\d+)\/(Channel_Input\/post_meter|Dynamics\/GR_meter|Output\/meter)$/;
|
||||||
|
|
||||||
/** Map an SD leaf address to { target, prop, spec } or null when there is no such thing. */
|
/** Map an SD leaf address to { target, prop, spec } or null when there is no such thing. */
|
||||||
function sdResolve(address) {
|
function sdResolve(address) {
|
||||||
@@ -776,11 +785,13 @@ function sdResolve(address) {
|
|||||||
const prop = fixedProp || m[2];
|
const prop = fixedProp || m[2];
|
||||||
return { target, prop, spec: SD_PROPS[prop] };
|
return { target, prop, spec: SD_PROPS[prop] };
|
||||||
}
|
}
|
||||||
|
m = SD_RE_MINPUT_NAME.exec(address);
|
||||||
|
if (m) return sdMatrixIn[Number(m[1])] ? { target: sdMatrixIn[Number(m[1])], prop: 'name', spec: SD_PROPS.name } : null;
|
||||||
m = SD_RE_PARAM.exec(address);
|
m = SD_RE_PARAM.exec(address);
|
||||||
if (m) {
|
if (m) {
|
||||||
const kind = { Input_Channels: 'input', Matrix_Outputs: 'matrix' }[m[1]] || 'buss';
|
const kind = { Input_Channels: 'input', Matrix_Outputs: 'matrix', Matrix_Inputs: 'minput' }[m[1]] || 'buss';
|
||||||
const entry = SD_PARAM_SPECS[kind][m[3]];
|
const entry = SD_PARAM_SPECS[kind][m[3]];
|
||||||
const owner = { Input_Channels: sdInputs, Aux_Outputs: sdAuxes, Group_Outputs: sdGroups, Matrix_Outputs: sdMatrix }[m[1]][Number(m[2])];
|
const owner = { Input_Channels: sdInputs, Aux_Outputs: sdAuxes, Group_Outputs: sdGroups, Matrix_Outputs: sdMatrix, Matrix_Inputs: sdMatrixIn }[m[1]][Number(m[2])];
|
||||||
if (!entry || !owner) return null;
|
if (!entry || !owner) return null;
|
||||||
if (!owner.params) owner.params = {};
|
if (!owner.params) owner.params = {};
|
||||||
if (!(m[3] in owner.params)) owner.params[m[3]] = entry.def;
|
if (!(m[3] in owner.params)) owner.params[m[3]] = entry.def;
|
||||||
@@ -888,8 +899,16 @@ function sdSet(address, args, types) {
|
|||||||
}
|
}
|
||||||
const sub = SD_RE_METER_SUB.exec(address);
|
const sub = SD_RE_METER_SUB.exec(address);
|
||||||
if (sub) {
|
if (sub) {
|
||||||
if (!sdInputs[Number(sub[1])] || typeof args[0] !== 'number') return ignore(`meter subscription ${address} ${formatArgs(args)}`);
|
const [, tree, numText, path] = sub;
|
||||||
sdMeterSubs.set(Math.trunc(args[0]), -1);
|
const owner = { Input_Channels: sdInputs, Aux_Outputs: sdAuxes, Group_Outputs: sdGroups, Matrix_Outputs: sdMatrix }[tree];
|
||||||
|
const input = tree === 'Input_Channels';
|
||||||
|
if (!owner[Number(numText)] || (input && path === 'Output/meter') || (!input && path !== 'Output/meter') || typeof args[0] !== 'number') {
|
||||||
|
return ignore(`meter subscription ${address} ${formatArgs(args)}`);
|
||||||
|
}
|
||||||
|
const num = Number(numText);
|
||||||
|
const kind = path === 'Dynamics/GR_meter' ? 'gr' : 'level';
|
||||||
|
const live = kind === 'level' && ((input && num <= SD_LIVE_INPUTS) || (tree === 'Aux_Outputs' && num === 1) || (tree === 'Group_Outputs' && num === 9) || (tree === 'Matrix_Outputs' && num === 1));
|
||||||
|
sdMeterSubs.set(Math.trunc(args[0]), { last: -1, kind, live });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,14 +925,20 @@ function sdSet(address, args, types) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ~5 Hz: random 0..127 for every subscribed id whose value changed, as /Meters/values [id, v, id, v ...]. */
|
/**
|
||||||
|
* ~5 Hz: /Meters/values [id, v, id, v ...] for every subscribed id whose value changed. The scale is inverted, as on the
|
||||||
|
* real console: a level meter reads about the dB below full scale, so silence is 127 and live channels read 20-60;
|
||||||
|
* a gain reduction meter reads the dB of reduction (0-12, now and then 127 = inactive).
|
||||||
|
*/
|
||||||
function sdMeterTick() {
|
function sdMeterTick() {
|
||||||
if (!sdMeterSubs.size || !replyTarget) return;
|
if (!sdMeterSubs.size || !replyTarget) return;
|
||||||
const pairs = [];
|
const pairs = [];
|
||||||
for (const [id, last] of sdMeterSubs) {
|
for (const [id, meter] of sdMeterSubs) {
|
||||||
const v = Math.random() < 0.15 ? 0 : Math.floor(Math.random() * 128);
|
let v;
|
||||||
if (v === last) continue;
|
if (meter.kind === 'gr') v = Math.random() < 0.1 ? 127 : Math.floor(Math.random() * 13);
|
||||||
sdMeterSubs.set(id, v);
|
else v = meter.live ? 20 + Math.floor(Math.random() * 41) : 127;
|
||||||
|
if (v === meter.last) continue;
|
||||||
|
meter.last = v;
|
||||||
pairs.push(int(id), int(v));
|
pairs.push(int(id), int(v));
|
||||||
}
|
}
|
||||||
for (let i = 0; i < pairs.length; i += 40) send('/Meters/values', pairs.slice(i, i + 40));
|
for (let i = 0; i < pairs.length; i += 40) send('/Meters/values', pairs.slice(i, i + 40));
|
||||||
|
|||||||
Reference in New Issue
Block a user