Write on/off switches as float 0/1, as the S31 requires

Verified on a live S31 against an unused, silent input (restored after):
a float fader write is accepted and echoed, but a mute written as int 0/1
or as an OSC True/False is silently ignored, while a float 1.0/0.0 works.
DiGiCo's 2014 SD command list says int, which is wrong for the S31.

Every switch (mute, solo, send on, group assign, EQ/dynamics/filter/
insert/delay switches, phantom, phase, pad, GEQ in) now goes out as a
float. The mock console now ignores int and T/F switch writes the same
way the console does, so a regression to int fails 38 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 17:32:17 -04:00
co-authored by Claude Opus 5.5
parent dad8010239
commit 57645be1e9
6 changed files with 87 additions and 40 deletions
+10 -7
View File
@@ -23,7 +23,8 @@
* 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, solo and send_on come back as floats 0/1). It also streams
* types (mute, solo and send_on come back as floats 0/1, and a 0/1
* switch only accepts a SET as a float: int and T/F writes are ignored). 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
@@ -165,8 +166,8 @@ function mulberry32(seed) {
* s string
* f float32 with min/max clamp
* b boolean (sseries; sent as T/F, or int 0/1 with --bool-int)
* flag 0/1 number (sd; accepted as int, float or boolean; reported as a float,
* or as an int when the spec says wire: 'i')
* flag 0/1 number (sd; a SET is accepted only as a float 0.0 / 1.0, exactly like the S31: int and
* T/F writes are silently ignored; reported as a float, or as an int when the spec says wire: 'i')
* int integer with min/max clamp (sd)
*/
@@ -803,12 +804,12 @@ function sdResolve(address) {
return null;
}
function handleSd(address, args) {
function handleSd(address, args, types) {
if (address.endsWith('/?')) {
if (args.length !== 0) return ignore(`query ${address} carries arguments ${formatArgs(args)}`);
return sdQuery(address.slice(0, -2), address);
}
return sdSet(address, args);
return sdSet(address, args, types);
}
/** The nine counts the console pushes once, shortly after first contact (catalog: unsolicited, order as captured). */
@@ -859,7 +860,7 @@ function sdFireSnapshot(index) {
send('/Snapshots/Current_Snapshot', [int(sdSnapshot)]);
}
function sdSet(address, args) {
function sdSet(address, args, types) {
// 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)}`);
@@ -895,6 +896,8 @@ function sdSet(address, args) {
if (!r) return ignore(`no reply for ${address} (unknown or unsupported address)`);
const spec = r.spec;
if (spec.readOnly) return ignore(`${address} is read-only`);
// Verified on a real S31 (2026-10-15): a 0/1 switch only takes a float write; ints and T/F are ignored without any reply.
if (spec.kind === 'flag' && types !== 'f') return ignore(`${address} is a switch: only a float 0.0 / 1.0 is accepted, not ,${types} (no reply, no change)`);
if (args.length !== 1) return ignore(`${address} needs exactly one argument (got ${formatArgs(args)})`);
const value = coerce(spec, args[0]);
if (value === undefined) return ignore(`unusable value ${formatArgs(args)} for ${address}`);
@@ -963,7 +966,7 @@ socket.on('message', (msg, rinfo) => {
setTimeout(() => startPaced('count burst', sdCountBurst(), { append: true }), SD_BURST_DELAY_MS);
}
if (packet.raw === undefined) handle(packet.address, packet.args);
if (packet.raw === undefined) handle(packet.address, packet.args, packet.types);
});
socket.on('error', (err) => {