2026-10-15 17:11:17 -04:00
import { COLORS } from './feedbacks.js'
2026-10-20 10:25:39 -04:00
import { VAR_PREFIX } from './variables.js'
2026-10-15 17:11:17 -04:00
/** Hard limits agreed for the preset list (Companion keeps all of it in memory and sends it to the web UI). */
2026-10-20 10:25:39 -04:00
export const MAX_PRESETS = 3200
2026-10-15 17:11:17 -04:00
export const MAX_PRESET_BYTES = 4 * 1024 * 1024
2026-10-20 09:50:31 -04:00
const { white : WHITE , dark : DARK , red : RED , green : GREEN , amber : AMBER } = COLORS
// Layered presets (native gauges). Colours are 0xRRGGBB numbers.
const BLACK = 0x000000
const TEXT = 0xffffff
const GAUGE_GREEN = 0x00b050
/** Level gauge fill: white, so it stays visible on a green (send on) or red (muted) background. */
const GAUGE_LEVEL = 0xffffff
const METER_AMBER = 0xf0a000
const METER_RED = 0xe00000
const ON_GREEN = 0x00a050 // a send is on / an input is assigned
const MUTED_RED = 0xb01010 // muted, peak
const SIGNAL_BG = 0x00501e // signal present on a meter button
2026-10-15 17:11:17 -04:00
const fb = ( feedbackId , options , bg , color = WHITE ) => ({ feedbackId , options , style : { bgcolor : bg , color } })
const act = ( actionId , options ) => ({ actionId , options })
2026-10-19 15:44:43 -04:00
/**
* Build every preset from the live state. When the list is too big `grMeters` (the gate / compressor reduction
* meter presets) is dropped first, then `sendKnobs` (the per-input aux send level knobs).
*/
function build ( inst , { grMeters = true , sendKnobs : withSendKnobs = true } = {}) {
2026-10-15 17:11:17 -04:00
const s = inst . state
const L = inst . label
const presets = {}
const structure = []
2026-10-20 10:25:39 -04:00
const nameVar = ( ent , n ) => `$( ${ L } : ${ VAR_PREFIX [ ent ] } _ ${ n } _name)`
2026-10-15 17:11:17 -04:00
const localFeedback = ( variableName , feedbackId , options ) => ({ variableName , variableType : 'feedback' , feedbackId , options })
const simple = ( name , text , down , feedbacks = [], extra = {}) => ({
type : 'simple' ,
name ,
style : { text , size : 'auto' , color : WHITE , bgcolor : DARK },
steps : [{ down , up : [] }],
feedbacks ,
... extra ,
})
2026-10-20 09:50:31 -04:00
/** A rotary step: press = `press`, turning = `adjust` with the action's delta option set to -1 / +1. */
const knobSteps = ( press , adjustActionId , adjustOptions ) => [
{
down : press ,
up : [],
rotate_left : [ act ( adjustActionId , { ... adjustOptions , delta : - 1 })],
rotate_right : [ act ( adjustActionId , { ... adjustOptions , delta : 1 })],
},
]
const text = ( id , x , y , width , height , value ) => ({ type : 'text' , id , x , y , width , height , text : value , fontsize : 100 , fontsizeAllowShrink : true , color : TEXT , halign : 'center' , valign : 'center' })
/** Boolean feedback on a layered preset: it colours the background box. */
const tint = ( feedbackId , options , color ) => ({ feedbackId , options , styleOverrides : [{ elementId : 'bg' , elementProperty : 'color' , override : color }] })
2026-10-20 11:20:07 -04:00
/** Green / amber / red zones for a meter gauge on the -60..0 dBFS scale. */
const meterStops = () => [
{ value : 0 , color : GAUGE_GREEN , gradient : false },
{ value : 70 , color : METER_AMBER , gradient : false },
{ value : 90 , color : METER_RED , gradient : false },
]
/** Width (%) of the optional meter column at the right edge of a knob preset. */
const KNOB_METER_WIDTH = 13
/** Track brightness (%) behind multi-colour meter gauges: faint, so an empty meter reads as empty. */
const METER_TRACK_AMOUNT = 20
2026-10-20 09:50:31 -04:00
/**
* Layered rotary preset with a native Companion gauge along the bottom: black background, the entity name on top, the
* dB text below it, and a gauge fed by the `pos` local variable. `level` and `pos` are the text and the numeric value
2026-10-20 11:20:07 -04:00
* feedbacks for the same thing. With `meter: [feedbackId, options]` a vertical meter column (fed by the `meter` local
* variable, a meter position feedback) runs down the right edge and the rest of the button narrows to make room.
2026-10-20 09:50:31 -04:00
*/
2026-10-20 11:20:07 -04:00
const layeredKnob = ({ name , title , press , adjust : [ adjustActionId , adjustOptions ], level : [ levelId , levelOptions ], pos : [ posId , posOptions ], meter , feedbacks }) => {
const w = meter ? 100 - KNOB_METER_WIDTH : 100
const elements = [
2026-10-20 09:50:31 -04:00
{ type : 'box' , id : 'bg' , x : 0 , y : 0 , width : 100 , height : 100 , color : BLACK },
2026-10-20 11:20:07 -04:00
text ( 'title' , 0 , 0 , w , 55 , title ),
text ( 'level' , 0 , 52 , w , 32 , '$(local:level) dB' ),
2026-10-15 17:11:17 -04:00
{
2026-10-20 09:50:31 -04:00
type : 'gauge' ,
id : 'gauge' ,
x : 0 ,
y : 84 ,
2026-10-20 11:20:07 -04:00
width : w ,
2026-10-20 09:50:31 -04:00
height : 16 ,
value : { isExpression : true , value : '$(local:pos)' },
min : 0 ,
max : 100 ,
orientation : 'horizontal' ,
trackStyle : 'dimmed' ,
fillEnabled : true ,
multiColour : false ,
stops : [{ value : 0 , color : GAUGE_LEVEL , gradient : false }],
2026-10-15 17:11:17 -04:00
},
2026-10-20 11:20:07 -04:00
]
const localVariables = [ localFeedback ( 'level' , levelId , levelOptions ), localFeedback ( 'pos' , posId , posOptions )]
if ( meter ) {
elements . push ({
type : 'gauge' ,
id : 'meter' ,
x : w + 1 ,
y : 0 ,
width : KNOB_METER_WIDTH - 1 ,
height : 100 ,
value : { isExpression : true , value : '$(local:meter)' },
min : 0 ,
max : 100 ,
orientation : 'vertical' ,
trackStyle : 'dimmed' ,
fillEnabled : true ,
multiColour : true ,
trackAmount : METER_TRACK_AMOUNT ,
stops : meterStops (),
})
localVariables . push ( localFeedback ( 'meter' , meter [ 0 ], meter [ 1 ]))
}
return { type : 'layered' , name , elements , steps : knobSteps ( press , adjustActionId , adjustOptions ), feedbacks , localVariables }
}
2026-10-20 10:25:39 -04:00
/**
* Layered meter: name on the left, a vertical gauge (green / amber / red zones) at the right edge; the position comes
* from `posFeedback`. A signal feedback tints the background dark green and, for level meters, a peak feedback red.
*/
const layeredMeter = ({ name , title , posFeedback , signal , peak }) => ({
2026-10-20 09:50:31 -04:00
type : 'layered' ,
name ,
elements : [
{ type : 'box' , id : 'bg' , x : 0 , y : 0 , width : 100 , height : 100 , color : BLACK },
text ( 'title' , 0 , 0 , 86 , 100 , title ),
{
type : 'gauge' ,
id : 'gauge' ,
x : 86 ,
y : 0 ,
width : 14 ,
height : 100 ,
value : { isExpression : true , value : '$(local:pos)' },
min : 0 ,
max : 100 ,
orientation : 'vertical' ,
trackStyle : 'dimmed' ,
fillEnabled : true ,
multiColour : true ,
2026-10-20 11:20:07 -04:00
trackAmount : METER_TRACK_AMOUNT ,
stops : meterStops (),
2026-10-20 09:50:31 -04:00
},
],
steps : [{ down : [], up : [] }],
2026-10-20 10:25:39 -04:00
feedbacks : [ tint ( signal [ 0 ], signal [ 1 ], SIGNAL_BG ), ...( peak ? [ tint ( peak [ 0 ], peak [ 1 ], MUTED_RED )] : [])],
localVariables : [ localFeedback ( 'pos' , posFeedback [ 0 ], posFeedback [ 1 ])],
2026-10-15 17:11:17 -04:00
})
const group = ( id , name , ids ) => ({ id , type : 'simple' , name , presets : ids })
// ---- Inputs
{
const muteIds = []
const knobIds = []
const soloIds = []
const meterIds = { level : [], gate : [], comp : [] }
for ( const n of s . numbers ( 'input' )) {
const name = s . displayName ( 'input' , n )
const tag = ` ${ n } · ${ name } `
presets [ `input_mute_ ${ n } ` ] = simple ( `Mute: ${ tag } ` , nameVar ( 'input' , n ), [ act ( 'input_mute' , { channels : [ n ], state : 'toggle' })], [ fb ( 'input_muted' , { channel : n }, RED )])
2026-10-20 09:50:31 -04:00
presets [ `input_knob_ ${ n } ` ] = layeredKnob ({
name : `Fader knob: ${ tag } ` ,
title : nameVar ( 'input' , n ),
press : [ act ( 'input_mute' , { channels : [ n ], state : 'toggle' })],
adjust : [ 'input_fader_adjust' , { channel : n }],
level : [ 'input_fader_value' , { channel : n }],
pos : [ 'input_fader_pos' , { channel : n }],
feedbacks : [ tint ( 'input_muted' , { channel : n }, MUTED_RED )],
})
2026-10-15 17:11:17 -04:00
presets [ `input_solo_ ${ n } ` ] = simple ( `Solo: ${ tag } ` , ` ${ nameVar ( 'input' , n ) } \\nSOLO` , [ act ( 'input_solo' , { channels : [ n ], state : 'toggle' })], [ fb ( 'input_solo' , { channel : n }, AMBER , COLORS . black )])
for ( const [ meter , prefix , label ] of [
[ 'level' , 'input_meter' , 'Meter' ],
[ 'gate' , 'input_gate_gr' , 'Gate reduction' ],
[ 'comp' , 'input_comp_gr' , 'Compressor reduction' ],
]) {
2026-10-19 15:44:43 -04:00
if ( meter !== 'level' && ! grMeters ) continue
2026-10-20 10:25:39 -04:00
const options = { channel : n , meter }
presets [ ` ${ prefix } _ ${ n } ` ] = layeredMeter ({
name : ` ${ label } : ${ tag } ` ,
title : nameVar ( 'input' , n ),
posFeedback : [ 'input_meter_pos' , options ],
signal : [ 'input_signal' , { ... options , thresholdDb : - 50 }],
// a gain reduction meter has no peak
peak : meter === 'level' ? [ 'input_peak' , { ... options , peakDb : - 1 , hold : 2 }] : null ,
})
2026-10-15 17:11:17 -04:00
meterIds [ meter ]. push ( ` ${ prefix } _ ${ n } ` )
}
muteIds . push ( `input_mute_ ${ n } ` )
knobIds . push ( `input_knob_ ${ n } ` )
soloIds . push ( `input_solo_ ${ n } ` )
}
if ( muteIds . length ) {
structure . push ({
id : 'inputs' ,
name : 'Inputs' ,
definitions : [ group ( 'inputs_mute' , 'Mute' , muteIds ), group ( 'inputs_knob' , 'Fader (knob)' , knobIds ), group ( 'inputs_solo' , 'Solo' , soloIds )],
})
structure . push ({
id : 'meters' ,
name : 'Input meters' ,
description : 'Meters subscribe on the console when the button first renders.' ,
definitions : [
group ( 'meters_input' , 'Level' , meterIds . level ),
2026-10-19 15:44:43 -04:00
...( grMeters ? [ group ( 'meters_gate' , 'Gate gain reduction' , meterIds . gate ), group ( 'meters_comp' , 'Compressor gain reduction' , meterIds . comp )] : []),
2026-10-15 17:11:17 -04:00
],
})
}
}
// ---- Busses
{
const muteIds = []
const knobIds = []
for ( const n of s . named ( 'buss' )) {
const tag = ` ${ n } · ${ s . displayName ( 'buss' , n ) } ( ${ s . bussMode ( n ) } )`
presets [ `buss_mute_ ${ n } ` ] = simple ( `Master mute: ${ tag } ` , nameVar ( 'buss' , n ), [ act ( 'buss_mute' , { busses : [ n ], state : 'toggle' })], [ fb ( 'buss_muted' , { buss : n }, RED )])
2026-10-20 09:50:31 -04:00
presets [ `buss_knob_ ${ n } ` ] = layeredKnob ({
name : `Master fader (knob): ${ tag } ` ,
title : nameVar ( 'buss' , n ),
press : [ act ( 'buss_mute' , { busses : [ n ], state : 'toggle' })],
adjust : [ 'buss_fader_adjust' , { buss : n }],
level : [ 'buss_fader_value' , { buss : n }],
pos : [ 'buss_fader_pos' , { buss : n }],
feedbacks : [ tint ( 'buss_muted' , { buss : n }, MUTED_RED )],
})
2026-10-15 17:11:17 -04:00
muteIds . push ( `buss_mute_ ${ n } ` )
knobIds . push ( `buss_knob_ ${ n } ` )
}
if ( muteIds . length ) {
structure . push ({ id : 'busses' , name : 'Busses' , definitions : [ group ( 'busses_mute' , 'Master mute' , muteIds ), group ( 'busses_knob' , 'Master fader (knob)' , knobIds )] })
}
}
// ---- Sends to each aux buss
const inputs = s . numbers ( 'input' )
for ( const b of s . namedBusses ( 'aux' )) {
const bname = s . displayName ( 'buss' , b )
const onIds = []
const knobIds = []
for ( const ch of inputs ) {
const cname = s . displayName ( 'input' , ch )
const tag = ` ${ ch } · ${ cname } → ${ b } · ${ bname } `
const text = ` ${ nameVar ( 'input' , ch ) } \\n$(local:level) dB`
const lv = [ localFeedback ( 'level' , 'send_level_value' , { channel : ch , buss : b })]
presets [ `send_on_ ${ ch } _ ${ b } ` ] = simple ( `Send on/off: ${ tag } ` , text , [ act ( 'send_on' , { channel : ch , buss : b , state : 'toggle' })], [ fb ( 'send_on' , { channel : ch , buss : b }, GREEN )], { localVariables : lv })
onIds . push ( `send_on_ ${ ch } _ ${ b } ` )
if ( withSendKnobs ) {
2026-10-20 09:50:31 -04:00
presets [ `send_knob_ ${ ch } _ ${ b } ` ] = layeredKnob ({
name : `Send level (knob): ${ tag } ` ,
title : nameVar ( 'input' , ch ),
press : [ act ( 'send_on' , { channel : ch , buss : b , state : 'toggle' })],
adjust : [ 'send_level_adjust' , { channel : ch , buss : b }],
level : [ 'send_level_value' , { channel : ch , buss : b }],
pos : [ 'send_level_pos' , { channel : ch , buss : b }],
feedbacks : [ tint ( 'send_on' , { channel : ch , buss : b }, ON_GREEN )],
})
2026-10-15 17:11:17 -04:00
knobIds . push ( `send_knob_ ${ ch } _ ${ b } ` )
}
}
const defs = [ group ( `sends_ ${ b } _on` , 'On/off' , onIds )]
if ( withSendKnobs ) defs . push ( group ( `sends_ ${ b } _knob` , 'Level (knob)' , knobIds ))
if ( onIds . length ) structure . push ({ id : `sends_ ${ b } ` , name : `Sends to ${ bname } ` , definitions : defs })
}
// ---- Assign to each group buss
for ( const b of s . namedBusses ( 'group' )) {
const bname = s . displayName ( 'buss' , b )
const ids = []
for ( const ch of inputs ) {
presets [ `group_ ${ ch } _ ${ b } ` ] = simple (
`Assign: ${ ch } · ${ s . displayName ( 'input' , ch ) } → ${ b } · ${ bname } ` ,
nameVar ( 'input' , ch ),
[ act ( 'group_assign' , { channel : ch , buss : b , state : 'toggle' })],
[ fb ( 'group_assigned' , { channel : ch , buss : b }, GREEN )],
)
ids . push ( `group_ ${ ch } _ ${ b } ` )
}
if ( ids . length ) structure . push ({ id : `group_ ${ b } ` , name : `Group ${ bname } ` , definitions : [ group ( `group_ ${ b } _assign` , 'Assign' , ids )] })
}
// ---- Matrix and control groups
{
const muteIds = []
const knobIds = []
for ( const n of s . named ( 'matrix' )) {
const tag = ` ${ n } · ${ s . displayName ( 'matrix' , n ) } `
presets [ `matrix_mute_ ${ n } ` ] = simple ( `Mute: ${ tag } ` , nameVar ( 'matrix' , n ), [ act ( 'matrix_mute' , { matrix : n , state : 'toggle' })], [ fb ( 'matrix_muted' , { matrix : n }, RED )])
2026-10-20 09:50:31 -04:00
presets [ `matrix_knob_ ${ n } ` ] = layeredKnob ({
name : `Fader (knob): ${ tag } ` ,
title : nameVar ( 'matrix' , n ),
press : [ act ( 'matrix_mute' , { matrix : n , state : 'toggle' })],
adjust : [ 'matrix_fader_adjust' , { matrix : n }],
level : [ 'matrix_fader_value' , { matrix : n }],
pos : [ 'matrix_fader_pos' , { matrix : n }],
feedbacks : [ tint ( 'matrix_muted' , { matrix : n }, MUTED_RED )],
})
2026-10-15 17:11:17 -04:00
muteIds . push ( `matrix_mute_ ${ n } ` )
knobIds . push ( `matrix_knob_ ${ n } ` )
}
if ( muteIds . length ) structure . push ({ id : 'matrix' , name : 'Matrix' , definitions : [ group ( 'matrix_mute' , 'Mute' , muteIds ), group ( 'matrix_knob' , 'Fader (knob)' , knobIds )] })
}
{
const muteIds = []
const knobIds = []
for ( const n of s . named ( 'cg' )) {
const tag = ` ${ n } · ${ s . displayName ( 'cg' , n ) } `
presets [ `cg_mute_ ${ n } ` ] = simple ( `Mute: ${ tag } ` , nameVar ( 'cg' , n ), [ act ( 'cg_mute' , { cg : n , state : 'toggle' })], [ fb ( 'cg_muted' , { cg : n }, RED )])
2026-10-20 09:50:31 -04:00
presets [ `cg_knob_ ${ n } ` ] = layeredKnob ({
name : `Fader (knob): ${ tag } ` ,
title : nameVar ( 'cg' , n ),
press : [ act ( 'cg_mute' , { cg : n , state : 'toggle' })],
adjust : [ 'cg_fader_adjust' , { cg : n }],
level : [ 'cg_fader_value' , { cg : n }],
pos : [ 'cg_fader_pos' , { cg : n }],
feedbacks : [ tint ( 'cg_muted' , { cg : n }, MUTED_RED )],
})
2026-10-15 17:11:17 -04:00
muteIds . push ( `cg_mute_ ${ n } ` )
knobIds . push ( `cg_knob_ ${ n } ` )
}
if ( muteIds . length ) structure . push ({ id : 'cgs' , name : 'Control groups' , definitions : [ group ( 'cgs_mute' , 'Mute' , muteIds ), group ( 'cgs_knob' , 'Fader (knob)' , knobIds )] })
}
2026-10-19 15:44:43 -04:00
// ---- Matrix sends: one section per matrix output, per matrix input an on/off button and a level knob
for ( const out of s . numbers ( 'matrix' )) {
const oname = s . displayName ( 'matrix' , out )
const onIds = []
const knobIds = []
for ( const inN of s . numbers ( 'minput' )) {
2026-10-20 10:25:39 -04:00
const tag = ` ${ s . displayName ( 'minput' , inN ) } → ${ out } · ${ oname } `
const text = ` ${ nameVar ( 'minput' , inN ) } \\n$(local:level) dB`
2026-10-19 15:44:43 -04:00
const options = { matrixIn : inN , matrixOut : out }
const lv = [ localFeedback ( 'level' , 'matrix_send_level_value' , options )]
2026-10-20 11:20:07 -04:00
// A send that is off looks like a mute (red); an enabled send keeps the black background.
presets [ `msend_on_ ${ inN } _ ${ out } ` ] = simple ( `Matrix send on/off: ${ tag } ` , text , [ act ( 'matrix_send_on' , { ... options , state : 'toggle' })], [ fb ( 'matrix_send_off' , options , MUTED_RED )], {
style : { text , size : 'auto' , color : WHITE , bgcolor : BLACK },
localVariables : lv ,
})
2026-10-20 09:50:31 -04:00
presets [ `msend_knob_ ${ inN } _ ${ out } ` ] = layeredKnob ({
name : `Matrix send level (knob): ${ tag } ` ,
2026-10-20 10:25:39 -04:00
title : nameVar ( 'minput' , inN ),
2026-10-20 09:50:31 -04:00
press : [ act ( 'matrix_send_on' , { ... options , state : 'toggle' })],
adjust : [ 'matrix_send_level_adjust' , options ],
level : [ 'matrix_send_level_value' , options ],
pos : [ 'matrix_send_level_pos' , options ],
2026-10-20 11:20:07 -04:00
// The console has no meter for matrix inputs (no address answers), so the column shows the destination output.
meter : [ 'matrix_meter_pos' , { matrix : out }],
feedbacks : [ tint ( 'matrix_send_off' , options , MUTED_RED )],
2026-10-20 09:50:31 -04:00
})
2026-10-19 15:44:43 -04:00
onIds . push ( `msend_on_ ${ inN } _ ${ out } ` )
knobIds . push ( `msend_knob_ ${ inN } _ ${ out } ` )
}
if ( onIds . length ) {
structure . push ({
id : `msends_ ${ out } ` ,
name : `Matrix sends to ${ oname } ` ,
definitions : [ group ( `msends_ ${ out } _on` , 'On/off' , onIds ), group ( `msends_ ${ out } _knob` , 'Level (knob)' , knobIds )],
})
}
}
2026-10-20 10:25:39 -04:00
// ---- Output meters: one layered meter per buss and per matrix output
{
const bussIds = []
for ( const n of s . named ( 'buss' )) {
presets [ `buss_meter_ ${ n } ` ] = layeredMeter ({
name : `Buss meter: ${ n } · ${ s . displayName ( 'buss' , n ) } ( ${ s . bussMode ( n ) } )` ,
title : nameVar ( 'buss' , n ),
posFeedback : [ 'buss_meter_pos' , { buss : n }],
signal : [ 'buss_signal' , { buss : n , thresholdDb : - 50 }],
peak : [ 'buss_peak' , { buss : n , peakDb : - 1 , hold : 2 }],
})
bussIds . push ( `buss_meter_ ${ n } ` )
}
if ( bussIds . length ) {
structure . push ({ id : 'buss_meters' , name : 'Buss meters' , description : 'Meters subscribe on the console when the button first renders.' , definitions : [ group ( 'buss_meters_level' , 'Level' , bussIds )] })
}
const matrixIds = []
for ( const n of s . numbers ( 'matrix' )) {
presets [ `matrix_meter_ ${ n } ` ] = layeredMeter ({
name : `Matrix meter: ${ n } · ${ s . displayName ( 'matrix' , n ) } ` ,
title : nameVar ( 'matrix' , n ),
posFeedback : [ 'matrix_meter_pos' , { matrix : n }],
signal : [ 'matrix_signal' , { matrix : n , thresholdDb : - 50 }],
peak : [ 'matrix_peak' , { matrix : n , peakDb : - 1 , hold : 2 }],
})
matrixIds . push ( `matrix_meter_ ${ n } ` )
}
if ( matrixIds . length ) {
structure . push ({ id : 'matrix_meters' , name : 'Matrix meters' , description : 'Meters subscribe on the console when the button first renders.' , definitions : [ group ( 'matrix_meters_level' , 'Level' , matrixIds )] })
}
}
2026-10-19 15:44:43 -04:00
// ---- Graphic EQs: in / out
{
const ids = []
for ( const n of s . numbers ( 'geq' )) {
presets [ `geq_in_ ${ n } ` ] = simple ( `In/out: ${ n } · ${ s . displayName ( 'geq' , n ) } ` , ` ${ s . displayName ( 'geq' , n ) } \\nIN` , [ act ( 'geq_on' , { geq : n , state : 'toggle' })], [ fb ( 'geq_on' , { geq : n }, GREEN )])
ids . push ( `geq_in_ ${ n } ` )
}
if ( ids . length ) structure . push ({ id : 'geqs' , name : 'Graphic EQs' , definitions : [ group ( 'geqs_in' , 'In/out' , ids )] })
}
2026-10-15 17:11:17 -04:00
// ---- Snapshots
{
const fireIds = []
for ( const [ index , snap ] of [... s . snapshots . list ]. sort (( a , b ) => a [ 0 ] - b [ 0 ])) {
presets [ `snap_fire_ ${ index } ` ] = simple ( `Fire ${ index } · ${ snap . name } ` , ` ${ index } \\n ${ snap . name } ` , [ act ( 'snapshot_fire' , { snapshot : index })], [ fb ( 'snapshot_current' , { snapshot : index }, GREEN )])
fireIds . push ( `snap_fire_ ${ index } ` )
}
presets . snap_next = simple ( 'Next snapshot' , 'NEXT ▶' , [ act ( 'snapshot_next' , {})])
presets . snap_prev = simple ( 'Previous snapshot' , '◀ PREV' , [ act ( 'snapshot_prev' , {})])
presets . snap_current = simple ( 'Current snapshot (display)' , `$( ${ L } :snapshot_index)\\n$(local:snap)` , [], [], {
localVariables : [ localFeedback ( 'snap' , 'snapshot_name_value' , {})],
})
const defs = []
if ( fireIds . length ) defs . push ( group ( 'snapshots_fire' , 'Fire' , fireIds ))
defs . push ( group ( 'snapshots_nav' , 'Next / previous / current' , [ 'snap_next' , 'snap_prev' , 'snap_current' ]))
structure . push ({ id : 'snapshots' , name : 'Snapshots' , definitions : defs })
}
// ---- Console
{
presets . console_reload = simple ( 'Reload from console' , `$( ${ L } :console_name)\\nReload` , [ act ( 'reload' , {})], [ fb ( 'connected' , {}, GREEN )])
presets . console_status = simple ( 'Connection status' , 'Console' , [], [ fb ( 'connected' , {}, GREEN ), { ... fb ( 'connected' , {}, RED ), isInverted : true }])
presets . console_relay = simple ( 'Relay status' , `iPads\\n$( ${ L } :relay_clients)` , [], [ fb ( 'relay_clients' , {}, GREEN )])
structure . push ({ id : 'console' , name : 'Console' , definitions : [ group ( 'console_all' , 'Console' , [ 'console_reload' , 'console_status' , 'console_relay' ])] })
}
return { structure , presets }
}
/**
2026-10-19 15:44:43 -04:00
* Presets for the current state: { structure, presets, stats }. If the list would exceed the agreed limits the
* gate / compressor reduction meter presets are left out first, then the per-input aux send level knobs;
* stats.dropped says which.
2026-10-15 17:11:17 -04:00
*/
2026-10-19 15:44:43 -04:00
export function getPresetDefinitions ( inst , limits = { count : MAX_PRESETS , bytes : MAX_PRESET_BYTES }) {
const tiers = [
{ options : { grMeters : true , sendKnobs : true }, dropped : [] },
{ options : { grMeters : false , sendKnobs : true }, dropped : [ 'the gate / compressor reduction meter presets' ] },
{ options : { grMeters : false , sendKnobs : false }, dropped : [ 'the gate / compressor reduction meter presets' , 'the per-input send level knobs' ] },
]
let built
let count = 0
let bytes = 0
let dropped = []
for ( const tier of tiers ) {
built = build ( inst , tier . options )
2026-10-15 17:11:17 -04:00
count = Object . keys ( built . presets ). length
bytes = JSON . stringify ( built . presets ). length
2026-10-19 15:44:43 -04:00
dropped = tier . dropped
if ( count <= limits . count && bytes <= limits . bytes ) break
2026-10-15 17:11:17 -04:00
}
2026-10-19 15:44:43 -04:00
return { ... built , stats : { count , bytes , dropped } }
2026-10-15 17:11:17 -04:00
}