New Tool: Interpolate Between Patterns

The pan/vol/delay columns do not work properly for me Sample commands work fine.

I have 16 patterns,64 lines each

Vol 10 on line 00 in pattern one
Vol 80 on line 63 in pattern 16.

Nothing happens.

rfgd.garbanzo.InterpolateBeyond_V1.61.xrnx (4.5 KB)

sorry, all fixed. i had set it to not run if a value was > “80” (because you can mis type higher values and i didn’t want to throw an error). i needed to set it to >=“80”. Should be fixed now. I’m always mixing up whether something goes between 0-127 and 1-128.

Fixed a few minor bugs while I was in there. Hopefully this saves another issue haha.

I used the delay column this time and put 01 on a pattern and F0 on the 24th pattern.

It tried to interpolate but it just put 01 all the way up to F0

Volume it puts 00 all the way to 80

Also it does not interpolate at all if the end command is on the last line of a pattern.

rfgd.garbanzo.InterpolateBeyond_V1.62.xrnx (4.4 KB)

another missed variable, this time when it was finding the next point on the next pattern it was using the current pattern, not the next pattern. thanks for testing so much and your patience, there’s a lot for me to check. Let me know what else is wrong lol.

2 Likes

No problems so far, I’m wondering if it would be easy to add some shapes to it.

For example you have linear and logarithmic.

Could you add like sinewave,squarewave,sawtooth??

2 Likes

Great idea. Would also be awesome to have randomization within a selectable range of values, at a given line frequency, but maybe that’s asking too much?

rfgd.garbanzo.InterpolateBeyond_V1.7.xrnx (5.0 KB)
behold! sin, square, saw, tri, and random. within your determined range. The ultimate one stop shop for automation in the pattern editor. Until Toybox suggests something else.

Some notes:

  • Uses your step length you determine how the rate of the “LFO”. A 1 will be one cycle, and so on. A 0 will be bumped up to 1, so don’t be afraid to use it with 0.
  • Sin, Tri, Square will prioritize ending on their end value. Only every OTHER step length will change its rate, because I found it annoying when it wouldn’t finish on the value I specified. Saw always finishes on its high point so it doesn’t matter.
  • Random is random between the specified values, like the other waves.

Interpolate Automation_3

3 Likes

Pretty cool new features!

However your waveform functions have multiple problems. As a structural suggestion I think it would be much easier to debug an extend these different waveforms if you unified all of them to just receive a time value between 0 and 1 and a frequency (what you can numOsc in your code) and return a modified time value that contains the wave, still between 0 and 1.

This way you could then input this value into the same old interpolation function to get the final value. The waveform functions have no need to know about the start and end range, in fact, them handling this just makes them harder to write and reason about.

I also see that you don’t allow for odd frequency inputs and your sin and triangle wave has some issues in general. I understand that it is a bit confusing to apply these waves in a way that their cycle ends on the peak instead of returning to zero so let me provide a working implementation for each wave, allowing the frequency to be 0 for triangle and sine (which will result in a half-wave) while letting both odd and even numbers to be used. Essentially this will result in always having cycles of (edit step + a half wave) to ensure you always end on the highest point of the waveform (and so correctly reach your endValue).

:chart_with_upwards_trend: Exploration time!
Here is a graph on desmos.com that shows the sinewave modified for your purpose, you can use the f slider to see how it behaves for different frequency values. (the x axis there is essentially t from below)
In general I highly recommend this site as a quick way to test out equations especially when it comes to waveforms. It’s much easier to see if something is off than for example in a hex pattern generated inside renoise. One downside is it’s using math notation instead of code, but most things are easy to transfer. Sure, it has 𝜏 (tau), the constant so radical it got its own manifesto :nerd_face: but that’s just math.pi * 2 in lua.

Now lets see some waves…

-- math.max here returns the larger from the two numbers it receives.
-- this is to make sure f is always at least 1 without an "if" statement
local function saw(t, f)
  local tf = t * math.max(1, f)
  return tf - math.floor(tf)
end

-- lets reuse the saw function to have an easy squarewave.
-- in a sense a squarewave is just a rounded sawtooth
-- (not the most efficient way but it's fine for our purpose)
local function sqr(t, f)
  return math.floor(saw(t, f) + 0.5)
end

-- this is the modified sinewave, shown on the desmos graph above
local function sin(t, f)
  local tf = t * math.pi * 2 * (f + 0.5) 
  return math.sin(tf - math.pi / 2) * 0.5 + 0.5
end

-- tri gets the same treatment as the sinewave
local function tri(t, f)
  local tf =  t * (f + 0.5) * 2 + 1
  return math.abs((tf % 2) - 1.0)
end

-- random is dead simple, maybe it could make use of f somehow?
local function ran(t, f)
  return math.random()
end

Ok, so how would you actually use these functions? You just call them with your numOsc (without doing any modification to it) and the t value you were already calculating in each of your original implementations.

λ

A cool thing about lua is that functions are first-class citizens, they can be passed to other functions just like numbers, strings or what-have-you. Instead of passing a string like “saw” and then checking that string inside another function to decide what function to call, you can just pass the saw function itself.
Since each of our waveform functions have the same shape (that is, they expect the same kind of inputs and return the same output) this receiving function can just call any of them with the same inputs without having to know anything more.

-- our old friend the linear interpolation included for completeness
function lerp(a, b, t)
    return a + t * (b - a)
end

-- originally your wave functions each received the following values
-- (startValue, endValue, stepCounter, numOsc, curStep)
-- and had to deal with them all alone
-- our new function will get these as well but also a wave function to apply
function interpolate_with(waveFun, startValue, endValue, curStep, stepCounter, numOsc)
  -- do the prep-work of normalizing the time
  -- so that our waves can stay clean unlike our ocean
  local t = curStep / (stepCounter + 1)
  -- here we call whatever wave function we got
  -- it can be sin, saw etc, it's all groovy!
  local wavyTime = waveFun(t, numOsc)
  -- do the remapping from 0..1 range to our actual values using lerp
  return lerp(startValue, endValue, wavyTime)
end

-- you can just call this with any of the functions we defined
-- to get back the value in the start/end range, waving included.
local a_test_value  = interpolate_with(saw, 0, 10, 5, 20, 3)
local another_value = interpolate_with(tri, 0, 10, 5, 20, 12)

While it requires a bit of adaptation both in code and in your head, in the end this can make your program considerably simpler to follow and extend.

I recommend using this to achieve the regular interpolation modes as well so that you can just use the same apply function to do everything. This way you can get rid of the parts where you check for the other variable to see what function to call. Instead you’ll be passing in a different wave function for each type and use it without any further checks.

For example

-- your "wave" function for the default linear interpolation
local function lin(t, f)
  return t
end

But wait, where is the log function?

:rabbit: Rabbit hole alert!
Check out different easing functions to get more ideas on easings.net.
Click on an easing, scroll down and you’ll be presented with a math function in code, conveniently it will always be implemented to expect a value (x) in the range 0…1, just like our other waves! Bouncing ball interpolation when?

Doing this will also fix the bug (maybe introduced with this last update?) with your linear interpolation mode where the last value will be repeated. You need to add +1 to the stepCounter there as well, doing it one place is also a good way to make these kinds of issues easier to handle.


A few more things to note regarding cleaning up the code. :broom:

In lua you can access the length of arrays with the # sign. Your countSequences function isn’t necessary, you can simply get the length of the pattern sequence with

local number_of_sequences = #renoise.song().sequencer.pattern_sequence
--                          ^--this thing here will return the length like 10

The way you have written the column operations to each get the same line makes the code quite noisy as the actual calculations you care about get lost in the sea of renoise… calls, just store the thing that all cases use in a local variable to clean up a bit, just like you do with other values.

Instead of

-- ....
    if not beginningOfProject then
      if subColumnID == 3 then
        parameterStart = renoise.song().patterns[patternID_from_sequenceID(seqID)]:track(trackID):line(lineID).note_columns[columnID].volume_value
       elseif subColumnID == 4 then
        parameterStart = renoise.song().patterns[patternID_from_sequenceID(seqID)]:track(trackID):line(lineID).note_columns[columnID].panning_value
      elseif subColumnID == 5 then
        parameterStart = renoise.song().patterns[patternID_from_sequenceID(seqID)]:track(trackID):line(lineID).note_columns[columnID].delay_value
      else
        parameterStart = renoise.song().patterns[patternID_from_sequenceID(seqID)]:track(trackID):line(lineID).effect_columns[columnID].amount_value
      end
    elseif beginningOfProject and isSample(deviceStringEnd) then
-- .... and so on

Do it like

-- ...
    -- see what gets used repeatedly and bring it out for easy access
    -- you could do it with the .note_column as well 
    -- but this is already much more readable.
    local line = renoise.song().patterns[patternID_from_sequenceID(seqID)]:track(trackID):line(lineID)
    
    if not beginningOfProject then
      if subColumnID == 3 then
        parameterStart = line.note_columns[columnID].volume_value
       elseif subColumnID == 4 then
        parameterStart = line.note_columns[columnID].panning_value
      elseif subColumnID == 5 then
        parameterStart = line.note_columns[columnID].delay_value
      else
        parameterStart = line.effect_columns[columnID].amount_value
      end
    elseif beginningOfProject and isSample(deviceStringEnd) then
--- ... and so on

You can do the same thing in the getParameterValues function.


Consider using constants instead of magic numbers, for example instead of
subColumnID == 3
you could use
subColumnID == renoise.SUB_COLUMN_VOLUME
it might look more verbose but you are less likely to make mistakes because of a mistyped number and if you look at the start of the case, you can immediately understand what is it about, not a big deal in these cases with a single line each, but it’s something that might come handy at some point.


:mag: Not sure what you use to write your tools with, but I highly recommend checking out VSCode and the new lua definitions for Renoise, this can help you by

  • providing autocompletion for renoise’ built-in functions, constants and other values
  • popups with descriptions about functions, what they expect and what they return
  • handy warnings about your own code, like unused or redefined variables etc
  • syntax errors highlighted right in the editor
  • auto-indent your entire code by right-click → “Format Document”

This makes it so that you can catch a lot of bugs before even running your code. If you want to try this but need help figuring out how, just ask!

And again, sorry for the long talk, got a bit carried away, hope I at least managed to keep it easy to follow and not too annoying.

Have fun and good luck!

3 Likes

Thank you, I will try to digest this

2 Likes

Nice!

I’m really happy to see how people are contributing to the tool with suggestions and mathematical observations (thanks @unless). This gives me hope for the future of Renoise :')

2 Likes

I will have to spend a lot of time tinkering with everything else first, but I wanted to share my idea for the random function in case you had an idea on implementation.
Similar to the other waves, it could repeat its values over the whole course of the run based on the step length. So if its a step length of 1, every value is random. If there is a step length of 2, it gets to the middle of the steps and starts over, repeating the previous half’s values. And so on. I didn’t know where to start with that so I hadn’t attempted it yet.

Take your time!

Repeating random would be cool too but I was thinking more along the lines of some gradient noise-type solution.

It’s a common technique in computer graphics which basically boils down to generating a low resolution grid of random values, then interpolating between them at a finer resolution to achieve smooth gradients.

I think this would be more similar to the other functions: at edit step 1 you’d have 1 random point in the middle where the value would interpolate to, as you increase the edit step you get more random “poles” between your end points and the lines inbetween them get filled with interpolation. So a higher edit step would mean a higher rate of varying, similar to the standard waves.

A bit of an ergonomic problem with random and this tool is that you typically want to regenerate random stuff multiple times while looking for interesting outputs, but the tool doesn’t lend itself well to this because once you generated an interpolation you have to undo or delete it before trying again.

While I don’t think this should be your focus now, it might be worth exploring an additional ability for the tool where it could work on already filled interpolation “islands”, like if you run it from the middle of a filled pattern, it would greedily flood the rows in the column until it finds the same type of value (like a bucket fill tool in paint), and once it has reached edges on both sides, it can essentially do the same thing as if it was empty space between the two end points. This would be useful for the waveforms as well because it is quite likely that you want to test a few different frequencies before settling on one.

1 Like

just want to say that I love what’s happening in this thread :slight_smile:

3 Likes

sorry I broke everything again

ScreenFlow

oops, I got the wrong topic. this message is addressed here:

1 Like

No problem, i may have been neglecting that tool and forgotten to add those settings to plock. Will take a look after i finish up the interpolate stuff i am working on.

3 Likes

rfgd.garbanzo.InterpolateBeyond_V1.71.xrnx (5.6 KB)

Update:

  • I revised the math per Master Unless to work much more nicely.
  • Cleaned up some code, lots of nice code stuck between some garbo code.
  • I added some fancy waves:
  1. log in
  2. log out
  3. bounce in
  4. bounce out
  • I could add some more custom easings but I didn’t want to be obnoxious about it haha, I thought these were most essential and coolest, although the elastic ones peak my interest.
  • This would have been done a few days ago but I spent some time on Unless’s gradient noise solution, here is an explanation of what I came up with:
  1. editStep of 0 = random values, this replaces random as an option before, it lives here now
  2. editStep >= 1 places that editStep number of random values equally between your start and end point. so edit step 1 puts a random value halfway between your start and end point. edit step 4 puts a random value every 1/5 of your selection. and so on. then it interpolates between them linearly. so you get this random linear path of peaks and valleys. I think its kind of neat. Wondering how well I understood the assignment, astounded I got something that “works” at all though haha.

Next steps:
repair parameter lock with some extra functionality for Jalex
look into programming the “islands” thing unless talked about so you don’t have to undo/redo until you get an interpolation you like

3 Likes

Hey forgive me if I’m wrong, I am judging based on the gif and it is difficult to see. Are you trying to use the “Long” parameter lock when you only have one pattern? the “long” version simply places the locked value at the first point in the next pattern, if you don’t have another pattern it won’t work.

If that isn’t the case, could you give me more information, maybe about the next pattern you’re trying to lock to? It works fine for me if I replicate your gif unless I only have one pattern (which, again, is what I think is going on)

rfgd.garbanzo.InterpolateBeyond_V1.75.xrnx (5.3 KB)

added “islands” per Unless. Now you can use this on top of existing modulation to overwrite. warning, very easy to modulate everything!
Can still use it on blank lines and it works the same.
if you have different modulation in the same lane it will treat the different modulation as the end.

As usually, let me know when it breaks! I ran out of time for the next few days but its been working on all of my tests and I didn’t want to wait.

3 Likes

Just wanna say thanks for making this tool,i use it all the time now. :face_with_monocle:

1 Like

Could not have done it without some of your suggestions and testing.