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