Remote: Relative Knobs On Stepped Parameters

Want to talk about music hardware or software that doesn't include Reason?
electrofux
Posts: 924
Joined: 21 Jan 2015

Post 09 Apr 2025

With resolution i meant what the Twister send when it does eg half a turn and how smooth that refelcts on the controlled parameter. I might be wrong in the feeling but i never liked it when i adjusted the the delta range on my encoders and they jumped eg 4 points per resolution. In an ideal world i would like my encoders to behave like potis and the twister ones come near to that i feel IF you can keep them at high resolution/low delta values.

To emulate an inbuilt "per parameter" scale value or variable min max values i add these manually into the map file at least for the few stepped parameters where it is needed. There are two ways to do this.

- mode value
- custom text mapping object that you automatically parse for the values in the codec.

It IS work though. But since it only happens in the map file it can be done whenever you have to edit it anyways eg because you add a new device.

You COULD also use the push function of the Twister to increase or reduce the scaling btw.

Edit: This might be a brainfart or a non starter. But normal controls have a local min max value defind in remote init. This value then gets scaled to the hosts actual item min max values - thats why with normal potis you can easily turn the knob through stepped values since they are correctly scaled. Now in the remote docs deltas are defined without a min max value but maybe you can add them and then you could scale this up by eg factor 10 (0-1270) plus a scale value of 10 and then you could dial down the scale value to your needs. I am pretty sure its not going to work like that but maybe it does.

Tiefflieger Rüdiger
Posts: 67
Joined: 05 Jun 2024

Post 09 Apr 2025

electrofux wrote:
09 Apr 2025
With resolution i meant what the Twister send when it does eg half a turn and how smooth that refelcts on the controlled parameter. I might be wrong in the feeling but i never liked it when i adjusted the the delta range on my encoders and they jumped eg 4 points per resolution. In an ideal world i would like my encoders to behave like potis and the twister ones come near to that i feel IF you can keep them at high resolution/low delta values.
I see. The script however does the opposite. Its default scale variable (which can be simply set to "1" to neutralize it) doesn't make the control jump 4 points per delta but 0.1 point per delta. This slowdown was necessary because the tiniest movement with a not so slow movement speed would result in drastic knob movements. So the scale value actually gives some physical space to the knob.

But I think you have a point though. When applying that scale, you'll have the Twister's lowest value 1/-1 become a fraction and that fraction is not a meaningful step for Reason's devices. So there is a chain of slow delta values that are only relevant within the script and disregarded by Reason until they reach a new integer value. I've made some adjustments to the script so that the scale value is now an exponent. This allows to keep the lowest value at 1/-1 but tweak the higher values when rotating faster to the take value range declared towards Reason:

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 = 1.5 * (math.log(KNOB_MAX + 1) / math.log(128)) - 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 = match.x - KNOB_DELTA_BASE
			local sign = current_delta < 0 and -1 or 1
			current_delta = sign * math.abs(current_delta)^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 < KNOB_MIN) or (new_value + current_delta > KNOB_MAX) 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
You can also adjust the KNOB_MAX variable of my script to something higher like 16383 if you want Reason to think you're using a 14 bit controller. I've also made some adjustments to the scaling so that it works much better when changing the controller's value range. The acceleration graphs can be viewed here: https://www.desmos.com/calculator/pdvljrbjmf
The first is for KNOB_MAX=127, the second is for KNOB_MAX=16383. The third one is blending between the previous two and is the one actually used for KNOB_SCALE.
There were also some magic numbers still not replaced by variables which caused stepped parameters to behave wrong when using a different value than 127 for KNOB_MAX.

Here I'm comparing the step size of the MF Twister in the script's 14 bit relative mode against the fine and coarse mouse step size:
Reason-MFT-Relative.gif
And here's the same physical knob movement applied to a frequency (float) parameter and a mode (stepped) parameter:
Reason-MFT-RelativeStepped.gif
electrofux wrote:
09 Apr 2025
To emulate an inbuilt "per parameter" scale value or variable min max values i add these manually into the map file at least for the few stepped parameters where it is needed. There are two ways to do this.

- mode value
- custom text mapping object that you automatically parse for the values in the codec.

It IS work though. But since it only happens in the map file it can be done whenever you have to edit it anyways eg because you add a new device.
Interesting! What is that custom text mapping object? Do you mean the remotable item's value string?
Do you know how to access the scale value of an item from the map? There's no remote.get_item_scale() so I wonder if that's only used for delta auto inputs?
electrofux wrote:
09 Apr 2025
Edit: This might be a brainfart or a non starter. But normal controls have a local min max value defind in remote init. This value then gets scaled to the hosts actual item min max values - thats why with normal potis you can easily turn the knob through stepped values since they are correctly scaled. Now in the remote docs deltas are defined without a min max value but maybe you can add them and then you could scale this up by eg factor 10 (0-1270) plus a scale value of 10 and then you could dial down the scale value to your needs. I am pretty sure its not going to work like that but maybe it does.
That's what I tried initially before creating this thread. Sadly, that doesn't work. The min/max values just get ignored for the a delta input. This is what led me to create the lua codec that acts towards Reason as if the controls were all in absolute mode but communicates with the MF Twister in relative mode.
You do not have the required permissions to view the files attached to this post.

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 11 Apr 2025

to get values over from the map to the codec you add a map item eg:

map newmapitem "string"

and then you get the value with remote.get_item_value. You can add delimiters and explode the string to have more values encoded in it.

OR you add a mode value in the map and do the same but with remote.get_item_mode.

Getting the scale value seems not to be possible. It looks like it is applied to anything that is delta automatically.

Tiefflieger Rüdiger
Posts: 67
Joined: 05 Jun 2024

Post 25 minutes ago

electrofux wrote:
11 Apr 2025
to get values over from the map to the codec you add a map item eg:

map newmapitem "string"
I'm neither finding this in the Remote Codec SDK manual nor am I able to get Reason to accept this. Here's what I do in my map file:

Code: Select all

Scope	Propellerhead Software	se.propellerheads.Europa

Map	CC 00		Osc1 Filter Freq
// Adding the line below causes Reason to reject the map/codec
Map newmapitem "test"
When I add the last line, Reason rejects the codec:
Failed to load control surface: DJ TechTools, MIDI Fighter Twister (Relative Custom).
Map File Errors:
Line 645: Undefined command.
electrofux wrote:
11 Apr 2025
and then you get the value with remote.get_item_value. You can add delimiters and explode the string to have more values encoded in it.
Really strange, because according to the Reason Remote SDK manual, this call will retrieve the value from the currently controlled device's parameter (e.g. a knob like Filter Cutoff on a Reason Rack device synth). The value returned is usually a number - but you imply when this is a custom text mapping, this function will return a string instead?
electrofux wrote:
11 Apr 2025
Getting the scale value seems not to be possible. It looks like it is applied to anything that is delta automatically.
Yes, that's also my observation.

The advantage of the current state of my script is that all those ranges are scaled automatically by Reason due to having all controls declared as absolute. But with the MFT actually being used in relative mode and the relative message processing being done in the lua script, you can set its range as high as you like and it'll automatically scale the accelerated values to a range that feels right while you'll still be able to make single step increments when turning the encoders slowly. This is applied to all parameters so you don't need to manually set scale values for e.g. stepped parameters.

  • Information
  • Who is online

    Users browsing this forum: CommonCrawl [Bot] and 4 guests