From 103f620fb7fd544080719b25e73bace507daa358 Mon Sep 17 00:00:00 2001 From: dan derks Date: Sat, 22 Jul 2023 03:35:38 -0700 Subject: [PATCH] lfo library: v2 (#1692) * 'saw' -> 'tri', add 'up' and 'down', fix init * build params from lfo spec * add phase --- lua/lib/lfo.lua | 122 +++++++++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 52 deletions(-) diff --git a/lua/lib/lfo.lua b/lua/lib/lfo.lua index f8c1f7729..e0c92cb06 100755 --- a/lua/lib/lfo.lua +++ b/lua/lib/lfo.lua @@ -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 @@ -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 = { @@ -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 / @@ -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 @@ -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 @@ -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; @@ -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{ @@ -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 @@ -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 @@ -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') @@ -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) @@ -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,"") @@ -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) @@ -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]) @@ -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 @@ -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 @@ -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)