I've updated the lua script with a scale value that brings the knobs to roughly the same feel when used in absolute mode (applied generically, so stepped and non-stepped parameters have the same feeling). It also adds supports all buttons from the sides and push encoders and improves support for value ranges higher than 127:
Code: Select all
-- MIDI Fighter Twister Remote Codec
-- with custom implementation for relative knobs
-- const variables --
--_________________--
-- the amount of knobs
KNOBS_COUNT = 64
-- the amount of knobs that are also a button
KNOB_BUTTONS_COUNT = 64
KNOB_BUTTONS_NOTE_START = 16
-- the minimum value of a knob
KNOB_MIN = 0
-- the maximum value of a knob
KNOB_MAX = 127
-- how much a single delta value is scaled (basically a speed parameter)
KNOB_SCALE = 0.1
-- in delta mode, the center/neutral value (which we never receive because it's a rotation of nothing/zero)
KNOB_DELTA_BASE = 64
-- data structures --
--_________________--
-- current parameter value from Reason (Rack devices, Mixer, ...)
reason_param_values = {}
-- previously processed parameter value from Reason (Rack devices, Mixer, ...)
reason_param_values_previous = {}
-- the accumulated delta of a physical control knob/encoder on top of its assigned reason_param_value
controller_accumulated_deltas = {}
-- Reason Remote Codec implementations --
-- ____________________________________--
function remote_init(manufacturer, model)
local items = {}
local inputs = {}
local outputs = {}
for cc = 0, KNOBS_COUNT - 1 do
local name = string.format("CC %02d", cc)
table.insert(items, {name = name, input = "value", output = "value", min = KNOB_MIN, max = KNOB_MAX})
table.insert(inputs, {pattern = string.format("b0 %02x xx", cc), name = name})
reason_param_values[cc + 1] = -1
controller_accumulated_deltas[cc + 1] = 0
end
for push_encoder_index = 0, KNOB_BUTTONS_COUNT - 1 do
local name = string.format("Push Encoder %02d", push_encoder_index)
table.insert(items, { name = name, input = "button" })
table.insert(inputs, { pattern = string.format("91 %02x ?<???x>", push_encoder_index + KNOB_BUTTONS_NOTE_START), name = name })
end
table.insert(items, { name = "Left Button 1", input = "button" })
table.insert(inputs, { pattern = string.format("93 08 ?<???x>"), name = "Left Button 1" })
table.insert(items, { name = "Left Button 2", input = "button" })
table.insert(inputs, { pattern = string.format("93 09 ?<???x>"), name = "Left Button 2" })
table.insert(items, { name = "Left Button 3", input = "button" })
table.insert(inputs, { pattern = string.format("93 0a ?<???x>"), name = "Left Button 3" })
table.insert(items, { name = "Right Button 1", input = "button" })
table.insert(inputs, { pattern = string.format("93 0b ?<???x>"), name = "Right Button 1" })
table.insert(items, { name = "Right Button 2", input = "button" })
table.insert(inputs, { pattern = string.format("93 0c ?<???x>"), name = "Right Button 2" })
table.insert(items, { name = "Right Button 3", input = "button" })
table.insert(inputs, { pattern = string.format("93 0d ?<???x>"), name = "Right Button 3" })
remote.define_items(items)
remote.define_auto_inputs(inputs)
remote.define_auto_outputs(outputs)
end
function remote_probe()
return {
}
end
function remote_prepare_for_use()
local retEvents={
}
return retEvents
end
function remote_release_from_use()
local retEvents={
}
return retEvents
end
function remote_process_midi(event)
for cc = 0, KNOBS_COUNT - 1 do
local match = remote.match_midi(string.format("b0 %02x xx", cc), event)
if match then
local index = cc + 1
local current_value= remote.get_item_value(index)
local current_delta = (KNOB_DELTA_BASE - match.x) * -1 * KNOB_SCALE
local new_value = controller_accumulated_deltas[index] + current_delta + current_value
new_value = math.max(KNOB_MIN, math.min(KNOB_MAX, new_value))
controller_accumulated_deltas[index] = new_value - current_value
remote.handle_input({
time_stamp = event.time_stamp,
item = index,
value = new_value
})
return true
end
end
return false
end
function remote_set_state(changed_items)
for _, index in ipairs(changed_items) do
reason_param_values[index] = remote.get_item_value(index)
end
end
function remote_deliver_midi()
local events = {}
for index = 1, KNOBS_COUNT do
local new_value = reason_param_values[index]
local old_value = reason_param_values_previous[index]
if new_value == old_value then
-- nothing to do yet, but apply range limit to delta
local current_delta = controller_accumulated_deltas[index]
if (new_value + current_delta < 0) or (new_value + current_delta > 127) then
controller_accumulated_deltas[index] = 0
end
else
controller_accumulated_deltas[index] = 0
local midi = remote.make_midi(string.format("b0 %02x %02x", index - 1, new_value/KNOB_MAX * 127))
table.insert(events, midi)
reason_param_values_previous[index] = new_value
end
end
return events
end
Another idea would be to declare the knobs as delta controls but handle the moment you create the MIDI message for the next step in a way that is more suitable for the MF Twister. But for this to work, I'll need the value range of the current remotable item. Is that possible somehow? Didn't find anything in the Remote Coded SDK manual about this.
electrofux wrote: ↑04 Apr 2025
Your issue seem to be definitly related to the high resolution of the Twister (which is good on the one hand as i exactly want to buy one for that high resolution.). On my Pocket Dial this is a non issue because of the rasterization and on my Akai Mini i think i use the encoders only for continouus parameters and use its buttons for stepped parameters or its resolution is low enough to not pose a problem that i notice.
The issue is that regardless of how you define the scaling of the remote items eg the Malstrom Filter Mode (which i have just checked) allways sends 0-4. I was under the misconception that it send eg 0,25,50,75,100,127 IF the item was set to 0-127. So this IS an issue for very high resolution encoders.
Exactly!
electrofux wrote: ↑04 Apr 2025
I dont exactly understand your code but doesnt it also affect how the Twister behaves on normal continuous parameters like it is then turning slower?
Well, yes and no. The twister has three sensitivity profiles and I'm using the velocity sensitive profile. This means that the faster I turn a knob, the more the value will deviate from 64 and so the quicker the Remotable Item from Reason reaches the min/max value. This is however not due to my custom code but is also true when using Reason's default delta implementation.
electrofux wrote: ↑04 Apr 2025
From the top of my head ( and as a last resort if the scaling cant be fixed automatically) i would add a scaling factor to the mode in the map or more flexible as a scope specific text item for the stepped parameters and make process midi discard a number of received events (in one direction) before it actually changes a value only on the items as defined in the map file.
Eg Filter Mode in Malstrom is mapped to "CC12" you could add an item under Malstrom in the map called eg "Scaling Values" mapped to the text value "CC12-25, CC22-10...." and another one called "_scope" mapped to "Malstroem" and then parse this for entries, find CC12 and apply 25 or probably less events of discards only on CC12 and only when you are on Malstroem.
"_scope" and "Scaling Values" also need to be added local items in the lua file to get access to its content:
{name="_scope", output="text"}
Something along this line. That way even though it is a bit more complicated (defining, parsing, counting) you can define your scaling in the map file, device and parameter specific but retain the normal encoder behaviour on not defined (the majority) of parameters. And you can use the scaling information to display the stepped value on the led ring.
In this case, I wouldn't do that via the map because to avoid having to define those scale values for all stepped parameters. It's easier to do it in the lua code and have it applied to all stepped and non-stepped parameters. Less maintenance and more consistent results.
electrofux wrote: ↑04 Apr 2025
Btw there is so much stuff you can do with the Twister. For example i would use the Mode in the map to pass parameter specific color information to the Twister via remote deliver midi so you can go Map-> CC12-> Filter Cut off-> green (needs correct number of tabs though) and then you can get the green knob on Malstrom and on another Synth it can be a different color.
I would also make two different codecs one for the devices and one for the Mixer and switch between them with the bank buttons where the bank button deactivates the output side of the inactive codec. You can then lock the Mixer codec to the Mixer and the device codec follows the selected track. You could then use the knobs push function to select a specific Mixer channel (your other issue right?).
I haven't used modes yet or fully grasped what they are there for. But do I get this right, I could add a "green" mode to a specific parameter and then query that value from the lua script?
electrofux wrote: ↑05 Apr 2025
I guess if you increment and item value by a decimal and then ask for the value with remote.get_item_value it will still return an integer.
In that case counting events and discarding a number might be the last resort.
Yeah, the values you get via remote.get_item_value() are integer values. Any smaller deltas you add to this via remote.handle_input() will be quantized. So my script tracks those incremental deltas in the "controller_accumulated_deltas" variable.
electrofux wrote: ↑05 Apr 2025
Btw isnt there a mode on the twister where the encoders behave like continuous controllers BUT you can send them their current Position via Midi? Its been a long time since i read the manual but i kindof remember it had a couple of modes.
True, the Twister also has an absolute mode. The advantage of the custom relative script is that you can increase the value range and thus have higher parameter resolution for Reason's parameters that support more than 128 values. Also, I use the MF Twister in other DAWs and am already set for relative mode there and don't want to switch configurations or banks or whatever.