THX, I used this with chatGPT and this works:
-- Anti-jitter retrofit for standard NRPN/CC based presets
-- v4: restore robust per-step echo protection for continuous controls,
-- but with much shorter timing than v2 so encoders feel smoother.
--
-- Requirement: firmware 4.1.0 or newer (parameterMap.updateValue)
assert(
controller.isRequired(controller.getNumModel(), "4.1.0"),
“Anti-jitter Lua requires firmware 4.1.0 or newer”
)
local touchedParams = {}
local lastInternalValues = {}
local suppressUntil = {}
local paramModes = {}
local MODE_CONTINUOUS = 1
local MODE_DISCRETE = 2
-- Continuous controls:
-- Short rolling guard refreshed on each INTERNAL move.
-- This keeps live encoder motion protected from immediate DAW echo,
-- without the heavy/sticky feel of the old 120 ms window.
local CONTINUOUS_STEP_GUARD_MS = 24
local CONTINUOUS_RELEASE_GUARD_MS = 32
-- Discrete controls:
-- Keep these stricter because feel is less important than stability.
local DISCRETE_STEP_GUARD_MS = 120
local DISCRETE_RELEASE_GUARD_MS = 120
-- Bass Station preset specific discrete controls
local DISCRETE_CONTROL_REFS = {
[11] = true, – OSC1 Waveform
[19] = true, – Sub Waveform
[25] = true, – Range
[26] = true, – OSC2 Waveform
[41] = true, – OSC1 PWM
[42] = true, – OSC2 PWM
[17] = true, – LFO Retrigger
[18] = true, – OSC1 LFO Sync
[29] = true, – OSC1 Oct
[30] = true, – OSC2 Sync
[52] = true, – 12/24db
[77] = true, – MONO
[78] = true, – POLY
[79] = true, – LEGATO
[80] = true, – UNISON
[89] = true, – DIST ON/OFF
[90] = true, – CHORUS ON/OFF
[91] = true, – DELAY ON/OFF
[92] = true – REVERB ON/OFF
}
local function nowMs()
return controller.uptime()
end
local function paramKey(deviceId, paramType, parameterNumber)
return string.format(“%d:%d:%d”, deviceId, paramType, parameterNumber)
end
local function getMessageKey(message)
return paramKey(
message:getDeviceId(),
message:getType(),
message:getParameterNumber()
)
end
local function getControlMode(controlId)
if DISCRETE_CONTROL_REFS[controlId] then
return MODE_DISCRETE
end
return MODE_CONTINUOUS
end
local function getStepGuardMs(mode)
if mode == MODE_DISCRETE then
return DISCRETE_STEP_GUARD_MS
end
return CONTINUOUS_STEP_GUARD_MS
end
local function getReleaseGuardMs(mode)
if mode == MODE_DISCRETE then
return DISCRETE_RELEASE_GUARD_MS
end
return CONTINUOUS_RELEASE_GUARD_MS
end
local function rememberCurrentValue(message, key)
local currentValue = parameterMap.get(
message:getDeviceId(),
message:getType(),
message:getParameterNumber()
)
if currentValue ~= nil then
lastInternalValues\[key\] = currentValue
end
end
local function markControlParameters(controlId, isTouched)
if controlId == nil or controlId == 0 then
return
end
local control = controls.get(controlId)
if control == nil then
return
end
local values = control:getValues()
if values == nil then
return
end
local mode = getControlMode(controlId)
local stepGuardMs = getStepGuardMs(mode)
local releaseGuardMs = getReleaseGuardMs(mode)
for _, valueObject in ipairs(values) do
local message = valueObject:getMessage()
if message ~= nil then
local key = getMessageKey(message)
paramModes\[key\] = mode
if isTouched then
touchedParams\[key\] = true
suppressUntil\[key\] = nowMs() + stepGuardMs
rememberCurrentValue(message, key)
else
touchedParams\[key\] = nil
suppressUntil\[key\] = nowMs() + releaseGuardMs
end
end
end
end
events.subscribe(POTS)
function events.onPotTouch(potId, controlId, touched)
markControlParameters(controlId, touched)
end
function parameterMap.onChange(valueObjects, origin, midiValue)
if valueObjects == nil or #valueObjects == 0 then
return
end
local valueObject = valueObjects[1]
local message = valueObject:getMessage()
if message == nil then
return
end
local deviceId = message:getDeviceId()
local paramType = message:getType()
local parameterNumber = message:getParameterNumber()
local key = paramKey(deviceId, paramType, parameterNumber)
local mode = paramModes[key] or MODE_CONTINUOUS
if origin == INTERNAL then
lastInternalValues\[key\] = midiValue
suppressUntil\[key\] = nowMs() + getStepGuardMs(mode)
return
end
if origin == MIDI then
local stillProtected = touchedParams[key] or ((suppressUntil[key] or 0) >= nowMs())
if stillProtected then
local restoreValue = lastInternalValues[key]
if restoreValue ~= nil and parameterMap.updateValue ~= nil then
parameterMap.updateValue(deviceId, paramType, parameterNumber, restoreValue)
end
end
return
end
end