2026-10-20 11:20:07 -04:00
import { FADE_CURVES , LEVEL , LIMITS , PARAMS } from '../protocol.js'
2026-10-15 17:11:17 -04:00
export const STATE_CHOICES = [
{ id : 'on' , label : 'On' },
{ id : 'off' , label : 'Off' },
{ id : 'toggle' , label : 'Toggle' },
]
const firstId = ( choices , fallback = 1 ) => ( choices . length ? choices [ 0 ]. id : fallback )
/** Entity option builders shared by actions and feedbacks. Choices are read from the live state when the definitions are built. */
export const opt = {
dropdown ( id , label , choices , extra = {}) {
return { type : 'dropdown' , id , label , choices , default : firstId ( choices ), minChoicesForSearch : 0 , ... extra }
},
multi ( id , label , choices ) {
const def = choices . length ? [ choices [ 0 ]. id ] : []
return { type : 'multidropdown' , id , label , choices , default : def , minSelection : 1 , minChoicesForSearch : 0 }
},
channel : ( inst , id = 'channel' , label = 'Input' ) => opt . dropdown ( id , label , inst . state . choices . inputs ()),
channels : ( inst , id = 'channels' , label = 'Inputs' ) => opt . multi ( id , label , inst . state . choices . inputs ()),
buss : ( inst , kind = 'all' , id = 'buss' , label ) => {
const choices = kind === 'aux' ? inst . state . choices . auxChoices () : kind === 'group' ? inst . state . choices . groupChoices () : inst . state . choices . busses ()
return opt . dropdown ( id , label || ( kind === 'aux' ? 'Aux buss' : kind === 'group' ? 'Group buss' : 'Buss' ), choices )
},
busses : ( inst , id = 'busses' , label = 'Busses' ) => opt . multi ( id , label , inst . state . choices . busses ()),
matrix : ( inst , id = 'matrix' , label = 'Matrix output' ) => opt . dropdown ( id , label , inst . state . choices . matrix ()),
matrixIn : ( inst , id = 'matrixIn' , label = 'Matrix input' ) => opt . dropdown ( id , label , inst . state . choices . matrixInputs ()),
cg : ( inst , id = 'cg' , label = 'Control group' ) => opt . dropdown ( id , label , inst . state . choices . cgs ()),
geq : ( inst , id = 'geq' , label = 'Graphic EQ' ) => opt . dropdown ( id , label , inst . state . choices . geqs ()),
snapshot : ( inst , id = 'snapshot' , label = 'Snapshot' ) => opt . dropdown ( id , label , inst . state . choices . snapshots (), { allowCustom : true , default : firstId ( inst . state . choices . snapshots (), 0 ) }),
2026-10-19 15:44:43 -04:00
band : ( id = 'band' , label = 'Band' ) => ({ type : 'number' , id , label , default : 1 , min : 1 , max : LIMITS . geqBands , step : 1 }),
2026-10-15 17:11:17 -04:00
state : ( label = 'State' ) => ({ type : 'dropdown' , id : 'state' , label , choices : STATE_CHOICES , default : 'toggle' }),
2026-10-19 15:44:43 -04:00
level : ( id = 'level' , label = 'Level (dB)' , min = LEVEL . min ) => ({
2026-10-15 17:11:17 -04:00
type : 'number' ,
id ,
label ,
default : 0 ,
2026-10-19 15:44:43 -04:00
min ,
2026-10-15 17:11:17 -04:00
max : LEVEL . max ,
step : 0.5 ,
showMinAsNegativeInfinity : true ,
}),
2026-10-20 11:20:07 -04:00
/** Fade time of a "set level" action; the curve choice shows only while it is above 0 (so `duration` cannot be an expression). */
duration : ( id = 'duration' ) => ({
type : 'number' ,
id ,
label : 'Fade time (ms, 0 = instant)' ,
default : 0 ,
min : 0 ,
max : 60000 ,
step : 100 ,
asInteger : true ,
disableAutoExpression : true ,
}),
curve : ( id = 'curve' ) => ({
type : 'dropdown' ,
id ,
label : 'Fade curve' ,
default : 'fader' ,
choices : [
{ id : 'fader' , label : 'Fader travel (natural)' },
{ id : 'ease' , label : 'Fader travel, ease in/out' },
{ id : 'db' , label : 'Linear in dB' },
],
isVisibleExpression : '$(options:duration) > 0' ,
}),
2026-10-15 17:11:17 -04:00
delta : ( id = 'delta' , label = 'Change (dB, for encoders)' ) => ({ type : 'number' , id , label , default : 1 , min : - 30 , max : 30 , step : 0.5 }),
pan : ( id = 'pan' , label = 'Pan (0 = left, 0.5 = centre, 1 = right)' ) => ({ type : 'number' , id , label , default : 0.5 , min : 0 , max : 1 , step : 0.01 }),
pdelta : ( id = 'pdelta' , label = 'Pan change (for encoders)' ) => ({ type : 'number' , id , label , default : 0.05 , min : - 1 , max : 1 , step : 0.01 }),
}
/** Normalise a multidropdown / dropdown value to an array of numbers. */
2026-10-19 15:44:43 -04:00
/** Said in every level adjust action: a relative step cannot start from -inf, so the first step up jumps to -60 dB. */
const ADJUST_NOTE = 'From −∞ the first step up jumps to − 60 dB.'
2026-10-20 11:20:07 -04:00
/** Said in every level set action: the optional fade. */
const FADE_NOTE = 'With a fade time above 0 the level moves from where it is now to the target in steps of 50 ms; another set, adjust or fade on the same level ends it.'
/** [fade time in ms (0 = instant), curve] from a set-level action's options; missing options (buttons made before fades existed) mean instant. */
const fadeArgs = ( options ) => [ Number ( options ? . duration ) > 0 ? Math . min ( 60000 , Math . round ( Number ( options . duration ))) : 0 , FADE_CURVES . includes ( options ? . curve ) ? options . curve : 'fader' ]
2026-10-15 17:11:17 -04:00
export const numbers = ( v ) => ( Array . isArray ( v ) ? v : v === undefined || v === '' ? [] : [ v ]). map ( Number ). filter ( Number . isFinite )
2026-10-20 10:25:39 -04:00
/** Which entity kinds get actions / feedbacks for a parameter: 'input', 'buss', 'matrix' and/or 'minput' (matrix input). */
2026-10-15 17:11:17 -04:00
export const paramEntities = ( param ) => Object . keys ( PARAMS [ param ]. paths )
/** Entity dropdown for a parameter's entity kind. */
2026-10-20 10:25:39 -04:00
export const paramEntityOption = ( inst , ent ) => ( ent === 'input' ? opt . channel ( inst ) : ent === 'matrix' ? opt . matrix ( inst ) : ent === 'minput' ? opt . matrixIn ( inst ) : opt . buss ( inst , 'all' ))
/** Display name of an entity kind in action and feedback names. */
export const ENTITY_TITLE = { input : 'Input' , buss : 'Buss' , matrix : 'Matrix output' , minput : 'Matrix input' }
2026-10-15 17:11:17 -04:00
/** Switch toggles and value set/adjust actions for the lazily loaded per-channel parameters (PARAMS). */
function getParamActions ( inst ) {
const out = {}
for ( const [ param , p ] of Object . entries ( PARAMS )) {
for ( const ent of paramEntities ( param )) {
const entOpt = paramEntityOption ( inst , ent )
2026-10-20 10:25:39 -04:00
const who = ENTITY_TITLE [ ent ]
2026-10-15 17:11:17 -04:00
const target = ( options ) => Number ( options [ entOpt . id ])
if ( p . type === 'switch' ) {
out [ ` ${ ent } _ ${ param } ` ] = {
name : ` ${ who } : ${ p . label } (on/off)` ,
description : 'The current state is read from the console the first time it is needed.' ,
options : [ paramEntityOption ( inst , ent ), opt . state ()],
callback : async ({ options }) => inst . setParamSwitch ( ent , target ( options ), param , options . state ),
}
continue
}
const unit = p . adjust === 'percent' ? '%' : p . unit
out [ ` ${ ent } _ ${ param } _set` ] = {
name : ` ${ who } : set ${ p . label . toLowerCase () } ` ,
description : `Value in ${ p . unit } , as the console reports it (not verified on hardware: read the value back first).` ,
options : [ paramEntityOption ( inst , ent ), { type : 'number' , id : 'value' , label : ` ${ p . label } ( ${ p . unit } )` , default : p . min < 0 ? 0 : p . min , min : p . min , max : p . max , step : p . step }],
callback : async ({ options }) => inst . setParamValue ( ent , target ( options ), param , Number ( options . value )),
}
out [ ` ${ ent } _ ${ param } _adjust` ] = {
name : ` ${ who } : adjust ${ p . label . toLowerCase () } (relative)` ,
description : p . adjust === 'percent' ? 'For rotary encoders: each step changes the frequency by this percentage.' : 'For rotary encoders.' ,
options : [
paramEntityOption ( inst , ent ),
{ type : 'number' , id : 'delta' , label : `Change ( ${ unit } )` , default : p . delta , min : - ( p . adjust === 'percent' ? 100 : ( p . max - p . min ) / 2 ), max : p . adjust === 'percent' ? 100 : ( p . max - p . min ) / 2 , step : p . adjust === 'percent' ? 1 : p . step },
],
callback : async ({ options }) => inst . adjustParamValue ( ent , target ( options ), param , Number ( options . delta )),
}
}
}
return out
}
/** Action callbacks return nothing, and an error inside one is logged instead of thrown into Companion. */
function quiet ( inst , defs ) {
for ( const def of Object . values ( defs )) {
const run = def . callback
def . callback = async ( event , context ) => {
try {
await run ( event , context )
} catch ( err ) {
inst . log ( 'error' , ` ${ event ? . actionId || 'action' } : ${ err . message } ` )
}
}
}
return defs
}
export function getActionDefinitions ( inst ) {
const each = ( list , fn ) => Promise . all ( numbers ( list ). map ( fn ))
const n = ( v ) => Number ( v )
return quiet ( inst , {
... getParamActions ( inst ),
input_mute : {
name : 'Input: mute' ,
description : 'Mute, unmute or toggle one or more inputs.' ,
options : [ opt . channels ( inst ), opt . state ()],
callback : async ({ options }) => void ( await each ( options . channels , ( ch ) => inst . setSwitch ( 'input' , 'mute' , ch , options . state ))),
},
input_solo : {
name : 'Input: solo' ,
options : [ opt . channels ( inst ), opt . state ()],
callback : async ({ options }) => void ( await each ( options . channels , ( ch ) => inst . setSwitch ( 'input' , 'solo' , ch , options . state ))),
},
input_fader_set : {
name : 'Input: set fader' ,
2026-10-20 11:20:07 -04:00
description : FADE_NOTE ,
options : [ opt . channel ( inst ), opt . level (), opt . duration (), opt . curve ()],
callback : async ({ options }) => inst . setFader ( 'input' , n ( options . channel ), n ( options . level ), ... fadeArgs ( options )),
2026-10-15 17:11:17 -04:00
},
input_fader_adjust : {
name : 'Input: adjust fader (relative)' ,
2026-10-19 15:44:43 -04:00
description : `For rotary encoders. ${ ADJUST_NOTE } ` ,
2026-10-15 17:11:17 -04:00
options : [ opt . channel ( inst ), opt . delta ()],
callback : async ({ options }) => inst . adjustFader ( 'input' , n ( options . channel ), n ( options . delta )),
},
input_pan_set : {
name : 'Input: set pan' ,
options : [ opt . channel ( inst ), opt . pan ()],
callback : async ({ options }) => inst . setPan ( n ( options . channel ), n ( options . pan )),
},
input_pan_adjust : {
name : 'Input: adjust pan (relative)' ,
options : [ opt . channel ( inst ), opt . pdelta ()],
callback : async ({ options }) => inst . adjustPan ( n ( options . channel ), n ( options . pdelta )),
},
input_name_set : {
name : 'Input: set name' ,
options : [ opt . channel ( inst ), { type : 'textinput' , id : 'name' , label : 'Name' , default : '' , useVariables : true }],
callback : async ({ options }) => inst . setInputName ( n ( options . channel ), String ( options . name ?? '' )),
},
send_on : {
name : 'Aux send: on/off' ,
options : [ opt . channel ( inst ), opt . buss ( inst , 'aux' ), opt . state ()],
callback : async ({ options }) => inst . setSendOn ( n ( options . channel ), n ( options . buss ), options . state ),
},
send_level_set : {
name : 'Aux send: set level' ,
2026-10-20 11:20:07 -04:00
description : FADE_NOTE ,
options : [ opt . channel ( inst ), opt . buss ( inst , 'aux' ), opt . level (), opt . duration (), opt . curve ()],
callback : async ({ options }) => inst . setSendLevel ( n ( options . channel ), n ( options . buss ), n ( options . level ), ... fadeArgs ( options )),
2026-10-15 17:11:17 -04:00
},
send_level_adjust : {
name : 'Aux send: adjust level (relative)' ,
2026-10-19 15:44:43 -04:00
description : `For rotary encoders. ${ ADJUST_NOTE } ` ,
2026-10-15 17:11:17 -04:00
options : [ opt . channel ( inst ), opt . buss ( inst , 'aux' ), opt . delta ()],
callback : async ({ options }) => inst . adjustSendLevel ( n ( options . channel ), n ( options . buss ), n ( options . delta )),
},
send_pan_set : {
name : 'Aux send: set pan' ,
options : [ opt . channel ( inst ), opt . buss ( inst , 'aux' ), opt . pan ()],
callback : async ({ options }) => inst . setSendPan ( n ( options . channel ), n ( options . buss ), n ( options . pan )),
},
send_pan_adjust : {
name : 'Aux send: adjust pan (relative)' ,
options : [ opt . channel ( inst ), opt . buss ( inst , 'aux' ), opt . pdelta ()],
callback : async ({ options }) => inst . adjustSendPan ( n ( options . channel ), n ( options . buss ), n ( options . pdelta )),
},
group_assign : {
name : 'Group: assign input on/off' ,
options : [ opt . channel ( inst ), opt . buss ( inst , 'group' ), opt . state ()],
callback : async ({ options }) => inst . setGroup ( n ( options . channel ), n ( options . buss ), options . state ),
},
buss_mute : {
name : 'Buss: mute' ,
options : [ opt . busses ( inst ), opt . state ()],
callback : async ({ options }) => void ( await each ( options . busses , ( b ) => inst . setSwitch ( 'buss' , 'mute' , b , options . state ))),
},
buss_solo : {
name : 'Buss: solo' ,
options : [ opt . busses ( inst ), opt . state ()],
callback : async ({ options }) => void ( await each ( options . busses , ( b ) => inst . setSwitch ( 'buss' , 'solo' , b , options . state ))),
},
buss_fader_set : {
name : 'Buss: set fader' ,
2026-10-20 11:20:07 -04:00
description : FADE_NOTE ,
options : [ opt . buss ( inst ), opt . level (), opt . duration (), opt . curve ()],
callback : async ({ options }) => inst . setFader ( 'buss' , n ( options . buss ), n ( options . level ), ... fadeArgs ( options )),
2026-10-15 17:11:17 -04:00
},
buss_fader_adjust : {
name : 'Buss: adjust fader (relative)' ,
2026-10-19 15:44:43 -04:00
description : `For rotary encoders. ${ ADJUST_NOTE } ` ,
2026-10-15 17:11:17 -04:00
options : [ opt . buss ( inst ), opt . delta ()],
callback : async ({ options }) => inst . adjustFader ( 'buss' , n ( options . buss ), n ( options . delta )),
},
matrix_mute : {
name : 'Matrix output: mute' ,
options : [ opt . matrix ( inst ), opt . state ()],
callback : async ({ options }) => inst . setSwitch ( 'matrix' , 'mute' , n ( options . matrix ), options . state ),
},
2026-10-19 15:44:43 -04:00
matrix_solo : {
name : 'Matrix output: solo' ,
options : [ opt . matrix ( inst ), opt . state ()],
callback : async ({ options }) => inst . setSwitch ( 'matrix' , 'solo' , n ( options . matrix ), options . state ),
},
2026-10-15 17:11:17 -04:00
matrix_fader_set : {
name : 'Matrix output: set fader' ,
2026-10-20 11:20:07 -04:00
description : FADE_NOTE ,
options : [ opt . matrix ( inst ), opt . level (), opt . duration (), opt . curve ()],
callback : async ({ options }) => inst . setFader ( 'matrix' , n ( options . matrix ), n ( options . level ), ... fadeArgs ( options )),
2026-10-15 17:11:17 -04:00
},
matrix_fader_adjust : {
name : 'Matrix output: adjust fader (relative)' ,
2026-10-19 15:44:43 -04:00
description : `For rotary encoders. ${ ADJUST_NOTE } ` ,
2026-10-15 17:11:17 -04:00
options : [ opt . matrix ( inst ), opt . delta ()],
callback : async ({ options }) => inst . adjustFader ( 'matrix' , n ( options . matrix ), n ( options . delta )),
},
matrix_send_on : {
name : 'Matrix send: on/off' ,
options : [ opt . matrixIn ( inst ), opt . matrix ( inst , 'matrixOut' ), opt . state ()],
callback : async ({ options }) => inst . setMatrixSendOn ( n ( options . matrixIn ), n ( options . matrixOut ), options . state ),
},
matrix_send_level_set : {
name : 'Matrix send: set level' ,
2026-10-20 11:20:07 -04:00
description : `A matrix send goes down to − 90 dB (−∞). ${ FADE_NOTE } ` ,
options : [ opt . matrixIn ( inst ), opt . matrix ( inst , 'matrixOut' ), opt . level ( 'level' , 'Level (dB)' , LEVEL . matrixSendMin ), opt . duration (), opt . curve ()],
callback : async ({ options }) => inst . setMatrixSendLevel ( n ( options . matrixIn ), n ( options . matrixOut ), n ( options . level ), ... fadeArgs ( options )),
2026-10-15 17:11:17 -04:00
},
matrix_send_level_adjust : {
name : 'Matrix send: adjust level (relative)' ,
2026-10-19 15:44:43 -04:00
description : `For rotary encoders. ${ ADJUST_NOTE } ` ,
2026-10-15 17:11:17 -04:00
options : [ opt . matrixIn ( inst ), opt . matrix ( inst , 'matrixOut' ), opt . delta ()],
callback : async ({ options }) => inst . adjustMatrixSendLevel ( n ( options . matrixIn ), n ( options . matrixOut ), n ( options . delta )),
},
cg_mute : {
name : 'Control group: mute' ,
options : [ opt . cg ( inst ), opt . state ()],
callback : async ({ options }) => inst . setSwitch ( 'cg' , 'mute' , n ( options . cg ), options . state ),
},
cg_solo : {
name : 'Control group: solo' ,
options : [ opt . cg ( inst ), opt . state ()],
callback : async ({ options }) => inst . setSwitch ( 'cg' , 'solo' , n ( options . cg ), options . state ),
},
cg_fader_set : {
name : 'Control group: set fader' ,
2026-10-20 11:20:07 -04:00
description : FADE_NOTE ,
options : [ opt . cg ( inst ), opt . level (), opt . duration (), opt . curve ()],
callback : async ({ options }) => inst . setFader ( 'cg' , n ( options . cg ), n ( options . level ), ... fadeArgs ( options )),
2026-10-15 17:11:17 -04:00
},
cg_fader_adjust : {
name : 'Control group: adjust fader (relative)' ,
2026-10-19 15:44:43 -04:00
description : `For rotary encoders. ${ ADJUST_NOTE } ` ,
2026-10-15 17:11:17 -04:00
options : [ opt . cg ( inst ), opt . delta ()],
callback : async ({ options }) => inst . adjustFader ( 'cg' , n ( options . cg ), n ( options . delta )),
},
geq_on : {
name : 'Graphic EQ: in/out' ,
options : [ opt . geq ( inst ), opt . state ( 'State' )],
callback : async ({ options }) => inst . setGeqOn ( n ( options . geq ), options . state ),
},
geq_band_set : {
name : 'Graphic EQ: set band gain' ,
description : 'Band 1-32, gain in dB.' ,
options : [
opt . geq ( inst ),
2026-10-19 15:44:43 -04:00
opt . band (),
2026-10-15 17:11:17 -04:00
{ type : 'number' , id : 'gain' , label : 'Gain (dB)' , default : 0 , min : - 12 , max : 12 , step : 0.5 },
],
callback : async ({ options }) => inst . setGeqBand ( n ( options . geq ), n ( options . band ), n ( options . gain )),
},
geq_band_adjust : {
name : 'Graphic EQ: adjust band gain (relative)' ,
options : [
opt . geq ( inst ),
2026-10-19 15:44:43 -04:00
opt . band (),
2026-10-15 17:11:17 -04:00
{ type : 'number' , id : 'delta' , label : 'Change (dB, for encoders)' , default : 1 , min : - 12 , max : 12 , step : 0.5 },
],
callback : async ({ options }) => inst . adjustGeqBand ( n ( options . geq ), n ( options . band ), n ( options . delta )),
},
snapshot_fire : {
name : 'Snapshot: fire' ,
description : 'Pick a snapshot, or type a snapshot index. Fires by snapshot list position (0 = first). Verify once on your console before using live.' ,
options : [ opt . snapshot ( inst )],
callback : async ({ options }) => inst . fireSnapshot ( n ( options . snapshot )),
},
snapshot_next : { name : 'Snapshot: next' , options : [], callback : async () => inst . fireNextSnapshot () },
snapshot_prev : { name : 'Snapshot: previous' , options : [], callback : async () => inst . firePrevSnapshot () },
2026-10-20 11:20:07 -04:00
fade_stop : {
name : 'Stop all fades' ,
description : 'Ends every running level fade where it is.' ,
options : [],
callback : async () => inst . stopFades (),
},
2026-10-15 17:11:17 -04:00
reload : {
name : 'Console: reload names and levels' ,
options : [],
callback : async () => inst . reload (),
},
raw_osc : {
name : 'Send raw OSC message' ,
description : 'For experiments: any address, optional single argument. Append /? to an address to query it.' ,
options : [
{ type : 'textinput' , id : 'address' , label : 'Address' , default : '/Console/Name/?' , useVariables : true },
{
type : 'dropdown' ,
id : 'argType' ,
label : 'Argument type' ,
default : 'none' ,
choices : [
{ id : 'none' , label : 'None' },
{ id : 'int' , label : 'Integer' },
{ id : 'float' , label : 'Float' },
{ id : 'string' , label : 'String' },
{ id : 'bool' , label : 'Boolean (T/F)' },
],
},
{ type : 'textinput' , id : 'value' , label : 'Value' , default : '' , useVariables : true },
],
callback : async ({ options }) => inst . sendRaw ( options ),
},
relay_forget : {
name : 'Relay: forget all iPad clients' ,
options : [],
callback : async () => inst . forgetRelayClients (),
},
})
}