Add Group and Aux master actions, feedbacks and preset sections
Groups were only reachable through the generic "Buss:" actions, whose dropdown mixes all 25 busses, so users could not find group fader control. - New "Group:" and "Aux master:" mute, solo, set fader (with fade time) and adjust fader actions; their dropdowns list only busses of that type. Matching muted, solo, fader text/position, meter, signal and peak feedbacks. - The old buss_* actions and feedbacks keep working for existing buttons and are now labelled "Any buss (aux or group)". - Presets: "Busses" is split into "Groups" and "Aux masters", and the bus meters into "Group meters" and "Aux meters". A bus that changes type on the console moves section on the next rebuild. npm test: 542 checks pass on Node 25 and on Companion's Node 22. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+127
-9
@@ -425,12 +425,12 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
||||
check(entityOptions.length > 40 && entityOptions.every((o) => (o.type === 'dropdown' || o.type === 'multidropdown') && o.minChoicesForSearch === 0 && o.choices.every((c) => typeof c.id === 'number')), `all ${entityOptions.length} entity options are searchable dropdowns with numeric ids`)
|
||||
const levels = all.flatMap((d) => d.options).filter((o) => o.id === 'level')
|
||||
const thresholds = all.flatMap((d) => d.options).filter((o) => o.id === 'threshold' && o.min === -150)
|
||||
check(levels.length === 6 && levels.every((o) => o.type === 'number' && o.showMinAsNegativeInfinity === true && (o.min === -150 || o.min === -90)) && thresholds.every((o) => o.showMinAsNegativeInfinity === true), `level options use showMinAsNegativeInfinity (${levels.length} level + ${thresholds.length} threshold options)`)
|
||||
check(levels.length === 8 && levels.every((o) => o.type === 'number' && o.showMinAsNegativeInfinity === true && (o.min === -150 || o.min === -90)) && thresholds.every((o) => o.showMinAsNegativeInfinity === true), `level options use showMinAsNegativeInfinity (${levels.length} level + ${thresholds.length} threshold options)`)
|
||||
check(ctx.actions.matrix_send_level_set.options[2].min === -90 && ctx.actions.matrix_send_level_set.options[2].showMinAsNegativeInfinity === true && ctx.actions.send_level_set.options[2].min === -150 && ctx.actions.input_fader_set.options[1].min === -150 && ctx.actions.cg_fader_set.options[1].min === -150, 'per-kind floor: a matrix send level goes down to -90, aux sends and faders to -150')
|
||||
const adjusts = Object.entries(ctx.actions).filter(([id]) => /(fader|send_level)_adjust$/.test(id))
|
||||
check(adjusts.length === 6 && adjusts.every(([, a]) => a.description.includes('From \u2212\u221e the first step up jumps to \u221260 dB')), `all ${adjusts.length} level adjust actions say that the first step up from -inf jumps to -60 dB`)
|
||||
check(adjusts.length === 8 && adjusts.every(([, a]) => a.description.includes('From \u2212\u221e the first step up jumps to \u221260 dB')), `all ${adjusts.length} level adjust actions say that the first step up from -inf jumps to -60 dB`)
|
||||
check(ctx.actions.input_mute.options[0].choices[0].label === '1 · KICK In' && ctx.actions.send_on.options[1].choices.every((c) => S.bussMode(c.id) === 'aux'), 'action dropdowns show the live labels; send actions list aux busses only')
|
||||
const idsExpected = ['input_mute', 'input_solo', 'input_fader_set', 'input_fader_adjust', 'input_pan_set', 'input_pan_adjust', 'input_name_set', 'send_on', 'send_level_set', 'send_level_adjust', 'send_pan_set', 'send_pan_adjust', 'group_assign', 'buss_mute', 'buss_solo', 'buss_fader_set', 'buss_fader_adjust', 'matrix_mute', 'matrix_solo', 'matrix_fader_set', 'matrix_fader_adjust', 'matrix_send_on', 'matrix_send_level_set', 'matrix_send_level_adjust', 'cg_mute', 'cg_solo', 'cg_fader_set', 'cg_fader_adjust', 'geq_band_set', 'geq_band_adjust', 'geq_on', 'snapshot_fire', 'snapshot_next', 'snapshot_prev', 'reload', 'raw_osc', 'relay_forget', 'fade_stop']
|
||||
const idsExpected = ['input_mute', 'input_solo', 'input_fader_set', 'input_fader_adjust', 'input_pan_set', 'input_pan_adjust', 'input_name_set', 'send_on', 'send_level_set', 'send_level_adjust', 'send_pan_set', 'send_pan_adjust', 'group_assign', 'buss_mute', 'buss_solo', 'buss_fader_set', 'buss_fader_adjust', 'matrix_mute', 'matrix_solo', 'matrix_fader_set', 'matrix_fader_adjust', 'matrix_send_on', 'matrix_send_level_set', 'matrix_send_level_adjust', 'cg_mute', 'cg_solo', 'cg_fader_set', 'cg_fader_adjust', 'geq_band_set', 'geq_band_adjust', 'geq_on', 'snapshot_fire', 'snapshot_next', 'snapshot_prev', 'reload', 'raw_osc', 'relay_forget', 'fade_stop', 'group_mute', 'group_solo', 'group_fader_set', 'group_fader_adjust', 'aux_mute', 'aux_solo', 'aux_fader_set', 'aux_fader_adjust']
|
||||
// per-channel switches and values, generated from the PARAMS table
|
||||
for (const [param, p] of Object.entries(PARAMS)) {
|
||||
for (const ent of Object.keys(p.paths)) idsExpected.push(...(p.type === 'switch' ? [`${ent}_${param}`] : [`${ent}_${param}_set`, `${ent}_${param}_adjust`]))
|
||||
@@ -438,6 +438,7 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
||||
check(idsExpected.every((id) => ctx.actions[id]) && Object.keys(ctx.actions).length === idsExpected.length, `all ${idsExpected.length} designed actions exist (${Object.keys(ctx.actions).length} registered)`)
|
||||
check(!('macro_press' in ctx.actions), 'no first-class macro action (the console never answered a macro query)')
|
||||
const fbExpected = ['input_muted', 'input_solo', 'send_on', 'group_assigned', 'buss_muted', 'buss_solo', 'matrix_muted', 'cg_muted', 'snapshot_current', 'level_compare', 'send_level_compare', 'connected', 'relay_clients', 'fade_running', 'input_signal', 'input_peak', 'input_fader_value', 'input_pan_value', 'send_level_value', 'send_on_value', 'buss_fader_value', 'matrix_fader_value', 'cg_fader_value', 'snapshot_name_value', 'input_meter_value', 'input_fader_bar', 'send_level_bar', 'buss_fader_bar', 'input_meter_bar']
|
||||
for (const k of ['group', 'aux']) fbExpected.push(`${k}_muted`, `${k}_solo`, `${k}_fader_value`, `${k}_fader_pos`, `${k}_meter_value`, `${k}_meter_pos`, `${k}_signal`, `${k}_peak`)
|
||||
fbExpected.push('buss_signal', 'buss_peak', 'buss_meter_value', 'buss_meter_pos', 'matrix_signal', 'matrix_peak', 'matrix_meter_value', 'matrix_meter_pos')
|
||||
fbExpected.push('input_fader_pos', 'buss_fader_pos', 'matrix_fader_pos', 'cg_fader_pos', 'send_level_pos', 'matrix_send_level_pos', 'input_pan_num', 'send_pan_num', 'geq_band_db', 'input_meter_pos')
|
||||
fbExpected.push('matrix_solo', 'cg_solo', 'matrix_send_on', 'matrix_send_off', 'geq_on', 'send_pan_value', 'matrix_send_level_value', 'geq_band_value', 'matrix_fader_bar', 'cg_fader_bar', 'matrix_send_level_bar', 'geq_band_bar')
|
||||
@@ -456,6 +457,8 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
||||
geq_on: ['geq_on'], geq_band_set: ['geq_band_value', 'geq_band_bar'], geq_band_adjust: ['geq_band_value', 'geq_band_bar'],
|
||||
snapshot_fire: ['snapshot_current', 'snapshot_name_value'], snapshot_next: ['snapshot_current', 'snapshot_name_value'], snapshot_prev: ['snapshot_current', 'snapshot_name_value'],
|
||||
reload: ['connected'], relay_forget: ['relay_clients'], fade_stop: ['fade_running'],
|
||||
group_mute: ['group_muted'], group_solo: ['group_solo'], group_fader_set: ['group_fader_value'], group_fader_adjust: ['group_fader_value'],
|
||||
aux_mute: ['aux_muted'], aux_solo: ['aux_solo'], aux_fader_set: ['aux_fader_value'], aux_fader_adjust: ['aux_fader_value'],
|
||||
input_name_set: [], // the name is the variable input_N_name
|
||||
raw_osc: [], // free-form
|
||||
}
|
||||
@@ -465,18 +468,18 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
||||
else PARITY[`${ent}_${param}_set`] = PARITY[`${ent}_${param}_adjust`] = [`${ent}_${param}_value`]
|
||||
}
|
||||
}
|
||||
const GAUGE = { input_fader: 'input_fader_pos', send_level: 'send_level_pos', buss_fader: 'buss_fader_pos', matrix_fader: 'matrix_fader_pos', matrix_send_level: 'matrix_send_level_pos', cg_fader: 'cg_fader_pos', input_pan: 'input_pan_num', send_pan: 'send_pan_num', geq_band: 'geq_band_db' }
|
||||
const GAUGE = { input_fader: 'input_fader_pos', send_level: 'send_level_pos', buss_fader: 'buss_fader_pos', group_fader: 'group_fader_pos', aux_fader: 'aux_fader_pos', matrix_fader: 'matrix_fader_pos', matrix_send_level: 'matrix_send_level_pos', cg_fader: 'cg_fader_pos', input_pan: 'input_pan_num', send_pan: 'send_pan_num', geq_band: 'geq_band_db' }
|
||||
for (const id of Object.keys(PARITY)) {
|
||||
const g = GAUGE[id.replace(/_(set|adjust)$/, '')]
|
||||
if (g && /_(set|adjust)$/.test(id)) PARITY[id] = [...PARITY[id], g]
|
||||
}
|
||||
const noGauge = Object.keys(ctx.actions).filter((id) => /^(input_fader|send_level|buss_fader|matrix_fader|matrix_send_level|cg_fader|input_pan|send_pan|geq_band)_(set|adjust)$/.test(id) && !(PARITY[id] || []).some((f) => /_(pos|num|db)$/.test(f)))
|
||||
const noGauge = Object.keys(ctx.actions).filter((id) => /^(input_fader|send_level|buss_fader|group_fader|aux_fader|matrix_fader|matrix_send_level|cg_fader|input_pan|send_pan|geq_band)_(set|adjust)$/.test(id) && !(PARITY[id] || []).some((f) => /_(pos|num|db)$/.test(f)))
|
||||
check(noGauge.length === 0 && Object.values(GAUGE).every((f) => ctx.feedbacks[f]?.type === 'value'), `every level and pan action has a numeric value feedback for gauges (${Object.values(GAUGE).join(', ')})`)
|
||||
const noFeedback = Object.keys(ctx.actions).filter((id) => !(id in PARITY))
|
||||
const missing = Object.entries(PARITY).filter(([id, fbs]) => id in ctx.actions && fbs.some((f) => !ctx.feedbacks[f])).map(([id]) => id)
|
||||
const levelActions = Object.keys(ctx.actions).filter((id) => /_(set|adjust)$/.test(id) && /(fader|level|pan|band|trim|gain|freq)/.test(id))
|
||||
const lacksValue = levelActions.filter((id) => !(PARITY[id] || []).some((f) => /_value$/.test(f)))
|
||||
const lacksBar = levelActions.filter((id) => /(fader|send_level)/.test(id) && !(PARITY[id] || []).some((f) => /_bar$/.test(f)))
|
||||
const lacksBar = levelActions.filter((id) => /(fader|send_level)/.test(id) && !/^(group|aux)_/.test(id) && !(PARITY[id] || []).some((f) => /_bar$/.test(f)))
|
||||
check(noFeedback.length === 0 && missing.length === 0 && lacksValue.length === 0 && lacksBar.length === 0, `every action has its feedback (${Object.keys(ctx.actions).length} actions; no feedback listed: ${noFeedback.join(', ') || 'none'}; unknown feedback: ${missing.join(', ') || 'none'}; level without value: ${lacksValue.join(', ') || 'none'}; fader/send level without bar: ${lacksBar.join(', ') || 'none'})`)
|
||||
check(Object.values(ctx.feedbacks).filter((f) => f.type === 'advanced').every((f) => Array.isArray(f.affectedProperties)), 'advanced feedbacks declare affectedProperties')
|
||||
}
|
||||
@@ -529,7 +532,7 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
||||
const knobs = ids.filter((id) => /_knob_/.test(id))
|
||||
check(knobs.length > 100 && knobs.every((id) => P[id].steps[0].rotate_left?.length && P[id].steps[0].rotate_right?.length), `all ${knobs.length} knob presets have rotate_left and rotate_right`)
|
||||
const sections = ctx.presetStructure.map((s) => s.id)
|
||||
check(['inputs', 'busses', 'cgs', 'matrix', 'snapshots', 'console', 'meters', 'geqs'].every((s) => sections.includes(s)) && sections.some((s) => s.startsWith('sends_')) && sections.some((s) => s.startsWith('group_')) && sections.some((s) => s.startsWith('msends_')), `preset sections cover inputs, busses, sends, groups, matrix, matrix sends, graphic EQs, control groups, snapshots, meters, console (${sections.length} sections)`)
|
||||
check(['inputs', 'groups', 'aux_masters', 'cgs', 'matrix', 'snapshots', 'console', 'meters', 'geqs'].every((s) => sections.includes(s)) && sections.some((s) => s.startsWith('sends_')) && sections.some((s) => s.startsWith('group_')) && sections.some((s) => s.startsWith('msends_')), `preset sections cover inputs, busses, sends, groups, matrix, matrix sends, graphic EQs, control groups, snapshots, meters, console (${sections.length} sections)`)
|
||||
// matrix sends: one section per matrix output, per matrix input an on/off button and a knob
|
||||
const msendSections = ctx.presetStructure.filter((x) => x.id.startsWith('msends_'))
|
||||
const msendOn = ids.filter((id) => id.startsWith('msend_on_'))
|
||||
@@ -553,7 +556,7 @@ check('input_60_pan' in ctx.variableDefs && 'cg_10_mute' in ctx.variableDefs &&
|
||||
const layered = ids.filter((id) => P[id].type === 'layered')
|
||||
const knobsLayered = layered.filter((id) => /_knob_/.test(id))
|
||||
const meterPresets = layered.filter((id) => /^input_(meter|gate_gr|comp_gr)_/.test(id))
|
||||
check(knobsLayered.length === ids.filter((id) => /_knob_/.test(id)).length && meterPresets.length === 180 && layered.filter((id) => /^(buss|matrix)_meter_/.test(id)).length === 32 && layered.length === knobsLayered.length + 180 + 32 && !ids.some((id) => /_(mute|solo|on)_/.test(id) && P[id].type === 'layered'), `every knob (${knobsLayered.length}) and meter (${meterPresets.length} input + 32 buss / matrix output) preset is layered; plain toggles stay simple`)
|
||||
check(knobsLayered.length === ids.filter((id) => /_knob_/.test(id)).length && meterPresets.length === 180 && layered.filter((id) => /^(group|aux|matrix)_meter_/.test(id)).length === 32 && layered.length === knobsLayered.length + 180 + 32 && !ids.some((id) => /_(mute|solo|on)_/.test(id) && P[id].type === 'layered'), `every knob (${knobsLayered.length}) and meter (${meterPresets.length} input + 32 buss / matrix output) preset is layered; plain toggles stay simple`)
|
||||
const ELEMENT_TYPES = new Set(['group', 'composite', 'text', 'image', 'box', 'line', 'circle', 'gauge'])
|
||||
const LAYERED_PROPS = { box: ['id', 'type', 'x', 'y', 'width', 'height', 'color'], text: ['id', 'type', 'x', 'y', 'width', 'height', 'text', 'fontsize', 'fontsizeAllowShrink', 'color', 'halign', 'valign'], gauge: ['id', 'type', 'x', 'y', 'width', 'height', 'value', 'min', 'max', 'orientation', 'trackStyle', 'fillEnabled', 'multiColour', 'stops', 'trackAmount'] }
|
||||
const isExpr = (v) => !!v && typeof v === 'object' && v.isExpression === true && typeof v.value === 'string'
|
||||
@@ -1527,9 +1530,12 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
||||
await sleep(inst.timing.rebuildDebounceMs + 400)
|
||||
const label3 = ctx.actions.buss_mute.options[0].choices.find((c) => c.id === 3)?.label
|
||||
check(wasAux && label3 === '3 · Group 3 (group)' && ctx.pushes.actions === JSON.parse(p0).actions + 1 && ctx.pushes.presets === JSON.parse(p0).presets + 1, `a changed buss type rebuilds and pushes once (${label3})`)
|
||||
const optIds = (id, o) => ctx.actions[id].options.find((x) => x.id === o).choices.map((c) => c.id)
|
||||
check(optIds('group_fader_set', 'buss').includes(3) && !optIds('aux_fader_set', 'buss').includes(3) && optIds('group_mute', 'busses').includes(3) && !optIds('aux_mute', 'busses').includes(3) && ctx.actions.group_fader_set.options[0].choices.find((c) => c.id === 3).label === '3 · Group 3' && ctx.feedbacks.group_muted.options[0].choices.some((c) => c.id === 3) && !ctx.feedbacks.aux_muted.options[0].choices.some((c) => c.id === 3), 'the buss moved from the aux dropdowns to the group dropdowns (actions and feedbacks)')
|
||||
check(!ctx.presets.aux_mute_3 && !ctx.presets.aux_knob_3 && !ctx.presets.aux_meter_3 && ctx.presetStructure.find((x) => x.id === 'aux_masters').definitions[0].presets.length === 14, 'and its aux master presets are gone from "Aux masters" (it has no group-side name, so no group presets either)')
|
||||
inst.onConsoleMessage(decode(encode('/Console/Aux_Outputs/types', types.map((i) => ({ i })))), { address: '127.0.0.1', port: P.mock })
|
||||
await sleep(inst.timing.rebuildDebounceMs + 400)
|
||||
check(ctx.actions.buss_mute.options[0].choices.find((c) => c.id === 3)?.label === '3 · TRACKS (aux)', 'and back again')
|
||||
check(ctx.actions.buss_mute.options[0].choices.find((c) => c.id === 3)?.label === '3 · TRACKS (aux)' && optIds('aux_fader_set', 'buss').includes(3) && !optIds('group_fader_set', 'buss').includes(3) && !!ctx.presets.aux_mute_3 && ctx.presetStructure.find((x) => x.id === 'aux_masters').definitions[0].presets.includes('aux_mute_3'), 'and back again (dropdowns and the "Aux masters" presets)')
|
||||
|
||||
// a changed snapshot list rebuilds
|
||||
const p1 = ctx.pushes.actions
|
||||
@@ -1618,6 +1624,22 @@ const cb = (feedbackId, options, image) => ctx.feedbacks[feedbackId].callback(fb
|
||||
run1(lc)
|
||||
check(inst.watchers.get('cg:2')?.has('inst-L'), 'level_compare registers under the entity kind it compares')
|
||||
ctx.feedbacks.level_compare.unsubscribe(lc, {})
|
||||
// the group and aux master feedbacks watch the buss's own key, like the buss_* ones
|
||||
const bm = mk('buss_muted', { buss: 24 }, 'inst-BM')
|
||||
const gm = mk('group_muted', { buss: 24 }, 'inst-GM')
|
||||
const gf = mk('group_fader_pos', { buss: 24 }, 'inst-GF')
|
||||
const gx = mk('group_fader_value', { buss: 25 }, 'inst-GX')
|
||||
const ax = mk('aux_fader_value', { buss: 3 }, 'inst-AX')
|
||||
const gp = mk('group_signal', { buss: 24, thresholdDb: -50 }, 'inst-GP')
|
||||
for (const f of [bm, gm, gf, gx, ax, gp]) run1(f)
|
||||
check(['inst-BM', 'inst-GM', 'inst-GF'].every((id) => inst.watchers.get('buss:24')?.has(id)) && inst.watchers.get('buss:25')?.has('inst-GX') && inst.watchers.get('buss:3')?.has('inst-AX') && inst.watchers.get(`meter:${outputMeterId('buss', 24)}`)?.has('inst-GP'), 'group_* and aux_* feedbacks register under the buss key (meters under the buss meter id)')
|
||||
await sleep(150)
|
||||
ctx.byId.length = 0
|
||||
inst.onConsoleMessage(decode(encode('/Group_Outputs/24/fader', [-12.25])), { address: '127.0.0.1', port: P.mock })
|
||||
await sleep(80)
|
||||
const idsG = ctx.byId.flatMap(([, x]) => x)
|
||||
check(idsG.includes('inst-BM') && idsG.includes('inst-GM') && idsG.includes('inst-GF') && !idsG.includes('inst-GX') && !idsG.includes('inst-AX') && !ctx.checks.some(([, t]) => /^(group|aux|buss)_/.test(String(t))), 'a group fader change re-checks only the instances watching that buss, by id and with no per-type check')
|
||||
for (const f of [bm, gm, gf, gx, ax, gp]) ctx.feedbacks[f.feedbackId].unsubscribe(f, {})
|
||||
|
||||
// every option referenced by an isVisibleExpression must not become an expression
|
||||
const bad = []
|
||||
@@ -1873,6 +1895,102 @@ const idle = () => waitFor(() => inst.fades.size === 0, 3000)
|
||||
}
|
||||
check(inst.fades.size === 0 && inst.fadeStarts.size === 0, 'no fade or fade start is left over after the fade tests')
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 16c. Group and aux master actions, feedbacks and presets
|
||||
// ---------------------------------------------------------------------------
|
||||
{
|
||||
const GROUPS = [9, 10, 11, 12, 13, 18, 23, 24, 25]
|
||||
const allIds = S.choices.busses().map((c) => c.id)
|
||||
const AUXES = allIds.filter((id) => !GROUPS.includes(id))
|
||||
const ids = (opts, id) => opts.find((o) => o.id === id).choices.map((c) => c.id)
|
||||
const labels = (opts, id) => opts.find((o) => o.id === id).choices.map((c) => c.label)
|
||||
check(S.choices.groupChoices().map((c) => c.id).join() === GROUPS.join() && AUXES.length === 16, `the mock has ${GROUPS.length} group busses and ${AUXES.length} aux busses`)
|
||||
let ok = true
|
||||
for (const [prefix, want] of [['group', GROUPS], ['aux', AUXES]]) {
|
||||
for (const a of ['fader_set', 'fader_adjust']) ok = ok && ids(ctx.actions[`${prefix}_${a}`].options, 'buss').join() === want.join()
|
||||
for (const a of ['mute', 'solo']) ok = ok && ids(ctx.actions[`${prefix}_${a}`].options, 'busses').join() === want.join() && ctx.actions[`${prefix}_${a}`].options[0].type === 'multidropdown'
|
||||
for (const f of ['muted', 'solo', 'fader_value', 'fader_pos', 'meter_value', 'meter_pos', 'signal', 'peak']) ok = ok && ids(ctx.feedbacks[`${prefix}_${f}`].options, 'buss').join() === want.join()
|
||||
}
|
||||
check(ok, 'every group_* dropdown (actions and feedbacks) lists exactly the nine group busses, every aux_* one exactly the sixteen aux busses')
|
||||
const gl = labels(ctx.actions.group_fader_set.options, 'buss')
|
||||
const al = labels(ctx.actions.aux_fader_set.options, 'buss')
|
||||
check(gl.includes('24 · Studio') && al.includes('4 · CLK-GD-MD') && !gl.concat(al, labels(ctx.actions.group_mute.options, 'busses'), labels(ctx.actions.aux_mute.options, 'busses')).some((l) => /\((aux|group)\)/.test(l)), 'their labels carry no "(group)" / "(aux)" suffix')
|
||||
check(ids(ctx.actions.buss_fader_set.options, 'buss').length === 25 && labels(ctx.actions.buss_fader_set.options, 'buss').includes('24 · Studio (group)') && ids(ctx.actions.buss_mute.options, 'busses').length === 25, 'the buss_* dropdowns still list all 25 busses with the suffix')
|
||||
check(ctx.actions.group_fader_set.options.map((o) => o.id).join() === 'buss,level,duration,curve' && ctx.actions.aux_fader_set.options.map((o) => o.id).join() === 'buss,level,duration,curve' && ctx.actions.group_fader_adjust.options.map((o) => o.id).join() === 'buss,delta' && ctx.actions.aux_mute.options.map((o) => o.id).join() === 'busses,state', 'option ids are the same as the buss_* ones (fade options on the set actions)')
|
||||
check(ctx.actions.group_mute.name === 'Group: mute' && ctx.actions.group_solo.name === 'Group: solo' && ctx.actions.group_fader_set.name === 'Group: set fader' && ctx.actions.group_fader_adjust.name === 'Group: adjust fader (relative)' && ctx.actions.aux_mute.name === 'Aux master: mute' && ctx.actions.aux_solo.name === 'Aux master: solo' && ctx.actions.aux_fader_set.name === 'Aux master: set fader' && ctx.actions.aux_fader_adjust.name === 'Aux master: adjust fader (relative)' && ctx.actions.group_assign.name === 'Group: assign input on/off', 'action names: "Group: ..." and "Aux master: ..." (group_assign keeps its name)')
|
||||
check(/fade time above 0/.test(ctx.actions.group_fader_set.description) && /fade time above 0/.test(ctx.actions.aux_fader_set.description), 'the two set actions describe the fade')
|
||||
// the old ids stay, with names that sort apart
|
||||
const oldActions = ['buss_mute', 'buss_solo', 'buss_fader_set', 'buss_fader_adjust']
|
||||
const oldFeedbacks = ['buss_muted', 'buss_solo', 'buss_fader_value', 'buss_fader_pos', 'buss_fader_bar', 'buss_signal', 'buss_peak', 'buss_meter_value', 'buss_meter_pos']
|
||||
check(oldActions.every((id) => ctx.actions[id]?.name.startsWith('Any buss (aux or group): ')) && oldFeedbacks.every((id) => ctx.feedbacks[id]?.name.startsWith('Any buss (aux or group)')), `the old buss_* ids still exist and their names start with "Any buss (aux or group)" (${oldActions.length} actions, ${oldFeedbacks.length} feedbacks)`)
|
||||
check(ctx.feedbacks.group_muted.name === 'Group: muted' && ctx.feedbacks.aux_fader_pos.name === 'Aux master: fader position (0-100, for gauges)' && ctx.feedbacks.group_peak.name.startsWith('Group meter: ') && ctx.feedbacks.aux_signal.name.startsWith('Aux master meter: ') && !('group_fader_bar' in ctx.feedbacks) && !('aux_fader_bar' in ctx.feedbacks), 'feedback names, and no legacy image-bar variants for group / aux masters')
|
||||
|
||||
// writes: the group side for a group buss, the aux side for an aux
|
||||
let r = await run('group_fader_set', { buss: 24, level: -7 })
|
||||
expectWrite(r, '/Group_Outputs/24/fader', ',f', -7, 'group_fader_set')
|
||||
r = await run('aux_fader_set', { buss: 1, level: -9 })
|
||||
expectWrite(r, '/Aux_Outputs/1/fader', ',f', -9, 'aux_fader_set')
|
||||
r = await run('group_mute', { busses: [9, 24], state: 'on' })
|
||||
expectWrite(r, '/Group_Outputs/9/mute', ',f', 1, 'group_mute (first)')
|
||||
expectWrite(r, '/Group_Outputs/24/mute', ',f', 1, 'group_mute (second)')
|
||||
r = await run('aux_mute', { busses: [3, 4], state: 'on' })
|
||||
expectWrite(r, '/Aux_Outputs/3/mute', ',f', 1, 'aux_mute (first)')
|
||||
expectWrite(r, '/Aux_Outputs/4/mute', ',f', 1, 'aux_mute (second)')
|
||||
r = await run('group_solo', { busses: [24], state: 'on' })
|
||||
expectWrite(r, '/Group_Outputs/24/solo', ',f', 1, 'group_solo')
|
||||
r = await run('aux_solo', { busses: [3], state: 'off' })
|
||||
expectWrite(r, '/Aux_Outputs/3/solo', ',f', 0, 'aux_solo off')
|
||||
r = await run('group_mute', { busses: [9], state: 'toggle' })
|
||||
expectWrite(r, '/Group_Outputs/9/mute', ',f', 0, 'group_mute toggle (from muted)')
|
||||
await run('group_fader_set', { buss: 23, level: -20 })
|
||||
r = await run('group_fader_adjust', { buss: 23, delta: 3 })
|
||||
expectWrite(r, '/Group_Outputs/23/fader', ',f', -17, 'group_fader_adjust')
|
||||
await run('aux_fader_set', { buss: 2, level: -20 })
|
||||
r = await run('aux_fader_adjust', { buss: 2, delta: -2.5 })
|
||||
expectWrite(r, '/Aux_Outputs/2/fader', ',f', -22.5, 'aux_fader_adjust')
|
||||
// feedbacks read the same state as the buss_* ones
|
||||
check(cb('group_muted', { buss: 24 }) === true && cb('buss_muted', { buss: 24 }) === true && cb('aux_muted', { buss: 3 }) === true && cb('aux_muted', { buss: 1 }) === false && cb('group_solo', { buss: 24 }) === true && cb('aux_solo', { buss: 3 }) === false && cb('group_fader_value', { buss: 24 }) === '-7' && cb('aux_fader_value', { buss: 1 }) === '-9' && near(cb('group_fader_pos', { buss: 24 }), cb('buss_fader_pos', { buss: 24 })) && near(cb('aux_fader_pos', { buss: 2 }), faderPosition(-22.5)), 'group_* / aux_* feedbacks follow the buss state (muted, solo, fader text and position)')
|
||||
|
||||
// fades through the same engine
|
||||
const mark = sent.length
|
||||
await go('group_fader_set', { buss: 25, level: -40 })
|
||||
const m2 = sent.length
|
||||
await go('group_fader_set', { buss: 25, level: -10, duration: 300, curve: 'db' })
|
||||
check(inst.fades.size === 1, 'a group fader fade is running')
|
||||
check(await idle(), 'the group fade finishes')
|
||||
const gw = writesTo('/Group_Outputs/25/fader', m2).map((x) => x.db)
|
||||
check(gw.length === 6 && gw.at(-1) === -10 && gw.every((v, k) => near(v, -40 + 5 * (k + 1), 0.01)), `group_fader_set fades on /Group_Outputs/25/fader (${gw.map((v) => Math.round(v * 100) / 100).join(', ')})`)
|
||||
await go('aux_fader_set', { buss: 5, level: -40 })
|
||||
const m3 = sent.length
|
||||
await go('aux_fader_set', { buss: 5, level: -10, duration: 200, curve: 'db' })
|
||||
check(await idle(), 'the aux master fade finishes')
|
||||
const aw = writesTo('/Aux_Outputs/5/fader', m3).map((x) => x.db)
|
||||
check(aw.length === 4 && aw.at(-1) === -10 && !sent.slice(m3).some((m) => m.address.startsWith('/Group_Outputs/5/')), `aux_fader_set fades on /Aux_Outputs/5/fader (${aw.map((v) => Math.round(v * 100) / 100).join(', ')})`)
|
||||
void mark
|
||||
|
||||
// presets: Groups and Aux masters sections, group / aux meter sections, no "Busses" section
|
||||
const P2 = ctx.presets
|
||||
const secs = ctx.presetStructure
|
||||
const sec = (id) => secs.find((x) => x.id === id)
|
||||
const idsOf = (section) => section.definitions.flatMap((d) => d.presets)
|
||||
const namedGroups = S.namedBusses('group')
|
||||
const namedAux = S.namedBusses('aux')
|
||||
check(namedGroups.join() === GROUPS.join() && namedAux.length === 15 && sec('groups')?.name === 'Groups' && sec('aux_masters')?.name === 'Aux masters' && !sec('busses') && !sec('buss_meters'), `sections "Groups" (${namedGroups.length} named group busses) and "Aux masters" (${namedAux.length} named aux busses) replace "Busses"`)
|
||||
check(idsOf(sec('groups')).length === 2 * namedGroups.length && idsOf(sec('aux_masters')).length === 2 * namedAux.length && sec('groups').definitions.map((d) => d.name).join() === 'Mute,Fader (knob)' && sec('aux_masters').definitions.map((d) => d.name).join() === 'Mute,Fader (knob)', 'each has a Mute and a Fader (knob) preset per named buss of that type')
|
||||
const gMute = P2.group_mute_24
|
||||
const gKnob = P2.group_knob_24
|
||||
const aMute = P2.aux_mute_4
|
||||
const aKnob = P2.aux_knob_4
|
||||
check(gMute.name === 'Group mute: 24 · Studio' && gMute.steps[0].down[0].actionId === 'group_mute' && JSON.stringify(gMute.steps[0].down[0].options) === JSON.stringify({ busses: [24], state: 'toggle' }) && gMute.feedbacks[0].feedbackId === 'group_muted' && gMute.style.text === '$(S31:buss_24_name)', 'group mute preset uses group_mute / group_muted')
|
||||
check(gKnob.type === 'layered' && gKnob.name === 'Group fader (knob): 24 · Studio' && gKnob.steps[0].rotate_right[0].actionId === 'group_fader_adjust' && gKnob.steps[0].down[0].actionId === 'group_mute' && gKnob.feedbacks[0].feedbackId === 'group_muted' && gKnob.localVariables.map((v) => `${v.variableName}=${v.feedbackId}`).join() === 'level=group_fader_value,pos=group_fader_pos', 'group fader knob preset uses group_fader_adjust / group_fader_value / group_fader_pos / group_muted')
|
||||
check(aMute.name === 'Aux master mute: 4 · CLK-GD-MD' && aMute.steps[0].down[0].actionId === 'aux_mute' && aMute.feedbacks[0].feedbackId === 'aux_muted' && aKnob.steps[0].rotate_left[0].actionId === 'aux_fader_adjust' && aKnob.localVariables.map((v) => v.feedbackId).join() === 'aux_fader_value,aux_fader_pos' && aKnob.feedbacks[0].feedbackId === 'aux_muted', 'aux master mute and knob presets use aux_*')
|
||||
check(!Object.keys(P2).some((id) => /^buss_(mute|knob|meter)_/.test(id)) && !JSON.stringify(P2).includes('"buss_') , 'no preset refers to a buss_* action or feedback any more')
|
||||
const gMeters = sec('meters_groups')
|
||||
const aMeters = sec('meters_aux')
|
||||
check(gMeters?.name === 'Group meters' && aMeters?.name === 'Aux meters' && idsOf(gMeters).length === 9 && idsOf(aMeters).length === 15 && P2.group_meter_24.localVariables.some((v) => v.feedbackId === 'group_meter_pos') && P2.group_meter_24.feedbacks.map((f) => f.feedbackId).join() === 'group_signal,group_peak' && P2.aux_meter_4.feedbacks.map((f) => f.feedbackId).join() === 'aux_signal,aux_peak' && !sec('buss_meters'), 'group meter and aux meter sections (9 and 15 meters) use group_* / aux_* meter feedbacks')
|
||||
check(idsOf(gMeters).concat(idsOf(aMeters)).every((id) => P2[id].type === 'layered'), 'the group and aux meter presets are layered gauges')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 17. A slow console (every answer and echo 150 ms late): bind failures, encoder clicks, lazy loads
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user