Implementing fader curves/crossfades using LUA

Hello all,

I am looking for assistance with a custom LUA workaround to the lack of fader curves and equal power crossfades in certain software/hardware. I do not have a programming background but I have done some basic tutorials for Python, PHP, Perl, BASH scripting etc. I have a basic understanding of core programming concepts and I’ve read through some of the Grid documentation on the Intech site. My mathematics is pretty terrible though :|.

Specifically what I would like to achieve is:

  • Ability to choose a custom curve for faders (could apply to pots as well); linear, exponential, or logarithmic.
  • Ability to set one or more breakpoints; have a fixed value for a certain amount of fader travel before incrementing/decrementing of values over a segment. Segments should be able to be configured with a curve type.
  • Extending the above to setup crossfades between two or more values assigned to opposite ends of a controller range.

I can see some functions that I will need to call to get controller position and I’m aware that I will need to find suitable functions in the LUA ‘math’ section to deal with the curves. But, I wouldn’t even know where to begin. In plain English, to do the crossfader (which is of the most importance to me), I would need to:

  • Set start and end values for the full range of the controller.
  • Set the MIDI CC values for the top and bottom of the fader (this could be set to other supported data types as well I assume but I’m only needing to work with MIDI).
  • Set a breakpoint to determine when the value change should occur (this would be inverted/duplicated to represent both values at the top and bottom of the fader throw).
  • Set the curve type for the segment.
  • Send all of this as output from Grid.

Personally, I would only require two different CC values to crossfade in between but it would be quite powerful to assign multiple values to the top and bottom of the fader with their own breakpoints and curves. I assume that transmitting too much MIDI data would be problematic and users should be aware of this.

This is a representation of the fader curves on my Xone PX5 mixer. The middle crossfade is what I would like to achieve but ideally, a person could program any type of curve/crossfade:

Many thanks in advance!

Oh, I just received my EN16 and EF44 yesterday afternoon so I haven’t even had the chance to start looking at LUA scripting for this.

Christopher

So I got the chance to look into this and I was able to set something up that does what I need. It could likely use some tweaks but, in use, it’s working. I setup a fader element with the following:

local num, val_1 = self:element_index(), self:potmeter_value()
local val_2 = 127 - self:potmeter_value()
local function cnst(min, value, max)
	return math.max(min, math.min(max, value))
end;
local function ln(v)
	m = 8 * math.sqrt(v) + v;
	return m
end;
v1 = cnst(0, math.floor(ln(val_1)), 127)
v2 = cnst(0, math.floor(ln(val_2)), 127)

midi_send(11, 176, 74, v1)
midi_send(11, 176, 73, v2)

It works well enough for what I need and was a good first scripting experience. It could likely use tweaks to the function that’s transforming things but so far, it’s enough.

EDIT: I had to ‘gorilla math’ the function together because I essentially have Grade 9 math skills. I didn’t get to trigonometry before I was done with school. It’s what I could cobble together with doing some searches.

Hey finding this now and also looking to setup crossfades on my PBF4. Your code here I can’t paste into a code block because it says the character limit is too long. Or does it go someplace else besides a code block? Thanks.

It just goes in a code block. Just make sure to delete everything else. That block declares the variables etc. It’s been awhile since I was poking at this so I may have added more to get things going. Or, there could be things that could be cleaned up as it was the first thing that I was working on with Grid.

1 Like

Yes works now, thanks a bunch!

1 Like

Hey I have one more question, if you don’t mind…

If I wanted to add a min/max to each MIDI value being crossfaded…

Like this code I found elsewhere:

self:potmeter_min(0)
self:potmeter_max(90)

Where would I put that in the crossfade code you posted? I keep trying to insert it in different places and it doesn’t work.

Thanks for any help!

EDIT: and if the code gets too long, I really just need a MAX.

1 Like

You could put it on the ‘Init’ tab in a ‘Code Block’.

self:potmeter_min(0)
self:potmeter_max(90)

Is it possible to set min/max independently for the two midi values bring crossfaded?

You can but I think I would need more context. The potmeter_min and max function constrain the value ranges read from the pot whereas you want to constrain or scale the MIDI value outputs. There’s probably a better way to achieve what you’re wanting.

Out of curiosity, are you crossfading audio or device parameters?

I’m also off to bed here so you’ll get radio silence from me until tomorrow.

I would advise the use of the function map_saturate() in this use case.

Let’s say you have two MIDI values coming from the self:potmeter_value() and want to create a crossfader using them. You would also like to assign individual min and max values to these parameters.

The function map_saturate(val, imin, imax, omin, omax) causes the inputted val value to be linearly remapped from the input imin, imax to output omin, omax value pairs.

When using the Nightly Grid FW (download here for ESP32 modules) you can even flip the min max values to invert this remapping.


local num, v1, v2 = self:element_index(), self:potmeter_value(), self:potmeter_value()
local min1, max1 = 0, 127
local min2, max2 = 127, 0
v1 = map_saturate(v1, self:potmeter_min(), self:potmeter_max(), min1, max1) // 1
v2 = map_saturate(v2, self:potmeter_min(), self:potmeter_max(), min2, max2) // 1
midi_send(0, 176, 42, v1)
midi_send(0, 176, 43, v2)

Here you can see a demo of the above code controlling two track volumes in Ableton.

xfade

1 Like

Thanks for posting @narayb!

For my use case, I need equal power crossfades and that function only provides linear curves. Equal power crossfades are important for audio crossfading because the existing audio source will drop too quickly as you move the fader over. The result is a very noticeable dip in audio. The hack job code that I wrote provides an equal power crossfade. What would be immensely helpful is if map_saturate() could take an additional parameter (eqp, lin, cst) so that the output could conform to the three most commonly used curves; equal power (my use case), linear, and constant power.

This blurb of code I’m using provides the equal power curve that I need:

local function ln(v)
	m = 8 * math.sqrt(v) + v;
	return m
end;
v1 = cnst(0, math.floor(ln(val_1)), 127)
v2 = cnst(0, math.floor(ln(val_2)), 127)

Would it be possible to incorporate the above curves as a parameter to that function? It would be such a tremendous help!!

I was trying to do an audio and video example in Ableton but OBS is super picky with ASIO sources and loopback. I’ll post later if I can get it sorted out before work.

Thankks Narayb but this isn’t working for me, because

  1. the when at the bottom of the fader i get 0 for both values. Other weird behavior. Please see the video:Screen Recording 2024-09-06 at 8.12.22 AM.mov - Google Drive

  2. this crossfade is missing the curve so that in the middle both values are at 100% . Needs to be equal power as d4ydream pointed out.

Yeah I’m trying to constrain the output of the midi values which are controlling CC values on a synth (Octatrack). So at 127 those values are way too high. A minimum value is less important but I could def see myself needing one day.

Yeah trying to put “self:potmeter_min(0), self:potmeter_max(90)” in int didn’t really seem to work, it constrained one of the values, i think, as you mentioned because its the whole fader being constrained.

Thanks again for your help!

Actually, reviewing my code:

local function cnst(min, value, max)
	return math.max(min, math.min(max, value))
end;
local function ln(v)
	m = 8 * math.sqrt(v) + v;
	return m
end;
v1 = cnst(0, math.floor(ln(val_1)), 127)
v2 = cnst(0, math.floor(ln(val_2)), 127)

The cnst() function is what I setup to constrain the value ranges. Try using this snippet:

v1 = cnst(0, math.floor(ln(val_1)), 90)
v2 = cnst(0, math.floor(ln(val_2)), 90)

To replace this snippet:

v1 = cnst(0, math.floor(ln(val_1)), 127)
v2 = cnst(0, math.floor(ln(val_2)), 127)

I would add to all of this that, this was my first bit of coding. I’ve done a bit more Grid programming since this but I haven’t done much in the last few months. What I would do now is define those functions in the ‘System’ element so that they can be called anywhere on that module’s page. This would free up more room to add more element-specific code like transforming/xfading more MIDI values from a single control element. I wouldn’t worry about doing that. For now, it’s more rewarding to get it working. I have a bit more time and would like to clean this up a bit. If I do, I can make a template to share.

I do have an OT so if there’s something specific you’re looking to do, I can provide relevant code.

The mythical “OT crossfader on MIDI tracks” :wink: (but for use with anything that uses MIDI really).

1 Like

@CountDoooooku
You will have to use the Nightly firmware I linked above to invert the ranges, because map_saturate() is lacks this feature on live FW.

@_d4ydream
For equal power curves we’ll have to implement some extra functionality into map_saturate() for it to support curves, not just inversion and remapping.

So first I would invert what needs to be inverted with the above code, and then we have to make it logarithmic.

For that we’ll have to use another function, this time it’s a custom one, borrowed from SC called explin().

The explin() function does support remapping, but its original code doesn’t support inversion for the crossfader so I just combine the two here to save some time.

-- under init add the following code to the usual blocks there

function explin(f, slo, shi, dlo, dhi)
    if f <= slo then
        return dlo
    elseif f >= shi then
        return dhi
    else
        return math.log(f / slo) / math.log(shi / slo) * (dhi - dlo) + dlo
    end
end

-- under potmeter the code below does everything except LED control

local num, v1, v2 = self:element_index(), self:potmeter_value(), self:potmeter_value()
local min1, max1 = 0, 127
v1 =
    explin(
    map_saturate(v1, self:potmeter_min(), self:potmeter_max(), min1, max1) + 1,
    min1 + 1,
    max1 + 1,
    min1 + 1,
    max1 + 1
) //
    1 -
    1
v2 =
    explin(
    map_saturate(v2, self:potmeter_min(), self:potmeter_max(), max1, min1) + 1,
    min1 + 1,
    max1 + 1,
    min1 + 1,
    max1 + 1
) //
    1 -
    1
midi_send(0, 176, 42, v1)
midi_send(0, 176, 43, v2)

Here’s the result in Ableton:
log_xfade

2 Likes

Big thanks for the quick reply and code! When I’ve got time, I’ll implement this is and give it the ear test. Visually, it appears to function as I would need. My code needed tweaking to get the crest of the midpoint crossfade where I wanted. It sounds and meters out correctly but I’m sure I’m missing something to it. Yours is spot on based on the master out meters.

If it were at all possible to incorporate something like this by passing a parameter to map_saturate(), it would be such a blessing.

The whole reason I went down this xfader rabbit hole is because Loopy Pro doesn’t have:

  • A crossfader element (though the fader element can be clumsily programmed to do it).
  • Fader curves.

<3

2 Likes

For now, map saturate() is linear only, but I’ll add this to the feature request list.

1 Like

Ok nice this is working now!

I get the gist of what putting this code in system would do, but need to read up more on system vs element coding. I’m new to Grid.

Yeah basically I’m just trying to have more than one Crossfader for the OT. This basically does it. One thing I’d love to add to this is being able to cross fade more than 2 parameters on the fade.

And good call, this could also be used to midi cross fade.

I need to browse the cloud to see what other grid configs people have for the OT. I have high hopes for these controllers + OT, and knot makes it possible without a computer. (I was using MidiPipe before).

One thing I had no idea about is the time based stuff Grid can do. I could see that really blowing open some new capabilities on the OT.

Keep in touch would love to assist in developing an OT template!

And thanks again!

1 Like

You can. And when I get home I’ll show you how.

Also, you can scene lock a lot more than 2 parameters to the crossfader and in varying depths per lock.

Also, don’t sleep on parameter slides ;).

What were you hoping to do? I’m well-versed on the OT.

If the Octatrack-specific stuff gets too off topic, feel free to DM me.

Edit:

midi_send(11, 176, 74, v1)
midi_send(11, 176, 73, v2)

That code is what is actually sending the MIDI data. The parameters are as follows:

  • ‘11’ is the channel you want to send on. Grid uses 0-15 for channels so this is actually sending on MIDI channel 10.
  • ‘176’ is the MIDI data type. ‘176’ means that it is sending a CC message. If you add a ‘MIDI’ block to the Editor, you can see what each data type is (CC, note, etc.).
  • ‘74’ is the CC that’s being sent
  • ‘v1’ and ‘v2’ are the values being sent.

So, if you wanted to send out more than one value, just use that function again. ‘v1’ is one side of the crossfade and ‘v2’ is the other. You don’t even need to assign values in pairs i.e. you don’t need a parameter assigned to each side of the crossfade. You could just fade in/out a single parameter.

midi_send(11, 176, 74, v1)
midi_send(11, 176, 73, v2)
midi_send(9, 176, 23, v1)
midi_send(9, 176, 24, v2)

Would also cross fade CCs 23 and 24 on MIDI channel 8. You could go pretty bonkers with it if you wanted. You’re only limited by the number of characters you can use on the element’s tab. This is why I would move some of the function definitions to the ‘Init’ of the ‘System’ element. This way, any of the elements can call those functions without having to define them each time. They’re ‘globally’ available to use.

But, I would just stick with something simple at first. If you don’t need the extra character space then I wouldn’t bother moving things around. I will likely check out the nightly builds and re-work things with the stuff that @naryb posted. I’m sure there’s a lot of room for me to tighten up my code.

:slight_smile: