From 8c98c2daeba1ba136b18cd1186263cb4e7e8193e Mon Sep 17 00:00:00 2001 From: Hunter Date: Sat, 7 Feb 2026 15:46:27 -0700 Subject: [PATCH] Added Assignment updates/ variables Added in variables for if a channel is assigned to a mix, group, or fx. --- src/midi/parse/channel-parser.ts | 10 ++++++++++ src/mixer/mixer.ts | 33 +++++++++++++++++++++----------- src/mixer/nrpn/source-to-sink.ts | 24 +++++++++++++++++++++++ src/variables.ts | 10 +++++++++- 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/midi/parse/channel-parser.ts b/src/midi/parse/channel-parser.ts index 15f54786..fb7c165a 100644 --- a/src/midi/parse/channel-parser.ts +++ b/src/midi/parse/channel-parser.ts @@ -29,6 +29,12 @@ export interface MixerMessageEvents { * level specified by `vc`/`vf`. */ pan_level: [nrpn: NRPN<'panBalance'>, vc: number, vf: number] + + /** + * The assignment status of a source in a sink (assign NRPN) with the data + * bytes `vc`/`vf`. + */ + assign: [nrpn: NRPN<'assign'>, vc: number, vf: number] } /** @@ -149,6 +155,10 @@ export class ChannelParser extends EventEmitter { // Pan Level else if (0x50 <= msb && msb <= 0x5f) { this.emit('pan_level', makeNRPN(msb, lsb), vc, vf) + } + // Assign NRPNs + else if (0x60 <= msb && msb <= 0x6f) { + this.emit('assign', makeNRPN(msb, lsb), vc, vf) } else { verboseLog( `Unhandled MSB/LSB ${prettyByte(msb)}/${prettyByte(lsb)} in NRPN data message ${manyPrettyBytes(first, second, third, fourth)}`, diff --git a/src/mixer/mixer.ts b/src/mixer/mixer.ts index 3be2cf10..3ef29d69 100644 --- a/src/mixer/mixer.ts +++ b/src/mixer/mixer.ts @@ -23,6 +23,7 @@ import { AssignNRPNCalculator, BalanceNRPNCalculator, forEachSourceSinkLevel, + forEachSourceSinkAssign, LevelNRPNCalculator, type SourceForSourceInMixAndLRForNRPN, type SourceSinkForNRPN, @@ -308,6 +309,10 @@ export class Mixer { forEachSourceSinkLevel(model, getLevel) forEachOutputLevel(model, getLevel) + // Request assign NRPNs as well so assignment variables are populated at startup + const getAssign = (nrpn: NRPN<'assign'>) => buff.push(this.getNRPNValue(nrpn)) + forEachSourceSinkAssign(model, getAssign) + const delayStatusRetrieval = instance.config.retrieveStatusAtStartup === RetrieveStatusAtStartup.Delayed if (buff.length > 0 && this.#socket !== null) { @@ -423,6 +428,14 @@ export class Mixer { const variableValue = vcvfToReadablePanBalance(vc, vf) instance.setExtraVariable(variableId, name, variableValue) }) + mixerChannelParser.on('assign', (nrpn: NRPN<'assign'>, vc: number, vf: number) => { + verboseLog(`Assign received: ${prettyNRPN(nrpn)}, VC=${prettyByte(vc)}, VF=${prettyByte(vf)}`) + + const { MSB, LSB } = splitNRPN(nrpn) + const variableId = `assign_${MSB}.${LSB}` + // Use '1'/'0' string values for Companion variables + instance.setVariableValues({ [variableId]: vf !== 0 ? '1' : '0' }) + }) return parseMidi(instance.config.midiChannel, verboseLog, tokenizer, mixerChannelParser) } @@ -583,6 +596,8 @@ export class Mixer { */ #assignToSink(source: number, active: boolean, sink: number, calc: AssignNRPNCalculator): NRPNDataMessage { const nrpn = calc.calculate(source, sink) + const { MSB, LSB } = splitNRPN(nrpn) + this.#instance.setVariableValues({ [`assign_${MSB}.${LSB}`]: active ? '1' : '0' }) return this.#nrpnData(nrpn, 0, active ? 1 : 0) } @@ -608,16 +623,15 @@ export class Mixer { active: boolean, mixes: readonly MixOrLR[], ): void { - const mixNrpn = AssignNRPNCalculator.get(this.model, [sourceType, 'mix']) - const lrNrpn = AssignNRPNCalculator.get(this.model, [sourceType, 'lr']) + const mixNrpnCalc = AssignNRPNCalculator.get(this.model, [sourceType, 'mix']) + const lrNrpnCalc = AssignNRPNCalculator.get(this.model, [sourceType, 'lr']) - const commands = mixes.map((mixOrLR) => { + const commands: readonly number[][] = mixes.map((mixOrLR) => { return mixOrLR === LR - ? this.#assignToSink(source, active, 0, lrNrpn) - : this.#assignToSink(source, active, mixOrLR, mixNrpn) + ? this.#assignToSink(source, active, 0, lrNrpnCalc) + : this.#assignToSink(source, active, mixOrLR, mixNrpnCalc) }) - // XXX void this.sendCommands(commands) } @@ -646,13 +660,10 @@ export class Mixer { sinks: readonly number[], sourceSink: SourceSinkForNRPN<'assign'>, ): void { - const nrpn = AssignNRPNCalculator.get(this.model, sourceSink) + const calc = AssignNRPNCalculator.get(this.model, sourceSink) - const commands = sinks.map((sink) => { - return this.#assignToSink(source, active, sink, nrpn) - }) + const commands = sinks.map((sink) => this.#assignToSink(source, active, sink, calc)) - // XXX void this.sendCommands(commands) } diff --git a/src/mixer/nrpn/source-to-sink.ts b/src/mixer/nrpn/source-to-sink.ts index 15bb0374..50fdc34f 100644 --- a/src/mixer/nrpn/source-to-sink.ts +++ b/src/mixer/nrpn/source-to-sink.ts @@ -389,3 +389,27 @@ export function forEachSourceSinkLevel(model: Model, f: SourceSinkLevelFunctor): } } } + +type SourceSinkAssignFunctor = (nrpn: NRPN<'assign'>, sourceDesc: string, sinkDesc: string) => void + +/** + * For each source-sink relationship with assign parameters, invoke the given + * function. + */ +export function forEachSourceSinkAssign(model: Model, f: SourceSinkAssignFunctor): void { + for (const [sourceType, sinks] of Object.entries(SourceToSinkParameterBase)) { + for (const sinkType of Object.entries(sinks).flatMap(([sinkType, params]) => { + return 'assign' in params ? sinkType : [] + })) { + const sourceSink = [sourceType, sinkType] as SourceSinkForNRPN<'assign'> + + const calc = AssignNRPNCalculator.get(model, sourceSink) + model.forEach(sourceSink[0], (source, _sourceLabel, sourceDesc) => { + model.forEach(sourceSink[1], (sink, _sinkLabel, sinkDesc) => { + const nrpn = calc.calculate(source, sink) + f(nrpn, sourceDesc, sinkDesc) + }) + }) + } + } +} diff --git a/src/variables.ts b/src/variables.ts index e985c008..980f3631 100644 --- a/src/variables.ts +++ b/src/variables.ts @@ -2,7 +2,7 @@ import type { CompanionVariableDefinition } from '@companion-module/base' import type { Model } from './mixer/model.js' import { type NRPN, splitNRPN } from './mixer/nrpn/nrpn.js' import { forEachOutputLevel } from './mixer/nrpn/output.js' -import { forEachSourceSinkLevel } from './mixer/nrpn/source-to-sink.js' +import { forEachSourceSinkLevel, forEachSourceSinkAssign } from './mixer/nrpn/source-to-sink.js' /** * The variable ID for the variable containing the last recalled scene @@ -40,6 +40,14 @@ export function getVariables(model: Model): CompanionVariableDefinition[] { addVariable(nrpn, `${sourceDesc} -> ${sinkDesc} Level`) }) + forEachSourceSinkAssign(model, (nrpn: NRPN<'assign'>, sourceDesc: string, sinkDesc: string) => { + const { MSB, LSB } = splitNRPN(nrpn) + variables.push({ + name: `${sourceDesc} -> ${sinkDesc} Assigned`, + variableId: `assign_${MSB}.${LSB}`, + }) + }) + forEachOutputLevel(model, (nrpn, outputDesc) => { addVariable(nrpn, `${outputDesc} Output Level`) })