Remote: Relative Knobs On Stepped Parameters

Want to talk about music hardware or software that doesn't include Reason?
Tiefflieger Rüdiger
Posts: 67
Joined: 05 Jun 2024

Post 23 Mar 2025

Have been taking a look into Reason's Remote SDK today to create a lua codec for my MIDI Fighter Twister knobs in relative mode (3f=-1;41=+1). It seems that only absolute knobs work fine with Reason. While relative knobs work fine with non-stepped parameters, with stepped parameters there's always a drawback. An absolute knob's value range sending 0 to 127 is correctly mapped to the int range of a stepped value. The only niggle here is that value feedback from Reason is not stepped, so LED rings around a knob visualize Reason's internal understanding of the parameter, not how it behaves in the UI.

With relative mode knobs, however, there's always a deal breaker. I tried the following approaches:
  1. Using autoinput/autooutput with delta mode on stepped parameters like "Malström Filter A Mode" squeezes the physical knob range into a tiny range. This is because MIDI control in delta mode will be interpreted by Reason as a step even for the smallest relative MIDI message instead of scaling this to a virtual appropriate range. So within one degree, you're through all the filter's modes.

    Code: Select all

    // during remote_init in the item array
    {name="Knob 0", input="delta",	output="value", min=0, max=127},
    
    // during remote_init in the inputs array
    { pattern = "b0 01 xx", name = "Knob 0", value = "(64-x)*-1" },
    
    // during remote_init in the outputs array
    {name="Knob 0",	pattern="b0 01 xx"},
    
    EDIT: I also tried adding a Scale value to the stepped parameter in the remotemap, but only values > 1.0 seemed to have an effect (making the range even tinier, which is not what you want in this situation) while values < 1.0 had no influence at all.
  2. Using custom logic via remote_process_midi() and remote_deliver_midi() with letting Reason think this control is an absolute control (hoping this would make it scale the control's range properly to the int range of a stepped parameter):

    Code: Select all

    // remote_init(), item
    {name="Knob 0", input="value", output="value", min=0, max=127},
    
    // remote_init(), inputs
    { pattern = "b0 00 xx", name = "Knob 0"},
    
    // remote_init(), outputs stays empty for Knob 0
    
    // then later, quite messy code since I'm experimenting, so this only works for a single control
    g_change = 0
    
    function remote_process_midi(event)
    	ret=remote.match_midi("b0 yy xx",event)
    	if ret~=nil then
    		local index=ret.y + 1
    		local currentValue= remote.get_item_value(index)
    		local newValue= g_change + currentValue + (64 - ret.x) * -1
    		g_change = newValue - currentValue
    		local msg={ time_stamp=event.time_stamp, item=index, value=newValue }
    		remote.handle_input(msg)
    	   return true
    	end
    	return false
     end
    
     g_cc0_current_value=-1
     g_cc0_previous_out_value=-1
    
     function remote_set_state(changed_items)
    	for i,item_index in ipairs(changed_items) do
    		g_cc0_current_value=remote.get_item_value(item_index)
    	end
    end
    
    function remote_deliver_midi()
    	local ret_events={}
    
    	g_change = 0
    
    	if g_cc0_current_value==g_cc0_previous_out_value then
    		return ret_events
    	end
    
    	local event_string = "b0 00 " .. string.format("%02X", g_cc0_current_value)
    	local control_event = remote.make_midi(event_string)
    	table.insert(ret_events,control_event)
    	g_cc0_previous_out_value = g_cc0_current_value
    	return ret_events
    end
    
    While my initial idea for this approach turned out to be true, it also shifts the problem to the other extreme: now you cannot select the next/previous value step with slower relative values. So you have to use higher relative values but this will make you skip a step or two easily. I guess that is because Reason is quantizing away some smaller value changes between the function calls but since I cannot print debug logs this is hard to research further.
So I wonder: has anybody gotten relative controls to play nicely with Reason's stepped parameters?

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

Post 24 Mar 2025

Alternatively, if you could control the EQ params, dynamics params etc. of the currently selected mixer channel, then you could switch to the next/previous channel using the arrow keys. But I haven't found a way to do that as selecting usually activates the scope of the actual device, not its mixer channel.

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 29 Mar 2025

Let me check how i did it on my controller but i know it was a pain.
I remember having issues of getting the current value with remote.get_item_value from within process midi and instead i ended up using the value i received in remote set state (g_cc0_current_value) as current value. But i am not sure.

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

Post 31 Mar 2025

electrofux wrote:
29 Mar 2025
Let me check how i did it on my controller but i know it was a pain.
I remember having issues of getting the current value with remote.get_item_value from within process midi and instead i ended up using the value i received in remote set state (g_cc0_current_value) as current value. But i am not sure.
Basically you’d accumulate knob movements and store them as absolute values per physical control in a global variable, then manually translate to Reason‘s stepped parameter range instead of relying on their default implementation?

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 01 Apr 2025

Thats what at least worked for me. First i also put the remote.get_item_value in front of the increment/decrement but that gave me problems. Dont remember what the problems was could be jumping values or sticking values but it wouldnt work well.
Using the global value receivevd via remote set state however worked. It would be nice to see if it works for you. I am not sure if the solution is fast enough so that there isnt a delay resulting in sluggish behaviour. Would also know what the best practice is when using encoders in process midi. But all codecs i have seen handle it in the automatic section.

You have a Twister right? would be especially interesting since this is one of the faster controllers out there with alot of pulses per turn. I only have a few slow encoders for comparison.

User avatar
Carly(Poohbear)
Competition Winner
Posts: 2973
Joined: 25 Jan 2015
Location: UK

Post 01 Apr 2025

I have only just seen this thread.

I did a video on my channel about live logging, can't program without it (well I can but it's slower)..

Can you confirm that you are saying with your controller that turning the knob anticlockwise is 0x3f and clockwise is 0x41 ? (All other controllers I have come across set the 7 bit (0x40) for anticlockwise).

I don't really use the auto and manually control everything, My Delta\Relative knobs work fine with things like stepped controls (TBH I use buttons to step through options like those).




electrofux
Posts: 924
Joined: 21 Jan 2015

Post 02 Apr 2025

Hey,

i have seen your Akai controller video which is great. Could you post how you handle the encoders in process midi so that i can check if i do anything weird on mine? Was never sure if it was the optimal way to do especially where to fetch the current value that needs to be incremented/decremented.

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

Post 02 Apr 2025

Carly(Poohbear) wrote:
01 Apr 2025
I have only just seen this thread.

I did a video on my channel about live logging, can't program without it (well I can but it's slower)..
Very interesting and great idea to use MIDI for the logging!
Carly(Poohbear) wrote:
01 Apr 2025
Can you confirm that you are saying with your controller that turning the knob anticlockwise is 0x3f and clockwise is 0x41 ? (All other controllers I have come across set the 7 bit (0x40) for anticlockwise).
Yes, that's how they name the relative mode in the MF UI: ENC 3FH/41H
Carly(Poohbear) wrote:
01 Apr 2025
I don't really use the auto and manually control everything, My Delta\Relative knobs work fine with things like stepped controls (TBH I use buttons to step through options like those).
Well, buttons make this problem non-existent. I should mention that the MF's endless encoders are not rasterized. Reason's default behavior makes sense with a rasterized endless encoder, but it's really bad when the endless encoder isn't rasterized.

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

Post 02 Apr 2025

I think I found a solution. I was actually quite close with the custom relative implementation from the initial post. This is still experimental, so the code is bad and only works for the first control. But this makes a non-rasterized endless encoder feel best with normalized and stepped parameters in Reason:

Code: Select all


function remote_init(manufacturer, model)
	local items=
	{
		{name="CC 00", input="value", output="value", min=0, max=127},
	}
	remote.define_items(items)

	local inputs =
	{
		{ pattern = "b0 00 xx", name = "CC 00"},
	}
	remote.define_auto_inputs(inputs)

	local outputs={
		-- {name="CC 00"},
	}
	remote.define_auto_outputs(outputs)
end


-- lol, this is so bad
g_change = 0

function remote_process_midi(event)
	ret=remote.match_midi("b0 00 xx",event)
	if ret~=nil then
		local index=1
		local currentValue= remote.get_item_value(index)
		local newValue= g_change + currentValue + (64 - ret.x) * -1
		g_change = newValue - currentValue
		local msg={ time_stamp=event.time_stamp, item=index, value=newValue }
		remote.handle_input(msg)
	   return true
	end
	return false
 end

 g_cc0_current_value=-1
 g_cc0_previous_out_value=-1

 function remote_set_state(changed_items)
	for i,item_index in ipairs(changed_items) do
		g_cc0_current_value=remote.get_item_value(item_index)
	end
end

function remote_deliver_midi()
	local ret_events={}

	if g_cc0_current_value==g_cc0_previous_out_value then
		-- range limit
		if (g_cc0_current_value + g_change < 0) or (g_cc0_current_value + g_change > 127) then
			g_change = 0
		end

		return ret_events
	end

	g_change = 0

	local event_string = "b0 00 " .. string.format("%02X", g_cc0_current_value)
	local control_event = remote.make_midi(event_string)
	table.insert(ret_events,control_event)
	g_cc0_previous_out_value = g_cc0_current_value
	return ret_events
end
The trick was to only reset the stored delta (g_change) when there was a difference detected between the previous and the current value of the parameter of the rack device. Additionally, there needs to be a range check to avoid the stored delta to artificially increase the range with "empty" ranges.

Stepped parameters and normalized float parameters now behave the same (same physical range, same acceleration response) for relative knobs. This allows you to make use of the MF's fantastic acceleration profile because you can make very precise adjustments when moving it slowly and very coarse adjustments by moving quickly.

If there's a more idiomatic approach with Reason's Remote Codec SDK, please let us know.
electrofux wrote:
01 Apr 2025
Thats what at least worked for me. First i also put the remote.get_item_value in front of the increment/decrement but that gave me problems. Dont remember what the problems was could be jumping values or sticking values but it wouldnt work well.
Using the global value receivevd via remote set state however worked. It would be nice to see if it works for you. I am not sure if the solution is fast enough so that there isnt a delay resulting in sluggish behaviour. Would also know what the best practice is when using encoders in process midi. But all codecs i have seen handle it in the automatic section.

You have a Twister right? would be especially interesting since this is one of the faster controllers out there with alot of pulses per turn. I only have a few slow encoders for comparison.
Thanks a lot for your input. I was't sure what exactly to do when you said "Using the global value receivevd via remote set state however worked.". What global variable is available there? I'm using "remote.get_item_value" in both, remote_process_midi as well as remote_set_state.

User avatar
Carly(Poohbear)
Competition Winner
Posts: 2973
Joined: 25 Jan 2015
Location: UK

Post 02 Apr 2025

electrofux wrote:
02 Apr 2025
Hey,

i have seen your Akai controller video which is great. Could you post how you handle the encoders in process midi so that i can check if i do anything weird on mine? Was never sure if it was the optimal way to do especially where to fetch the current value that needs to be incremented/decremented.
I can't just post the code is it's quite complex the way it hangs together, I will try and keep it simple.

I track the current index and value because I use pages and the index number for a control can change***
So if the current index does not equal my tracking index I update the tracking index and value, then I update that value plus my encoder value and send that. (remote_process_midi / remote.handle_input)
I also update the tracking value if I manually change a parameter on a device. (remote_set_state)

***I don't assign device parameters to my controls, my parameters are assigned to a item called param X, I assign that item_index as a value to my control. snippet of my mapping.

Code: Select all

//**********************************************************************************
Scope	Propellerhead Software	se.propellerheads.Grain
//**********************************************************************************

Define Group	zpage	zpage 1	zpage 2	zpage 3	zpage 4

Map	scene 1		zpage=zpage 1
Map	scene 2		zpage=zpage 2
Map	scene 3		zpage=zpage 3
Map	scene 4		zpage=zpage 4
Map	device name		"Grain"		OTF
Map	device label		Device Name
Map	plugin view		Proxy Open Plugin Window

Map	param 1		Pitch Bend Range
Map	param 2		Voices
Map	param 3		Portamento
Map	param 4		Portamento Mode		3way
Map	param 5		Key Mode		3way
Map	param 6		Position
Map	param 7		End Pos
Map	param 8		Scroll
Map	param 9		Zoom
Map	param 10		Jitter
Map	param 11		Global Position		2way
Map	param 12		Motion		6way
Map	param 13		Algorithm		4way
Map	param 14		Oct		5way
.....
.....
Map	knob 1		16			zpage 1
Map	knob 2		32			zpage 1
Map	knob 3		17			zpage 1
Map	knob 4		18			zpage 1
Map	knob 5		67			zpage 1
Map	knob 6		68			zpage 1
Map	knob 7		71			zpage 1
Map	knob 8		55			zpage 1
Map	knob 1		36			zpage 2
Map	knob 2		35			zpage 2
Map	knob 3		38			zpage 2
Map	knob 4		37			zpage 2
Map	knob 5		67			zpage 2
Map	knob 6		68			zpage 2
Map	knob 7		71			zpage 2
Map	knob 8		59			zpage 2
Map	knob 1		43			zpage 3
Map	knob 2		44			zpage 3
Map	knob 3		45			zpage 3
Map	knob 4		46			zpage 3
Map	knob 5		12			zpage 3
Map	knob 6		27			zpage 3
Map	knob 7		10			zpage 3
Map	knob 8		6			zpage 3
From the mapping above the last 2 knobs, Knob 7 and Knob 8 on zpage 3 point to 10(Jitter) and 6(Position).

I do this for a number of reasons, it allows me to do on the fly mappings, morphing controls and very small changes, small changes as the param X is defined with min=0 and max=4194304, this breaks me free from the midi standard 0-127 when using encoders.

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 03 Apr 2025

Was asking because i initially ran into similar issues with encoders like Tiefflieger.
The intuitive way to fetch the current value that the encoder needs to increment/decrement on a turn of the knob was to me to call remote.get_item_value right before the increment/decrement in process midi. But that led to issues and didnt really work. So instead i used a tracked variable were the value came from a variable updated in remote.set_state. And this then worked. But i dont know if that is the best practice and if that is fast enough for fast encoders.
For the parameter assignment i also go different ways and assign them dynamically within process midi also to go around the 1-127 scaling.

User avatar
Carly(Poohbear)
Competition Winner
Posts: 2973
Joined: 25 Jan 2015
Location: UK

Post 04 Apr 2025

electrofux wrote:
03 Apr 2025
Was asking because i initially ran into similar issues with encoders like Tiefflieger.
The intuitive way to fetch the current value that the encoder needs to increment/decrement on a turn of the knob was to me to call remote.get_item_value right before the increment/decrement in process midi. But that led to issues and didnt really work. So instead i used a tracked variable were the value came from a variable updated in remote.set_state. And this then worked. But i dont know if that is the best practice and if that is fast enough for fast encoders.
For the parameter assignment i also go different ways and assign them dynamically within process midi also to go around the 1-127 scaling.
You are also updating your global variable the new value including the encoder value?
Remember that a value change with remote.handle_input on a parameter is also passed back to remote_set_state, remote_set_state and remote_deliver_midi is called a few times a second but can be less depending on how utilised your system is.

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 04 Apr 2025

"remote_set_state and remote_deliver_midi is called a few times a second but can be less depending on how utilised your system is"

Thats exactly the reason why i initially thought calling remote.get_item_value() in process midi is the way to go. But like i said it resulted in sketchy parameter behaviour.

But when relying on the global value passed into remote.set_state whenever the parameter got changed (either from inside Reason or via my controller) it worked smoothly, alas i am not sure if it is as smooth as when using eg automatic input.

Where do you fetch the current value from especially in the lines before you call remote.handle_input?

User avatar
Carly(Poohbear)
Competition Winner
Posts: 2973
Joined: 25 Jan 2015
Location: UK

Post 04 Apr 2025

electrofux wrote:
04 Apr 2025

Where do you fetch the current value from especially in the lines before you call remote.handle_input?
I track a lot in tables so the process goes like this.

In remote_deliver_midi
If am not currently tracking, then track and call remote.get_item_value(), this happens once then I use the value from my table.

In remote_set_state
I update my table if I am tracking and there is a change.

Note you could call remote_deliver_midi several times before remote_set_state is called, remote_set_state holds the last value set from remote.handle_input, so not every value is past to remote_set_state.

snippet of my log

Code: Select all

(PoohBear, Akai APC Key 25)76(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2588777
(PoohBear, Akai APC Key 25)77(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2654313
(PoohBear, Akai APC Key 25)78(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2687081
(PoohBear, Akai APC Key 25)79(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2719849
(PoohBear, Akai APC Key 25)80(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2752617
(PoohBear, Akai APC Key 25)81(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2785385
(PoohBear, Akai APC Key 25)82(remote_set_state)item_index=321 changed value 2785385 attached to knob 5
(PoohBear, Akai APC Key 25)83(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2752617
(PoohBear, Akai APC Key 25)84(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2719849
(PoohBear, Akai APC Key 25)85(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2687081
(PoohBear, Akai APC Key 25)86(remote_set_state)item_index=321 changed value 2687081 attached to knob 5
(PoohBear, Akai APC Key 25)87(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2654313
(PoohBear, Akai APC Key 25)88(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2621545
(PoohBear, Akai APC Key 25)89(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2588777
(PoohBear, Akai APC Key 25)90(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2556009
(PoohBear, Akai APC Key 25)91(remote_process_midi)knob 5 item_index=27 changing item_index 321 new value 2523241
(PoohBear, Akai APC Key 25)92(remote_set_state)item_index=321 changed value 2523241 attached to knob 5

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 04 Apr 2025

So there is no call of remote.get_item_value in your process midi and you get the current value from elsewhere, right?

That would be similar to how i do it. I was just never able to explain why remote.get_item_value in process midi didnt work for me at least for encoders.

And since set state can potentially be called less often than process midi i was unsure if that is best practice.

User avatar
Carly(Poohbear)
Competition Winner
Posts: 2973
Joined: 25 Jan 2015
Location: UK

Post 04 Apr 2025

electrofux wrote:
04 Apr 2025
So there is no call of remote.get_item_value in your process midi and you get the current value from elsewhere, right?

OMG sorry this should have read

In remote_process_midi
If am not currently tracking, then track and call remote.get_item_value(), this happens once then I use the value from my table.

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

Post 04 Apr 2025

Carly(Poohbear) wrote:
02 Apr 2025
I do this for a number of reasons, it allows me to do on the fly mappings, morphing controls and very small changes, small changes as the param X is defined with min=0 and max=4194304, this breaks me free from the midi standard 0-127 when using encoders.
.

This on the fly mapping sounds very interesting. And it sounds like that would be an interesting solution to the problem described here: viewtopic.php?f=4&t=7536479
Would you mind taking a look at that thread and give a quick rundown there if your approach could be applied to that? Would be interesting to see what the advantages of your approach are compared to mapping changes declared in a remotemap file via groups or variations.

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

Post 04 Apr 2025

Ok, so I'm trying to summarize this a bit.
The basic problem when using an encoder with a stepped parameter in Reason is:
  • when using auto input/auto output in delta mode, a tiny movement will already go to the next step. Ok if the encoder is rasterized, bad if it is not and the amount of deltas when rotating is quite dense (as it is the case with the MF Twister)
  • when declaring the knobs to be value knobs and writing your own relative knob handling via lua scripting, getting the value of a Reason control via remote.get_item_value() will have discarded any deltas that you communicated before in remote_process_midi() via remote.handle_input (). So you have to twist the knob fast enough to create a single delta that is high enough to make Reason go to the parameter's next step.
You cannot fix the first issue without access to Reason's source code. But you can workaround the latter issue by accumulating the delta yourself and continuously feeding that to Reason via remote.handle_input()

You then check in remote_deliver_midi() if there's finally a value change for the Reason parameter and only act in that situation (or act if the accumulated values are becoming too large).

Below is the updated version with improved code readability that works for any amount of knobs. This will make stepped parameters feel much more consistent with non-stepped parameters when using non-rasterized relative knobs:

Code: Select all

-- MIDI Fighter Twister Remote Codec
-- with custom implementation for relative knobs

-- const variables --
--_________________--

-- the amount of knobs
KNOBS_COUNT = 64
-- the minimum value of a knob
KNOB_MIN = 0
-- the maximum value of a knob
KNOB_MAX = 127
-- 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

	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

			local new_value = controller_accumulated_deltas[index] + current_delta + current_value
			new_value = math.max(0, math.min(127, 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))
			table.insert(events, midi)
			reason_param_values_previous[index] = new_value
		end
	end

	return events
end
Again, happy to know if there's a simpler or more idiomatic solution.

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 04 Apr 2025

ok, this isn't working for me and i dont know why.

this is how it currently looks for the left turn

Code: Select all

local itemnr--> deducted from remote.match_midi
local incrementor=1
local itemvalue=itemvalues[itemnr]-incrementor --itemvalues is an array filled from within remote.set_state
if itemvalue<0 then itemvalue=0 end
local msg={ time_stamp = event.time_stamp, item=itemnr, value =itemvalue}
remote.handle_input(msg)
If i would do this

Code: Select all

local itemnr--> deducted form remote.match_midi
local incrementor=1
local itemvalue=remote.get_item_value(itemnr)-incrementor
if itemvalue<0 then itemvalue=0 end
local msg={ time_stamp = event.time_stamp, item=itemnr, value =itemvalue}
remote.handle_input(msg)
it wouldnt work.

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

Post 04 Apr 2025

electrofux wrote:
04 Apr 2025
ok, this isn't working for me and i dont know why.
What exactly doesn't work and what kind of parameters are you controlling when it doesn't work?

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 04 Apr 2025

Ok i digged a bit deeper, forget about my issue, it is not related to yours and i am already using a solution that works albeit i am not sure if it is optimal but it is derailing your topic.

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.

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?

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.

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?).

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 05 Apr 2025

Instead of discarding events you could also add a scalling value to a text variable in the map like 0.25 IF parameter values can have decimal increments. But since you found that the remote scaling modifier only works with values>1 this might not be the case.
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.

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.

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

Post 05 Apr 2025

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.

electrofux
Posts: 924
Joined: 21 Jan 2015

Post 07 Apr 2025

Can you somewhere retrieve the min max stepped values of individual device prarameters? I dont think so.

Thats why one way or the other it has to be done by hand. Thats the reason i outsource these kind of operations to the map file.

If i understand your code correctly you scale down the resolution by the factor 10, right?

Not sure i would like that since the big advantage of the Twister is its high resolution - velocity scaling is something all other controllers do too but that isnt exactly smooth. Also that could be done easier by just asking if the current event has the same index as the last and then do the resolution scaling thing. obviously if you turn two knobs at the same time that wouldnt work but if not its way easier.
Also with a fixed scale number you cannot distinguish between parameters with different step magnitude (0-4 or 0-22).

Imho the best way is to go into maps and do some entries to get the few stepped parameters over to the codec and only do the scaling on these. But yeah that is all alot of work and it is probably better to get going with whatever works and do music instead of coding.

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

Post 08 Apr 2025

electrofux wrote:
07 Apr 2025
Can you somewhere retrieve the min max stepped values of individual device prarameters? I dont think so.
That is really unfortunate and limits the flexibility of encoders in the Remote Codec SDK.
electrofux wrote:
07 Apr 2025
Thats why one way or the other it has to be done by hand. Thats the reason i outsource these kind of operations to the map file.
By using higher scale values or how do you achieve this?
electrofux wrote:
07 Apr 2025
If i understand your code correctly you scale down the resolution by the factor 10, right?
Well yes and no. I’m scaling down the delta values to slow down the movement. It felt too quick for me. However, this does not affect the resolution of the controller. Everything between the Lua code and the controller is happening in relative mode. The scaled delta values are stored as floats. No resolution or quality gets reduced. Thus , the scaling is more a speed parameter. The idea of these constants is that you can easily adapt them to what you want.
electrofux wrote:
07 Apr 2025
Not sure i would like that since the big advantage of the Twister is its high resolution - velocity scaling is something all other controllers do too but that isnt exactly smooth.
The resolution is not affected, it’s just scaled to feel good within Reason. Velocity scaling is not done in the lua script, it’s the default mode of the Twister and can be configured with the MF Utility. It means the faster you move the knob, the higher the relative values are. So a slow event comes in with a value of +1 and a very quick event comes in with a value of +63. This means two very quick events from the Twister can bring your knob from total left to total right if you don’t scale them appropriately.
electrofux wrote:
07 Apr 2025
Also with a fixed scale number you cannot distinguish between parameters with different step magnitude (0-4 or 0-22).
That’s why my script claims to Reason that the MF Fighter is in absolute mode while it is actually in relative mode. The script accumulates the deltas and converts them to absolute values for Reason. This way, Reason scales the range of the controller to the range of the remotable item automatically.

  • Information
  • Who is online

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