Skip to content

Commit

Permalink
lfo library: v2 (#1692)
Browse files Browse the repository at this point in the history
* 'saw' -> 'tri', add 'up' and 'down', fix init

* build params from lfo spec

* add phase
  • Loading branch information
dndrks authored Jul 22, 2023
1 parent 2bad325 commit 103f620
Showing 1 changed file with 70 additions and 52 deletions.
122 changes: 70 additions & 52 deletions lua/lib/lfo.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ LFO.__index = LFO

local lfo_rates = {1/16,1/8,1/4,5/16,1/3,3/8,1/2,3/4,1,1.5,2,3,4,6,8,16,32,64,128,256,512,1024}
local lfo_rates_as_strings = {"1/16","1/8","1/4","5/16","1/3","3/8","1/2","3/4","1","1.5","2","3","4","6","8","16","32","64","128","256","512","1024"}
local lfo_shapes = {'sine','tri','square','random','up','down'}

local params_per_entry = 14

Expand All @@ -19,30 +20,53 @@ function LFO.init()
end
end

local function scale_lfo(target)
if target.baseline == 'min' then
target.scaled_min = target.min
target.scaled_max = target.min + target.percentage
target.mid = util.linlin(target.min,target.max,target.scaled_min,target.scaled_max,(target.min+target.max)/2)
elseif target.baseline == 'center' then
target.mid = (target.min+target.max)/2
local centroid_mid = math.abs(target.min-target.max) * (target.depth/2)
target.scaled_min = util.clamp(target.mid - centroid_mid,target.min,target.max)
target.scaled_max = util.clamp(target.mid + centroid_mid,target.min,target.max)
elseif target.baseline == 'max' then
target.mid = (target.min+target.max)/2
target.scaled_min = target.max * (1-(target.depth))
target.scaled_max = target.max
target.mid = math.abs(util.linlin(target.min,target.max,target.scaled_min,target.scaled_max,target.mid))
end
end

--- construct an LFO
-- @param string shape The shape for this LFO (options: 'sine','saw','square','random'; default: 'sine')
-- @param string shape The shape for this LFO (options: 'sine', 'tri', 'up', 'down', 'square', 'random'; default: 'sine')
-- @param number min The minimum bound for this LFO (default: 0)
-- @param number max The maximum bound for this LFO (default: 1)
-- @param number depth The depth of modulation between min/max (range: 0.0 to 1.0; default: 0.0)
-- @param string mode How to advance the LFO (options: 'clocked', 'free'; default: 'clocked')
-- @param number period The timing of this LFO's advancement. If mode is 'clocked', argument is in beats. If mode is 'free', argument is in seconds.
-- @param function action A callback function to perform as the LFO advances. This library passes both the scaled and the raw value to the callback function.
function LFO.new(shape, min, max, depth, mode, period, action)
-- @param number phase The phase shift amount for this LFO (range: 0.0 to 1.0,; default: 0)
-- @param string baseline From where the LFO should start (options: 'min', 'center', 'max'; default: 'min')
function LFO.new(shape, min, max, depth, mode, period, action, phase, baseline)
local i = {}
setmetatable(i, LFO)
i.init()
i.scaled = 0
i.raw = 0
i.phase_counter = 0
if shape == 'saw' then
shape = 'up'
end
i.shape = shape == nil and 'sine' or shape
i.min = min == nil and 0 or min
i.max = max == nil and 1 or max
i.depth = depth == nil and 1 or depth
i.depth = depth == nil and 0 or depth
i.enabled = 0
i.mode = mode == nil and 'clocked' or mode
i.period = period == nil and 4 or period
i.reset_target = 'floor'
i.baseline = 'min'
i.baseline = baseline == nil and 'min' or baseline
i.offset = 0
i.ppqn = 96
i.controlspec = {
Expand All @@ -59,27 +83,24 @@ function LFO.new(shape, min, max, depth, mode, period, action)
i.scaled_max = i.max
i.mid = 0
i.rand_value = 0
i.phase = phase == nil and 0 or phase
scale_lfo(i)
return i
end

--- construct an LFO via table arguments
-- eg. my_lfo:add{shape = 'sine', min = 200, max = 12000}
-- @tparam string shape The shape for this LFO (options: 'sine','saw','square','random'; default: 'sine')
-- @tparam string shape The shape for this LFO (options: 'sine', 'tri', 'up', 'down', 'square', 'random'; default: 'sine')
-- @tparam number min The minimum bound for this LFO (default: 0)
-- @tparam number max The maximum bound for this LFO (default: 1)
-- @tparam number depth The depth of modulation between min/max (range: 0.0 to 1.0; default: 0.0)
-- @tparam string mode How to advance the LFO (options: 'clocked', 'free'; default: 'clocked')
-- @tparam number period The timing of this LFO's advancement. If mode is 'clocked', argument is in beats. If mode is 'free', argument is in seconds.
-- @tparam function action A callback function to perform as the LFO advances. This library passes both the scaled and the raw value to the callback function.
-- @param number phase The phase shift amount for this LFO (range: 0.0 to 1.0,; default: 0)
-- @param string baseline From where the LFO should start (options: 'min', 'center', 'max'; default: 'min')
function LFO:add(args)
local shape = args.shape == nil and 'sine' or args.shape
local min = args.min == nil and 0 or args.min
local max = args.max == nil and 1 or args.max
local depth = args.depth == nil and 1 or args.depth
local mode = args.mode == nil and 'clocked' or args.mode
local period = args.period == nil and 4 or args.period
local action = args.action == nil and (function(scaled, raw) end) or args.action
return self.new(shape, min, max, depth, mode, period, action)
return self.new(args.shape, args.min, args.max, args.depth, args.mode, args.period, args.action, args.phase, args.baseline)
end

-- PARAMETERS UI /
Expand Down Expand Up @@ -153,24 +174,6 @@ end

-- SCRIPTING /

local function scale_lfo(target)
if target.baseline == 'min' then
target.scaled_min = target.min
target.scaled_max = target.min + target.percentage
target.mid = util.linlin(target.min,target.max,target.scaled_min,target.scaled_max,(target.min+target.max)/2)
elseif target.baseline == 'center' then
target.mid = (target.min+target.max)/2
local centroid_mid = math.abs(target.min-target.max) * (target.depth/2)
target.scaled_min = util.clamp(target.mid - centroid_mid,target.min,target.max)
target.scaled_max = util.clamp(target.mid + centroid_mid,target.min,target.max)
elseif target.baseline == 'max' then
target.mid = (target.min+target.max)/2
target.scaled_min = target.max * (1-(target.depth))
target.scaled_max = target.max
target.mid = math.abs(util.linlin(target.min,target.max,target.scaled_min,target.scaled_max,target.mid))
end
end

local function change_bound(target, which, value)
target[which] = value
target.percentage = math.abs(target.min-target.max) * target.depth
Expand Down Expand Up @@ -221,15 +224,19 @@ local function process_lfo(id)
else
phase = _lfo.phase_counter * clock.get_beat_sec() / _lfo.period
end
phase = phase % 1
phase = (phase + _lfo.phase) % 1

if _lfo.enabled == 1 then

local current_val;
if _lfo.shape == 'sine' then
current_val = (math.sin(2*math.pi*phase) + 1)/2
elseif _lfo.shape == 'saw' then
elseif _lfo.shape == 'tri' then
current_val = phase < 0.5 and phase/0.5 or 1-(phase-0.5)/(0.5)
elseif _lfo.shape == 'up' then
current_val = phase
elseif _lfo.shape == 'down' then
current_val = 1-phase
elseif _lfo.shape == 'square' then
current_val = phase < 0.5 and 1 or 0
elseif _lfo.shape == 'random' then
Expand All @@ -253,16 +260,16 @@ local function process_lfo(id)
value = util.linlin(0,1,_lfo.scaled_max,_lfo.scaled_min,current_val)
end

if _lfo.shape == "sine" or _lfo.shape == "saw" then
if _lfo.shape == 'sine' or _lfo.shape == 'tri' or _lfo.shape == 'up' or _lfo.shape == 'down' then
value = util.clamp(value,min,max)
_lfo.scaled = value
elseif _lfo.shape == "square" then
elseif _lfo.shape == 'square' then
local square_value = value >= _lfo.mid and max or min
square_value = util.linlin(min,max,_lfo.scaled_min,_lfo.scaled_max,square_value)
square_value = util.clamp(square_value,_lfo.scaled_min,_lfo.scaled_max)
_lfo.scaled = square_value
_lfo.raw = util.linlin(_lfo.scaled_min,_lfo.scaled_max,0,1,square_value)
elseif _lfo.shape == "random" then
elseif _lfo.shape == 'random' then
local prev_value = _lfo.rand_value
_lfo.rand_value = value >= _lfo.mid and max or min
local rand_value;
Expand All @@ -286,7 +293,7 @@ local function process_lfo(id)
end

--- start LFO
function LFO:start()
function LFO:start(from_parameter)
if self.sprocket == nil then
self:reset_phase()
self.sprocket = norns.lfo.lattice:new_sprocket{
Expand All @@ -298,6 +305,9 @@ function LFO:start()
norns.lfo.lattice:start()
end
self.enabled = 1
if not from_parameter and self.parameter_id ~= nil then
params:set('lfo_'..self.parameter_id,2)
end
end
end

Expand All @@ -314,7 +324,7 @@ function LFO:stop()
end

--- set LFO variable state
-- @tparam string var The variable to target (options: 'shape', 'min', 'max', 'depth', 'offset', 'mode', 'period', 'reset_target', 'baseline', 'action', 'ppqn')
-- @tparam string var The variable to target (options: 'shape', 'min', 'max', 'depth', 'offset', 'phase', 'mode', 'period', 'reset_target', 'baseline', 'action', 'ppqn')
-- @tparam various arg The argument to pass to the target (often numbers + strings, but 'action' expects a function)
function LFO:set(var, arg)
if var == nil then
Expand All @@ -339,7 +349,7 @@ function LFO:set(var, arg)
end

--- get LFO variable state
-- @tparam string var The variable to query (options: 'shape', 'min', 'max', 'depth', 'offset', 'mode', 'period', 'reset_target', 'baseline', 'action', 'enabled', 'controlspec')
-- @tparam string var The variable to query (options: 'shape', 'min', 'max', 'depth', 'offset', 'phase', 'mode', 'period', 'reset_target', 'baseline', 'action', 'enabled', 'controlspec')
function LFO:get(var)
if var == nil then
error('scripted LFO variable required')
Expand Down Expand Up @@ -398,7 +408,7 @@ function LFO:add_params(id,sep,group)
params:add_separator("lfo_sep_"..sep,sep)
end

params:add_option("lfo_"..id,"lfo state",{"off","on"},1)
params:add_option("lfo_"..id,"lfo state",{"off","on"},self:get('enabled')+1)
params:set_action("lfo_"..id,function(x)
if x == 1 then
lfo_params_visibility("hide", id)
Expand All @@ -407,16 +417,16 @@ function LFO:add_params(id,sep,group)
self:stop()
elseif x == 2 then
lfo_params_visibility("show", id)
self:start()
self:start(true)
end
self:set('enabled',x-1)
lfo_bang(id)
end)

params:add_option("lfo_shape_"..id, "lfo shape", {"sine","saw","square","random"},1)
params:add_option("lfo_shape_"..id, "lfo shape", lfo_shapes, tab.key(lfo_shapes,self:get('shape')))
params:set_action("lfo_shape_"..id, function(x) self:set('shape', params:lookup_param("lfo_shape_"..id).options[x]) end)

params:add_number("lfo_depth_"..id,"lfo depth",0,100,0,function(param) return (param:get().."%") end)
params:add_number("lfo_depth_"..id,"lfo depth",0,100,self:get('depth')*100,function(param) return (param:get().."%") end)
params:set_action("lfo_depth_"..id, function(x)
if x == 0 then
params:set("lfo_scaled_"..id,"")
Expand All @@ -425,7 +435,12 @@ function LFO:add_params(id,sep,group)
self:set('depth',x/100)
end)

params:add_number('lfo_offset_'..id, 'lfo offset', -100, 100, 0, function(param) return (param:get().."%") end)
params:add_number('lfo_phase_'..id, 'lfo phase', 0, 100, self:get('phase') * 100, function(param) return (param:get()..'%') end)
params:set_action('lfo_phase_'..id, function(x)
self:set('phase',x/100)
end)

params:add_number('lfo_offset_'..id, 'lfo offset', -100, 100, self:get('offset')*100, function(param) return (param:get().."%") end)
params:set_action("lfo_offset_"..id, function(x)
self:set('offset',x/100)
end)
Expand All @@ -436,15 +451,15 @@ function LFO:add_params(id,sep,group)
build_lfo_spec(self,id,"min")
build_lfo_spec(self,id,"max")

local baseline_options;
baseline_options = {"from min", "from center", "from max"}
params:add_option("lfo_baseline_"..id, "lfo baseline", baseline_options, 1)
local baseline_options = {"from min", "from center", "from max"}
params:add_option("lfo_baseline_"..id, "lfo baseline", baseline_options, tab.key(baseline_options,'from '..self:get('baseline')))
params:set_action("lfo_baseline_"..id, function(x)
self:set('baseline',string.gsub(params:lookup_param("lfo_baseline_"..id).options[x],"from ",""))
_menu.rebuild_params()
end)

params:add_option("lfo_mode_"..id, "lfo mode", {"clocked","free"},1)

local mode_options = {'clocked', 'free'}
params:add_option("lfo_mode_"..id, "lfo mode", mode_options, mode_options[self:get('mode')])
params:set_action("lfo_mode_"..id,
function(x)
self:set('mode',params:lookup_param("lfo_mode_"..id).options[x])
Expand All @@ -463,7 +478,9 @@ function LFO:add_params(id,sep,group)
end
)

params:add_option("lfo_clocked_"..id, "lfo rate", lfo_rates_as_strings, 9)
local current_period_as_rate = self:get('mode') == 'clocked' and lfo_rates[lfo_rates_as_strings[self:get('period')]] or lfo_rates[self:get('period')]
local rate_index = tab.key(lfo_rates,self:get('period'))
params:add_option("lfo_clocked_"..id, "lfo rate", lfo_rates_as_strings, rate_index)
params:set_action("lfo_clocked_"..id,
function(x)
if params:string("lfo_mode_"..id) == "clocked" then
Expand All @@ -476,7 +493,7 @@ function LFO:add_params(id,sep,group)
type='control',
id="lfo_free_"..id,
name="lfo rate",
controlspec=controlspec.new(0.1,300,'exp',0.1,1,'sec')
controlspec=controlspec.new(0.1,300,'exp',0.1,current_period_as_rate,'sec')
}
params:set_action("lfo_free_"..id, function(x)
if params:string("lfo_mode_"..id) == "free" then
Expand All @@ -487,7 +504,8 @@ function LFO:add_params(id,sep,group)
params:add_trigger("lfo_reset_"..id, "reset lfo")
params:set_action("lfo_reset_"..id, function(x) self:reset_phase() end)

params:add_option("lfo_reset_target_"..id, "reset lfo to", {"floor","ceiling","mid: rising","mid: falling"}, 1)
local reset_destinations = {"floor","ceiling","mid: rising","mid: falling"}
params:add_option("lfo_reset_target_"..id, "reset lfo to", reset_destinations, reset_destinations[self:get('reset_target')])
params:set_action("lfo_reset_target_"..id, function(x)
self:set('reset_target', params:lookup_param("lfo_reset_target_"..id).options[x])
end)
Expand Down

0 comments on commit 103f620

Please sign in to comment.