import { FADE_CURVES, LEVEL, LIMITS, PARAMS } from '../protocol.js' 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) }, /** Several busses; `kind` 'aux' or 'group' lists only busses of that type (without the suffix), 'all' lists every buss. */ busses: (inst, id = 'busses', label = 'Busses', kind = 'all') => opt.multi(id, label, kind === 'aux' ? inst.state.choices.auxChoices() : kind === 'group' ? inst.state.choices.groupChoices() : 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) }), band: (id = 'band', label = 'Band') => ({ type: 'number', id, label, default: 1, min: 1, max: LIMITS.geqBands, step: 1 }), state: (label = 'State') => ({ type: 'dropdown', id: 'state', label, choices: STATE_CHOICES, default: 'toggle' }), level: (id = 'level', label = 'Level (dB)', min = LEVEL.min) => ({ type: 'number', id, label, default: 0, min, max: LEVEL.max, step: 0.5, showMinAsNegativeInfinity: true, }), /** 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', }), 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. */ /** 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.' /** 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'] export const numbers = (v) => (Array.isArray(v) ? v : v === undefined || v === '' ? [] : [v]).map(Number).filter(Number.isFinite) /** Which entity kinds get actions / feedbacks for a parameter: 'input', 'buss', 'matrix' and/or 'minput' (matrix input). */ export const paramEntities = (param) => Object.keys(PARAMS[param].paths) /** Entity dropdown for a parameter's entity kind. */ 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' } /** 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) const who = ENTITY_TITLE[ent] 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) /** * Mute, solo and fader actions for one type of buss (`prefix` 'group' or 'aux'). They are thin wrappers over the same * buss operations as the buss_* actions (the address follows the buss's aux / group side), with the dropdowns * filtered by type. The option ids are the same as the buss_* ones. */ const bussActions = (prefix, title, one, many) => ({ [`${prefix}_mute`]: { name: `${title}: mute`, description: 'Mute, unmute or toggle one or more.', options: [opt.busses(inst, 'busses', many, prefix), opt.state()], callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'mute', b, options.state))), }, [`${prefix}_solo`]: { name: `${title}: solo`, options: [opt.busses(inst, 'busses', many, prefix), opt.state()], callback: async ({ options }) => void (await each(options.busses, (b) => inst.setSwitch('buss', 'solo', b, options.state))), }, [`${prefix}_fader_set`]: { name: `${title}: set fader`, description: FADE_NOTE, options: [opt.buss(inst, prefix, 'buss', one), opt.level(), opt.duration(), opt.curve()], callback: async ({ options }) => inst.setFader('buss', n(options.buss), n(options.level), ...fadeArgs(options)), }, [`${prefix}_fader_adjust`]: { name: `${title}: adjust fader (relative)`, description: `For rotary encoders. ${ADJUST_NOTE}`, options: [opt.buss(inst, prefix, 'buss', one), opt.delta()], callback: async ({ options }) => inst.adjustFader('buss', n(options.buss), n(options.delta)), }, }) 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', 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)), }, input_fader_adjust: { name: 'Input: adjust fader (relative)', description: `For rotary encoders. ${ADJUST_NOTE}`, 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', 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)), }, send_level_adjust: { name: 'Aux send: adjust level (relative)', description: `For rotary encoders. ${ADJUST_NOTE}`, 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), }, ...bussActions('group', 'Group', 'Group', 'Groups'), ...bussActions('aux', 'Aux master', 'Aux master', 'Aux masters'), buss_mute: { name: 'Any buss (aux or group): mute', description: 'Mute, unmute or toggle one or more busses, aux and group alike (one list of all busses).', 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: 'Any buss (aux or group): 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: 'Any buss (aux or group): set fader', 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)), }, buss_fader_adjust: { name: 'Any buss (aux or group): adjust fader (relative)', description: `For rotary encoders. ${ADJUST_NOTE}`, 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), }, matrix_solo: { name: 'Matrix output: solo', options: [opt.matrix(inst), opt.state()], callback: async ({ options }) => inst.setSwitch('matrix', 'solo', n(options.matrix), options.state), }, matrix_fader_set: { name: 'Matrix output: set fader', 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)), }, matrix_fader_adjust: { name: 'Matrix output: adjust fader (relative)', description: `For rotary encoders. ${ADJUST_NOTE}`, 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', 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)), }, matrix_send_level_adjust: { name: 'Matrix send: adjust level (relative)', description: `For rotary encoders. ${ADJUST_NOTE}`, 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', 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)), }, cg_fader_adjust: { name: 'Control group: adjust fader (relative)', description: `For rotary encoders. ${ADJUST_NOTE}`, 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), opt.band(), { 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), opt.band(), { 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() }, fade_stop: { name: 'Stop all fades', description: 'Ends every running level fade where it is.', options: [], callback: async () => inst.stopFades(), }, 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(), }, }) }