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:
+118
-8
@@ -3,7 +3,7 @@ import { ConsoleLink } from './console-link.js'
|
||||
import { Relay } from './relay.js'
|
||||
import { decode, describe, foldPattern } from './osc.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 { getFeedbackDefinitions, feedbackIdsForEvent, watchKey, watchKeyForEvent } from './definitions/feedbacks.js'
|
||||
import { getVariableDefinitions, getAllVariableValues, getEventVariableValues, describeChange } from './definitions/variables.js'
|
||||
@@ -11,6 +11,11 @@ import { getPresetDefinitions } from './definitions/presets.js'
|
||||
|
||||
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. */
|
||||
const TIMING = {
|
||||
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.waiters = new Map() // event key -> Set<fn>
|
||||
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.connectedFlag = false
|
||||
this.statusName = 'connecting'
|
||||
@@ -532,6 +540,11 @@ export default class DigicoS31Instance extends InstanceBase {
|
||||
for (const t of this.timers) clearTimeout(t)
|
||||
this.timers.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)
|
||||
this.rebuildTimer = null
|
||||
this.resubscribeTimer = null
|
||||
@@ -787,9 +800,10 @@ export default class DigicoS31Instance extends InstanceBase {
|
||||
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) {
|
||||
if (!this.addressOk(address)) return false
|
||||
this.cancelFade(address)
|
||||
const v = clamp(Number(dB), lo, LEVEL.max)
|
||||
if (!Number.isFinite(v)) return false
|
||||
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) {
|
||||
if (!this.addressOk(address)) return false
|
||||
this.cancelFade(address)
|
||||
const cur = await this.current(key, get, address)
|
||||
if (cur === undefined) {
|
||||
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)
|
||||
}
|
||||
|
||||
// ---------- 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) {
|
||||
if (!this.addressOk(address)) return false
|
||||
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)
|
||||
}
|
||||
|
||||
setFader(ent, n, dB) {
|
||||
return this.writeLevel(addr.fader(ent, n, this.sideOf(ent, n)), dB)
|
||||
/** Set a fader; with `durationMs` > 0 it moves there over that time (see fadeLevel). */
|
||||
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) {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
setSendLevel(ch, buss, dB) {
|
||||
return this.writeLevel(addr.sendLevel(ch, buss), dB)
|
||||
setSendLevel(ch, buss, dB, durationMs = 0, curve = 'fader') {
|
||||
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) {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
setMatrixSendLevel(inN, outN, dB) {
|
||||
return this.writeLevel(addr.matrixSendLevel(inN, outN), dB, LEVEL.matrixSendMin)
|
||||
setMatrixSendLevel(inN, outN, dB, durationMs = 0, curve = 'fader') {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user