Timed fades, mute-style matrix sends, meters on matrix send knobs
- Every set-level action (input, bus, matrix and control group faders, aux and matrix sends) has a fade time and a curve: fader travel (natural), fader travel with ease in/out, or linear in dB. Fades step at 20 Hz, land exactly on the target, never overshoot, and are cancelled by any new move on the same fader; new "Stop all fades" action, fades_running variable and fade_running feedback. Verified on the S31 with a switched-off matrix send: 20 writes in 1 s, ending exactly on target, then restored. - Matrix sends now look like mutes in the presets: red when the send is off, black when it is on (new matrix_send_off feedback, true only when the send is known to be off). - Matrix send knobs carry a meter column on the right. The S31 has no meter for matrix inputs, so it shows the destination output's meter. Meter gauges use a faint track so an empty meter reads as empty. - Test fixes: the meter-resubscribe check no longer depends on a silent channel changing value, plus back-to-back /Meters/clear coverage. npm test: 504 checks pass on Node 25 and on Companion's Node 22. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+21
-5
@@ -94,13 +94,29 @@ is not known yet.
|
|||||||
| 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 and matrix inputs, phase also on matrix inputs) |
|
| 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, stop all fades |
|
||||||
|
|
||||||
The channel-strip values are what the console reports (trim and gain in dB, filters in Hz). In the console's
|
The channel-strip values are what the console reports (trim and gain in dB, filters in Hz). In the console's
|
||||||
own naming `lo_filter` is the high-pass filter and `hi_filter` the low-pass filter; the actions are named for
|
own naming `lo_filter` is the high-pass filter and `hi_filter` the low-pass filter; the actions are named for
|
||||||
what they do. Delay times and the send pre/post fader switch are not offered: their units and meaning were
|
what they do. Delay times and the send pre/post fader switch are not offered: their units and meaning were
|
||||||
never verified.
|
never verified.
|
||||||
|
|
||||||
|
**Fades.** Every *set level* action (input, buss, matrix output and control group faders, aux send level, matrix send
|
||||||
|
level) has a **Fade time (ms, 0 = instant)**, 0 to 60000 ms. With 0 the level jumps to the target at once, as before; above 0
|
||||||
|
the module moves the level there smoothly, starting from where it is now (it reads the level first when it is not known
|
||||||
|
yet; if the console does not answer, the target is set at once and a warning is logged). A new level is written 20 times a
|
||||||
|
second at most (every 50 ms, steps that would change the level by less than 0.05 dB are left out), and the last write is
|
||||||
|
exactly the target. The **Fade curve** (shown once the time is above 0) is *Fader travel (natural)*, which moves the way a
|
||||||
|
fader's travel does (the same taper as the gauges, so the level change sounds even; the default), *Fader travel, ease in/out*
|
||||||
|
(the same with a slow start and a slow end), or *Linear in dB* (the same number of dB every step; a fade from or to −∞ runs between −90 dB, where sound
|
||||||
|
stops, and the target, so no part of it is a silent stretch; a fade to −∞ still ends at exactly −∞). A matrix send never goes below −90 dB.
|
||||||
|
There is one fade per level: starting another fade, a plain *set* or an *adjust* on the same level ends the running one
|
||||||
|
where it is, and the new one takes over from that level. The action returns at once and the fade runs on in the
|
||||||
|
background; the **Stop all fades** action ends every fade where it is. Fades end too when the connection to the console is
|
||||||
|
closed or reconfigured, and while one runs the module keeps writing its level, so a fader moved at the console in the
|
||||||
|
meantime is overwritten. The variable `fades_running` is the number of fades running and the feedback **A fade is
|
||||||
|
running** is on while there is one.
|
||||||
|
|
||||||
**Encoders and writes.** Every write updates the module's state at once, so a fast encoder turn accumulates. For
|
**Encoders and writes.** Every write updates the module's state at once, so a fast encoder turn accumulates. For
|
||||||
0.4 seconds after the last write to a value, a different value arriving for it is treated as a late echo of an
|
0.4 seconds after the last write to a value, a different value arriving for it is treated as a late echo of an
|
||||||
earlier step and ignored; then one query confirms what the console has. The snapshot *Fire* action sends the
|
earlier step and ignored; then one query confirms what the console has. The snapshot *Fire* action sends the
|
||||||
@@ -117,8 +133,8 @@ graphic EQ bands, channel-strip values) is loaded the first time one is shown, a
|
|||||||
own value changes.
|
own value changes.
|
||||||
|
|
||||||
* Boolean (button colour): input muted / solo; aux send on; group assigned; buss muted / solo; matrix output muted /
|
* Boolean (button colour): input muted / solo; aux send on; group assigned; 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**; **matrix send is off** (red like a mute; false while the state is not yet known); **graphic EQ in**; snapshot is current; fader level above/below
|
||||||
a threshold; aux send level above/below; console link alive; relay has an iPad; every channel-strip switch
|
a threshold; aux send level above/below; console link alive; relay has an iPad; a level fade is running; every channel-strip switch
|
||||||
(phantom, phase, pad, EQ in, compressor in, gate in, high-pass in, low-pass in, delay in, insert A/B in; phase also
|
(phantom, phase, pad, EQ in, compressor in, gate in, high-pass in, low-pass in, delay in, insert A/B in; phase also
|
||||||
per matrix input); and the meter feedbacks (signal present, peak hold) for inputs, busses and matrix outputs.
|
per matrix input); and the meter feedbacks (signal present, peak hold) for inputs, 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
|
||||||
@@ -163,7 +179,7 @@ To add a gauge to your own button in Companion 5.1:
|
|||||||
|
|
||||||
`input_N_name|fader|mute|pan`, `buss_N_name|mode|stereo|fader|mute`, `matrix_N_name|fader|mute`, `matrix_in_N_name`,
|
`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`, `fades_running` (number of level fades running) 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.
|
||||||
|
|
||||||
## Presets
|
## Presets
|
||||||
@@ -181,7 +197,7 @@ 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 the matrix input's name 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 (black when the send is on, red when it is off, like a mute; shows the matrix input's name and the level) and a **Level (knob)** (turn = send level, press = on/off, turns red when the send is off, with a level gauge along the bottom and a meter column on the right; the S31 has no meter for matrix inputs, so that column shows the destination matrix output's meter) |
|
||||||
| 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) |
|
||||||
|
|||||||
+55
-14
@@ -1,4 +1,4 @@
|
|||||||
import { LEVEL, LIMITS, PARAMS } from '../protocol.js'
|
import { FADE_CURVES, LEVEL, LIMITS, PARAMS } from '../protocol.js'
|
||||||
|
|
||||||
export const STATE_CHOICES = [
|
export const STATE_CHOICES = [
|
||||||
{ id: 'on', label: 'On' },
|
{ id: 'on', label: 'On' },
|
||||||
@@ -41,6 +41,30 @@ export const opt = {
|
|||||||
step: 0.5,
|
step: 0.5,
|
||||||
showMinAsNegativeInfinity: true,
|
showMinAsNegativeInfinity: true,
|
||||||
}),
|
}),
|
||||||
|
/** Fade time of a "set level" action; the curve choice shows only while it is above 0 (so `duration` cannot be an expression). */
|
||||||
|
duration: (id = 'duration') => ({
|
||||||
|
type: 'number',
|
||||||
|
id,
|
||||||
|
label: 'Fade time (ms, 0 = instant)',
|
||||||
|
default: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 60000,
|
||||||
|
step: 100,
|
||||||
|
asInteger: true,
|
||||||
|
disableAutoExpression: true,
|
||||||
|
}),
|
||||||
|
curve: (id = 'curve') => ({
|
||||||
|
type: 'dropdown',
|
||||||
|
id,
|
||||||
|
label: 'Fade curve',
|
||||||
|
default: 'fader',
|
||||||
|
choices: [
|
||||||
|
{ id: 'fader', label: 'Fader travel (natural)' },
|
||||||
|
{ id: 'ease', label: 'Fader travel, ease in/out' },
|
||||||
|
{ id: 'db', label: 'Linear in dB' },
|
||||||
|
],
|
||||||
|
isVisibleExpression: '$(options:duration) > 0',
|
||||||
|
}),
|
||||||
delta: (id = 'delta', label = 'Change (dB, for encoders)') => ({ type: 'number', id, label, default: 1, min: -30, max: 30, step: 0.5 }),
|
delta: (id = 'delta', label = 'Change (dB, for encoders)') => ({ type: 'number', id, label, default: 1, min: -30, max: 30, step: 0.5 }),
|
||||||
pan: (id = 'pan', label = 'Pan (0 = left, 0.5 = centre, 1 = right)') => ({ type: 'number', id, label, default: 0.5, min: 0, max: 1, step: 0.01 }),
|
pan: (id = 'pan', label = 'Pan (0 = left, 0.5 = centre, 1 = right)') => ({ type: 'number', id, label, default: 0.5, min: 0, max: 1, step: 0.01 }),
|
||||||
pdelta: (id = 'pdelta', label = 'Pan change (for encoders)') => ({ type: 'number', id, label, default: 0.05, min: -1, max: 1, step: 0.01 }),
|
pdelta: (id = 'pdelta', label = 'Pan change (for encoders)') => ({ type: 'number', id, label, default: 0.05, min: -1, max: 1, step: 0.01 }),
|
||||||
@@ -50,6 +74,12 @@ export const opt = {
|
|||||||
/** Said in every level adjust action: a relative step cannot start from -inf, so the first step up jumps to -60 dB. */
|
/** Said in every level adjust action: a relative step cannot start from -inf, so the first step up jumps to -60 dB. */
|
||||||
const ADJUST_NOTE = 'From −∞ the first step up jumps to −60 dB.'
|
const ADJUST_NOTE = 'From −∞ the first step up jumps to −60 dB.'
|
||||||
|
|
||||||
|
/** Said in every level set action: the optional fade. */
|
||||||
|
const FADE_NOTE = 'With a fade time above 0 the level moves from where it is now to the target in steps of 50 ms; another set, adjust or fade on the same level ends it.'
|
||||||
|
|
||||||
|
/** [fade time in ms (0 = instant), curve] from a set-level action's options; missing options (buttons made before fades existed) mean instant. */
|
||||||
|
const fadeArgs = (options) => [Number(options?.duration) > 0 ? Math.min(60000, Math.round(Number(options.duration))) : 0, FADE_CURVES.includes(options?.curve) ? options.curve : 'fader']
|
||||||
|
|
||||||
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', 'matrix' and/or 'minput' (matrix input). */
|
/** Which entity kinds get actions / feedbacks for a parameter: 'input', 'buss', 'matrix' and/or 'minput' (matrix input). */
|
||||||
@@ -132,8 +162,9 @@ export function getActionDefinitions(inst) {
|
|||||||
},
|
},
|
||||||
input_fader_set: {
|
input_fader_set: {
|
||||||
name: 'Input: set fader',
|
name: 'Input: set fader',
|
||||||
options: [opt.channel(inst), opt.level()],
|
description: FADE_NOTE,
|
||||||
callback: async ({ options }) => inst.setFader('input', n(options.channel), n(options.level)),
|
options: [opt.channel(inst), opt.level(), opt.duration(), opt.curve()],
|
||||||
|
callback: async ({ options }) => inst.setFader('input', n(options.channel), n(options.level), ...fadeArgs(options)),
|
||||||
},
|
},
|
||||||
input_fader_adjust: {
|
input_fader_adjust: {
|
||||||
name: 'Input: adjust fader (relative)',
|
name: 'Input: adjust fader (relative)',
|
||||||
@@ -164,8 +195,9 @@ export function getActionDefinitions(inst) {
|
|||||||
},
|
},
|
||||||
send_level_set: {
|
send_level_set: {
|
||||||
name: 'Aux send: set level',
|
name: 'Aux send: set level',
|
||||||
options: [opt.channel(inst), opt.buss(inst, 'aux'), opt.level()],
|
description: FADE_NOTE,
|
||||||
callback: async ({ options }) => inst.setSendLevel(n(options.channel), n(options.buss), n(options.level)),
|
options: [opt.channel(inst), opt.buss(inst, 'aux'), opt.level(), opt.duration(), opt.curve()],
|
||||||
|
callback: async ({ options }) => inst.setSendLevel(n(options.channel), n(options.buss), n(options.level), ...fadeArgs(options)),
|
||||||
},
|
},
|
||||||
send_level_adjust: {
|
send_level_adjust: {
|
||||||
name: 'Aux send: adjust level (relative)',
|
name: 'Aux send: adjust level (relative)',
|
||||||
@@ -201,8 +233,9 @@ export function getActionDefinitions(inst) {
|
|||||||
},
|
},
|
||||||
buss_fader_set: {
|
buss_fader_set: {
|
||||||
name: 'Buss: set fader',
|
name: 'Buss: set fader',
|
||||||
options: [opt.buss(inst), opt.level()],
|
description: FADE_NOTE,
|
||||||
callback: async ({ options }) => inst.setFader('buss', n(options.buss), n(options.level)),
|
options: [opt.buss(inst), opt.level(), opt.duration(), opt.curve()],
|
||||||
|
callback: async ({ options }) => inst.setFader('buss', n(options.buss), n(options.level), ...fadeArgs(options)),
|
||||||
},
|
},
|
||||||
buss_fader_adjust: {
|
buss_fader_adjust: {
|
||||||
name: 'Buss: adjust fader (relative)',
|
name: 'Buss: adjust fader (relative)',
|
||||||
@@ -223,8 +256,9 @@ export function getActionDefinitions(inst) {
|
|||||||
},
|
},
|
||||||
matrix_fader_set: {
|
matrix_fader_set: {
|
||||||
name: 'Matrix output: set fader',
|
name: 'Matrix output: set fader',
|
||||||
options: [opt.matrix(inst), opt.level()],
|
description: FADE_NOTE,
|
||||||
callback: async ({ options }) => inst.setFader('matrix', n(options.matrix), n(options.level)),
|
options: [opt.matrix(inst), opt.level(), opt.duration(), opt.curve()],
|
||||||
|
callback: async ({ options }) => inst.setFader('matrix', n(options.matrix), n(options.level), ...fadeArgs(options)),
|
||||||
},
|
},
|
||||||
matrix_fader_adjust: {
|
matrix_fader_adjust: {
|
||||||
name: 'Matrix output: adjust fader (relative)',
|
name: 'Matrix output: adjust fader (relative)',
|
||||||
@@ -239,9 +273,9 @@ export function getActionDefinitions(inst) {
|
|||||||
},
|
},
|
||||||
matrix_send_level_set: {
|
matrix_send_level_set: {
|
||||||
name: 'Matrix send: set level',
|
name: 'Matrix send: set level',
|
||||||
description: 'A matrix send goes down to −90 dB (−∞).',
|
description: `A matrix send goes down to −90 dB (−∞). ${FADE_NOTE}`,
|
||||||
options: [opt.matrixIn(inst), opt.matrix(inst, 'matrixOut'), opt.level('level', 'Level (dB)', LEVEL.matrixSendMin)],
|
options: [opt.matrixIn(inst), opt.matrix(inst, 'matrixOut'), opt.level('level', 'Level (dB)', LEVEL.matrixSendMin), opt.duration(), opt.curve()],
|
||||||
callback: async ({ options }) => inst.setMatrixSendLevel(n(options.matrixIn), n(options.matrixOut), n(options.level)),
|
callback: async ({ options }) => inst.setMatrixSendLevel(n(options.matrixIn), n(options.matrixOut), n(options.level), ...fadeArgs(options)),
|
||||||
},
|
},
|
||||||
matrix_send_level_adjust: {
|
matrix_send_level_adjust: {
|
||||||
name: 'Matrix send: adjust level (relative)',
|
name: 'Matrix send: adjust level (relative)',
|
||||||
@@ -262,8 +296,9 @@ export function getActionDefinitions(inst) {
|
|||||||
},
|
},
|
||||||
cg_fader_set: {
|
cg_fader_set: {
|
||||||
name: 'Control group: set fader',
|
name: 'Control group: set fader',
|
||||||
options: [opt.cg(inst), opt.level()],
|
description: FADE_NOTE,
|
||||||
callback: async ({ options }) => inst.setFader('cg', n(options.cg), n(options.level)),
|
options: [opt.cg(inst), opt.level(), opt.duration(), opt.curve()],
|
||||||
|
callback: async ({ options }) => inst.setFader('cg', n(options.cg), n(options.level), ...fadeArgs(options)),
|
||||||
},
|
},
|
||||||
cg_fader_adjust: {
|
cg_fader_adjust: {
|
||||||
name: 'Control group: adjust fader (relative)',
|
name: 'Control group: adjust fader (relative)',
|
||||||
@@ -305,6 +340,12 @@ export function getActionDefinitions(inst) {
|
|||||||
},
|
},
|
||||||
snapshot_next: { name: 'Snapshot: next', options: [], callback: async () => inst.fireNextSnapshot() },
|
snapshot_next: { name: 'Snapshot: next', options: [], callback: async () => inst.fireNextSnapshot() },
|
||||||
snapshot_prev: { name: 'Snapshot: previous', options: [], callback: async () => inst.firePrevSnapshot() },
|
snapshot_prev: { name: 'Snapshot: previous', options: [], callback: async () => inst.firePrevSnapshot() },
|
||||||
|
fade_stop: {
|
||||||
|
name: 'Stop all fades',
|
||||||
|
description: 'Ends every running level fade where it is.',
|
||||||
|
options: [],
|
||||||
|
callback: async () => inst.stopFades(),
|
||||||
|
},
|
||||||
reload: {
|
reload: {
|
||||||
name: 'Console: reload names and levels',
|
name: 'Console: reload names and levels',
|
||||||
options: [],
|
options: [],
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const watchKey = {
|
|||||||
snapshot: 'snapshot',
|
snapshot: 'snapshot',
|
||||||
connected: 'connected',
|
connected: 'connected',
|
||||||
relay: 'relay',
|
relay: 'relay',
|
||||||
|
fades: 'fades',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFeedbackDefinitions(inst) {
|
export function getFeedbackDefinitions(inst) {
|
||||||
@@ -207,6 +208,7 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
matrix_solo: bool('Matrix output: solo on', AMBER, [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => s.getSolo('matrix', n(options.matrix)) === true),
|
matrix_solo: bool('Matrix output: solo on', AMBER, [opt.matrix(inst)], entityKey('matrix', 'matrix'), ({ options }) => s.getSolo('matrix', n(options.matrix)) === true),
|
||||||
cg_solo: bool('Control group: solo on', AMBER, [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => s.getSolo('cg', n(options.cg)) === true),
|
cg_solo: bool('Control group: solo on', AMBER, [opt.cg(inst)], entityKey('cg', 'cg'), ({ options }) => s.getSolo('cg', n(options.cg)) === true),
|
||||||
matrix_send_on: bool('Matrix send: on', GREEN, msendOptions(), msendKey, ({ options }) => msendOf(options).on === true),
|
matrix_send_on: bool('Matrix send: on', GREEN, msendOptions(), msendKey, ({ options }) => msendOf(options).on === true),
|
||||||
|
matrix_send_off: bool('Matrix send: is off (shows like a mute)', RED, msendOptions(), msendKey, ({ options }) => msendOf(options).on === false),
|
||||||
geq_on: bool('Graphic EQ: in', GREEN, [opt.geq(inst)], geqOnKey, ({ options }) => {
|
geq_on: bool('Graphic EQ: in', GREEN, [opt.geq(inst)], geqOnKey, ({ options }) => {
|
||||||
inst.ensureGeqOn(n(options.geq))
|
inst.ensureGeqOn(n(options.geq))
|
||||||
return s.getGeq(n(options.geq)).on === true
|
return s.getGeq(n(options.geq)).on === true
|
||||||
@@ -246,6 +248,7 @@ export function getFeedbackDefinitions(inst) {
|
|||||||
}),
|
}),
|
||||||
connected: bool('Console link is alive', GREEN, [], () => watchKey.connected, () => inst.connectedFlag === true),
|
connected: bool('Console link is alive', GREEN, [], () => watchKey.connected, () => inst.connectedFlag === true),
|
||||||
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),
|
||||||
|
fade_running: bool('A fade is running', AMBER, [], () => watchKey.fades, () => inst.fades.size > 0),
|
||||||
|
|
||||||
input_signal: bool(
|
input_signal: bool(
|
||||||
'Input meter: signal present (level above a dBFS threshold; gain reduction meters: any reduction)',
|
'Input meter: signal present (level above a dBFS threshold; gain reduction meters: any reduction)',
|
||||||
@@ -409,7 +412,7 @@ export function feedbackIdsForEvent(ev) {
|
|||||||
if (ev.field === 'pan') return ['send_pan_value', 'send_pan_num']
|
if (ev.field === 'pan') return ['send_pan_value', 'send_pan_num']
|
||||||
return []
|
return []
|
||||||
case 'msend':
|
case 'msend':
|
||||||
return ev.field === 'level' ? ['matrix_send_level_value', 'matrix_send_level_pos', 'matrix_send_level_bar'] : ['matrix_send_on']
|
return ev.field === 'level' ? ['matrix_send_level_value', 'matrix_send_level_pos', 'matrix_send_level_bar'] : ['matrix_send_on', 'matrix_send_off']
|
||||||
case 'geq':
|
case 'geq':
|
||||||
return ev.field === 'on' ? ['geq_on'] : ['geq_band_value', 'geq_band_db', 'geq_band_bar']
|
return ev.field === 'on' ? ['geq_on'] : ['geq_band_value', 'geq_band_db', 'geq_band_bar']
|
||||||
case 'group':
|
case 'group':
|
||||||
|
|||||||
+52
-20
@@ -54,24 +54,34 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
const text = (id, x, y, width, height, value) => ({ type: 'text', id, x, y, width, height, text: value, fontsize: 100, fontsizeAllowShrink: true, color: TEXT, halign: 'center', valign: 'center' })
|
const text = (id, x, y, width, height, value) => ({ type: 'text', id, x, y, width, height, text: value, fontsize: 100, fontsizeAllowShrink: true, color: TEXT, halign: 'center', valign: 'center' })
|
||||||
/** Boolean feedback on a layered preset: it colours the background box. */
|
/** Boolean feedback on a layered preset: it colours the background box. */
|
||||||
const tint = (feedbackId, options, color) => ({ feedbackId, options, styleOverrides: [{ elementId: 'bg', elementProperty: 'color', override: color }] })
|
const tint = (feedbackId, options, color) => ({ feedbackId, options, styleOverrides: [{ elementId: 'bg', elementProperty: 'color', override: color }] })
|
||||||
|
/** Green / amber / red zones for a meter gauge on the -60..0 dBFS scale. */
|
||||||
|
const meterStops = () => [
|
||||||
|
{ value: 0, color: GAUGE_GREEN, gradient: false },
|
||||||
|
{ value: 70, color: METER_AMBER, gradient: false },
|
||||||
|
{ value: 90, color: METER_RED, gradient: false },
|
||||||
|
]
|
||||||
|
/** Width (%) of the optional meter column at the right edge of a knob preset. */
|
||||||
|
const KNOB_METER_WIDTH = 13
|
||||||
|
/** Track brightness (%) behind multi-colour meter gauges: faint, so an empty meter reads as empty. */
|
||||||
|
const METER_TRACK_AMOUNT = 20
|
||||||
/**
|
/**
|
||||||
* Layered rotary preset with a native Companion gauge along the bottom: black background, the entity name on top, the
|
* Layered rotary preset with a native Companion gauge along the bottom: black background, the entity name on top, the
|
||||||
* dB text below it, and a gauge fed by the `pos` local variable. `level` and `pos` are the text and the numeric value
|
* dB text below it, and a gauge fed by the `pos` local variable. `level` and `pos` are the text and the numeric value
|
||||||
* feedbacks for the same thing.
|
* feedbacks for the same thing. With `meter: [feedbackId, options]` a vertical meter column (fed by the `meter` local
|
||||||
|
* variable, a meter position feedback) runs down the right edge and the rest of the button narrows to make room.
|
||||||
*/
|
*/
|
||||||
const layeredKnob = ({ name, title, press, adjust: [adjustActionId, adjustOptions], level: [levelId, levelOptions], pos: [posId, posOptions], feedbacks }) => ({
|
const layeredKnob = ({ name, title, press, adjust: [adjustActionId, adjustOptions], level: [levelId, levelOptions], pos: [posId, posOptions], meter, feedbacks }) => {
|
||||||
type: 'layered',
|
const w = meter ? 100 - KNOB_METER_WIDTH : 100
|
||||||
name,
|
const elements = [
|
||||||
elements: [
|
|
||||||
{ type: 'box', id: 'bg', x: 0, y: 0, width: 100, height: 100, color: BLACK },
|
{ type: 'box', id: 'bg', x: 0, y: 0, width: 100, height: 100, color: BLACK },
|
||||||
text('title', 0, 0, 100, 55, title),
|
text('title', 0, 0, w, 55, title),
|
||||||
text('level', 0, 52, 100, 32, '$(local:level) dB'),
|
text('level', 0, 52, w, 32, '$(local:level) dB'),
|
||||||
{
|
{
|
||||||
type: 'gauge',
|
type: 'gauge',
|
||||||
id: 'gauge',
|
id: 'gauge',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 84,
|
y: 84,
|
||||||
width: 100,
|
width: w,
|
||||||
height: 16,
|
height: 16,
|
||||||
value: { isExpression: true, value: '$(local:pos)' },
|
value: { isExpression: true, value: '$(local:pos)' },
|
||||||
min: 0,
|
min: 0,
|
||||||
@@ -82,11 +92,30 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
multiColour: false,
|
multiColour: false,
|
||||||
stops: [{ value: 0, color: GAUGE_LEVEL, gradient: false }],
|
stops: [{ value: 0, color: GAUGE_LEVEL, gradient: false }],
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
steps: knobSteps(press, adjustActionId, adjustOptions),
|
const localVariables = [localFeedback('level', levelId, levelOptions), localFeedback('pos', posId, posOptions)]
|
||||||
feedbacks,
|
if (meter) {
|
||||||
localVariables: [localFeedback('level', levelId, levelOptions), localFeedback('pos', posId, posOptions)],
|
elements.push({
|
||||||
})
|
type: 'gauge',
|
||||||
|
id: 'meter',
|
||||||
|
x: w + 1,
|
||||||
|
y: 0,
|
||||||
|
width: KNOB_METER_WIDTH - 1,
|
||||||
|
height: 100,
|
||||||
|
value: { isExpression: true, value: '$(local:meter)' },
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
orientation: 'vertical',
|
||||||
|
trackStyle: 'dimmed',
|
||||||
|
fillEnabled: true,
|
||||||
|
multiColour: true,
|
||||||
|
trackAmount: METER_TRACK_AMOUNT,
|
||||||
|
stops: meterStops(),
|
||||||
|
})
|
||||||
|
localVariables.push(localFeedback('meter', meter[0], meter[1]))
|
||||||
|
}
|
||||||
|
return { type: 'layered', name, elements, steps: knobSteps(press, adjustActionId, adjustOptions), feedbacks, localVariables }
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Layered meter: name on the left, a vertical gauge (green / amber / red zones) at the right edge; the position comes
|
* 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.
|
* from `posFeedback`. A signal feedback tints the background dark green and, for level meters, a peak feedback red.
|
||||||
@@ -111,11 +140,8 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
trackStyle: 'dimmed',
|
trackStyle: 'dimmed',
|
||||||
fillEnabled: true,
|
fillEnabled: true,
|
||||||
multiColour: true,
|
multiColour: true,
|
||||||
stops: [
|
trackAmount: METER_TRACK_AMOUNT,
|
||||||
{ value: 0, color: GAUGE_GREEN, gradient: false },
|
stops: meterStops(),
|
||||||
{ value: 70, color: METER_AMBER, gradient: false },
|
|
||||||
{ value: 90, color: METER_RED, gradient: false },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
steps: [{ down: [], up: [] }],
|
steps: [{ down: [], up: [] }],
|
||||||
@@ -306,7 +332,11 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
const text = `${nameVar('minput', 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 })
|
// A send that is off looks like a mute (red); an enabled send keeps the black background.
|
||||||
|
presets[`msend_on_${inN}_${out}`] = simple(`Matrix send on/off: ${tag}`, text, [act('matrix_send_on', { ...options, state: 'toggle' })], [fb('matrix_send_off', options, MUTED_RED)], {
|
||||||
|
style: { text, size: 'auto', color: WHITE, bgcolor: BLACK },
|
||||||
|
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: nameVar('minput', inN),
|
title: nameVar('minput', inN),
|
||||||
@@ -314,7 +344,9 @@ function build(inst, { grMeters = true, sendKnobs: withSendKnobs = true } = {})
|
|||||||
adjust: ['matrix_send_level_adjust', options],
|
adjust: ['matrix_send_level_adjust', options],
|
||||||
level: ['matrix_send_level_value', options],
|
level: ['matrix_send_level_value', options],
|
||||||
pos: ['matrix_send_level_pos', options],
|
pos: ['matrix_send_level_pos', options],
|
||||||
feedbacks: [tint('matrix_send_on', options, ON_GREEN)],
|
// The console has no meter for matrix inputs (no address answers), so the column shows the destination output.
|
||||||
|
meter: ['matrix_meter_pos', { matrix: out }],
|
||||||
|
feedbacks: [tint('matrix_send_off', options, MUTED_RED)],
|
||||||
})
|
})
|
||||||
onIds.push(`msend_on_${inN}_${out}`)
|
onIds.push(`msend_on_${inN}_${out}`)
|
||||||
knobIds.push(`msend_knob_${inN}_${out}`)
|
knobIds.push(`msend_knob_${inN}_${out}`)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function getVariableDefinitions(inst) {
|
|||||||
connected: { name: 'Console link alive (Yes/No)' },
|
connected: { name: 'Console link alive (Yes/No)' },
|
||||||
relay_clients: { name: 'iPad apps connected through the relay' },
|
relay_clients: { name: 'iPad apps connected through the relay' },
|
||||||
relay_client_ips: { name: 'iPad client IPs' },
|
relay_client_ips: { name: 'iPad client IPs' },
|
||||||
|
fades_running: { name: 'Number of level fades running' },
|
||||||
last_change: { name: 'Last change reported by the console' },
|
last_change: { name: 'Last change reported by the console' },
|
||||||
snapshot_index: { name: 'Current snapshot index' },
|
snapshot_index: { name: 'Current snapshot index' },
|
||||||
snapshot_name: { name: 'Current snapshot name' },
|
snapshot_name: { name: 'Current snapshot name' },
|
||||||
@@ -75,7 +76,7 @@ export function snapshotVariableValues(inst) {
|
|||||||
/** Everything, after the definitions were rebuilt. */
|
/** Everything, after the definitions were rebuilt. */
|
||||||
export function getAllVariableValues(inst) {
|
export function getAllVariableValues(inst) {
|
||||||
const s = inst.state
|
const s = inst.state
|
||||||
const out = { console_name: s.console.name, session_name: s.console.session, connected: inst.connectedFlag ? 'Yes' : 'No', ...snapshotVariableValues(inst) }
|
const out = { console_name: s.console.name, session_name: s.console.session, connected: inst.connectedFlag ? 'Yes' : 'No', fades_running: inst.fades?.size ?? 0, ...snapshotVariableValues(inst) }
|
||||||
for (const ent of Object.keys(ENTITY_VARS)) for (const n of s.numbers(ent)) Object.assign(out, entityVariableValues(inst, ent, n))
|
for (const ent of Object.keys(ENTITY_VARS)) for (const n of s.numbers(ent)) Object.assign(out, entityVariableValues(inst, ent, n))
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-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, outputMeterId, outputMeterSubscription, parse, query, sameValue } from './protocol.js'
|
import { FADE_CURVES, GEQ_RANGE, LEVEL, PARAMS, K, addr, adjustedLevel, clamp, fadeLevelAt, 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'
|
||||||
@@ -11,6 +11,11 @@ import { getPresetDefinitions } from './definitions/presets.js'
|
|||||||
|
|
||||||
const LEARN_CAP = 400
|
const LEARN_CAP = 400
|
||||||
|
|
||||||
|
/** A fade writes a new level this often (20 steps per second at most) and skips steps that would change the level by less than FADE_MIN_DB. */
|
||||||
|
const FADE_STEP_MS = 50
|
||||||
|
const FADE_MIN_DB = 0.05
|
||||||
|
const FADE_MAX_MS = 60000
|
||||||
|
|
||||||
/** Timings in ms. An instance copies these to `this.timing` so tests can shorten them. */
|
/** Timings in ms. An instance copies these to `this.timing` so tests can shorten them. */
|
||||||
const TIMING = {
|
const TIMING = {
|
||||||
keepaliveMs: 5000, // /Console/Name/? ping
|
keepaliveMs: 5000, // /Console/Name/? ping
|
||||||
@@ -196,6 +201,9 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
this.requested = new Map() // lazily loaded pairs -> time requested, de-duplicated for a while
|
this.requested = new Map() // lazily loaded pairs -> time requested, de-duplicated for a while
|
||||||
this.waiters = new Map() // event key -> Set<fn>
|
this.waiters = new Map() // event key -> Set<fn>
|
||||||
this.pendingWrites = new Map() // address -> { value, until, timer }
|
this.pendingWrites = new Map() // address -> { value, until, timer }
|
||||||
|
this.fades = new Map() // address -> the running fade (one per address)
|
||||||
|
this.fadeStarts = new Map() // address -> token of a fade still waiting for the level it starts from
|
||||||
|
this.fadeSeq = 0
|
||||||
this.errorLog = new Map() // error code + destination -> time last logged
|
this.errorLog = new Map() // error code + destination -> time last logged
|
||||||
this.connectedFlag = false
|
this.connectedFlag = false
|
||||||
this.statusName = 'connecting'
|
this.statusName = 'connecting'
|
||||||
@@ -532,6 +540,11 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
for (const t of this.timers) clearTimeout(t)
|
for (const t of this.timers) clearTimeout(t)
|
||||||
this.timers.clear()
|
this.timers.clear()
|
||||||
this.pendingWrites.clear()
|
this.pendingWrites.clear()
|
||||||
|
this.fadeStarts.clear()
|
||||||
|
if (this.fades.size) {
|
||||||
|
this.fades.clear() // their step timers were cleared with the others above
|
||||||
|
this.fadesChanged()
|
||||||
|
}
|
||||||
if (this.rebuildTimer) clearTimeout(this.rebuildTimer)
|
if (this.rebuildTimer) clearTimeout(this.rebuildTimer)
|
||||||
this.rebuildTimer = null
|
this.rebuildTimer = null
|
||||||
this.resubscribeTimer = null
|
this.resubscribeTimer = null
|
||||||
@@ -787,9 +800,10 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
return this.write(address, [on ? 1 : 0], on ? 1 : 0)
|
return this.write(address, [on ? 1 : 0], on ? 1 : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Write a dB level, clamped to [lo, +10]; `lo` is -150 except for matrix sends (-90). */
|
/** Write a dB level, clamped to [lo, +10]; `lo` is -150 except for matrix sends (-90). A running fade on the address ends. */
|
||||||
writeLevel(address, dB, lo = LEVEL.min) {
|
writeLevel(address, dB, lo = LEVEL.min) {
|
||||||
if (!this.addressOk(address)) return false
|
if (!this.addressOk(address)) return false
|
||||||
|
this.cancelFade(address)
|
||||||
const v = clamp(Number(dB), lo, LEVEL.max)
|
const v = clamp(Number(dB), lo, LEVEL.max)
|
||||||
if (!Number.isFinite(v)) return false
|
if (!Number.isFinite(v)) return false
|
||||||
return this.write(address, [v], v)
|
return this.write(address, [v], v)
|
||||||
@@ -797,6 +811,7 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
|
|
||||||
async adjustLevel(address, key, get, delta, lo = LEVEL.min) {
|
async adjustLevel(address, key, get, delta, lo = LEVEL.min) {
|
||||||
if (!this.addressOk(address)) return false
|
if (!this.addressOk(address)) return false
|
||||||
|
this.cancelFade(address)
|
||||||
const cur = await this.current(key, get, address)
|
const cur = await this.current(key, get, address)
|
||||||
if (cur === undefined) {
|
if (cur === undefined) {
|
||||||
this.log('warn', `${address}: current level unknown (no reply from the console), not adjusted`)
|
this.log('warn', `${address}: current level unknown (no reply from the console), not adjusted`)
|
||||||
@@ -805,6 +820,93 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
return this.writeLevel(address, adjustedLevel(cur, delta, lo), lo)
|
return this.writeLevel(address, adjustedLevel(cur, delta, lo), lo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- fades ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a level from where it is now to `targetDb` over `durationMs`: a new level every FADE_STEP_MS (20 Hz), each
|
||||||
|
* a normal write(), the last one exactly the target. Curves are those of `fadeLevelAt`. There is one fade per
|
||||||
|
* address: a new fade, a plain set or an adjust on the same address ends the running one, and closing the console
|
||||||
|
* link ends them all. When the current level cannot be read, the target is written at once.
|
||||||
|
*/
|
||||||
|
async fadeLevel(address, key, get, targetDb, durationMs, curve = 'fader', lo = LEVEL.min) {
|
||||||
|
if (!this.addressOk(address)) return false
|
||||||
|
const target = clamp(Number(targetDb), lo, LEVEL.max)
|
||||||
|
if (!Number.isFinite(target)) return false
|
||||||
|
const ms = Math.min(Number(durationMs), FADE_MAX_MS)
|
||||||
|
if (!(ms > 0)) return this.writeLevel(address, target, lo)
|
||||||
|
this.cancelFade(address)
|
||||||
|
const token = ++this.fadeSeq
|
||||||
|
this.fadeStarts.set(address, token)
|
||||||
|
const from = await this.current(key, get, address)
|
||||||
|
if (this.fadeStarts.get(address) !== token) return false // another action took the address, fades were stopped or the link was closed meanwhile
|
||||||
|
this.fadeStarts.delete(address)
|
||||||
|
if (from === undefined || !Number.isFinite(from)) {
|
||||||
|
this.log('warn', `${address}: current level unknown (no reply from the console), set without a fade`)
|
||||||
|
return this.write(address, [target], target)
|
||||||
|
}
|
||||||
|
if (!this.link) return false
|
||||||
|
const steps = Math.max(1, Math.ceil(ms / FADE_STEP_MS))
|
||||||
|
const fade = { address, from, target, curve: FADE_CURVES.includes(curve) ? curve : 'fader', lo, steps, interval: ms / steps, k: 0, last: from, startedAt: Date.now(), timer: null }
|
||||||
|
this.fades.set(address, fade)
|
||||||
|
this.fadesChanged()
|
||||||
|
this.scheduleFadeStep(fade)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleFadeStep(fade) {
|
||||||
|
const due = fade.startedAt + Math.round((fade.k + 1) * fade.interval)
|
||||||
|
fade.timer = this.later(() => this.fadeStep(fade), Math.max(0, due - Date.now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fadeStep(fade) {
|
||||||
|
if (this.fades.get(fade.address) !== fade) return
|
||||||
|
fade.k++
|
||||||
|
const final = fade.k >= fade.steps
|
||||||
|
const db = fadeLevelAt(fade.curve, fade.from, fade.target, fade.k / fade.steps, fade.lo)
|
||||||
|
if (final ? db !== fade.last : Math.abs(db - fade.last) >= FADE_MIN_DB) {
|
||||||
|
if (!this.write(fade.address, [db], db)) {
|
||||||
|
this.cancelFade(fade.address)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fade.last = db
|
||||||
|
}
|
||||||
|
if (final) {
|
||||||
|
this.fades.delete(fade.address)
|
||||||
|
this.fadesChanged()
|
||||||
|
} else {
|
||||||
|
this.scheduleFadeStep(fade)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** End the fade running on `address` (or waiting for its starting level) where the level is now. */
|
||||||
|
cancelFade(address) {
|
||||||
|
this.fadeStarts.delete(address)
|
||||||
|
const fade = this.fades.get(address)
|
||||||
|
if (!fade) return false
|
||||||
|
clearTimeout(fade.timer)
|
||||||
|
this.timers.delete(fade.timer)
|
||||||
|
this.fades.delete(address)
|
||||||
|
this.fadesChanged()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** End every fade; each level stays where it is. */
|
||||||
|
stopFades() {
|
||||||
|
this.fadeStarts.clear()
|
||||||
|
if (!this.fades.size) return
|
||||||
|
for (const fade of this.fades.values()) {
|
||||||
|
clearTimeout(fade.timer)
|
||||||
|
this.timers.delete(fade.timer)
|
||||||
|
}
|
||||||
|
this.fades.clear()
|
||||||
|
this.fadesChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fadesChanged() {
|
||||||
|
this.queueVariables({ fades_running: this.fades.size })
|
||||||
|
this.touch(watchKey.fades, ['fade_running'])
|
||||||
|
}
|
||||||
|
|
||||||
writePan(address, pan) {
|
writePan(address, pan) {
|
||||||
if (!this.addressOk(address)) return false
|
if (!this.addressOk(address)) return false
|
||||||
const v = clamp(Number(pan), 0, 1)
|
const v = clamp(Number(pan), 0, 1)
|
||||||
@@ -832,8 +934,12 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
return this.writeFlag(addr[field](ent, n, side), K.value(ent, n, field, side), get, mode)
|
return this.writeFlag(addr[field](ent, n, side), K.value(ent, n, field, side), get, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
setFader(ent, n, dB) {
|
/** Set a fader; with `durationMs` > 0 it moves there over that time (see fadeLevel). */
|
||||||
return this.writeLevel(addr.fader(ent, n, this.sideOf(ent, n)), dB)
|
setFader(ent, n, dB, durationMs = 0, curve = 'fader') {
|
||||||
|
const side = this.sideOf(ent, n)
|
||||||
|
const address = addr.fader(ent, n, side)
|
||||||
|
if (!(durationMs > 0)) return this.writeLevel(address, dB)
|
||||||
|
return this.fadeLevel(address, K.value(ent, n, 'fader', side), () => this.state.getFader(ent, n), dB, durationMs, curve)
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustFader(ent, n, delta) {
|
adjustFader(ent, n, delta) {
|
||||||
@@ -862,8 +968,10 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
return this.writeFlag(addr.sendOn(ch, buss), K.send(ch, buss, 'on'), () => this.state.getSend(ch, buss).on, mode)
|
return this.writeFlag(addr.sendOn(ch, buss), K.send(ch, buss, 'on'), () => this.state.getSend(ch, buss).on, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
setSendLevel(ch, buss, dB) {
|
setSendLevel(ch, buss, dB, durationMs = 0, curve = 'fader') {
|
||||||
return this.writeLevel(addr.sendLevel(ch, buss), dB)
|
const address = addr.sendLevel(ch, buss)
|
||||||
|
if (!(durationMs > 0)) return this.writeLevel(address, dB)
|
||||||
|
return this.fadeLevel(address, K.send(ch, buss, 'level'), () => this.state.getSend(ch, buss).level, dB, durationMs, curve)
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustSendLevel(ch, buss, delta) {
|
adjustSendLevel(ch, buss, delta) {
|
||||||
@@ -886,8 +994,10 @@ export default class DigicoS31Instance extends InstanceBase {
|
|||||||
return this.writeFlag(addr.matrixSendOn(inN, outN), K.msend(inN, outN, 'on'), () => this.state.getMatrixSend(inN, outN).on, mode)
|
return this.writeFlag(addr.matrixSendOn(inN, outN), K.msend(inN, outN, 'on'), () => this.state.getMatrixSend(inN, outN).on, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
setMatrixSendLevel(inN, outN, dB) {
|
setMatrixSendLevel(inN, outN, dB, durationMs = 0, curve = 'fader') {
|
||||||
return this.writeLevel(addr.matrixSendLevel(inN, outN), dB, LEVEL.matrixSendMin)
|
const address = addr.matrixSendLevel(inN, outN)
|
||||||
|
if (!(durationMs > 0)) return this.writeLevel(address, dB, LEVEL.matrixSendMin)
|
||||||
|
return this.fadeLevel(address, K.msend(inN, outN, 'level'), () => this.state.getMatrixSend(inN, outN).level, dB, durationMs, curve, LEVEL.matrixSendMin)
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustMatrixSendLevel(inN, outN, delta) {
|
adjustMatrixSendLevel(inN, outN, delta) {
|
||||||
|
|||||||
@@ -349,6 +349,54 @@ export function faderPosition(dB, floor = LEVEL.floor) {
|
|||||||
return 1 + ((dB - floor) / (prev[0] - floor)) * (prev[1] - 1)
|
return 1 + ((dB - floor) / (prev[0] - floor)) * (prev[1] - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exact inverse of `faderPosition` (same anchor table, same `floor`): position 0..100 -> dB. Positions up to 1 (the
|
||||||
|
* "off" end, which `faderPosition` gives only to levels at or below `floor`) read as `floor`; 100 or more is +10 dB.
|
||||||
|
* Non-numbers give `floor`.
|
||||||
|
*/
|
||||||
|
export function positionToDb(pos, floor = LEVEL.floor) {
|
||||||
|
if (typeof pos !== 'number' || !Number.isFinite(pos) || pos <= 1) return floor
|
||||||
|
if (pos >= FADER_TAPER[0][1]) return FADER_TAPER[0][0]
|
||||||
|
let prev = FADER_TAPER[0]
|
||||||
|
for (const point of FADER_TAPER.slice(1)) {
|
||||||
|
if (pos >= point[1]) return point[0] + ((pos - point[1]) / (prev[1] - point[1])) * (prev[0] - point[0])
|
||||||
|
prev = point
|
||||||
|
}
|
||||||
|
return floor + ((pos - 1) / (prev[1] - 1)) * (prev[0] - floor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fade curves of the "set level" actions (see `fadeLevelAt`). */
|
||||||
|
export const FADE_CURVES = ['fader', 'ease', 'db']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dB value a fade has reached at progress `t` (0..1, by time) when it runs from `from` to `to` dB. Curves:
|
||||||
|
* 'fader' moves in fader position (the taper of a physical fader, so it sounds even), 'ease' is the same with a
|
||||||
|
* smoothstep on the time (slow start and end), 'db' moves linearly in dB. The result stays within [lo, +10]; t >= 1 is
|
||||||
|
* exactly the (clamped) target. `floor` is the level the taper treats as off.
|
||||||
|
*/
|
||||||
|
export function fadeLevelAt(curve, from, to, t, lo = LEVEL.min, floor = LEVEL.floor) {
|
||||||
|
const a = clamp(from, lo, LEVEL.max)
|
||||||
|
const b = clamp(to, lo, LEVEL.max)
|
||||||
|
if (!(t < 1)) return b
|
||||||
|
const u = t > 0 ? t : 0
|
||||||
|
if (curve === 'db') {
|
||||||
|
// Everything below the floor (-90) is inaudible, so a dB line from -inf starts at the floor and one to -inf
|
||||||
|
// ends there (the last step, t = 1 above, still writes the exact target). A fade that stays below the floor
|
||||||
|
// simply runs between its two ends.
|
||||||
|
if (a <= floor && b <= floor) return clamp(a + (b - a) * u, lo, LEVEL.max)
|
||||||
|
const a2 = Math.max(a, floor)
|
||||||
|
const b2 = Math.max(b, floor)
|
||||||
|
return clamp(a2 + (b2 - a2) * u, lo, LEVEL.max)
|
||||||
|
}
|
||||||
|
const e = curve === 'ease' ? u * u * (3 - 2 * u) : u
|
||||||
|
// Below the floor the fader taper is flat (all of it reads as -inf), so a fade that stays down there runs in dB.
|
||||||
|
if (a <= floor && b <= floor) return clamp(a + (b - a) * e, lo, LEVEL.max)
|
||||||
|
const p0 = faderPosition(a, floor)
|
||||||
|
const p1 = faderPosition(b, floor)
|
||||||
|
// Never step outside the start..target range (the taper's floor step could otherwise land above a target below -90).
|
||||||
|
return clamp(positionToDb(p0 + (p1 - p0) * e, floor), Math.max(lo, Math.min(a, b)), Math.min(LEVEL.max, Math.max(a, b)))
|
||||||
|
}
|
||||||
|
|
||||||
/** Snapshot cue number as the surface shows it: raw 100 -> "1", 250 -> "2.5". */
|
/** Snapshot cue number as the surface shows it: raw 100 -> "1", 250 -> "2.5". */
|
||||||
export function fmtCue(raw) {
|
export function fmtCue(raw) {
|
||||||
if (typeof raw !== 'number' || !Number.isFinite(raw)) return ''
|
if (typeof raw !== 'number' || !Number.isFinite(raw)) return ''
|
||||||
|
|||||||
+334
-15
@@ -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, cleanName, faderPosition, fmtCue, fmtDb, fmtMeterLevel, fmtPan, indexProblem, meterGrDb, meterId, meterIdForInput, meterLevelDb, meterPosition, outputMeterId, parse } from '../src/protocol.js'
|
import { FADE_CURVES, LEVEL, PARAMS, adjustedLevel, cleanName, fadeLevelAt, faderPosition, fmtCue, fmtDb, fmtMeterLevel, fmtPan, indexProblem, meterGrDb, meterId, meterIdForInput, meterLevelDb, meterPosition, outputMeterId, parse, positionToDb } 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'
|
||||||
@@ -44,7 +44,7 @@ const near = (a, b, eps = 1e-3) => typeof a === 'number' && Math.abs(a - b) < ep
|
|||||||
const sent = []
|
const sent = []
|
||||||
const originalSend = ConsoleLink.prototype.send
|
const originalSend = ConsoleLink.prototype.send
|
||||||
ConsoleLink.prototype.send = function (address, args = []) {
|
ConsoleLink.prototype.send = function (address, args = []) {
|
||||||
if (!this.noRecord) sent.push({ address, args })
|
if (!this.noRecord) sent.push({ address, args, at: Date.now() })
|
||||||
return originalSend.call(this, address, args)
|
return originalSend.call(this, address, args)
|
||||||
}
|
}
|
||||||
const logs = []
|
const logs = []
|
||||||
@@ -430,17 +430,17 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
const adjusts = Object.entries(ctx.actions).filter(([id]) => /(fader|send_level)_adjust$/.test(id))
|
const adjusts = Object.entries(ctx.actions).filter(([id]) => /(fader|send_level)_adjust$/.test(id))
|
||||||
check(adjusts.length === 6 && adjusts.every(([, a]) => a.description.includes('From \u2212\u221e the first step up jumps to \u221260 dB')), `all ${adjusts.length} level adjust actions say that the first step up from -inf jumps to -60 dB`)
|
check(adjusts.length === 6 && adjusts.every(([, a]) => a.description.includes('From \u2212\u221e the first step up jumps to \u221260 dB')), `all ${adjusts.length} level adjust actions say that the first step up from -inf jumps to -60 dB`)
|
||||||
check(ctx.actions.input_mute.options[0].choices[0].label === '1 · KICK In' && ctx.actions.send_on.options[1].choices.every((c) => S.bussMode(c.id) === 'aux'), 'action dropdowns show the live labels; send actions list aux busses only')
|
check(ctx.actions.input_mute.options[0].choices[0].label === '1 · KICK In' && ctx.actions.send_on.options[1].choices.every((c) => S.bussMode(c.id) === 'aux'), 'action dropdowns show the live labels; send actions list aux busses only')
|
||||||
const idsExpected = ['input_mute', 'input_solo', 'input_fader_set', 'input_fader_adjust', 'input_pan_set', 'input_pan_adjust', 'input_name_set', 'send_on', 'send_level_set', 'send_level_adjust', 'send_pan_set', 'send_pan_adjust', 'group_assign', 'buss_mute', 'buss_solo', 'buss_fader_set', 'buss_fader_adjust', 'matrix_mute', 'matrix_solo', 'matrix_fader_set', 'matrix_fader_adjust', 'matrix_send_on', 'matrix_send_level_set', 'matrix_send_level_adjust', 'cg_mute', 'cg_solo', 'cg_fader_set', 'cg_fader_adjust', 'geq_band_set', 'geq_band_adjust', 'geq_on', 'snapshot_fire', 'snapshot_next', 'snapshot_prev', 'reload', 'raw_osc', 'relay_forget']
|
const idsExpected = ['input_mute', 'input_solo', 'input_fader_set', 'input_fader_adjust', 'input_pan_set', 'input_pan_adjust', 'input_name_set', 'send_on', 'send_level_set', 'send_level_adjust', 'send_pan_set', 'send_pan_adjust', 'group_assign', 'buss_mute', 'buss_solo', 'buss_fader_set', 'buss_fader_adjust', 'matrix_mute', 'matrix_solo', 'matrix_fader_set', 'matrix_fader_adjust', 'matrix_send_on', 'matrix_send_level_set', 'matrix_send_level_adjust', 'cg_mute', 'cg_solo', 'cg_fader_set', 'cg_fader_adjust', 'geq_band_set', 'geq_band_adjust', 'geq_on', 'snapshot_fire', 'snapshot_next', 'snapshot_prev', 'reload', 'raw_osc', 'relay_forget', 'fade_stop']
|
||||||
// per-channel switches and values, generated from the PARAMS table
|
// per-channel switches and values, generated from the PARAMS table
|
||||||
for (const [param, p] of Object.entries(PARAMS)) {
|
for (const [param, p] of Object.entries(PARAMS)) {
|
||||||
for (const ent of Object.keys(p.paths)) idsExpected.push(...(p.type === 'switch' ? [`${ent}_${param}`] : [`${ent}_${param}_set`, `${ent}_${param}_adjust`]))
|
for (const ent of Object.keys(p.paths)) idsExpected.push(...(p.type === 'switch' ? [`${ent}_${param}`] : [`${ent}_${param}_set`, `${ent}_${param}_adjust`]))
|
||||||
}
|
}
|
||||||
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', 'fade_running', 'input_signal', 'input_peak', 'input_fader_value', 'input_pan_value', 'send_level_value', 'send_on_value', 'buss_fader_value', 'matrix_fader_value', 'cg_fader_value', 'snapshot_name_value', 'input_meter_value', 'input_fader_bar', 'send_level_bar', 'buss_fader_bar', 'input_meter_bar']
|
||||||
fbExpected.push('buss_signal', 'buss_peak', 'buss_meter_value', 'buss_meter_pos', 'matrix_signal', 'matrix_peak', 'matrix_meter_value', 'matrix_meter_pos')
|
fbExpected.push('buss_signal', 'buss_peak', 'buss_meter_value', 'buss_meter_pos', 'matrix_signal', 'matrix_peak', 'matrix_meter_value', 'matrix_meter_pos')
|
||||||
fbExpected.push('input_fader_pos', 'buss_fader_pos', 'matrix_fader_pos', 'cg_fader_pos', 'send_level_pos', 'matrix_send_level_pos', 'input_pan_num', 'send_pan_num', 'geq_band_db', 'input_meter_pos')
|
fbExpected.push('input_fader_pos', 'buss_fader_pos', 'matrix_fader_pos', 'cg_fader_pos', 'send_level_pos', 'matrix_send_level_pos', 'input_pan_num', 'send_pan_num', 'geq_band_db', 'input_meter_pos')
|
||||||
fbExpected.push('matrix_solo', 'cg_solo', 'matrix_send_on', 'geq_on', 'send_pan_value', 'matrix_send_level_value', 'geq_band_value', 'matrix_fader_bar', 'cg_fader_bar', 'matrix_send_level_bar', 'geq_band_bar')
|
fbExpected.push('matrix_solo', 'cg_solo', 'matrix_send_on', 'matrix_send_off', 'geq_on', 'send_pan_value', 'matrix_send_level_value', 'geq_band_value', 'matrix_fader_bar', 'cg_fader_bar', 'matrix_send_level_bar', 'geq_band_bar')
|
||||||
for (const [param, p] of Object.entries(PARAMS)) for (const ent of Object.keys(p.paths)) fbExpected.push(`${ent}_${param}_${p.type === 'switch' ? 'state' : 'value'}`)
|
for (const [param, p] of Object.entries(PARAMS)) for (const ent of Object.keys(p.paths)) fbExpected.push(`${ent}_${param}_${p.type === 'switch' ? 'state' : 'value'}`)
|
||||||
check(fbExpected.every((id) => ctx.feedbacks[id]) && Object.keys(ctx.feedbacks).length === fbExpected.length, `all ${fbExpected.length} designed feedbacks exist (${Object.keys(ctx.feedbacks).length} registered)`)
|
check(fbExpected.every((id) => ctx.feedbacks[id]) && Object.keys(ctx.feedbacks).length === fbExpected.length, `all ${fbExpected.length} designed feedbacks exist (${Object.keys(ctx.feedbacks).length} registered)`)
|
||||||
// Feedback parity: every action that changes readable state has a feedback for that state
|
// Feedback parity: every action that changes readable state has a feedback for that state
|
||||||
@@ -451,11 +451,11 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
send_pan_set: ['send_pan_value'], send_pan_adjust: ['send_pan_value'], group_assign: ['group_assigned'],
|
send_pan_set: ['send_pan_value'], send_pan_adjust: ['send_pan_value'], group_assign: ['group_assigned'],
|
||||||
buss_mute: ['buss_muted'], buss_solo: ['buss_solo'], buss_fader_set: ['buss_fader_value', 'buss_fader_bar'], buss_fader_adjust: ['buss_fader_value', 'buss_fader_bar'],
|
buss_mute: ['buss_muted'], buss_solo: ['buss_solo'], buss_fader_set: ['buss_fader_value', 'buss_fader_bar'], buss_fader_adjust: ['buss_fader_value', 'buss_fader_bar'],
|
||||||
matrix_mute: ['matrix_muted'], matrix_solo: ['matrix_solo'], matrix_fader_set: ['matrix_fader_value', 'matrix_fader_bar'], matrix_fader_adjust: ['matrix_fader_value', 'matrix_fader_bar'],
|
matrix_mute: ['matrix_muted'], matrix_solo: ['matrix_solo'], matrix_fader_set: ['matrix_fader_value', 'matrix_fader_bar'], matrix_fader_adjust: ['matrix_fader_value', 'matrix_fader_bar'],
|
||||||
matrix_send_on: ['matrix_send_on'], matrix_send_level_set: ['matrix_send_level_value', 'matrix_send_level_bar'], matrix_send_level_adjust: ['matrix_send_level_value', 'matrix_send_level_bar'],
|
matrix_send_on: ['matrix_send_on', 'matrix_send_off'], matrix_send_level_set: ['matrix_send_level_value', 'matrix_send_level_bar'], matrix_send_level_adjust: ['matrix_send_level_value', 'matrix_send_level_bar'],
|
||||||
cg_mute: ['cg_muted'], cg_solo: ['cg_solo'], cg_fader_set: ['cg_fader_value', 'cg_fader_bar'], cg_fader_adjust: ['cg_fader_value', 'cg_fader_bar'],
|
cg_mute: ['cg_muted'], cg_solo: ['cg_solo'], cg_fader_set: ['cg_fader_value', 'cg_fader_bar'], cg_fader_adjust: ['cg_fader_value', 'cg_fader_bar'],
|
||||||
geq_on: ['geq_on'], geq_band_set: ['geq_band_value', 'geq_band_bar'], geq_band_adjust: ['geq_band_value', 'geq_band_bar'],
|
geq_on: ['geq_on'], geq_band_set: ['geq_band_value', 'geq_band_bar'], geq_band_adjust: ['geq_band_value', 'geq_band_bar'],
|
||||||
snapshot_fire: ['snapshot_current', 'snapshot_name_value'], snapshot_next: ['snapshot_current', 'snapshot_name_value'], snapshot_prev: ['snapshot_current', 'snapshot_name_value'],
|
snapshot_fire: ['snapshot_current', 'snapshot_name_value'], snapshot_next: ['snapshot_current', 'snapshot_name_value'], snapshot_prev: ['snapshot_current', 'snapshot_name_value'],
|
||||||
reload: ['connected'], relay_forget: ['relay_clients'],
|
reload: ['connected'], relay_forget: ['relay_clients'], fade_stop: ['fade_running'],
|
||||||
input_name_set: [], // the name is the variable input_N_name
|
input_name_set: [], // the name is the variable input_N_name
|
||||||
raw_osc: [], // free-form
|
raw_osc: [], // free-form
|
||||||
}
|
}
|
||||||
@@ -537,8 +537,17 @@ 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 === '$(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(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.style.bgcolor === 0x000000 && mOn.feedbacks.length === 1 && mOn.feedbacks[0].feedbackId === 'matrix_send_off' && mOn.feedbacks[0].style.bgcolor === 0xb01010 && mOn.feedbacks[0].style.color === 0xffffff && JSON.stringify(mOn.feedbacks[0].options) === JSON.stringify({ matrixIn: 1, matrixOut: 8 }), 'matrix send on/off preset: toggle matrix_send_on, black when the send is on, red with white text when it is off (like a mute), 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(!JSON.stringify(mOn.feedbacks).includes('matrix_send_on') && !JSON.stringify(P.msend_knob_1_8.feedbacks).includes('matrix_send_on') && !ids.filter((id) => id.startsWith('msend_')).some((id) => P[id].feedbacks.some((f) => f.style?.bgcolor === COLORS_GREEN || f.styleOverrides?.some((o) => o.override === 0x00a050))), 'no matrix send preset carries a green (send on) feedback any more')
|
||||||
|
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.length === 1 && mKnob.feedbacks[0].feedbackId === 'matrix_send_off' && mKnob.feedbacks[0].styleOverrides[0].elementId === 'bg' && mKnob.feedbacks[0].styleOverrides[0].elementProperty === 'color' && mKnob.feedbacks[0].styleOverrides[0].override === 0xb01010 && mKnob.localVariables.map((v) => `${v.variableName}=${v.feedbackId}`).join() === 'level=matrix_send_level_value,pos=matrix_send_level_pos,meter=matrix_meter_pos', 'matrix send knob preset (layered): rotate = matrix_send_level_adjust -1/+1, press toggles on/off, off tints the background red, level, gauge position and meter local variables')
|
||||||
|
{
|
||||||
|
const meter = mKnob.elements.find((e) => e.id === 'meter')
|
||||||
|
const title = mKnob.elements.find((e) => e.id === 'title')
|
||||||
|
const mv = mKnob.localVariables.find((v) => v.variableName === 'meter')
|
||||||
|
check(meter && meter.type === 'gauge' && meter.orientation === 'vertical' && meter.x + meter.width <= 100 && meter.x >= title.width && meter.height === 100 && meter.multiColour === true && meter.stops.length === 3 && meter.value.value === '$(local:meter)' && JSON.stringify(mv.options) === JSON.stringify({ matrix: 8 }), `matrix send knob has a vertical meter column on the right fed by the destination output's meter (x ${meter?.x}, width ${meter?.width}, title width ${title?.width})`)
|
||||||
|
const iKnob = P.input_knob_3 || Object.values(P).find((p) => p.type === 'layered' && p.localVariables?.some((v) => v.feedbackId === 'input_fader_pos'))
|
||||||
|
check(iKnob && !iKnob.elements.some((e) => e.id === 'meter') && iKnob.elements.find((e) => e.id === 'title').width === 100, 'other knob presets keep the full-width layout without a meter column')
|
||||||
|
}
|
||||||
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')
|
||||||
@@ -546,7 +555,7 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
const meterPresets = layered.filter((id) => /^input_(meter|gate_gr|comp_gr)_/.test(id))
|
const meterPresets = layered.filter((id) => /^input_(meter|gate_gr|comp_gr)_/.test(id))
|
||||||
check(knobsLayered.length === ids.filter((id) => /_knob_/.test(id)).length && meterPresets.length === 180 && layered.filter((id) => /^(buss|matrix)_meter_/.test(id)).length === 32 && layered.length === knobsLayered.length + 180 + 32 && !ids.some((id) => /_(mute|solo|on)_/.test(id) && P[id].type === 'layered'), `every knob (${knobsLayered.length}) and meter (${meterPresets.length} input + 32 buss / matrix output) preset is layered; plain toggles stay simple`)
|
check(knobsLayered.length === ids.filter((id) => /_knob_/.test(id)).length && meterPresets.length === 180 && layered.filter((id) => /^(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', 'trackAmount'] }
|
||||||
const isExpr = (v) => !!v && typeof v === 'object' && v.isExpression === true && typeof v.value === 'string'
|
const isExpr = (v) => !!v && typeof v === 'object' && v.isExpression === true && typeof v.value === 'string'
|
||||||
const lp = []
|
const lp = []
|
||||||
for (const id of layered) {
|
for (const id of layered) {
|
||||||
@@ -563,7 +572,7 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
|||||||
if (e.type === 'box' && typeof e.color !== 'number') lp.push(`${id}: box color`)
|
if (e.type === 'box' && typeof e.color !== 'number') lp.push(`${id}: box color`)
|
||||||
if (e.type === 'gauge') {
|
if (e.type === 'gauge') {
|
||||||
const okStops = Array.isArray(e.stops) && e.stops.length > 0 && e.stops.every((st) => typeof st.value === 'number' && typeof st.color === 'number' && typeof st.gradient === 'boolean')
|
const okStops = Array.isArray(e.stops) && e.stops.length > 0 && e.stops.every((st) => typeof st.value === 'number' && typeof st.color === 'number' && typeof st.gradient === 'boolean')
|
||||||
if (!isExpr(e.value) || e.value.value !== '$(local:pos)' || e.min !== 0 || e.max !== 100 || !['horizontal', 'vertical', 'ring'].includes(e.orientation) || !['transparent', 'dimmed'].includes(e.trackStyle) || !okStops) lp.push(`${id}: gauge`)
|
if (!isExpr(e.value) || e.value.value !== `$(local:${e.id === 'meter' ? 'meter' : 'pos'})` || e.min !== 0 || e.max !== 100 || !['horizontal', 'vertical', 'ring'].includes(e.orientation) || !['transparent', 'dimmed'].includes(e.trackStyle) || !okStops) lp.push(`${id}: gauge`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const f of p.feedbacks) for (const o of f.styleOverrides || []) {
|
for (const f of p.feedbacks) for (const o of f.styleOverrides || []) {
|
||||||
@@ -898,6 +907,17 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
check(cb('matrix_send_level_value', { matrixIn: 5, matrixOut: 5 }) === '--' && cb('matrix_send_on', { matrixIn: 5, matrixOut: 5 }) === false, 'an unloaded matrix send reads "--" / false and is queued for loading')
|
check(cb('matrix_send_level_value', { matrixIn: 5, matrixOut: 5 }) === '--' && cb('matrix_send_on', { matrixIn: 5, matrixOut: 5 }) === false, 'an unloaded matrix send reads "--" / false and is queued for loading')
|
||||||
check(await waitFor(() => sent.some((m) => m.address === '/Matrix_Inputs/5/Matrix_Send/5/send_level/?') && S.getMatrixSend(5, 5).level !== undefined, 2500), 'the unloaded matrix send is queried once and its state arrives')
|
check(await waitFor(() => sent.some((m) => m.address === '/Matrix_Inputs/5/Matrix_Send/5/send_level/?') && S.getMatrixSend(5, 5).level !== undefined, 2500), 'the unloaded matrix send is queried once and its state arrives')
|
||||||
check(cb('matrix_send_level_value', { matrixIn: 5, matrixOut: 5 }) === '-inf' && cb('matrix_send_on', { matrixIn: 5, matrixOut: 5 }) === false, 'a matrix send at its -90 floor reads "-inf"')
|
check(cb('matrix_send_level_value', { matrixIn: 5, matrixOut: 5 }) === '-inf' && cb('matrix_send_on', { matrixIn: 5, matrixOut: 5 }) === false, 'a matrix send at its -90 floor reads "-inf"')
|
||||||
|
// matrix_send_off: true only when the console says the send is off (like a mute); unknown is not red, and starts the lazy load
|
||||||
|
check(cb('matrix_send_on', { matrixIn: 2, matrixOut: 3 }) === true && cb('matrix_send_off', { matrixIn: 2, matrixOut: 3 }) === false, 'matrix_send_off is false for a send that is on')
|
||||||
|
check(cb('matrix_send_off', { matrixIn: 6, matrixOut: 6 }) === false && cb('matrix_send_on', { matrixIn: 6, matrixOut: 6 }) === false, 'matrix_send_off is false for an unknown send (not red until the console says off) and the send is queued for loading')
|
||||||
|
check(await waitFor(() => sent.some((m) => m.address === '/Matrix_Inputs/6/Matrix_Send/6/send_on/?') && S.getMatrixSend(6, 6).on !== undefined, 2500), 'the unknown send is queried lazily and its on/off arrives')
|
||||||
|
{
|
||||||
|
const sendOn = (v) => inst.onConsoleMessage(decode(encode('/Matrix_Inputs/6/Matrix_Send/6/send_on', [v])), { address: '127.0.0.1', port: P.mock })
|
||||||
|
sendOn(0.0)
|
||||||
|
check(S.getMatrixSend(6, 6).on === false && cb('matrix_send_off', { matrixIn: 6, matrixOut: 6 }) === true && cb('matrix_send_on', { matrixIn: 6, matrixOut: 6 }) === false, 'matrix_send_off is true for a send the console reports off')
|
||||||
|
sendOn(1.0)
|
||||||
|
check(cb('matrix_send_off', { matrixIn: 6, matrixOut: 6 }) === false && cb('matrix_send_on', { matrixIn: 6, matrixOut: 6 }) === true, 'and false again when it is switched on')
|
||||||
|
}
|
||||||
check(cb('matrix_solo', { matrix: 2 }) === false && cb('cg_solo', { cg: 10 }) === true && cb('cg_solo', { cg: 1 }) === false, 'matrix_solo / cg_solo follow the state')
|
check(cb('matrix_solo', { matrix: 2 }) === false && cb('cg_solo', { cg: 10 }) === true && cb('cg_solo', { cg: 1 }) === false, 'matrix_solo / cg_solo follow the state')
|
||||||
inst.onConsoleMessage(decode(encode('/Matrix_Outputs/2/solo', [1.0])), { address: '127.0.0.1', port: P.mock })
|
inst.onConsoleMessage(decode(encode('/Matrix_Outputs/2/solo', [1.0])), { address: '127.0.0.1', port: P.mock })
|
||||||
check(cb('matrix_solo', { matrix: 2 }) === true, 'matrix_solo goes true when the console reports solo')
|
check(cb('matrix_solo', { matrix: 2 }) === true, 'matrix_solo goes true when the console reports solo')
|
||||||
@@ -1099,7 +1119,11 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
await sleep(200)
|
await sleep(200)
|
||||||
check(ipadRx.some((m) => m.address === '/Input_Channels/21/fader'), 'a change made by the module reaches the iPad too')
|
check(ipadRx.some((m) => m.address === '/Input_Channels/21/fader'), 'a change made by the module reaches the iPad too')
|
||||||
|
|
||||||
// /Meters/clear from the iPad: forwarded verbatim, then the module resubscribes
|
// /Meters/clear from the iPad: forwarded verbatim, then the module resubscribes. The silent input 7 reports its 127 once per
|
||||||
|
// subscription, so to see the subscription come back its state is first moved off 127 (a timestamp would race the mock's tick).
|
||||||
|
const injectMeter = (id, value) => inst.onConsoleMessage(decode(encode('/Meters/values', [{ i: id }, { i: value }])), { address: '127.0.0.1', port: P.mock })
|
||||||
|
injectMeter(meterIdForInput(7), 42)
|
||||||
|
check(S.getMeter(meterIdForInput(7)).value === 42, 'input 7 meter state is parked at 42 before the clear')
|
||||||
const mark = sent.length
|
const mark = sent.length
|
||||||
const t0 = Date.now()
|
const t0 = Date.now()
|
||||||
ipad.send(encode('/Meters/clear', []), P.relay, '127.0.0.1')
|
ipad.send(encode('/Meters/clear', []), P.relay, '127.0.0.1')
|
||||||
@@ -1111,7 +1135,38 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
await sleep(200)
|
await sleep(200)
|
||||||
const reOut = sent.slice(mark).filter((m) => /\/Output\/meter$/.test(m.address)).map((m) => `${m.address} ${decodeArgs(m).args[0]}`)
|
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(['/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))?.value === 127, 3000), 'the silent meter reports its 127 again after the resubscribe: the console really has the subscription back')
|
||||||
|
check(await waitFor(() => (S.getMeter(meterIdForInput(2))?.at ?? 0) > t0, 3000), 'a live meter streams again after the resubscribe (it had stopped at the first /Meters/clear)')
|
||||||
|
|
||||||
|
// back-to-back clears: every clear pushes the resubscribe back, and the newest one always gets a resubscribe after it
|
||||||
|
{
|
||||||
|
const id7 = meterIdForInput(7)
|
||||||
|
const subsSince = (m) => sent.slice(m).filter((x) => x.address === '/Input_Channels/7/Channel_Input/post_meter').length
|
||||||
|
injectMeter(id7, 42)
|
||||||
|
ipad.send(encode('/Meters/clear', []), P.relay, '127.0.0.1') // clear 1
|
||||||
|
await sleep(110)
|
||||||
|
const m2 = sent.length
|
||||||
|
ipad.send(encode('/Meters/clear', []), P.relay, '127.0.0.1') // clear 2, while the resubscribe for clear 1 is still waiting
|
||||||
|
await sleep(150)
|
||||||
|
check(subsSince(m2) === 0, 'a second clear while the resubscribe is pending moves it back: nothing was re-sent yet, so nothing can land before the newest clear')
|
||||||
|
check(await waitFor(() => subsSince(m2) === 1, 2000), 'the resubscribe follows the newest clear')
|
||||||
|
check(await waitFor(() => S.getMeter(id7).value === 127, 3000), 'and the console reports again after it (clear 2 did not leave the meters unsubscribed)')
|
||||||
|
// a clear right after a resubscribe went out: it needs its own resubscribe
|
||||||
|
injectMeter(id7, 42)
|
||||||
|
const m3 = sent.length
|
||||||
|
const t3 = Date.now()
|
||||||
|
ipad.send(encode('/Meters/clear', []), P.relay, '127.0.0.1') // clear 3
|
||||||
|
check(await waitFor(() => subsSince(m3) === 1, 2000), 'a clear after the previous resubscribe schedules another one')
|
||||||
|
check(await waitFor(() => S.getMeter(id7).value === 127, 3000) && (await waitFor(() => (S.getMeter(meterIdForInput(2))?.at ?? 0) > t3, 3000)), 'the third clear is followed by working subscriptions too (silent and live meters report again)')
|
||||||
|
// clears in quick succession all collapse into one resubscribe after the last of them
|
||||||
|
injectMeter(id7, 42)
|
||||||
|
const m4 = sent.length
|
||||||
|
for (let k = 0; k < 4; k++) {
|
||||||
|
ipad.send(encode('/Meters/clear', []), P.relay, '127.0.0.1')
|
||||||
|
await sleep(60)
|
||||||
|
}
|
||||||
|
check(await waitFor(() => S.getMeter(id7).value === 127, 3000) && subsSince(m4) === 1, `four clears 60 ms apart give exactly one resubscribe, after the last (${subsSince(m4)})`)
|
||||||
|
}
|
||||||
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')
|
||||||
|
|
||||||
// unpadded datagram from an iPad is forwarded verbatim
|
// unpadded datagram from an iPad is forwarded verbatim
|
||||||
@@ -1539,10 +1594,11 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
// matrix sends and graphic EQ bands are tracked per send pair / per band
|
// matrix sends and graphic EQ bands are tracked per send pair / per band
|
||||||
const m1 = mk('matrix_send_on', { matrixIn: 2, matrixOut: 3 }, 'inst-M1')
|
const m1 = mk('matrix_send_on', { matrixIn: 2, matrixOut: 3 }, 'inst-M1')
|
||||||
const m2 = mk('matrix_send_level_value', { matrixIn: 3, matrixOut: 2 }, 'inst-M2')
|
const m2 = mk('matrix_send_level_value', { matrixIn: 3, matrixOut: 2 }, 'inst-M2')
|
||||||
|
const m3 = mk('matrix_send_off', { matrixIn: 2, matrixOut: 3 }, 'inst-M3')
|
||||||
const g1 = mk('geq_band_value', { geq: 1, band: 5 }, 'inst-G1')
|
const g1 = mk('geq_band_value', { geq: 1, band: 5 }, 'inst-G1')
|
||||||
const g2 = mk('geq_band_value', { geq: 1, band: 6 }, 'inst-G2')
|
const g2 = mk('geq_band_value', { geq: 1, band: 6 }, 'inst-G2')
|
||||||
const g3 = mk('geq_on', { geq: 1 }, 'inst-G3')
|
const g3 = mk('geq_on', { geq: 1 }, 'inst-G3')
|
||||||
for (const f of [m1, m2, g1, g2, g3]) run1(f)
|
for (const f of [m1, m2, m3, g1, g2, g3]) run1(f)
|
||||||
check(inst.watchers.get('msend:2:3')?.has('inst-M1') && inst.watchers.get('msend:3:2')?.has('inst-M2') && inst.watchers.get('geq:1:gain:5')?.has('inst-G1') && inst.watchers.get('geq:1:gain:6')?.has('inst-G2') && inst.watchers.get('geq:1:on')?.has('inst-G3'), 'matrix send pairs and graphic EQ bands register under msend:in:out and geq:n:gain:band keys')
|
check(inst.watchers.get('msend:2:3')?.has('inst-M1') && inst.watchers.get('msend:3:2')?.has('inst-M2') && inst.watchers.get('geq:1:gain:5')?.has('inst-G1') && inst.watchers.get('geq:1:gain:6')?.has('inst-G2') && inst.watchers.get('geq:1:on')?.has('inst-G3'), 'matrix send pairs and graphic EQ bands register under msend:in:out and geq:n:gain:band keys')
|
||||||
await sleep(150)
|
await sleep(150)
|
||||||
ctx.byId.length = 0
|
ctx.byId.length = 0
|
||||||
@@ -1551,7 +1607,12 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
await sleep(80)
|
await sleep(80)
|
||||||
const ids3 = ctx.byId.flatMap(([, x]) => x)
|
const ids3 = ctx.byId.flatMap(([, x]) => x)
|
||||||
check(ids3.includes('inst-M1') && !ids3.includes('inst-M2') && ids3.includes('inst-G1') && !ids3.includes('inst-G2') && !ids3.includes('inst-G3'), 'a matrix send change re-checks only that send pair, a band change only that band')
|
check(ids3.includes('inst-M1') && !ids3.includes('inst-M2') && ids3.includes('inst-G1') && !ids3.includes('inst-G2') && !ids3.includes('inst-G3'), 'a matrix send change re-checks only that send pair, a band change only that band')
|
||||||
for (const f of [m1, m2, g1, g2, g3]) ctx.feedbacks[f.feedbackId].unsubscribe(f, {})
|
ctx.byId.length = 0
|
||||||
|
inst.onConsoleMessage(decode(encode('/Matrix_Inputs/2/Matrix_Send/3/send_on', [0.0])), { address: '127.0.0.1', port: P.mock })
|
||||||
|
await sleep(80)
|
||||||
|
const ids3b = ctx.byId.flatMap(([, x]) => x)
|
||||||
|
check(ids3b.includes('inst-M1') && ids3b.includes('inst-M3') && !ids3b.includes('inst-M2') && cb('matrix_send_off', { matrixIn: 2, matrixOut: 3 }) === true, 'a matrix send on/off change re-checks matrix_send_on and matrix_send_off of that send pair only, and the off feedback turns true')
|
||||||
|
for (const f of [m1, m2, m3, g1, g2, g3]) ctx.feedbacks[f.feedbackId].unsubscribe(f, {})
|
||||||
// level_compare follows the kind option
|
// level_compare follows the kind option
|
||||||
const lc = mk('level_compare', { kind: 'cg', cg: 2, comparison: 'above', threshold: -50 }, 'inst-L')
|
const lc = mk('level_compare', { kind: 'cg', cg: 2, comparison: 'above', threshold: -50 }, 'inst-L')
|
||||||
run1(lc)
|
run1(lc)
|
||||||
@@ -1596,6 +1657,222 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
|||||||
expectWrite(r, '/Input_Channels/14/Filters/lo_filter_freq', ',f', 20, 'a filter frequency below the range is clamped to 20 Hz')
|
expectWrite(r, '/Input_Channels/14/Filters/lo_filter_freq', ',f', 20, 'a filter frequency below the range is clamped to 20 Hz')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 16b. Level fades: the duration / curve options of the set-level actions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
// pure helpers: positionToDb is the exact inverse of faderPosition, fadeLevelAt walks a curve
|
||||||
|
const ANCHORS = [[10, 100], [5, 87.5], [0, 75], [-5, 65], [-10, 55], [-20, 40], [-30, 28], [-40, 18], [-50, 11], [-60, 6]]
|
||||||
|
check(ANCHORS.every(([db, pos]) => near(positionToDb(pos), db, 1e-9) && near(faderPosition(db), pos, 1e-9)), 'positionToDb returns every anchor of the fader taper exactly')
|
||||||
|
check(positionToDb(0) === -90 && positionToDb(1) === -90 && positionToDb(0, -150) === -150 && positionToDb(100) === 10 && positionToDb(250) === 10 && positionToDb(-5) === -90 && positionToDb(NaN) === -90 && positionToDb(undefined, -150) === -150, 'positionToDb: position 0 is the floor (-90, or -150 when asked), 100 and more is +10, junk is the floor')
|
||||||
|
let worst = 0
|
||||||
|
for (let db = -89.9; db <= 10; db += 0.1) worst = Math.max(worst, Math.abs(positionToDb(faderPosition(db)) - db))
|
||||||
|
let worst150 = 0
|
||||||
|
for (let db = -149.5; db <= 10; db += 0.5) worst150 = Math.max(worst150, Math.abs(positionToDb(faderPosition(db, -150), -150) - db))
|
||||||
|
check(worst < 0.01 && worst150 < 0.01, `positionToDb(faderPosition(dB)) returns dB within 0.01 dB over the whole range, for floor -90 and -150 (worst ${worst.toExponential(1)} / ${worst150.toExponential(1)})`)
|
||||||
|
check(FADE_CURVES.join() === 'fader,ease,db' && fadeLevelAt('fader', -90, 0, 1) === 0 && fadeLevelAt('db', -20, 5, 1.5) === 5 && fadeLevelAt('ease', -50, -10, 0) === -50 && fadeLevelAt('db', -150, 0, 0.5, -90) === -45 && fadeLevelAt('db', -30, -200, 0.5, -90) === -60 && fadeLevelAt('fader', -30, -150, 1, -90) === -90 && fadeLevelAt('fader', 0, 40, 1) === 10, 'fadeLevelAt: t >= 1 is exactly the clamped target, t = 0 the start, the lower limit holds')
|
||||||
|
check(fadeLevelAt('db', -150, 0, 0) === -90 && fadeLevelAt('db', -150, 0, 0.5) === -45 && fadeLevelAt('db', 0, -150, 0.5) === -45 && fadeLevelAt('db', 0, -150, 0.999) > -90.1 && fadeLevelAt('db', 0, -150, 1) === -150 && fadeLevelAt('db', -60, 0, 0.5) === -30, 'a dB-linear fade from or to -inf runs between the -90 floor and the target (no silent stretch below -90); the last step is the exact target')
|
||||||
|
{
|
||||||
|
const mono = (c, a, b) => { const v = [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1].map((t) => fadeLevelAt(c, a, b, t)); return v.every((x, i) => i === 0 || (b >= a ? x >= v[i - 1] - 1e-9 : x <= v[i - 1] + 1e-9)) && v.every((x) => x >= Math.min(a, b) - 1e-9 && x <= Math.max(a, b) + 1e-9) }
|
||||||
|
check(['fader', 'ease', 'db'].every((c) => mono(c, -150, -100) && mono(c, -100, -150) && mono(c, -150, 0) && mono(c, -95, -20) && mono(c, -20, -95) && mono(c, 5, -150)), 'no fade curve ever steps outside its start..target range or reverses (including fades that stay below -90 dB)')
|
||||||
|
check(fadeLevelAt('fader', -150, -100, 0.5) === -125, 'a fade that stays below the -90 floor runs linearly in dB (-150 -> -100 is -125 halfway)')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const go = (actionId, options) => ctx.actions[actionId].callback({ id: 'f1', controlId: 'c', actionId, options, surfaceId: undefined }, {})
|
||||||
|
const faderAddr = (n) => `/Input_Channels/${n}/fader`
|
||||||
|
const writesTo = (address, mark) => sent.slice(mark).filter((m) => m.address === address).map((m) => ({ db: m.args[0], at: m.at }))
|
||||||
|
const idle = () => waitFor(() => inst.fades.size === 0, 3000)
|
||||||
|
{
|
||||||
|
// option definitions
|
||||||
|
const setIds = ['input_fader_set', 'buss_fader_set', 'matrix_fader_set', 'cg_fader_set', 'send_level_set', 'matrix_send_level_set']
|
||||||
|
const good = setIds.every((id) => {
|
||||||
|
const o = ctx.actions[id].options
|
||||||
|
const d = o.find((x) => x.id === 'duration')
|
||||||
|
const c = o.find((x) => x.id === 'curve')
|
||||||
|
return d?.type === 'number' && d.default === 0 && d.min === 0 && d.max === 60000 && d.step === 100 && d.asInteger === true && d.disableAutoExpression === true && d.label === 'Fade time (ms, 0 = instant)' &&
|
||||||
|
c?.type === 'dropdown' && c.default === 'fader' && c.isVisibleExpression === '$(options:duration) > 0' && c.choices.map((x) => `${x.id}=${x.label}`).join() === 'fader=Fader travel (natural),ease=Fader travel, ease in/out,db=Linear in dB' &&
|
||||||
|
o.indexOf(d) === o.findIndex((x) => x.id === 'level') + 1 && o.indexOf(c) === o.indexOf(d) + 1
|
||||||
|
})
|
||||||
|
check(good, 'all six set-level actions end with a "Fade time" option (0..60000 ms, step 100, integer, no expression) and a curve dropdown shown only above 0 ms')
|
||||||
|
check(Object.keys(ctx.actions).filter((id) => /_adjust$/.test(id)).every((id) => !ctx.actions[id].options.some((o) => o.id === 'duration')) && !ctx.actions.input_mute.options.some((o) => o.id === 'duration'), 'adjust and switch actions have no fade option')
|
||||||
|
check(ctx.actions.fade_stop?.name === 'Stop all fades' && ctx.actions.fade_stop.options.length === 0 && ctx.feedbacks.fade_running?.type === 'boolean' && ctx.feedbacks.fade_running.name === 'A fade is running' && 'fades_running' in ctx.variableDefs, 'fade_stop action, fade_running feedback and fades_running variable are defined')
|
||||||
|
check(setIds.every((id) => /fade time above 0/.test(ctx.actions[id].description)), 'every set-level action describes the fade')
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// the 'fader' curve: -90 -> 0 in 500 ms is ten evenly spaced steps in fader position, ending exactly on 0
|
||||||
|
await go('input_fader_set', { channel: 11, level: -90 })
|
||||||
|
const mark = sent.length
|
||||||
|
const t0 = Date.now()
|
||||||
|
await go('input_fader_set', { channel: 11, level: 0, duration: 500, curve: 'fader' })
|
||||||
|
check(inst.fades.size === 1 && cb('fade_running', {}) === true, 'a fade is running right after the action (fade_running is true)')
|
||||||
|
await sleep(80)
|
||||||
|
check(ctx.variables.fades_running === 1, 'fades_running reads 1 while it runs')
|
||||||
|
check(await idle(), 'the fade finishes')
|
||||||
|
const w = writesTo(faderAddr(11), mark)
|
||||||
|
const dbs = w.map((x) => x.db)
|
||||||
|
const pos = dbs.map((v) => faderPosition(v))
|
||||||
|
const gaps = w.slice(1).map((x, k) => x.at - w[k].at).sort((a, b) => a - b)
|
||||||
|
check(w.length === 10, `500 ms at 20 Hz is ten writes (${w.length})`)
|
||||||
|
check(dbs.every((v, k) => k === 0 || v > dbs[k - 1]) && dbs[0] > -90 && dbs[0] < 0 && dbs.at(-1) === 0, `monotonic, the first write is above -90 and below the target, the last is exactly 0 (${dbs.map((v) => Math.round(v * 10) / 10).join(', ')})`)
|
||||||
|
check(pos.every((p, k) => near(p - (k ? pos[k - 1] : 0), 7.5, 0.01)), `the steps are even in fader position (7.5 each: ${pos.map((p) => Math.round(p * 10) / 10).join(', ')})`)
|
||||||
|
check(w.at(-1).at - t0 >= 495 && w.at(-1).at - t0 <= 700 && gaps[Math.floor(gaps.length / 2)] >= 35 && gaps[Math.floor(gaps.length / 2)] <= 75, `it ends between 500 and 700 ms after the action, one step about every 50 ms (ended after ${w.at(-1).at - t0} ms, median step ${gaps[Math.floor(gaps.length / 2)]} ms)`)
|
||||||
|
check(S.getFader('input', 11) === 0 && inst.fades.size === 0 && cb('fade_running', {}) === false, 'the state ends at 0 and fade_running is false again')
|
||||||
|
await sleep(80)
|
||||||
|
check(ctx.variables.fades_running === 0, 'fades_running reads 0 again')
|
||||||
|
check(await waitFor(() => sent.slice(mark).some((m) => m.address === `${faderAddr(11)}/?`), 2000), 'a confirm query follows the last step like after any write')
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// 'db': linear in dB
|
||||||
|
await go('input_fader_set', { channel: 12, level: -60 })
|
||||||
|
const mark = sent.length
|
||||||
|
await go('input_fader_set', { channel: 12, level: 0, duration: 500, curve: 'db' })
|
||||||
|
check(await idle(), 'the linear fade finishes')
|
||||||
|
const dbs = writesTo(faderAddr(12), mark).map((x) => x.db)
|
||||||
|
check(dbs.length === 10 && dbs.every((v, k) => near(v, -60 + 6 * (k + 1), 0.01)) && dbs.at(-1) === 0, `'Linear in dB' moves 6 dB per step (${dbs.map((v) => Math.round(v * 100) / 100).join(', ')})`)
|
||||||
|
// the curve option is optional: a button made before fades existed (no duration, no curve) is an instant set
|
||||||
|
const m2 = sent.length
|
||||||
|
await go('input_fader_set', { channel: 12, level: -10 })
|
||||||
|
check(writesTo(faderAddr(12), m2).length === 1 && inst.fades.size === 0, 'no duration option at all (an old button) is one instant write')
|
||||||
|
const m3 = sent.length
|
||||||
|
await go('input_fader_set', { channel: 12, level: -12, duration: 0, curve: 'ease' })
|
||||||
|
await sleep(200)
|
||||||
|
const w3 = writesTo(faderAddr(12), m3)
|
||||||
|
check(w3.length === 1 && w3[0].db === -12 && inst.fades.size === 0, 'duration 0 is exactly one write, whatever the curve')
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// 'ease': slow at both ends
|
||||||
|
await go('input_fader_set', { channel: 13, level: -90 })
|
||||||
|
const mark = sent.length
|
||||||
|
await go('input_fader_set', { channel: 13, level: 0, duration: 1000, curve: 'ease' })
|
||||||
|
check(await waitFor(() => inst.fades.size === 0, 4000), 'the eased fade finishes')
|
||||||
|
const dbs = writesTo(faderAddr(13), mark).map((x) => x.db)
|
||||||
|
const pos = dbs.map((v) => faderPosition(v))
|
||||||
|
const d = pos.map((p, k) => p - (k ? pos[k - 1] : 0))
|
||||||
|
const top = Math.max(...d)
|
||||||
|
const at = d.indexOf(top)
|
||||||
|
check(dbs.length >= 15 && dbs.every((v, k) => k === 0 || v > dbs[k - 1]) && dbs.at(-1) === 0 && d[0] < top * 0.5 && d.at(-1) < top * 0.25 && at > d.length * 0.3 && at < d.length * 0.7, `'ease' is slow at the start and the end and fastest in the middle (steps in fader position: first ${d[0].toFixed(1)}, fastest ${top.toFixed(1)} at step ${at + 1}, last ${d.at(-1).toFixed(1)}; ${dbs.length} writes)`)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// another set, a fade, an adjust, fade_stop: each ends a running fade where it is
|
||||||
|
let mark = sent.length
|
||||||
|
await go('input_fader_set', { channel: 14, level: -90 })
|
||||||
|
await go('input_fader_set', { channel: 14, level: 0, duration: 1000, curve: 'fader' })
|
||||||
|
await sleep(230)
|
||||||
|
const before = writesTo(faderAddr(14), mark).length
|
||||||
|
await go('input_fader_set', { channel: 14, level: -20 })
|
||||||
|
await sleep(350)
|
||||||
|
let w = writesTo(faderAddr(14), mark)
|
||||||
|
check(before >= 3 && w.length === before + 1 && w.at(-1).db === -20 && inst.fades.size === 0 && near(S.getFader('input', 14), -20), `a plain set ends the running fade: nothing is written after it but the set itself (${before} fade writes, then -20)`)
|
||||||
|
|
||||||
|
mark = sent.length
|
||||||
|
await go('input_fader_set', { channel: 14, level: -90 })
|
||||||
|
await go('input_fader_set', { channel: 14, level: 0, duration: 1000, curve: 'fader' })
|
||||||
|
await sleep(230)
|
||||||
|
const lastOfA = writesTo(faderAddr(14), mark).at(-1).db
|
||||||
|
const markB = sent.length
|
||||||
|
await go('input_fader_set', { channel: 14, level: -70, duration: 300, curve: 'db' })
|
||||||
|
check(inst.fades.size === 1, 'a second fade on the same fader replaces the first (one fade per address)')
|
||||||
|
check(await idle(), 'the second fade finishes')
|
||||||
|
w = writesTo(faderAddr(14), markB)
|
||||||
|
check(w.length === 6 && w.at(-1).db === -70 && w.every((x, k) => k === 0 || x.db < w[k - 1].db) && w[0].db < lastOfA && w[0].db > -70, `the second fade takes over from where the first one was (${Math.round(lastOfA)} dB, rising) and runs down to -70 without a write from the first (${w.map((x) => Math.round(x.db)).join(', ')})`)
|
||||||
|
|
||||||
|
mark = sent.length
|
||||||
|
await go('input_fader_set', { channel: 14, level: -90 })
|
||||||
|
await go('input_fader_set', { channel: 14, level: 0, duration: 1000, curve: 'fader' })
|
||||||
|
await sleep(230)
|
||||||
|
const markC = sent.length
|
||||||
|
await go('input_fader_adjust', { channel: 14, delta: 1 })
|
||||||
|
await sleep(350)
|
||||||
|
w = writesTo(faderAddr(14), markC)
|
||||||
|
check(w.length === 1 && inst.fades.size === 0, `an adjust ends the running fade and writes once (${w.length} write)`)
|
||||||
|
|
||||||
|
// fade_stop: both fades stop, levels stay where they are
|
||||||
|
await go('input_fader_set', { channel: 15, level: -90 })
|
||||||
|
await go('send_level_set', { channel: 15, buss: 1, level: -90 })
|
||||||
|
await go('input_fader_set', { channel: 15, level: 0, duration: 2000, curve: 'fader' })
|
||||||
|
await go('send_level_set', { channel: 15, buss: 1, level: 0, duration: 2000, curve: 'fader' })
|
||||||
|
await sleep(250)
|
||||||
|
check(inst.fades.size === 2 && ctx.variables.fades_running === 2, 'two fades on different levels run at the same time (fades_running 2)')
|
||||||
|
await go('fade_stop', {})
|
||||||
|
const markD = sent.length
|
||||||
|
const atStop = S.getFader('input', 15)
|
||||||
|
await sleep(400)
|
||||||
|
check(inst.fades.size === 0 && !sent.slice(markD).some((m) => m.address === faderAddr(15) || m.address === '/Input_Channels/15/Aux_Send/1/send_level') && S.getFader('input', 15) === atStop && atStop > -90 && atStop < 0 && ctx.variables.fades_running === 0 && cb('fade_running', {}) === false, `fade_stop ends every fade, nothing more is written, the levels stay where they were (${Math.round(atStop)} dB) and fades_running reads 0`)
|
||||||
|
await go('fade_stop', {})
|
||||||
|
check(inst.fades.size === 0, 'fade_stop with nothing running does nothing')
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// aux send and the other fader kinds go through the same engine
|
||||||
|
await go('send_level_set', { channel: 16, buss: 2, level: -30 })
|
||||||
|
let mark = sent.length
|
||||||
|
await go('send_level_set', { channel: 16, buss: 2, level: -10, duration: 200, curve: 'db' })
|
||||||
|
check(await idle(), 'an aux send level fades')
|
||||||
|
const sw = writesTo('/Input_Channels/16/Aux_Send/2/send_level', mark).map((x) => x.db)
|
||||||
|
check(sw.length === 4 && sw.at(-1) === -10 && sw.every((v, k) => near(v, -30 + 5 * (k + 1), 0.01)), `aux send fade: four steps of 5 dB (${sw.join(', ')})`)
|
||||||
|
await go('buss_fader_set', { buss: 4, level: -30 })
|
||||||
|
await go('matrix_fader_set', { matrix: 2, level: -30 })
|
||||||
|
await go('cg_fader_set', { cg: 2, level: -30 })
|
||||||
|
mark = sent.length
|
||||||
|
await go('buss_fader_set', { buss: 4, level: -20, duration: 100, curve: 'db' })
|
||||||
|
await go('matrix_fader_set', { matrix: 2, level: -20, duration: 100, curve: 'db' })
|
||||||
|
await go('cg_fader_set', { cg: 2, level: -20, duration: 100, curve: 'db' })
|
||||||
|
check(inst.fades.size === 3, 'buss, matrix output and control group faders fade too (three at once)')
|
||||||
|
check(await idle(), 'they finish')
|
||||||
|
const sides = [['buss', 4], ['matrix', 2], ['cg', 2]].map(([ent, n]) => [S.getFader(ent, n)])
|
||||||
|
check(sides.every(([v]) => v === -20), 'and every one ends exactly at the target')
|
||||||
|
await sleep(100)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// a matrix send never goes below -90
|
||||||
|
await go('matrix_send_level_set', { matrixIn: 4, matrixOut: 4, level: -30 })
|
||||||
|
const addrM = '/Matrix_Inputs/4/Matrix_Send/4/send_level'
|
||||||
|
let mark = sent.length
|
||||||
|
await go('matrix_send_level_set', { matrixIn: 4, matrixOut: 4, level: -90, duration: 300, curve: 'db' })
|
||||||
|
check(await idle(), 'a matrix send fades down')
|
||||||
|
let m = writesTo(addrM, mark).map((x) => x.db)
|
||||||
|
check(m.length === 6 && m.at(-1) === -90 && m.every((v) => v >= -90) && m.every((v, k) => k === 0 || v < m[k - 1]), `down to -90: ${m.map((v) => Math.round(v * 10) / 10).join(', ')} (never below -90)`)
|
||||||
|
mark = sent.length
|
||||||
|
await go('matrix_send_level_set', { matrixIn: 4, matrixOut: 4, level: 0, duration: 500, curve: 'fader' })
|
||||||
|
check(await idle(), 'and up from -90 again')
|
||||||
|
m = writesTo(addrM, mark).map((x) => x.db)
|
||||||
|
check(m.length === 10 && m[0] > -90 && m.at(-1) === 0 && m.every((v) => v >= -90 && v <= 10), `from the -90 floor on the fader taper (${m.map((v) => Math.round(v)).join(', ')})`)
|
||||||
|
// a fade to a target below the floor stops at -90 for a matrix send, at -150 (-inf) for a fader
|
||||||
|
mark = sent.length
|
||||||
|
await inst.setMatrixSendLevel(4, 4, -150, 200, 'db')
|
||||||
|
check(await idle(), 'a matrix send fade to a too low target finishes')
|
||||||
|
m = writesTo(addrM, mark).map((x) => x.db)
|
||||||
|
check(m.length > 0 && m.every((v) => v >= -90) && m.at(-1) === -90, `a matrix send fade clamps its target to -90 (${m.map((v) => Math.round(v)).join(', ')})`)
|
||||||
|
await go('input_fader_set', { channel: 17, level: -60 })
|
||||||
|
mark = sent.length
|
||||||
|
await go('input_fader_set', { channel: 17, level: -150, duration: 300, curve: 'fader' })
|
||||||
|
check(await idle(), 'a fader fades out to -inf')
|
||||||
|
const f = writesTo(faderAddr(17), mark).map((x) => x.db)
|
||||||
|
check(f.length >= 3 && f.at(-1) === -150 && f.slice(0, -1).every((v) => v >= -90 && v < -60) && f.every((v, k) => k === 0 || v <= f[k - 1]), `a fader fade to -inf ends exactly on -150 (${f.map((v) => Math.round(v)).join(', ')})`)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// a start level the console never gives: the target is written at once, with one warning
|
||||||
|
const saved = inst.timing.replyWaitMs
|
||||||
|
inst.timing.replyWaitMs = 250
|
||||||
|
const warns = logCount(/set without a fade/)
|
||||||
|
const mark = sent.length
|
||||||
|
await inst.fadeLevel(faderAddr(18), 'test:unknown-start', () => undefined, -5, 400, 'fader')
|
||||||
|
inst.timing.replyWaitMs = saved
|
||||||
|
const w = writesTo(faderAddr(18), mark)
|
||||||
|
check(logCount(/set without a fade/) === warns + 1 && w.length === 1 && w[0].db === -5 && inst.fades.size === 0, 'an unknown starting level writes the target at once and logs one warning')
|
||||||
|
// a fade waiting for its starting level is replaced by a later set
|
||||||
|
inst.timing.replyWaitMs = 250
|
||||||
|
const m2 = sent.length
|
||||||
|
const pending = inst.fadeLevel(faderAddr(18), 'test:unknown-start2', () => undefined, -5, 400, 'fader')
|
||||||
|
await sleep(40)
|
||||||
|
await go('input_fader_set', { channel: 18, level: -33 })
|
||||||
|
await pending
|
||||||
|
inst.timing.replyWaitMs = saved
|
||||||
|
const w2 = writesTo(faderAddr(18), m2)
|
||||||
|
check(w2.length === 1 && w2[0].db === -33 && inst.fades.size === 0, 'a set while a fade still waits for its starting level wins; the waiting fade does nothing')
|
||||||
|
}
|
||||||
|
check(inst.fades.size === 0 && inst.fadeStarts.size === 0, 'no fade or fade start is left over after the fade tests')
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 17. A slow console (every answer and echo 150 ms late): bind failures, encoder clicks, lazy loads
|
// 17. A slow console (every answer and echo 150 ms late): bind failures, encoder clicks, lazy loads
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1718,6 +1995,48 @@ const occupy = async (port) => {
|
|||||||
check(r.relay === null && r.relayRetryTimer === null && st()[0] === 'ok', 'disabling the relay clears the problem')
|
check(r.relay === null && r.relayRetryTimer === null && st()[0] === 'ok', 'disabling the relay clears the problem')
|
||||||
await r.destroy()
|
await r.destroy()
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
// Fades end with the instance: a config change that reopens the console link, and destroy
|
||||||
|
const fctx = makeContext()
|
||||||
|
const f = new Instance(fctx)
|
||||||
|
Object.assign(f.timing, fastTiming)
|
||||||
|
const loads0 = logCount(/load complete/)
|
||||||
|
await f.init(cfgB)
|
||||||
|
check(await waitFor(() => logCount(/load complete/) > loads0 && f.state.getFader('input', 3) !== undefined, 20000), 'an instance on the slow console has loaded')
|
||||||
|
const fAddr = '/Input_Channels/3/fader'
|
||||||
|
const fadeWrites = (mark) => sent.slice(mark).filter((m) => m.address === fAddr).length
|
||||||
|
const act = (actionId, options) => fctx.actions[actionId].callback(evOf(actionId, options), {})
|
||||||
|
await act('input_fader_set', { channel: 3, level: -90 })
|
||||||
|
await act('input_fader_set', { channel: 3, level: 0, duration: 4000, curve: 'fader' })
|
||||||
|
await sleep(250)
|
||||||
|
check(f.fades.size === 1 && fctx.variables.fades_running === 1 && f.timers.size > 0, 'a fade is running on the third instance')
|
||||||
|
const m1 = sent.length
|
||||||
|
await f.configUpdated({ ...cfgB, localPort: P.linkB + 2 })
|
||||||
|
check(f.fades.size === 0 && f.fadeStarts.size === 0 && f.link?.localPort === P.linkB + 2, 'a config change that reopens the console link ends the fade')
|
||||||
|
await sleep(500)
|
||||||
|
check(fadeWrites(m1) === 0 && fctx.variables.fades_running === 0, 'nothing more is written to the fader and fades_running reads 0')
|
||||||
|
await f.configUpdated(cfgB)
|
||||||
|
check(await waitFor(() => logCount(/load complete/) > loads0 + 1 && f.state.getFader('input', 3) !== undefined, 20000), 'the instance reconnects and loads again')
|
||||||
|
await act('input_fader_set', { channel: 3, level: 0, duration: 4000, curve: 'ease' })
|
||||||
|
await sleep(250)
|
||||||
|
check(f.fades.size === 1, 'a new fade runs after the reconnect')
|
||||||
|
const m2 = sent.length
|
||||||
|
await f.destroy()
|
||||||
|
check(f.fades.size === 0 && f.timers.size === 0, 'destroy ends the fade and leaves no timer behind')
|
||||||
|
await sleep(500)
|
||||||
|
check(fadeWrites(m2) === 0, 'nothing is written after destroy')
|
||||||
|
// a fade that was still waiting for its starting level when the instance went away does not start afterwards
|
||||||
|
const g = new Instance(makeContext())
|
||||||
|
Object.assign(g.timing, fastTiming)
|
||||||
|
await g.init(cfgB)
|
||||||
|
check(await waitFor(() => !!g.link, 3000), 'a fourth instance binds the console link')
|
||||||
|
const waiting = g.fadeLevel(fAddr, 'test:never', () => undefined, 0, 1000, 'fader')
|
||||||
|
await sleep(50)
|
||||||
|
check(g.fadeStarts.size === 1, 'a fade waits for its starting level')
|
||||||
|
await g.destroy()
|
||||||
|
await waiting
|
||||||
|
check(g.fades.size === 0 && g.fadeStarts.size === 0, 'destroy while a fade waits for its starting level: it never starts')
|
||||||
|
}
|
||||||
mockB.kill('SIGTERM')
|
mockB.kill('SIGTERM')
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user