606 lines
21 KiB
JavaScript
606 lines
21 KiB
JavaScript
import { InstanceBase, InstanceStatus, Regex } from '@companion-module/base'
|
|||
|
|
import { ConsoleLink } from './console-link.js'
|
||
|
|
import { Relay } from './relay.js'
|
||
|
|
import { BOOL_STYLES, DIALECT_CHOICES, getDialect } from './dialects.js'
|
||
|
|
import { decode, describe, foldPattern } from './osc.js'
|
||
|
|
import {
|
||
|
|
getActionDefinitions,
|
||
|
|
getFeedbackDefinitions,
|
||
|
|
getPresetDefinitions,
|
||
|
|
getVariableDefinitions,
|
||
|
|
MAX_INPUTS,
|
||
|
|
MAX_BUSSES,
|
||
|
|
BUS_FIRST,
|
||
|
|
BUS_LAST,
|
||
|
|
} from './definitions.js'
|
||
|
|
|
||
|
|
const KEEPALIVE_MS = 5000
|
||
|
|
const SILENCE_MS = 20000
|
||
|
|
const TX_PACE_MS = 20
|
||
|
|
const LEARN_CAP = 400
|
||
|
|
|
||
|
|
function emptyState() {
|
||
|
|
return {
|
||
|
|
consoleName: '',
|
||
|
|
names: new Map(), // input ch -> name
|
||
|
|
auxNames: new Map(), // buss -> name when in aux mode
|
||
|
|
groupNames: new Map(), // buss -> name when in group mode
|
||
|
|
busNames: new Map(), // S-Series OSC channel 70-93 -> name
|
||
|
|
bussModes: [], // 1 mono / 2 stereo per buss
|
||
|
|
sends: new Map(), // "ch:buss" -> { level, enabled, pan }
|
||
|
|
groups: new Map(), // "ch:buss" -> assigned
|
||
|
|
mutes: new Map(),
|
||
|
|
faders: new Map(),
|
||
|
|
bussMutes: new Map(),
|
||
|
|
bussFaders: new Map(),
|
||
|
|
snapshot: { index: null, names: new Map() },
|
||
|
|
counts: {},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export default class DigicoS31Instance extends InstanceBase {
|
||
|
|
constructor(internal) {
|
||
|
|
super(internal)
|
||
|
|
this.link = null
|
||
|
|
this.relay = null
|
||
|
|
this.timer = null
|
||
|
|
this.txQueue = []
|
||
|
|
this.txTimer = null
|
||
|
|
this.seen = new Set()
|
||
|
|
this.state = emptyState()
|
||
|
|
this.connectedFlag = false
|
||
|
|
this.lastMonMode = null
|
||
|
|
}
|
||
|
|
|
||
|
|
async init(config) {
|
||
|
|
this.config = config
|
||
|
|
this.dialect = getDialect(config.dialect)
|
||
|
|
this.initDefinitions()
|
||
|
|
await this.connect()
|
||
|
|
}
|
||
|
|
|
||
|
|
async destroy() {
|
||
|
|
await this.disconnect()
|
||
|
|
}
|
||
|
|
|
||
|
|
async configUpdated(config) {
|
||
|
|
this.config = config
|
||
|
|
this.dialect = getDialect(config.dialect)
|
||
|
|
await this.disconnect()
|
||
|
|
this.initDefinitions()
|
||
|
|
await this.connect()
|
||
|
|
}
|
||
|
|
|
||
|
|
getConfigFields() {
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
type: 'static-text',
|
||
|
|
id: 'info',
|
||
|
|
width: 12,
|
||
|
|
label: 'How this works',
|
||
|
|
value:
|
||
|
|
'Companion becomes the console\'s single remote (iPad) controller: on the console, Extensions → iPad, set the iPad IP to this computer, ' +
|
||
|
|
'Send Port = the local receive port below, Receive Port = the console receive port below. DiGiCo iPad apps then point at this computer ' +
|
||
|
|
'instead of the console and are relayed through, so several can be used at once.',
|
||
|
|
},
|
||
|
|
{ type: 'textinput', id: 'host', label: 'Console IP', width: 6, default: '10.3.4.221', regex: Regex.IP },
|
||
|
|
{ type: 'number', id: 'consolePort', label: 'Console receive port (we send here)', width: 3, default: 8000, min: 1, max: 65535 },
|
||
|
|
{ type: 'number', id: 'localPort', label: 'Local receive port (console sends here)', width: 3, default: 9000, min: 1, max: 65535 },
|
||
|
|
{ type: 'dropdown', id: 'dialect', label: 'Command set', width: 6, default: 'sd', choices: DIALECT_CHOICES },
|
||
|
|
{ type: 'dropdown', id: 'boolStyle', label: 'How to write on/off values', width: 6, default: 'int', choices: BOOL_STYLES },
|
||
|
|
{ type: 'checkbox', id: 'keepalive', label: 'Keepalive ping + reload on reconnect', width: 4, default: true },
|
||
|
|
{ type: 'number', id: 'channels', label: 'Input channels to load and build presets for', width: 4, default: 60, min: 1, max: MAX_INPUTS },
|
||
|
|
{ type: 'number', id: 'busses', label: 'Busses to load names for', width: 4, default: MAX_BUSSES, min: 1, max: MAX_BUSSES },
|
||
|
|
{
|
||
|
|
type: 'static-text',
|
||
|
|
id: 'monInfo',
|
||
|
|
width: 12,
|
||
|
|
label: 'Studio Mon buss',
|
||
|
|
value: 'Buss numbers follow the console\'s buss list (1-25). On this console buss 24 is the stereo group "Studio" and buss 17 is the aux "Studio TB". A group buss gives per-channel assign on/off; an aux buss gives send level + on/off.',
|
||
|
|
},
|
||
|
|
{ type: 'number', id: 'monSend', label: 'Studio Mon buss number', width: 4, default: 24, min: 1, max: MAX_BUSSES },
|
||
|
|
{
|
||
|
|
type: 'dropdown',
|
||
|
|
id: 'monMode',
|
||
|
|
label: 'Studio Mon buss type',
|
||
|
|
width: 4,
|
||
|
|
default: 'auto',
|
||
|
|
choices: [
|
||
|
|
{ id: 'auto', label: 'Auto (from the names the console reports)' },
|
||
|
|
{ id: 'aux', label: 'Aux (send level + on/off)' },
|
||
|
|
{ id: 'group', label: 'Group (assign on/off)' },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{ type: 'textinput', id: 'monLabel', label: 'Name for it in button labels', width: 4, default: 'Studio Mon' },
|
||
|
|
{
|
||
|
|
type: 'static-text',
|
||
|
|
id: 'relayInfo',
|
||
|
|
width: 12,
|
||
|
|
label: 'iPad relay',
|
||
|
|
value: 'In the DiGiCo S app enter this computer\'s IP as the console, Send Port = relay port, Receive Port = iPad receive port.',
|
||
|
|
},
|
||
|
|
{ type: 'checkbox', id: 'relayEnable', label: 'Enable iPad relay', width: 3, default: true },
|
||
|
|
{ type: 'number', id: 'relayPort', label: 'Relay port (iPad apps send here)', width: 3, default: 8000, min: 1, max: 65535 },
|
||
|
|
{ type: 'number', id: 'relayClientPort', label: 'iPad receive port (0 = reply to source port)', width: 3, default: 9000, min: 0, max: 65535 },
|
||
|
|
{ type: 'number', id: 'relayTimeout', label: 'Forget an iPad after (seconds idle)', width: 3, default: 180, min: 10, max: 3600 },
|
||
|
|
{ type: 'checkbox', id: 'learnLog', label: 'Learn mode: log each new address pattern once', width: 4, default: true },
|
||
|
|
{ type: 'checkbox', id: 'verboseLog', label: 'Log every message (debug level)', width: 4, default: false },
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- helpers used by definitions ----------
|
||
|
|
monSend() {
|
||
|
|
return Math.min(MAX_BUSSES, Math.max(1, Math.trunc(Number(this.config?.monSend) || 1)))
|
||
|
|
}
|
||
|
|
monLabel() {
|
||
|
|
return String(this.config?.monLabel || 'Studio Mon').trim() || 'Studio Mon'
|
||
|
|
}
|
||
|
|
monMode() {
|
||
|
|
const cfg = this.config?.monMode
|
||
|
|
if (cfg === 'aux' || cfg === 'group') return cfg
|
||
|
|
const n = this.monSend()
|
||
|
|
const g = (this.state.groupNames.get(n) || '').trim()
|
||
|
|
const a = (this.state.auxNames.get(n) || '').trim()
|
||
|
|
if (g && !a) return 'group'
|
||
|
|
return 'aux'
|
||
|
|
}
|
||
|
|
getSend(ch, send) {
|
||
|
|
return this.state.sends.get(`${Number(ch)}:${Number(send)}`) || {}
|
||
|
|
}
|
||
|
|
getGroup(ch, group) {
|
||
|
|
return this.state.groups.get(`${Number(ch)}:${Number(group)}`)
|
||
|
|
}
|
||
|
|
getMonOn(ch) {
|
||
|
|
return this.monMode() === 'group' ? this.getGroup(ch, this.monSend()) : this.getSend(ch, this.monSend()).enabled
|
||
|
|
}
|
||
|
|
isConnected() {
|
||
|
|
return this.connectedFlag
|
||
|
|
}
|
||
|
|
|
||
|
|
initDefinitions() {
|
||
|
|
this.setActionDefinitions(getActionDefinitions(this))
|
||
|
|
this.setFeedbackDefinitions(getFeedbackDefinitions(this))
|
||
|
|
this.setVariableDefinitions(getVariableDefinitions(this))
|
||
|
|
this.refreshPresets(true)
|
||
|
|
this.setVariableValues({
|
||
|
|
connected: this.connectedFlag ? 'Yes' : 'No',
|
||
|
|
console_host: this.config.host || '',
|
||
|
|
relay_clients: 0,
|
||
|
|
relay_client_ips: '',
|
||
|
|
mon_send: this.monSend(),
|
||
|
|
mon_mode: this.monMode(),
|
||
|
|
last_rx: '',
|
||
|
|
last_send_touched: '',
|
||
|
|
})
|
||
|
|
this.publishState()
|
||
|
|
}
|
||
|
|
|
||
|
|
refreshPresets(force = false) {
|
||
|
|
const mode = this.monMode()
|
||
|
|
if (!force && mode === this.lastMonMode) return
|
||
|
|
this.lastMonMode = mode
|
||
|
|
const { structure, presets } = getPresetDefinitions(this)
|
||
|
|
this.setPresetDefinitions(structure, presets)
|
||
|
|
this.setVariableValues({ mon_mode: mode })
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- lifecycle ----------
|
||
|
|
async connect() {
|
||
|
|
const cfg = this.config
|
||
|
|
if (!cfg.host) {
|
||
|
|
this.updateStatus(InstanceStatus.BadConfig, 'Console IP missing')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
this.updateStatus(InstanceStatus.Connecting)
|
||
|
|
this.seen.clear()
|
||
|
|
this.state = emptyState()
|
||
|
|
|
||
|
|
this.link = new ConsoleLink({ host: cfg.host, port: cfg.consolePort, localPort: cfg.localPort })
|
||
|
|
this.link.on('error', (err) => this.log('error', `Console socket: ${err.message}`))
|
||
|
|
this.link.on('stray', (buf, rinfo) => this.learn('stray', decode(buf), rinfo))
|
||
|
|
this.link.on('raw', (buf) => {
|
||
|
|
if (this.relay) this.relay.fanout(buf)
|
||
|
|
})
|
||
|
|
this.link.on('message', (msg, _buf, rinfo) => this.onConsoleMessage(msg, rinfo))
|
||
|
|
try {
|
||
|
|
const addr = await this.link.open()
|
||
|
|
this.log('info', `Console link: listening on udp ${addr.port}, sending to ${cfg.host}:${cfg.consolePort} (${this.dialect.label})`)
|
||
|
|
} catch (err) {
|
||
|
|
this.updateStatus(InstanceStatus.ConnectionFailure, `Cannot bind local port ${cfg.localPort}: ${err.code || err.message}`)
|
||
|
|
this.log('error', `Cannot bind local port ${cfg.localPort}: ${err.message}`)
|
||
|
|
this.link = null
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cfg.relayEnable) {
|
||
|
|
this.relay = new Relay({ port: cfg.relayPort, clientPort: cfg.relayClientPort, timeoutMs: (Number(cfg.relayTimeout) || 180) * 1000 })
|
||
|
|
this.relay.on('error', (err) => this.log('error', `Relay socket: ${err.message}`))
|
||
|
|
this.relay.on('client', (rinfo) => {
|
||
|
|
this.log('info', `Relay: new iPad client ${rinfo.address}:${rinfo.port}`)
|
||
|
|
this.publishRelay()
|
||
|
|
})
|
||
|
|
this.relay.on('clientGone', (ip) => {
|
||
|
|
this.log('info', `Relay: iPad client ${ip} idle, forgotten`)
|
||
|
|
this.publishRelay()
|
||
|
|
})
|
||
|
|
this.relay.on('fromClient', (buf, rinfo) => {
|
||
|
|
if (this.link) this.link.sendRaw(buf)
|
||
|
|
if (cfg.learnLog || cfg.verboseLog) {
|
||
|
|
const msg = decode(buf)
|
||
|
|
this.learn('ipad', msg, rinfo)
|
||
|
|
if (cfg.verboseLog) this.log('debug', `ipad ${rinfo.address} -> console: ${describe(msg)}`)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
try {
|
||
|
|
const addr = await this.relay.open()
|
||
|
|
this.log('info', `Relay: iPad apps may send to udp ${addr.port}; console traffic fans out to port ${cfg.relayClientPort || '(source)'}`)
|
||
|
|
} catch (err) {
|
||
|
|
this.log('error', `Relay: cannot bind port ${cfg.relayPort}: ${err.message}`)
|
||
|
|
this.relay = null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.dialect.id === 'raw') {
|
||
|
|
this.setConnected(true, 'relay only')
|
||
|
|
} else {
|
||
|
|
this.sendPing()
|
||
|
|
this.requestResend()
|
||
|
|
}
|
||
|
|
this.timer = setInterval(() => this.tick(), KEEPALIVE_MS)
|
||
|
|
this.publishRelay()
|
||
|
|
}
|
||
|
|
|
||
|
|
async disconnect() {
|
||
|
|
if (this.timer) clearInterval(this.timer)
|
||
|
|
this.timer = null
|
||
|
|
if (this.txTimer) clearInterval(this.txTimer)
|
||
|
|
this.txTimer = null
|
||
|
|
this.txQueue = []
|
||
|
|
if (this.link) this.link.close()
|
||
|
|
if (this.relay) this.relay.close()
|
||
|
|
this.link = null
|
||
|
|
this.relay = null
|
||
|
|
this.connectedFlag = false
|
||
|
|
}
|
||
|
|
|
||
|
|
tick() {
|
||
|
|
if (!this.link) return
|
||
|
|
if (this.dialect.id === 'raw') {
|
||
|
|
this.publishRelay()
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if (this.config.keepalive) this.sendPing()
|
||
|
|
const silent = Date.now() - this.link.lastRx
|
||
|
|
if (this.connectedFlag && silent > SILENCE_MS) this.setConnected(false)
|
||
|
|
this.publishRelay()
|
||
|
|
}
|
||
|
|
|
||
|
|
setConnected(on, note) {
|
||
|
|
const was = this.connectedFlag
|
||
|
|
this.connectedFlag = !!on
|
||
|
|
if (on) this.updateStatus(InstanceStatus.Ok, note)
|
||
|
|
else this.updateStatus(InstanceStatus.Disconnected, 'No reply from console (check the iPad/controller IP and ports on the console)')
|
||
|
|
if (was !== this.connectedFlag) {
|
||
|
|
this.setVariableValues({ connected: on ? 'Yes' : 'No' })
|
||
|
|
this.checkFeedbacks('connected')
|
||
|
|
this.log('info', on ? 'Console link alive' : 'Console link silent')
|
||
|
|
if (on && was === false && this.config.keepalive && this.link && this.link.rxCount > 1) this.requestResend()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- outbound ----------
|
||
|
|
/** Immediate send of a dialect-built message (user actions). */
|
||
|
|
sendDialect(fn, ...args) {
|
||
|
|
if (!this.link) return false
|
||
|
|
const built = this.dialect[fn]?.(...args)
|
||
|
|
if (!built) {
|
||
|
|
this.log('warn', `${fn}: not available in the "${this.dialect.label}" command set`)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if (this.config.verboseLog) this.log('debug', `-> ${built[0]} ${JSON.stringify(built[1])}`)
|
||
|
|
return this.link.send(built[0], built[1])
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Paced send for bulk queries so the console is not flooded. */
|
||
|
|
enqueue(list) {
|
||
|
|
for (const item of list) if (item) this.txQueue.push(item)
|
||
|
|
if (this.txTimer || !this.txQueue.length) return
|
||
|
|
this.txTimer = setInterval(() => {
|
||
|
|
const next = this.txQueue.shift()
|
||
|
|
if (!next || !this.link) {
|
||
|
|
clearInterval(this.txTimer)
|
||
|
|
this.txTimer = null
|
||
|
|
this.txQueue = []
|
||
|
|
return
|
||
|
|
}
|
||
|
|
this.link.send(next[0], next[1])
|
||
|
|
}, TX_PACE_MS)
|
||
|
|
}
|
||
|
|
|
||
|
|
sendPing() {
|
||
|
|
const p = this.dialect.ping()
|
||
|
|
if (p && this.link) this.link.send(p[0], p[1])
|
||
|
|
}
|
||
|
|
|
||
|
|
requestResend() {
|
||
|
|
if (!this.link) return false
|
||
|
|
const inputs = Math.min(MAX_INPUTS, Math.max(1, Number(this.config.channels) || MAX_INPUTS))
|
||
|
|
const auxes = Math.min(MAX_BUSSES, Math.max(1, Number(this.config.busses) || MAX_BUSSES))
|
||
|
|
const list = this.dialect.resendQueries({ inputs, auxes, monSend: this.monSend() })
|
||
|
|
if (!list.length) return false
|
||
|
|
this.log('info', `Loading ${list.length} values from the console`)
|
||
|
|
this.enqueue(list)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
resolveState(current, state) {
|
||
|
|
if (state === 'toggle') return !(current === true)
|
||
|
|
return state === 'on'
|
||
|
|
}
|
||
|
|
|
||
|
|
setMonOn(ch, state) {
|
||
|
|
return this.monMode() === 'group' ? this.setGroupAssign(ch, this.monSend(), state) : this.setSendEnabled(ch, this.monSend(), state)
|
||
|
|
}
|
||
|
|
|
||
|
|
setSendEnabled(ch, send, state) {
|
||
|
|
const cur = this.getSend(ch, send).enabled
|
||
|
|
const on = this.resolveState(cur, state)
|
||
|
|
const ok = this.sendDialect('sendEnabled', Number(ch), Number(send), on, this.config.boolStyle)
|
||
|
|
if (ok) this.enqueue(this.dialect.querySend(Number(ch), Number(send)))
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
|
||
|
|
setGroupAssign(ch, group, state) {
|
||
|
|
const cur = this.getGroup(ch, group)
|
||
|
|
const on = this.resolveState(cur, state)
|
||
|
|
const ok = this.sendDialect('groupAssign', Number(ch), Number(group), on, this.config.boolStyle)
|
||
|
|
if (ok) this.enqueue(this.dialect.queryGroup(Number(ch), Number(group)))
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
|
||
|
|
setSendLevel(ch, send, dB) {
|
||
|
|
const d = this.dialect
|
||
|
|
const v = Math.min(d.levelMax, Math.max(d.levelMin, Number(dB)))
|
||
|
|
const ok = this.sendDialect('sendLevel', Number(ch), Number(send), v)
|
||
|
|
if (ok) this.enqueue(this.dialect.querySend(Number(ch), Number(send)))
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
|
||
|
|
nudgeSendLevel(ch, send, delta) {
|
||
|
|
const cur = this.getSend(ch, send).level
|
||
|
|
if (typeof cur !== 'number') {
|
||
|
|
this.log('warn', `Send level for ch ${ch} buss ${send} unknown yet; asking the console`)
|
||
|
|
this.enqueue(this.dialect.querySend(Number(ch), Number(send)))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
const floor = this.dialect.levelMin
|
||
|
|
const base = cur <= floor ? floor : cur
|
||
|
|
return this.setSendLevel(ch, send, base + Number(delta))
|
||
|
|
}
|
||
|
|
|
||
|
|
setMute(ch, state) {
|
||
|
|
const cur = this.state.mutes.get(Number(ch))
|
||
|
|
const on = this.resolveState(cur, state)
|
||
|
|
return this.sendDialect('mute', Number(ch), on, this.config.boolStyle)
|
||
|
|
}
|
||
|
|
|
||
|
|
setBussMute(buss, state) {
|
||
|
|
const cur = this.state.bussMutes.get(Number(buss))
|
||
|
|
const on = this.resolveState(cur, state)
|
||
|
|
return this.sendDialect('auxMute', Number(buss), on, this.config.boolStyle)
|
||
|
|
}
|
||
|
|
|
||
|
|
sendRawAction(options) {
|
||
|
|
if (!this.link) return false
|
||
|
|
const address = String(options.address || '').trim()
|
||
|
|
if (!address.startsWith('/')) {
|
||
|
|
this.log('warn', `raw OSC: address must start with "/": ${address}`)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
const v = String(options.value ?? '')
|
||
|
|
let args = []
|
||
|
|
switch (options.argType) {
|
||
|
|
case 'int':
|
||
|
|
args = [{ i: Math.trunc(Number(v)) || 0 }]
|
||
|
|
break
|
||
|
|
case 'float':
|
||
|
|
args = [Number(v) || 0]
|
||
|
|
break
|
||
|
|
case 'string':
|
||
|
|
args = [v]
|
||
|
|
break
|
||
|
|
case 'bool':
|
||
|
|
args = [/^(1|true|on|yes)$/i.test(v.trim())]
|
||
|
|
break
|
||
|
|
default:
|
||
|
|
args = []
|
||
|
|
}
|
||
|
|
this.log('info', `raw OSC -> ${address} ${JSON.stringify(args)}`)
|
||
|
|
return this.link.send(address, args)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- inbound ----------
|
||
|
|
onConsoleMessage(msg, rinfo) {
|
||
|
|
if (!this.connectedFlag) this.setConnected(true)
|
||
|
|
if (msg.bundle) {
|
||
|
|
for (const el of msg.elements) this.onConsoleMessage(el, rinfo)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
this.learn('console', msg, rinfo)
|
||
|
|
if (this.config.verboseLog) this.log('debug', `console -> ${describe(msg)}`)
|
||
|
|
if (!msg.address) return
|
||
|
|
this.setVariableValues({ last_rx: describe(msg) })
|
||
|
|
const ev = this.dialect.parse(msg.address, msg.args)
|
||
|
|
if (ev) this.applyEvent(ev)
|
||
|
|
}
|
||
|
|
|
||
|
|
applyEvent(ev) {
|
||
|
|
const s = this.state
|
||
|
|
const values = {}
|
||
|
|
const mon = this.monSend()
|
||
|
|
switch (ev.kind) {
|
||
|
|
case 'pong':
|
||
|
|
if (ev.name && ev.name !== s.consoleName) {
|
||
|
|
s.consoleName = ev.name
|
||
|
|
values.console_name = ev.name
|
||
|
|
}
|
||
|
|
break
|
||
|
|
case 'count':
|
||
|
|
s.counts[ev.field] = ev.value
|
||
|
|
if (ev.field === 'inputs') values.input_count = ev.value
|
||
|
|
if (ev.field === 'auxes') values.aux_count = ev.value
|
||
|
|
break
|
||
|
|
case 'auxModes':
|
||
|
|
s.bussModes = ev.values
|
||
|
|
ev.values.forEach((m, i) => {
|
||
|
|
if (i < MAX_BUSSES) values[`buss_${i + 1}_mode`] = m === 2 ? 'Stereo' : 'Mono'
|
||
|
|
})
|
||
|
|
break
|
||
|
|
case 'name':
|
||
|
|
s.names.set(ev.ch, ev.value)
|
||
|
|
if (ev.ch >= 1 && ev.ch <= MAX_INPUTS) values[`ch_${ev.ch}_name`] = ev.value
|
||
|
|
else if (ev.ch >= BUS_FIRST && ev.ch <= BUS_LAST) {
|
||
|
|
s.busNames.set(ev.ch, ev.value)
|
||
|
|
values[`bus_${ev.ch}_name`] = ev.value
|
||
|
|
}
|
||
|
|
break
|
||
|
|
case 'auxName':
|
||
|
|
s.auxNames.set(ev.aux, ev.value)
|
||
|
|
if (ev.aux >= 1 && ev.aux <= MAX_BUSSES) values[`aux_${ev.aux}_name`] = ev.value
|
||
|
|
if (ev.aux === mon) this.afterMonNames(values)
|
||
|
|
break
|
||
|
|
case 'groupName':
|
||
|
|
s.groupNames.set(ev.group, ev.value)
|
||
|
|
if (ev.group >= 1 && ev.group <= MAX_BUSSES) values[`group_${ev.group}_name`] = ev.value
|
||
|
|
if (ev.group === mon) this.afterMonNames(values)
|
||
|
|
break
|
||
|
|
case 'send': {
|
||
|
|
const key = `${ev.ch}:${ev.send}`
|
||
|
|
const cur = s.sends.get(key) || {}
|
||
|
|
cur[ev.field] = ev.value
|
||
|
|
s.sends.set(key, cur)
|
||
|
|
values.last_send_touched = `ch ${ev.ch} buss ${ev.send} ${ev.field}`
|
||
|
|
if (ev.send === mon && ev.ch >= 1 && ev.ch <= MAX_INPUTS) {
|
||
|
|
if (ev.field === 'level') values[`mon_${ev.ch}_level`] = this.formatLevel(ev.value)
|
||
|
|
if (ev.field === 'enabled' && this.monMode() === 'aux') values[`mon_${ev.ch}_on`] = ev.value ? 'On' : 'Off'
|
||
|
|
}
|
||
|
|
this.checkFeedbacks('mon_on', 'send_on', 'mon_level_above')
|
||
|
|
break
|
||
|
|
}
|
||
|
|
case 'group': {
|
||
|
|
s.groups.set(`${ev.ch}:${ev.group}`, ev.value)
|
||
|
|
values.last_send_touched = `ch ${ev.ch} group ${ev.group} assign`
|
||
|
|
if (ev.group === mon && this.monMode() === 'group' && ev.ch >= 1 && ev.ch <= MAX_INPUTS) values[`mon_${ev.ch}_on`] = ev.value ? 'On' : 'Off'
|
||
|
|
this.checkFeedbacks('mon_on', 'group_assigned')
|
||
|
|
break
|
||
|
|
}
|
||
|
|
case 'fader':
|
||
|
|
s.faders.set(ev.ch, ev.value)
|
||
|
|
break
|
||
|
|
case 'mute':
|
||
|
|
s.mutes.set(ev.ch, ev.value)
|
||
|
|
this.checkFeedbacks('muted')
|
||
|
|
break
|
||
|
|
case 'auxFader':
|
||
|
|
s.bussFaders.set(ev.aux, ev.value)
|
||
|
|
break
|
||
|
|
case 'auxMute':
|
||
|
|
s.bussMutes.set(ev.aux, ev.value)
|
||
|
|
this.checkFeedbacks('buss_muted')
|
||
|
|
break
|
||
|
|
case 'snapshot':
|
||
|
|
s.snapshot.index = ev.index
|
||
|
|
values.snapshot_current = ev.index
|
||
|
|
values.snapshot_name = s.snapshot.names.get(ev.index) ?? ''
|
||
|
|
if (!s.snapshot.names.has(ev.index) && this.dialect.id === 'sd') this.enqueue([['/Snapshots/names/?', []]])
|
||
|
|
break
|
||
|
|
case 'snapshotName':
|
||
|
|
s.snapshot.names.set(ev.index, ev.name)
|
||
|
|
if (ev.index === s.snapshot.index) values.snapshot_name = ev.name
|
||
|
|
break
|
||
|
|
default:
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if (Object.keys(values).length) this.setVariableValues(values)
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Called when the Studio Mon buss name arrives (aux or group form). */
|
||
|
|
afterMonNames(values) {
|
||
|
|
const n = this.monSend()
|
||
|
|
const mode = this.monMode()
|
||
|
|
values.mon_name = mode === 'group' ? this.state.groupNames.get(n) || '' : this.state.auxNames.get(n) || ''
|
||
|
|
values.mon_mode = mode
|
||
|
|
if (mode !== this.lastMonMode) {
|
||
|
|
this.log('info', `Studio Mon buss ${n} is "${values.mon_name}" (${mode === 'group' ? 'group: assign on/off' : 'aux: send level + on/off'})`)
|
||
|
|
this.refreshPresets()
|
||
|
|
this.checkFeedbacks('mon_on')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
formatLevel(v) {
|
||
|
|
if (!Number.isFinite(v)) return ''
|
||
|
|
if (v <= this.dialect.levelMin) return '-inf'
|
||
|
|
return Number(v.toFixed(1))
|
||
|
|
}
|
||
|
|
|
||
|
|
learn(source, msg, rinfo) {
|
||
|
|
if (!this.config.learnLog) return
|
||
|
|
if (!msg || !msg.address) {
|
||
|
|
const key = `${source}:${msg?.bundle ? '#bundle' : 'raw'}`
|
||
|
|
if (this.seen.has(key) || this.seen.size > LEARN_CAP) return
|
||
|
|
this.seen.add(key)
|
||
|
|
this.log('info', `learn ${source}: ${describe(msg)} from ${rinfo?.address}:${rinfo?.port}`)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
const key = `${source}:${foldPattern(msg.address)} ${msg.tags || ''}${msg.unpadded ? ' (unpadded)' : ''}`
|
||
|
|
if (this.seen.has(key) || this.seen.size > LEARN_CAP) return
|
||
|
|
this.seen.add(key)
|
||
|
|
this.log('info', `learn ${source}: ${key} e.g. ${describe(msg)}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
publishRelay() {
|
||
|
|
const list = this.relay ? this.relay.clientList() : []
|
||
|
|
const now = Date.now()
|
||
|
|
const live = list.filter((c) => now - c.lastSeen <= (this.relay?.timeoutMs || 0))
|
||
|
|
this.setVariableValues({ relay_clients: live.length, relay_client_ips: live.map((c) => c.ip).join(', ') })
|
||
|
|
this.checkFeedbacks('relay_clients')
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Re-publish everything known (after definitions were rebuilt). */
|
||
|
|
publishState() {
|
||
|
|
const s = this.state
|
||
|
|
const values = {}
|
||
|
|
for (const [ch, name] of s.names) if (ch >= 1 && ch <= MAX_INPUTS) values[`ch_${ch}_name`] = name
|
||
|
|
for (const [ch, name] of s.busNames) values[`bus_${ch}_name`] = name
|
||
|
|
for (const [b, name] of s.auxNames) if (b >= 1 && b <= MAX_BUSSES) values[`aux_${b}_name`] = name
|
||
|
|
for (const [b, name] of s.groupNames) if (b >= 1 && b <= MAX_BUSSES) values[`group_${b}_name`] = name
|
||
|
|
s.bussModes.forEach((m, i) => {
|
||
|
|
if (i < MAX_BUSSES) values[`buss_${i + 1}_mode`] = m === 2 ? 'Stereo' : 'Mono'
|
||
|
|
})
|
||
|
|
const mon = this.monSend()
|
||
|
|
const mode = this.monMode()
|
||
|
|
for (const [key, v] of s.sends) {
|
||
|
|
const [ch, send] = key.split(':').map(Number)
|
||
|
|
if (send !== mon || ch < 1 || ch > MAX_INPUTS) continue
|
||
|
|
if (typeof v.level === 'number') values[`mon_${ch}_level`] = this.formatLevel(v.level)
|
||
|
|
if (typeof v.enabled === 'boolean' && mode === 'aux') values[`mon_${ch}_on`] = v.enabled ? 'On' : 'Off'
|
||
|
|
}
|
||
|
|
if (mode === 'group') {
|
||
|
|
for (const [key, on] of s.groups) {
|
||
|
|
const [ch, g] = key.split(':').map(Number)
|
||
|
|
if (g === mon && ch >= 1 && ch <= MAX_INPUTS) values[`mon_${ch}_on`] = on ? 'On' : 'Off'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
values.console_name = s.consoleName
|
||
|
|
values.mon_name = mode === 'group' ? s.groupNames.get(mon) || '' : s.auxNames.get(mon) || ''
|
||
|
|
values.mon_mode = mode
|
||
|
|
if (s.snapshot.index !== null) {
|
||
|
|
values.snapshot_current = s.snapshot.index
|
||
|
|
values.snapshot_name = s.snapshot.names.get(s.snapshot.index) ?? ''
|
||
|
|
}
|
||
|
|
this.setVariableValues(values)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export const UpgradeScripts = []
|