v2: generic S-Series module with live discovery, dynamic options and meters
Rewrite of the module around what the console itself reports: - Settings cut to console address/ports, the iPad relay and debug logging; nothing site-specific remains. An upgrade script carries v1 configs and remaps v1 actions/feedbacks whose ids were reused. - On connect the module reads every name, buss type (aux/group), stereo mode, fader/mute/solo/pan and the snapshot list; entities that never answer are hidden. Definitions rebuild (debounced, skipped when identical) when names, buss types or snapshots change. - Every entity option is a searchable dropdown of live names. 70 actions cover inputs, aux sends, group assigns, busses, matrix outputs and sends, control groups, graphic EQs, channel-strip switches, trim/gain/filters and snapshots, each level with set and encoder-friendly adjust variants. - Boolean, value and bar feedbacks for every state, input level and gain-reduction meters via the console's meter subscription protocol, and about 2800 presets including rotary knob presets with live level text. - A write guard keeps fast encoder turns from being undone by delayed echoes; feedbacks are re-checked per instance. - Relay: the client table survives config changes, idle iPads are kept for an hour by default (0 = forever), bind failures retry and show in status. npm test: 326 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:
+41
-25
@@ -12,7 +12,7 @@ Both tools speak two OSC dialects, chosen with `--dialect`:
|
||||
| Dialect | What it is |
|
||||
| --- | --- |
|
||||
| `sseries` (default) | The lowercase S-Series command list: `/console/ping`, `/channel/N/fader`, ... |
|
||||
| `sd` | The "DiGiCo Pad" dialect the **real S31 iPad extension actually speaks**: `/Console/Name/?`, `/Input_Channels/N/fader/?`, ... Verified against a live console. |
|
||||
| `sd` | The "DiGiCo Pad" dialect the **real S31 iPad extension actually speaks**: `/Console/Name/?`, `/Input_Channels/N/fader/?`, ... Verified against a live console (`tools/catalog/s31-catalog.md`); this is the dialect the Companion module uses. |
|
||||
|
||||
## `mock-s31.js` - fake S31 console
|
||||
|
||||
@@ -23,7 +23,7 @@ from whichever source port, like the real desk.
|
||||
```
|
||||
node tools/mock-s31.js [--dialect sseries|sd] [--listen 8000] [--reply-to IP:PORT]
|
||||
[--channels N] [--auxes N] [--bool-int] [--live]
|
||||
[--types] [--quiet]
|
||||
[--delay MS] [--types] [--quiet]
|
||||
```
|
||||
|
||||
| Flag | Meaning |
|
||||
@@ -34,6 +34,7 @@ node tools/mock-s31.js [--dialect sseries|sd] [--listen 8000] [--reply-to IP:POR
|
||||
| `--channels` | Input count, 1-60. Default 48 (`sseries`) or 60 (`sd`). |
|
||||
| `--auxes` | Aux busses / sends per channel. Default 16, max 16 (`sseries`); default 25, max 25 (`sd`). |
|
||||
| `--bool-int` | `sseries` only: send booleans as int 0/1 instead of OSC `T`/`F`. |
|
||||
| `--delay MS` | `sd`: answer and echo every message MS milliseconds late (0-5000, default 0), to test a slow console and stale echoes. |
|
||||
| `--live` | Every 3 s, change a random send level or send on/off and emit it. |
|
||||
| `--types` | Also print each message's OSC type tags, e.g. `/Input_Channels/1/fader ,f [-1.4375]`. |
|
||||
| `--quiet` | Don't print every message. |
|
||||
@@ -52,32 +53,47 @@ sent with fader changes), `/channel/N/send/M/{level,enabled,pan}`, and
|
||||
busses 70..70+auxes-1.
|
||||
|
||||
### sd dialect
|
||||
A **query** is the address plus a trailing `/?` and no args; the reply is the
|
||||
same address without `/?` plus the value. A **set** is the address without `/?`
|
||||
and one arg; it is stored and echoed back in the console's reply types.
|
||||
A **query** is the address plus a trailing `/?` and no args; the reply is the same address without `/?`
|
||||
plus the value. A **set** is the address without `/?` and one arg; it is stored and echoed back in the
|
||||
console's reply types. The mock answers the addresses listed in `tools/catalog/s31-catalog.md` (the real
|
||||
console's answers) with the catalog's reply types. Entity numbers are 1-based, **control groups
|
||||
included** (`/Control_Groups/1/name` is the first); snapshot indexes are 0-based. `--channels N` limits the
|
||||
inputs, `--auxes N` the busses (default 25), and the matrix (8 outputs, 10 inputs), 10 control groups and
|
||||
16 graphic EQs are fixed.
|
||||
|
||||
| Query | Reply type |
|
||||
| Address | Reply type |
|
||||
| --- | --- |
|
||||
| `/Console/Name/?` | `s` ("S31 AB") |
|
||||
| `/Console/{Input_Channels,Aux_Outputs,Group_Outputs,Control_Groups,Matrix_Outputs,Matrix_Inputs,Graphic_EQ,Talkback_Outputs,Multis}/?` | `i` |
|
||||
| `/Console/Aux_Outputs/modes/?` | one `i` per aux (2 = stereo, 1 = mono) |
|
||||
| `/Input_Channels/N/Channel_Input/name/?`, `/Aux_Outputs/N/Buss_Trim/name/?`, `/Group_Outputs/N/Buss_Trim/name/?` | `s` (read-only). The 25 busses appear under both Aux_Outputs and Group_Outputs; a buss reports its name on the side matching its mode and `""` on the other (captured names). |
|
||||
| `/Input_Channels/N/fader/?`, `/Aux_Outputs/N/fader/?`, `/Group_Outputs/N/fader/?` | `f`, dB, -150 = -inf, max +10 (group 24 = -1.6875) |
|
||||
| `/Input_Channels/N/mute/?`, `/Aux_Outputs/N/mute/?`, `/Group_Outputs/N/mute/?` | `f` 0/1 (a float, not an int) |
|
||||
| `/Input_Channels/N/Aux_Send/M/send_level/?` | `f`, dB, -150..10 |
|
||||
| `/Input_Channels/N/Aux_Send/M/send_on/?` | `f` 0/1 |
|
||||
| `/Input_Channels/N/Aux_Send/M/send_pan/?` | `f`, 0..1, 0.5 = centre |
|
||||
| `/Input_Channels/N/Aux_Send/M/send_pre-post/?` | `i` (captured as an int, default 0; settable, clamped 0..1 - range is an assumption) |
|
||||
| `/Input_Channels/N/Group_Send/M/group/?` | `f` 0/1 group assign, M = 1..25. Default: inputs 1-8 and 18-21 on group 24, nothing else. The reply type is the single constant `SD_GROUP_ASSIGN_WIRE_TYPE` in `mock-s31.js` (`'f'`, assumed like mute/send_on; set `'i'` if the real console reports an int). |
|
||||
| `/Snapshots/Current_Snapshot/?` | `i` |
|
||||
| `/Snapshots/names/?` | one `/Snapshots/name ,iiis [index, cue*100, 0, "name"]` per snapshot |
|
||||
| `/Console/Name/?`, `/Console/Session/Filename/?` | `s` |
|
||||
| `/Console/{Matrix_Outputs,Matrix_Inputs,Control_Groups,Graphic_EQ,Talkback_Outputs,Multis}/?` | `i` |
|
||||
| `/Console/{Input_Channels,Aux_Outputs,Group_Outputs}/?` | **silent**, like the real console. All nine counts are pushed once, ~200 ms after the first datagram it receives |
|
||||
| `/Console/Aux_Outputs/types/?` | one `i` per buss, 1 = aux, 0 = group (a buss named only on the Group side is a group) |
|
||||
| `/Console/{Aux_Outputs,Group_Outputs}/modes/?`, `/Console/Input_Channels/modes/?` | one `i` per buss / input (2 = stereo, 1 = mono, captured) |
|
||||
| `/Input_Channels/N/Channel_Input/name/?`, `/Aux_Outputs/N/Buss_Trim/name/?`, `/Group_Outputs/N/Buss_Trim/name/?`, `/Matrix_Outputs/N/Buss_Trim/name/?`, `/Control_Groups/N/name/?`, `/Graphic_EQ/N/name/?` | `s`. Only input names accept a write here (not verified on the real console); the others are read-only. The 25 busses appear under both Aux_Outputs and Group_Outputs; a buss reports its name on the side matching its mode and `""` on the other. Matrix inputs have no name address |
|
||||
| `/{Input_Channels,Aux_Outputs,Group_Outputs,Matrix_Outputs,Control_Groups}/N/fader/?` | `f`, dB, -150 = -inf, max +10 |
|
||||
| `/{...}/N/mute/?`, `/{...}/N/solo/?` | `f` 0/1 (a float, not an int) |
|
||||
| `/Input_Channels/N/Panner/pan/?` | `f`, 0..1, 0.5 = centre |
|
||||
| `/Input_Channels/N/Aux_Send/M/{send_level,send_on,send_pan}/?` | `f` (level dB -150..10, on 0/1, pan 0..1) |
|
||||
| `/Input_Channels/N/Aux_Send/M/send_pre-post/?` | `i` (captured as an int, default 0; settable 0..1, which value means "pre" is an assumption; the Companion module does not use it) |
|
||||
| `/Input_Channels/N/Group_Send/M/group/?` | `f` 0/1 group assign. Default: inputs 1-8 and 18-21 on group 24. The wire type is the constant `SD_GROUP_ASSIGN_WIRE_TYPE` |
|
||||
| `/Matrix_Inputs/N/Matrix_Send/M/{send_level,send_on}/?` | `f` (default -90 and 0, captured) |
|
||||
| `/Graphic_EQ/N/{geq_in,geq_gain_1..32}/?` | `f` |
|
||||
| `/Input_Channels/N/{Channel_Input/{trim,phase,phantom,analog_gain,input_pad},Filters/{hi,lo}_filter_{in,freq},Channel_Delay/{delay_on,delay},EQ/eq_in,Dynamics/{comp_in,gate_in},Insert/insert_{A,B}_in}/?` | `f` (switches 0/1) |
|
||||
| `/{Aux_Outputs,Group_Outputs}/N/{Buss_Trim/{trim,phase},EQ/eq_in,Dynamics/{comp_in,gate_in},Channel_Delay/{delay_on,delay},Insert/insert_{A,B}_in}/?`, `/Matrix_Outputs/N/Buss_Trim/trim/?` | `f` |
|
||||
| `/Snapshots/Current_Snapshot/?`, `/Snapshots/count/?` | `i` |
|
||||
| `/Snapshots/names/?` | one `/Snapshots/name ,iiis [index, cue*100, 0, "name"]` per snapshot. `/Snapshots/name/?` without an argument stays silent (unreliable on the real console) |
|
||||
| `/Snapshots/Fire_Snapshot_number [i]`, `/Snapshots/Fire_Next_Snapshot`, `/Snapshots/Fire_Prev_Snapshot` | write-only triggers; the mock moves its current snapshot and pushes `/Snapshots/Current_Snapshot` |
|
||||
| `/Macros/Buttons/press [i]` | logged only (no macro query is answered) |
|
||||
|
||||
Sets accept int or float for `mute`/`send_on` (any non-zero = on) and clamp
|
||||
`fader`/`send_level` to -150..10 and `send_pan` to 0..1. Like the real console
|
||||
it stays **silent** on everything else: all lowercase S-Series addresses,
|
||||
`/Console/Resend`, `/Console/Channels/?`, `/Input_Channels/N/Aux_Send/M/?`,
|
||||
`/Input_Channels/N/Aux_Send/?`, `/Macros/Buttons/?`, out-of-range channel/send
|
||||
numbers, and sets of read-only names. (`--bool-int` has no effect in `sd`.)
|
||||
**Meters.** Subscribe with `/Input_Channels/N/Channel_Input/post_meter ,ii [id, 1]` (post-fader level) or
|
||||
`/Input_Channels/N/Dynamics/GR_meter ,ii [id, 1|4]` (gate / compressor gain reduction), where the client
|
||||
chooses `id`. Every subscribed id then gets a random value 0..127 about five times a second, as
|
||||
`/Meters/values ,ii... [id, v, id, v, ...]` (only ids whose value changed). `/Meters/clear` drops every
|
||||
subscription, like the real console.
|
||||
|
||||
Sets accept int or float for the 0/1 switches (any non-zero = on) and clamp levels, pans and values to
|
||||
their ranges. Like the real console the mock stays **silent** on everything else: all lowercase S-Series
|
||||
addresses, `/Console/Resend`, `/Console/Channels/?`, whole-send queries, out-of-range entity numbers,
|
||||
sets of read-only names. (`--bool-int` has no effect in `sd`.)
|
||||
|
||||
## `fake-ipad.js` - stand-in for the DiGiCo iPad app
|
||||
|
||||
|
||||
+281
-66
@@ -18,19 +18,21 @@
|
||||
* the current value. A message with an arg is a SET -> store it, then echo
|
||||
* it to the controller. /console/resend dumps the whole state.
|
||||
*
|
||||
* sd - the "DiGiCo Pad" / SD dialect the real S31 actually speaks
|
||||
* (verified against a live console).
|
||||
* sd - the "DiGiCo Pad" / SD dialect the real S31 iPad extension speaks
|
||||
* (tools/catalog/s31-catalog.md lists every address it answers).
|
||||
* A QUERY is the address plus a trailing "/?" and no args; the reply is the
|
||||
* same address WITHOUT "/?" plus the value. A SET is the address without
|
||||
* "/?" and one argument; it is stored and echoed using the console's reply
|
||||
* types (mute and send_on come back as floats 0/1). Anything the real
|
||||
* console does not answer (lowercase S-Series addresses, /Console/Resend,
|
||||
* /Console/Channels/?, whole-send queries, ...) gets no reply here either.
|
||||
* types (mute, solo and send_on come back as floats 0/1). It also streams
|
||||
* meters to subscribed ids and implements the write-only snapshot triggers.
|
||||
* Anything the real console does not answer (lowercase S-Series addresses,
|
||||
* /Console/Resend, /Console/Channels/?, whole-send queries, ...) gets no
|
||||
* reply here either.
|
||||
*
|
||||
* Usage:
|
||||
* node tools/mock-s31.js [--dialect sseries|sd] [--listen 8000] [--reply-to IP:PORT]
|
||||
* [--channels N] [--auxes N] [--bool-int] [--live]
|
||||
* [--types] [--quiet]
|
||||
* [--delay MS] [--types] [--quiet]
|
||||
*/
|
||||
|
||||
const dgram = require('node:dgram');
|
||||
@@ -52,10 +54,12 @@ const USAGE = `Usage: node tools/mock-s31.js [options]
|
||||
controller). If omitted, lock onto the first sender's IP
|
||||
with port 9000.
|
||||
--channels N input channels, 1-60 [sseries 48, sd 60]
|
||||
--auxes N aux busses / sends per channel [sseries 16 (max 16),
|
||||
--auxes N aux busses / sends per channel (sd: busses) [sseries 16 (max 16),
|
||||
sd 25 (max 25)]
|
||||
--bool-int sseries only: send booleans as int 0/1 instead of OSC T/F
|
||||
(the console can be set to "normalise" booleans)
|
||||
--delay MS answer and echo every message MS milliseconds late (0-5000)
|
||||
[0]; for testing a slow console and stale echoes
|
||||
--live every 3 s change a random send level / send-enabled and
|
||||
emit it, to exercise feedback handling
|
||||
--types also print each message's OSC type tags, e.g. ",f"
|
||||
@@ -82,6 +86,7 @@ function parseCli() {
|
||||
'reply-to': { type: 'string' },
|
||||
channels: { type: 'string' },
|
||||
auxes: { type: 'string' },
|
||||
delay: { type: 'string' },
|
||||
'bool-int': { type: 'boolean', default: false },
|
||||
live: { type: 'boolean', default: false },
|
||||
types: { type: 'boolean', default: false },
|
||||
@@ -124,6 +129,7 @@ function parseCli() {
|
||||
replyTo,
|
||||
channels: intOption('channels', 1, 60, defaults.channels),
|
||||
auxes: intOption('auxes', 0, defaults.maxAuxes, defaults.auxes),
|
||||
delay: intOption('delay', 0, 5000, 0),
|
||||
boolInt: values['bool-int'],
|
||||
live: values.live,
|
||||
types: values.types,
|
||||
@@ -254,9 +260,12 @@ function send(address, args) {
|
||||
}
|
||||
try {
|
||||
const buf = encode(address, args);
|
||||
socket.send(buf, replyTarget.port, replyTarget.host, (err) => {
|
||||
const target = replyTarget;
|
||||
const deliver = () => socket.send(buf, target.port, target.host, (err) => {
|
||||
if (err) note(`send error: ${err.message}`);
|
||||
});
|
||||
if (opts.delay > 0) setTimeout(deliver, opts.delay);
|
||||
else deliver();
|
||||
} catch (err) {
|
||||
note(`could not send ${address}: ${err.message}`);
|
||||
return false;
|
||||
@@ -279,8 +288,14 @@ let dumpTimer = null;
|
||||
let dumpTotal = 0;
|
||||
let dumpLabel = '';
|
||||
|
||||
function startPaced(label, queue) {
|
||||
function startPaced(label, queue, { append = false } = {}) {
|
||||
if (!replyTarget) return; // cannot happen via the socket: the first datagram locks a target
|
||||
if (dumpTimer && append) {
|
||||
// sd dialect: bursts queue up behind each other instead of restarting the running one
|
||||
dumpQueue.push(...queue);
|
||||
dumpTotal += queue.length;
|
||||
return;
|
||||
}
|
||||
if (dumpTimer) {
|
||||
clearTimeout(dumpTimer);
|
||||
note(`${label} requested while a dump was running; restarting it`);
|
||||
@@ -503,23 +518,28 @@ function liveTickSseries() {
|
||||
// ===========================================================================
|
||||
// Dialect: sd (the "DiGiCo Pad" dialect the real S31 speaks)
|
||||
// query = address + "/?" (no args); reply = address without "/?" + value
|
||||
// Every address answered here is one the real console was seen to answer
|
||||
// (tools/catalog/s31-catalog.md), with the same reply types. Entity numbers
|
||||
// are 1-based, control groups included; snapshot indexes are 0-based.
|
||||
// ===========================================================================
|
||||
|
||||
const SD_CONSOLE_NAME = 'S31 AB';
|
||||
const SD_SESSION_NAME = 'Mock Session.session';
|
||||
|
||||
/** The console has 25 busses, each exposed as an Aux_Outputs and as a Group_Outputs entry. */
|
||||
const SD_BUSS_COUNT = 25;
|
||||
/** Number of busses (each exposed as an Aux_Outputs and as a Group_Outputs entry); --auxes sets it. */
|
||||
const SD_BUSS_MAX = 25;
|
||||
|
||||
/**
|
||||
* Wire type of /Input_Channels/N/Group_Send/M/group replies: 'f' (float 0/1, like
|
||||
* mute and send_on) or 'i' (int). Not yet verified against the real console -
|
||||
* flip this one constant if it turns out to be an int.
|
||||
* mute and send_on) or 'i' (int). The catalog shows 'f'.
|
||||
*/
|
||||
const SD_GROUP_ASSIGN_WIRE_TYPE = 'f';
|
||||
|
||||
/** Fixed counts the console reports besides Input_Channels / Aux_Outputs. */
|
||||
/**
|
||||
* Counts the console answers when asked (catalog). The Input_Channels, Aux_Outputs and Group_Outputs counts never answer a
|
||||
* query; the console pushes all nine once, ~200 ms after first contact (sdCountBurst).
|
||||
*/
|
||||
const SD_FIXED_COUNTS = {
|
||||
Group_Outputs: SD_BUSS_COUNT,
|
||||
Control_Groups: 10,
|
||||
Matrix_Outputs: 8,
|
||||
Matrix_Inputs: 10,
|
||||
@@ -527,9 +547,21 @@ const SD_FIXED_COUNTS = {
|
||||
Talkback_Outputs: 0,
|
||||
Multis: 0,
|
||||
};
|
||||
const SD_BURST_DELAY_MS = 200;
|
||||
let sdBurstDone = false;
|
||||
const SD_MATRIX_OUT = SD_FIXED_COUNTS.Matrix_Outputs;
|
||||
const SD_MATRIX_IN = SD_FIXED_COUNTS.Matrix_Inputs;
|
||||
const SD_CGS = SD_FIXED_COUNTS.Control_Groups;
|
||||
const SD_GEQS = SD_FIXED_COUNTS.Graphic_EQ;
|
||||
const SD_GEQ_BANDS = 32;
|
||||
|
||||
/** One int per aux: 2 = stereo, 1 = mono (captured from the real console). */
|
||||
const SD_AUX_MODES = [2, 2, 2, 1, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2];
|
||||
/** One int per buss: 2 = stereo, 1 = mono (captured from the real console; Aux and Group arrays were identical). */
|
||||
const SD_BUSS_MODES = [2, 2, 2, 1, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2];
|
||||
/** One int per input: 1 = mono, 2 = stereo (captured). */
|
||||
const SD_INPUT_MODES = [
|
||||
1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 2,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
];
|
||||
|
||||
const SD_INPUT_NAMES = [
|
||||
'KICK In', 'SNARE Top', 'SNARE Bot', 'HAT', 'TOM 1', 'TOM 2', 'FLOOR TOM', 'OH L', 'OH R',
|
||||
@@ -551,36 +583,95 @@ const SD_GROUP_NAMES = [
|
||||
'', '', '', '', '', '', '', '', 'BAND', 'VOCALS', 'MISC', 'DRUMS', 'DRUM CRUSH', '', '', '', '', 'Broadcast mix',
|
||||
'', '', '', '', 'CAFE', 'Studio', 'Master',
|
||||
];
|
||||
/** Snapshot list: index n has cue number (n+1)*100. */
|
||||
const SD_MATRIX_NAMES = ['MAINS L', 'MAINS R', 'SUB L', 'SUB R', 'ProP LR', 'Matrix 6', 'Matrix 7', 'Studio'];
|
||||
const SD_CG_NAMES = ['DRUMS', 'GTR', 'KEYS', 'TRKS', 'VOCALS', 'FX', '', 'ProP', 'MC & SPEAKER', ''];
|
||||
/** Snapshot list: index n has cue number (n+1)*100 (reported as iiis [index, cue*100, 0, name]). */
|
||||
const SD_SNAPSHOTS = [
|
||||
'START', 'Pre Service', 'Worship 1', 'Worship 2', 'Message', 'Offering', 'Closing', 'BYPASS WAVES SUPERRACK!',
|
||||
];
|
||||
|
||||
const SD_PROPS = {
|
||||
name: { kind: 's', readOnly: true },
|
||||
inputName: { kind: 's' }, // input names accept a write (not verified on the real console)
|
||||
fader: { kind: 'f', min: -150, max: 10 }, // dB, -150 = -inf
|
||||
mute: { kind: 'flag' },
|
||||
solo: { kind: 'flag' },
|
||||
pan: { kind: 'f', min: 0, max: 1 }, // 0.5 = centre (Panner/pan and send_pan)
|
||||
level: { kind: 'f', min: -150, max: 10 }, // send_level, dB
|
||||
on: { kind: 'flag' }, // send_on
|
||||
pan: { kind: 'f', min: 0, max: 1 }, // send_pan, 0.5 = centre
|
||||
prepost: { kind: 'int', min: 0, max: 1 }, // send_pre-post; captured as an int, range assumed 0 = pre, 1 = post
|
||||
on: { kind: 'flag' }, // send_on / geq_in
|
||||
prepost: { kind: 'int', min: 0, max: 1 }, // send_pre-post; captured as an int, range assumed
|
||||
group: { kind: 'flag', wire: SD_GROUP_ASSIGN_WIRE_TYPE }, // Group_Send/M/group assign 0/1
|
||||
geqGain: { kind: 'f', min: -12, max: 12 },
|
||||
};
|
||||
/**
|
||||
* Per-channel switches and values answered on inputs and busses (Aux_Outputs and Group_Outputs),
|
||||
* keyed by the path under /<tree>/N/. Switches are written as ints and reported as floats 0/1.
|
||||
*/
|
||||
const SD_SWITCH = { kind: 'flag' };
|
||||
const sdValue = (min, max) => ({ kind: 'f', min, max });
|
||||
const SD_PARAM_SPECS = {
|
||||
input: {
|
||||
'Channel_Input/trim': { spec: sdValue(-40, 40), def: 0 },
|
||||
'Channel_Input/phase': { spec: SD_SWITCH, def: 0 },
|
||||
'Channel_Input/phantom': { spec: SD_SWITCH, def: 0 },
|
||||
'Channel_Input/analog_gain': { spec: sdValue(-20, 80), def: 0 },
|
||||
'Channel_Input/input_pad': { spec: SD_SWITCH, def: 0 },
|
||||
'EQ/eq_in': { spec: SD_SWITCH, def: 1 },
|
||||
'Dynamics/comp_in': { spec: SD_SWITCH, def: 1 },
|
||||
'Dynamics/gate_in': { spec: SD_SWITCH, def: 0 },
|
||||
'Filters/hi_filter_in': { spec: SD_SWITCH, def: 0 },
|
||||
'Filters/lo_filter_in': { spec: SD_SWITCH, def: 0 },
|
||||
'Filters/hi_filter_freq': { spec: sdValue(20, 20000), def: 12000 },
|
||||
'Filters/lo_filter_freq': { spec: sdValue(20, 20000), def: 40 },
|
||||
'Channel_Delay/delay_on': { spec: SD_SWITCH, def: 0 },
|
||||
'Channel_Delay/delay': { spec: sdValue(0, 10000), def: 0 },
|
||||
'Insert/insert_A_in': { spec: SD_SWITCH, def: 0 },
|
||||
'Insert/insert_B_in': { spec: SD_SWITCH, def: 0 },
|
||||
},
|
||||
buss: {
|
||||
'Buss_Trim/trim': { spec: sdValue(-40, 40), def: 0 },
|
||||
'Buss_Trim/phase': { spec: SD_SWITCH, def: 0 },
|
||||
'EQ/eq_in': { spec: SD_SWITCH, def: 1 },
|
||||
'Dynamics/comp_in': { spec: SD_SWITCH, def: 0 },
|
||||
'Dynamics/gate_in': { spec: SD_SWITCH, def: 0 },
|
||||
'Channel_Delay/delay_on': { spec: SD_SWITCH, def: 0 },
|
||||
'Channel_Delay/delay': { spec: sdValue(0, 10000), def: 0 },
|
||||
'Insert/insert_A_in': { spec: SD_SWITCH, def: 0 },
|
||||
'Insert/insert_B_in': { spec: SD_SWITCH, def: 0 },
|
||||
},
|
||||
matrix: {
|
||||
'Buss_Trim/trim': { spec: sdValue(-40, 40), def: 0 },
|
||||
},
|
||||
};
|
||||
const SD_SEND_PROPS = { send_level: 'level', send_on: 'on', send_pan: 'pan', 'send_pre-post': 'prepost' };
|
||||
|
||||
/** Real SD faders land on 1/16 dB steps (e.g. -1.4375, -4.375). */
|
||||
const tidy16 = (x) => Math.fround(Math.round(x * 16) / 16);
|
||||
|
||||
const sdInputs = [null]; // 1-based: { name, fader, mute, sends:[null,{level,on,pan,prepost}...], groupSends:[null,{group}...] }
|
||||
const sdAuxes = [null]; // 1-based: { name, fader, mute }
|
||||
const sdGroups = [null]; // 1-based, always SD_BUSS_COUNT: { name, fader, mute }
|
||||
let sdBusCount = 0; // set from --auxes when the state is built
|
||||
const sdInputs = [null]; // 1-based: { name, fader, mute, solo, pan, sends:[null,{level,on,pan,prepost}...], groupSends:[null,{group}...] }
|
||||
const sdAuxes = [null]; // 1-based: { name, fader, mute, solo }
|
||||
const sdGroups = [null]; // 1-based: { name, fader, mute, solo }
|
||||
const sdMatrix = [null]; // 1-based matrix outputs: { name, fader, mute, solo }
|
||||
const sdMatrixIn = [null]; // 1-based matrix inputs: { sends:[null,{level,on}...] }
|
||||
const sdCgs = [null]; // 1-based control groups: { name, fader, mute, solo }
|
||||
const sdGeqs = [null]; // 1-based graphic EQs: { name, on, gains:[null, ...] }
|
||||
let sdSnapshot = 3; // /Snapshots/Current_Snapshot
|
||||
const sdMeterSubs = new Map(); // meter id -> last value sent
|
||||
let sdMeterTimer = null;
|
||||
const SD_METER_INTERVAL_MS = 200; // ~5 Hz
|
||||
|
||||
function sdBussTypes() {
|
||||
// 1 = aux, 0 = group; a buss named only on the Group side is a group.
|
||||
return Array.from({ length: sdBusCount }, (_, i) => (SD_GROUP_NAMES[i] && !SD_AUX_NAMES[i] ? 0 : 1));
|
||||
}
|
||||
|
||||
function buildStateSd() {
|
||||
const rng = mulberry32(31);
|
||||
sdBusCount = Math.min(opts.auxes, SD_BUSS_MAX);
|
||||
for (let n = 1; n <= opts.channels; n++) {
|
||||
const sends = [null];
|
||||
for (let m = 1; m <= opts.auxes; m++) {
|
||||
for (let m = 1; m <= sdBusCount; m++) {
|
||||
const active = rng() < 0.5;
|
||||
sends.push({
|
||||
level: active ? tidy16(-40 + rng() * 40) : -150,
|
||||
@@ -591,35 +682,39 @@ function buildStateSd() {
|
||||
}
|
||||
// Group assign: inputs 1-8 and 18-21 feed group 24, nothing else.
|
||||
const groupSends = [null];
|
||||
for (let g = 1; g <= SD_BUSS_COUNT; g++) groupSends.push({ group: 0 });
|
||||
if (n <= 8 || (n >= 18 && n <= 21)) groupSends[24].group = 1;
|
||||
for (let g = 1; g <= sdBusCount; g++) groupSends.push({ group: 0 });
|
||||
if (sdBusCount >= 24 && (n <= 8 || (n >= 18 && n <= 21))) groupSends[24].group = 1;
|
||||
sdInputs.push({
|
||||
name: SD_INPUT_NAMES[n - 1] || `INPUT ${n}`,
|
||||
fader: tidy16(-30 + rng() * 30),
|
||||
mute: rng() < 0.08 ? 1 : 0,
|
||||
solo: 0,
|
||||
pan: 0.5,
|
||||
sends,
|
||||
groupSends,
|
||||
});
|
||||
}
|
||||
for (let k = 0; k < opts.auxes; k++) {
|
||||
sdAuxes.push({
|
||||
name: SD_AUX_NAMES[k],
|
||||
fader: tidy16(-10 + rng() * 10),
|
||||
mute: 0,
|
||||
});
|
||||
for (let k = 0; k < sdBusCount; k++) {
|
||||
sdAuxes.push({ name: SD_AUX_NAMES[k], fader: tidy16(-10 + rng() * 10), mute: 0, solo: 0 });
|
||||
sdGroups.push({ name: SD_GROUP_NAMES[k], fader: tidy16(-10 + rng() * 10), mute: 0, solo: 0 });
|
||||
}
|
||||
for (let k = 0; k < SD_BUSS_COUNT; k++) {
|
||||
sdGroups.push({
|
||||
name: SD_GROUP_NAMES[k],
|
||||
fader: tidy16(-10 + rng() * 10),
|
||||
mute: 0,
|
||||
});
|
||||
if (sdBusCount >= 24) sdGroups[24].fader = -1.6875; // captured
|
||||
for (let k = 0; k < SD_MATRIX_OUT; k++) sdMatrix.push({ name: SD_MATRIX_NAMES[k], fader: tidy16(-30 + rng() * 25), mute: 0, solo: 0 });
|
||||
for (let k = 0; k < SD_MATRIX_IN; k++) {
|
||||
const sends = [null];
|
||||
for (let m = 1; m <= SD_MATRIX_OUT; m++) sends.push({ level: -90, on: 0 }); // captured: -90 / 0
|
||||
sdMatrixIn.push({ sends });
|
||||
}
|
||||
for (let k = 0; k < SD_CGS; k++) sdCgs.push({ name: SD_CG_NAMES[k], fader: tidy16(-20 + rng() * 20), mute: 0, solo: 0 });
|
||||
for (let k = 1; k <= SD_GEQS; k++) {
|
||||
const gains = [null];
|
||||
for (let b = 1; b <= SD_GEQ_BANDS; b++) gains.push(0);
|
||||
sdGeqs.push({ name: `Graphic EQ ${k}`, on: 1, gains });
|
||||
}
|
||||
sdGroups[24].fader = -1.6875; // captured
|
||||
// Pin the values captured from the real console so a probe sees the same numbers.
|
||||
sdInputs[1].fader = -1.4375;
|
||||
sdInputs[1].mute = 0;
|
||||
if (opts.auxes >= 1) {
|
||||
if (sdBusCount >= 1) {
|
||||
sdInputs[1].sends[1] = { level: -150, on: 0, pan: 0.5, prepost: 0 };
|
||||
sdAuxes[1].fader = -4.375;
|
||||
sdAuxes[1].mute = 0;
|
||||
@@ -627,40 +722,84 @@ function buildStateSd() {
|
||||
}
|
||||
|
||||
const SD_RE_INPUT_NAME = /^\/Input_Channels\/(\d+)\/Channel_Input\/name$/;
|
||||
const SD_RE_INPUT_PROP = /^\/Input_Channels\/(\d+)\/(fader|mute)$/;
|
||||
const SD_RE_INPUT_PROP = /^\/Input_Channels\/(\d+)\/(fader|mute|solo)$/;
|
||||
const SD_RE_INPUT_PAN = /^\/Input_Channels\/(\d+)\/Panner\/pan$/;
|
||||
const SD_RE_SEND = /^\/Input_Channels\/(\d+)\/Aux_Send\/(\d+)\/(send_level|send_on|send_pan|send_pre-post)$/;
|
||||
const SD_RE_GROUP_SEND = /^\/Input_Channels\/(\d+)\/Group_Send\/(\d+)\/group$/;
|
||||
const SD_RE_AUX_NAME = /^\/Aux_Outputs\/(\d+)\/Buss_Trim\/name$/;
|
||||
const SD_RE_AUX_PROP = /^\/Aux_Outputs\/(\d+)\/(fader|mute)$/;
|
||||
const SD_RE_AUX_PROP = /^\/Aux_Outputs\/(\d+)\/(fader|mute|solo)$/;
|
||||
const SD_RE_GROUP_NAME = /^\/Group_Outputs\/(\d+)\/Buss_Trim\/name$/;
|
||||
const SD_RE_GROUP_PROP = /^\/Group_Outputs\/(\d+)\/(fader|mute)$/;
|
||||
const SD_RE_GROUP_PROP = /^\/Group_Outputs\/(\d+)\/(fader|mute|solo)$/;
|
||||
const SD_RE_MATRIX_NAME = /^\/Matrix_Outputs\/(\d+)\/Buss_Trim\/name$/;
|
||||
const SD_RE_MATRIX_PROP = /^\/Matrix_Outputs\/(\d+)\/(fader|mute|solo)$/;
|
||||
const SD_RE_MATRIX_SEND = /^\/Matrix_Inputs\/(\d+)\/Matrix_Send\/(\d+)\/(send_level|send_on)$/;
|
||||
const SD_RE_CG_NAME = /^\/Control_Groups\/(\d+)\/name$/;
|
||||
const SD_RE_CG_PROP = /^\/Control_Groups\/(\d+)\/(fader|mute|solo)$/;
|
||||
const SD_RE_GEQ = /^\/Graphic_EQ\/(\d+)\/(name|geq_in|geq_gain_(\d+))$/;
|
||||
const SD_RE_PARAM = /^\/(Input_Channels|Aux_Outputs|Group_Outputs|Matrix_Outputs)\/(\d+)\/((?:Channel_Input|Filters|Channel_Delay|EQ|Dynamics|Insert|Buss_Trim)\/\w+)$/;
|
||||
const SD_RE_METER_SUB = /^\/Input_Channels\/(\d+)\/(?:Channel_Input\/post_meter|Dynamics\/GR_meter)$/;
|
||||
|
||||
/** Map an SD leaf address to { target, prop } or null when there is no such thing. */
|
||||
/** Map an SD leaf address to { target, prop, spec } or null when there is no such thing. */
|
||||
function sdResolve(address) {
|
||||
let m = SD_RE_INPUT_NAME.exec(address);
|
||||
if (m) return sdInputs[Number(m[1])] ? { target: sdInputs[Number(m[1])], prop: 'name' } : null;
|
||||
if (m) return sdInputs[Number(m[1])] ? { target: sdInputs[Number(m[1])], prop: 'name', spec: SD_PROPS.inputName } : null;
|
||||
m = SD_RE_INPUT_PROP.exec(address);
|
||||
if (m) return sdInputs[Number(m[1])] ? { target: sdInputs[Number(m[1])], prop: m[2] } : null;
|
||||
if (m) return sdInputs[Number(m[1])] ? { target: sdInputs[Number(m[1])], prop: m[2], spec: SD_PROPS[m[2]] } : null;
|
||||
m = SD_RE_INPUT_PAN.exec(address);
|
||||
if (m) return sdInputs[Number(m[1])] ? { target: sdInputs[Number(m[1])], prop: 'pan', spec: SD_PROPS.pan } : null;
|
||||
m = SD_RE_SEND.exec(address);
|
||||
if (m) {
|
||||
const input = sdInputs[Number(m[1])];
|
||||
const snd = input && input.sends[Number(m[2])];
|
||||
return snd ? { target: snd, prop: SD_SEND_PROPS[m[3]] } : null;
|
||||
const prop = SD_SEND_PROPS[m[3]];
|
||||
return snd ? { target: snd, prop, spec: SD_PROPS[prop] } : null;
|
||||
}
|
||||
m = SD_RE_GROUP_SEND.exec(address);
|
||||
if (m) {
|
||||
const input = sdInputs[Number(m[1])];
|
||||
const gs = input && input.groupSends[Number(m[2])];
|
||||
return gs ? { target: gs, prop: 'group' } : null;
|
||||
return gs ? { target: gs, prop: 'group', spec: SD_PROPS.group } : null;
|
||||
}
|
||||
const tables = [
|
||||
[SD_RE_GROUP_NAME, sdGroups, 'name'], [SD_RE_GROUP_PROP, sdGroups, null],
|
||||
[SD_RE_AUX_NAME, sdAuxes, 'name'], [SD_RE_AUX_PROP, sdAuxes, null],
|
||||
[SD_RE_MATRIX_NAME, sdMatrix, 'name'], [SD_RE_MATRIX_PROP, sdMatrix, null],
|
||||
[SD_RE_CG_NAME, sdCgs, 'name'], [SD_RE_CG_PROP, sdCgs, null],
|
||||
];
|
||||
for (const [re, list, fixedProp] of tables) {
|
||||
m = re.exec(address);
|
||||
if (!m) continue;
|
||||
const target = list[Number(m[1])];
|
||||
if (!target) return null;
|
||||
const prop = fixedProp || m[2];
|
||||
return { target, prop, spec: SD_PROPS[prop] };
|
||||
}
|
||||
m = SD_RE_PARAM.exec(address);
|
||||
if (m) {
|
||||
const kind = { Input_Channels: 'input', Matrix_Outputs: 'matrix' }[m[1]] || 'buss';
|
||||
const entry = SD_PARAM_SPECS[kind][m[3]];
|
||||
const owner = { Input_Channels: sdInputs, Aux_Outputs: sdAuxes, Group_Outputs: sdGroups, Matrix_Outputs: sdMatrix }[m[1]][Number(m[2])];
|
||||
if (!entry || !owner) return null;
|
||||
if (!owner.params) owner.params = {};
|
||||
if (!(m[3] in owner.params)) owner.params[m[3]] = entry.def;
|
||||
return { target: owner.params, prop: m[3], spec: entry.spec };
|
||||
}
|
||||
m = SD_RE_MATRIX_SEND.exec(address);
|
||||
if (m) {
|
||||
const input = sdMatrixIn[Number(m[1])];
|
||||
const snd = input && input.sends[Number(m[2])];
|
||||
const prop = m[3] === 'send_on' ? 'on' : 'level';
|
||||
return snd ? { target: snd, prop, spec: SD_PROPS[prop] } : null;
|
||||
}
|
||||
m = SD_RE_GEQ.exec(address);
|
||||
if (m) {
|
||||
const geq = sdGeqs[Number(m[1])];
|
||||
if (!geq) return null;
|
||||
if (m[2] === 'name') return { target: geq, prop: 'name', spec: SD_PROPS.name };
|
||||
if (m[2] === 'geq_in') return { target: geq, prop: 'on', spec: SD_PROPS.on };
|
||||
const band = Number(m[3]);
|
||||
return band >= 1 && band <= SD_GEQ_BANDS ? { target: geq.gains, prop: band, spec: SD_PROPS.geqGain } : null;
|
||||
}
|
||||
m = SD_RE_GROUP_NAME.exec(address);
|
||||
if (m) return sdGroups[Number(m[1])] ? { target: sdGroups[Number(m[1])], prop: 'name' } : null;
|
||||
m = SD_RE_GROUP_PROP.exec(address);
|
||||
if (m) return sdGroups[Number(m[1])] ? { target: sdGroups[Number(m[1])], prop: m[2] } : null;
|
||||
m = SD_RE_AUX_NAME.exec(address);
|
||||
if (m) return sdAuxes[Number(m[1])] ? { target: sdAuxes[Number(m[1])], prop: 'name' } : null;
|
||||
m = SD_RE_AUX_PROP.exec(address);
|
||||
if (m) return sdAuxes[Number(m[1])] ? { target: sdAuxes[Number(m[1])], prop: m[2] } : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -672,34 +811,89 @@ function handleSd(address, args) {
|
||||
return sdSet(address, args);
|
||||
}
|
||||
|
||||
/** The nine counts the console pushes once, shortly after first contact (catalog: unsolicited, order as captured). */
|
||||
function sdCountBurst() {
|
||||
return [
|
||||
['/Console/Input_Channels', [int(opts.channels)]],
|
||||
['/Console/Aux_Outputs', [int(sdBusCount)]],
|
||||
['/Console/Group_Outputs', [int(sdBusCount)]],
|
||||
['/Console/Control_Groups', [int(SD_CGS)]],
|
||||
['/Console/Talkback_Outputs', [int(0)]],
|
||||
['/Console/Matrix_Outputs', [int(SD_MATRIX_OUT)]],
|
||||
['/Console/Matrix_Inputs', [int(SD_MATRIX_IN)]],
|
||||
['/Console/Graphic_EQ', [int(SD_GEQS)]],
|
||||
['/Console/Multis', [int(0)]],
|
||||
];
|
||||
}
|
||||
|
||||
function sdQuery(base, original) {
|
||||
if (base === '/Console/Name') return void send(base, [SD_CONSOLE_NAME]);
|
||||
if (base === '/Console/Input_Channels') return void send(base, [int(opts.channels)]);
|
||||
if (base === '/Console/Aux_Outputs') return void send(base, [int(opts.auxes)]);
|
||||
if (base === '/Console/Aux_Outputs/modes') {
|
||||
return void send(base, SD_AUX_MODES.slice(0, opts.auxes).map((mode) => int(mode)));
|
||||
if (base === '/Console/Session/Filename') return void send(base, [SD_SESSION_NAME]);
|
||||
// The Input_Channels / Aux_Outputs / Group_Outputs counts never answer a query (catalog): only the burst does.
|
||||
if (base === '/Console/Aux_Outputs/types') return void send(base, sdBussTypes().map((t) => int(t)));
|
||||
if (base === '/Console/Aux_Outputs/modes' || base === '/Console/Group_Outputs/modes') {
|
||||
return void send(base, SD_BUSS_MODES.slice(0, sdBusCount).map((mode) => int(mode)));
|
||||
}
|
||||
if (base === '/Console/Input_Channels/modes') return void send(base, SD_INPUT_MODES.slice(0, opts.channels).map((mode) => int(mode)));
|
||||
const fixed = /^\/Console\/(\w+)$/.exec(base);
|
||||
if (fixed && Object.hasOwn(SD_FIXED_COUNTS, fixed[1])) return void send(base, [int(SD_FIXED_COUNTS[fixed[1]])]);
|
||||
|
||||
if (base === '/Snapshots/Current_Snapshot') return void send(base, [int(sdSnapshot)]);
|
||||
if (base === '/Snapshots/count') return void send(base, [int(SD_SNAPSHOTS.length)]);
|
||||
// /Snapshots/name/? without an argument is unreliable on the real console: left silent here.
|
||||
if (base === '/Snapshots/names') {
|
||||
startPaced('snapshot names', SD_SNAPSHOTS.map((name, index) => [
|
||||
'/Snapshots/name', [int(index), int((index + 1) * 100), int(0), name],
|
||||
]));
|
||||
]), { append: true });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const r = sdResolve(base);
|
||||
if (!r) return ignore(`no reply for ${original} (the real console is silent here)`);
|
||||
send(base, [toArg(SD_PROPS[r.prop], r.target[r.prop])]);
|
||||
send(base, [toArg(r.spec, r.target[r.prop])]);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sdFireSnapshot(index) {
|
||||
sdSnapshot = index;
|
||||
send('/Snapshots/Current_Snapshot', [int(sdSnapshot)]);
|
||||
}
|
||||
|
||||
function sdSet(address, args) {
|
||||
// Write-only trigger nodes
|
||||
if (address === '/Snapshots/Fire_Snapshot_number') {
|
||||
if (typeof args[0] !== 'number' || args[0] < 0 || args[0] >= SD_SNAPSHOTS.length) return ignore(`snapshot fire with ${formatArgs(args)}`);
|
||||
note(`snapshot fire: recall #${Math.trunc(args[0])}`);
|
||||
return void sdFireSnapshot(Math.trunc(args[0]));
|
||||
}
|
||||
if (address === '/Snapshots/Fire_Next_Snapshot') {
|
||||
note('snapshot fire next');
|
||||
return void sdFireSnapshot(Math.min(SD_SNAPSHOTS.length - 1, sdSnapshot + 1));
|
||||
}
|
||||
if (address === '/Snapshots/Fire_Prev_Snapshot') {
|
||||
note('snapshot fire previous');
|
||||
return void sdFireSnapshot(Math.max(0, sdSnapshot - 1));
|
||||
}
|
||||
if (address === '/Macros/Buttons/press') {
|
||||
note(`macro press ${formatArgs(args)} (logged only: the real console never answered a macro query)`);
|
||||
return undefined;
|
||||
}
|
||||
// Meter subscriptions: /Input_Channels/N/Channel_Input/post_meter [id, kind]; /Meters/clear drops them all
|
||||
if (address === '/Meters/clear') {
|
||||
sdMeterSubs.clear();
|
||||
note('meters cleared');
|
||||
return undefined;
|
||||
}
|
||||
const sub = SD_RE_METER_SUB.exec(address);
|
||||
if (sub) {
|
||||
if (!sdInputs[Number(sub[1])] || typeof args[0] !== 'number') return ignore(`meter subscription ${address} ${formatArgs(args)}`);
|
||||
sdMeterSubs.set(Math.trunc(args[0]), -1);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const r = sdResolve(address);
|
||||
if (!r) return ignore(`no reply for ${address} (unknown or unsupported address)`);
|
||||
const spec = SD_PROPS[r.prop];
|
||||
const spec = r.spec;
|
||||
if (spec.readOnly) return ignore(`${address} is read-only`);
|
||||
if (args.length !== 1) return ignore(`${address} needs exactly one argument (got ${formatArgs(args)})`);
|
||||
const value = coerce(spec, args[0]);
|
||||
@@ -708,14 +902,27 @@ function sdSet(address, args) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** ~5 Hz: random 0..127 for every subscribed id whose value changed, as /Meters/values [id, v, id, v ...]. */
|
||||
function sdMeterTick() {
|
||||
if (!sdMeterSubs.size || !replyTarget) return;
|
||||
const pairs = [];
|
||||
for (const [id, last] of sdMeterSubs) {
|
||||
const v = Math.random() < 0.15 ? 0 : Math.floor(Math.random() * 128);
|
||||
if (v === last) continue;
|
||||
sdMeterSubs.set(id, v);
|
||||
pairs.push(int(id), int(v));
|
||||
}
|
||||
for (let i = 0; i < pairs.length; i += 40) send('/Meters/values', pairs.slice(i, i + 40));
|
||||
}
|
||||
|
||||
function liveTickSd() {
|
||||
const n = 1 + Math.floor(Math.random() * opts.channels);
|
||||
const input = sdInputs[n];
|
||||
if (opts.auxes === 0) {
|
||||
if (sdBusCount === 0) {
|
||||
setAndEmit(input, 'fader', SD_PROPS.fader, `/Input_Channels/${n}/fader`, tidy16(-40 + Math.random() * 50));
|
||||
return;
|
||||
}
|
||||
const s = 1 + Math.floor(Math.random() * opts.auxes);
|
||||
const s = 1 + Math.floor(Math.random() * sdBusCount);
|
||||
const snd = input.sends[s];
|
||||
if (Math.random() < 0.5) {
|
||||
setAndEmit(snd, 'level', SD_PROPS.level, `/Input_Channels/${n}/Aux_Send/${s}/send_level`, tidy16(-60 + Math.random() * 60));
|
||||
@@ -750,6 +957,12 @@ socket.on('message', (msg, rinfo) => {
|
||||
note(`no --reply-to given; locking feedback to ${replyTarget.host}:${replyTarget.port} (first sender was ${rinfo.address}:${rinfo.port})`);
|
||||
}
|
||||
|
||||
// The real console pushes all its counts once, shortly after first contact, and not reliably later.
|
||||
if (isSd && !sdBurstDone) {
|
||||
sdBurstDone = true;
|
||||
setTimeout(() => startPaced('count burst', sdCountBurst(), { append: true }), SD_BURST_DELAY_MS);
|
||||
}
|
||||
|
||||
if (packet.raw === undefined) handle(packet.address, packet.args);
|
||||
});
|
||||
|
||||
@@ -768,6 +981,7 @@ socket.on('listening', () => {
|
||||
? `feedback goes to ${replyTarget.host}:${replyTarget.port}`
|
||||
: `feedback target not set; will lock onto the first sender's IP, port ${DEFAULT_REPLY_PORT}`);
|
||||
if (isSd && opts.boolInt) note('--bool-int has no effect in the sd dialect (the console reports mute / send_on as floats)');
|
||||
if (isSd) sdMeterTimer = setInterval(sdMeterTick, SD_METER_INTERVAL_MS);
|
||||
if (opts.live) {
|
||||
note(`live mode: changing a random send every ${LIVE_INTERVAL_MS / 1000}s`);
|
||||
setInterval(liveTick, LIVE_INTERVAL_MS);
|
||||
@@ -776,6 +990,7 @@ socket.on('listening', () => {
|
||||
|
||||
function shutdown(signal) {
|
||||
if (dumpTimer) clearTimeout(dumpTimer);
|
||||
if (sdMeterTimer) clearInterval(sdMeterTimer);
|
||||
note(`${signal}: received ${stats.received} packets (${stats.undecodable} undecodable), sent ${stats.sent}, ignored ${stats.ignored}`);
|
||||
try {
|
||||
socket.close();
|
||||
|
||||
Reference in New Issue
Block a user