Smooth Parameter Automation

Hi,

I’ve been trying to find a comparably easy solution (‘easy’ meaning: memorable, on a medium abstraction level, not to much code, possibly staying within the SP language range) to handle fading and other dynamic parameter operations within a live_loop without being entirely dependant on the runtime of a live_loop (this is to say: if you don’t work with control you will have to wait until the live_loop starts again until some change is going to happen); here is so far my solution using a function fade to provide an appropriate ring of values which will be used in connection with a double tick – in this case to fade amplitude.

type = (ring :up, :down, :wave)

define :fade do |min, max, len, type|
  case type
  when :up
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.ramp
  when :down
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.reverse.ramp
  when :wave
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.mirror
  end
end

live_loop :fade_amen1 do
  stop
  l = fade(0, 1, 5, :down)
  s = sample :loop_amen, beat_stretch: 2, amp: l.tick(:v)
  puts "From #{l.look(:v)} ..."
  control s, amp: l.tick(:v), amp_slide: 2
  puts "... to #{l.look(:v)}."
  sleep 2
end

live_loop :fade_amen2 do
  stop
  l = fade(0, 1, 5, :wave)
  puts ""
  puts l
  with_fx :level, amp: l.tick(:v) do |lev|
    puts "From #{l.look(:v)} ..."
    control lev, amp: l.tick(:v), amp_slide: 2
    puts "... to #{l.look(:v)}."
    sample :loop_amen, beat_stretch: 2
  end
  sleep 2
end

The problem with this solution is I can’t change the fade type e. g. from fading in to fading out (from up to down) without 1st: setting amp to the last reached value and 2nd: reset the tick. Maybe my solution is concpetionally flawed… In an ideal world I would imagine to have a funciton such as my :fade, which would receive a starting and end value (for amp or cutoff or any other parameter I’d like to change over time), a step value to control the speed and the type or direction of fading which would also make sure that a call to the fading function would handle any change in direction (from up to down, in or out aso.) on its own in the propper way.

Hope I could make myself clear (happy to go into detail if not). I’d be grateful for any hints or thoughts about that.

1 Like

Hi,

well, yes, this might be a topic I am the only one who is interested in. Nevertheless I’ll start one more attempt to make this interesting for others as well. Please check out this example to see a real world example (which is to say: a possible scenario e. g. in live coding :wink: ):

use_bpm 120

live_loop :metro do
  sleep 8
end

live_loop :bdrum, sync: :metro do
  at(ring 0,1,2,3,3.75,4,5,6,6.75,7) do
    sample :bd_haus, rate: 0.5
  end
  sleep 8
end

live_loop :amen, sync: :metro do
  3.times do
    sample :loop_amen, beat_stretch: 4
    sleep 4
  end
  sample :loop_amen, beat_stretch: 4, finish: 0.5
  sleep 2
  4.times do
    sample :loop_amen, beat_stretch: 4, num_slices: 8, slice: pick
    sleep 0.5
  end
end

live_loop :bass, sync: :metro do
  at(ring 0, 7.25, 7.75), (ring [:c1, 4], [:bb2, 0.25], [:g2, 0.25]) do |p|
    with_synth :fm do
      with_synth_defaults depth: 1, divisor: 1 do
        play p[0], release: p[1]
      end
    end
  end
  sleep 8
end

# example 1 - smooth fade-in and -out of filter
live_loop :chords_smooth, sync: :metro do
  #stop
  flt = (line 60, 100, steps: 10, inclusive: true).stretch(2).drop(1).butlast.reflect
  with_fx :rbpf, centre: flt.tick, res: 0.90 do |f|
    control f, centre: flt.tick, centre_slide: 8
    with_synth :dsaw do
      with_fx :reverb, room: 0.75, mix: 0.5 do
        at(ring 0.5, 2, 3, 5, 5.25, 6), (ring [:c3, 1], [:f3, 1], [:g3, 2], [:bb3, 0.25], [:d3, 0.25], [:c3, 0.25]) do |p|
          play (chord p[0], '7sus4', num_octaves: 3, invert: (ring 0, 1, 2).choose), release: p[1]
        end
      end
    end
  end
  sleep 8
end

# example 2 - stepwise (every 8 beats/2 bars) opening/closing of filter
live_loop :chords_steps, sync: :metro do
  stop
  flt = (line 60, 100, steps: 10, inclusive: true).reflect
  with_fx :rbpf, centre: flt.tick, res: 0.9 do
    with_synth :dsaw do
      with_fx :reverb, room: 0.75, mix: 0.5 do
        at(ring 0.5, 2, 3, 5, 5.25, 6), (ring [:c3, 1], [:f3, 1], [:g3, 2], [:bb3, 0.25], [:d3, 0.25], [:c3, 0.25]) do |p|
          play (chord p[0], '7sus4', num_octaves: 3, invert: (ring 0, 1, 2).choose), release: p[1]
        end
      end
    end
  end
  sleep 8
end

Do un/comment the stop in either the live_loop :chords_smooth or :chords_steps to hear the difference. Example 1 is like slowly opening and closing the filter (truning the knob). This is what I would like to have an easier solution for (see my first posting).

So, if anyone has an idea, I’d be grateful to go into discussion. And if I did not succeed in arousing your interes - well, than that’s how it goes :wink:

Martin

An interesting problem. I’ll have a think about it. By the way I think you need a live_loop :metro to get it going, or remove the sync :metro.

Hi @robin.newman,

yeah, sure. While editing most of the example got lost… I edited again and now it should be complete.

I am definitely interested in this topic. I have been playing around with sending OSC messages from p5.js to adjust parameters and was considering this very question. I’ve also run into it when playing around with micro-controllers to adjust parameters. I was curious if I could adjust the rate of a looped sample without having to wait until the loop had completed before changing, but making any real time adjustments without waiting out the completion of the loop is of interest to me. However, I’m looking at your code and trying to make some sense of how it is all working, so I don’t really feel like I have much to contribute at this point. but I am following this thread closely!

Here’s a video of what I have been playing with:

Hi @mrbombmusic

that’s actually not too complicated: I started with looking for a way to make fading of any paramater in a smooth way. Which means: make the fade somehow independant of the length of the live_loop it runs in. If I have a loop that runs for 8 beats I don’t want to wait that long until the parameter change and goes to the next value. So it was clear that I had to use control. My initial attempt goes like this (in the code above this is example 2):

  • Provide a ring of values and step through it using tick
  • use a control value and use tick again to advance within the ring
  • if e. g. I have a ring like (ring 0, 0.25, 0.5, 0.75, 1) the initial value for (let’s say) amp should be 0; then the control takes over and sets the amp to 0.25. And here comes, what’s wrong about this: Now, in the next loop run, the 3rd tick does not start at 0.25 but with 0.5 and goes to 0.75 with the next control.
  • so it was clear, I needed another ring for that (and I got input from Sam about that). The ring must look like: (ring 0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1). That is, what the expression: (line 60, 100, steps: 10, inclusive: true).stretch(2).drop(1).butlast in example 1 is about. See the discussion in Manipualation of Rings to get further details about that.
  • now the 1st tick (to stay with the example of the amp value) takes the 0, the following control will take 0.25 with the 2nd tick, and then the 3rd tick will also have the 0.25 because the ring provides every value except the first one (.drop(1)) and the last one (.butlast) twice. (The .reflect in example 1 just let’s the filter value go back, when it has reached the maximum value.)

Hope this makes the background of my solution search more understandable

Now I would like 1. to have a function that provides me with a simple way to construct rings of that kind (which I sketched out here) and furthermore a way to be able to fade in/out a live_loop at any time, which is a bit more complicated because you will have to deal with the tick value to set it back. One way is to a) set the filter, amp or whatever value to the last reached position and then b) reset tick. After that you can use you ring again with the desired values (e. g. first you fade in the loop and then, after a while, you want to fade it out). So I know how, but I was wondering if it is somehow possible to cover that with one conveniant function …

Martin

1 Like

Hi Martin
Had a play this afternoon and came up with this:

define :fade do |min, max, len, type|
  case type
  when :up
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.ramp
  when :down
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.reverse.ramp
  when :wave
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.mirror
  end
end
define :fadeControl do |min,max,duration,type|
  l=fade min,max,11,type #11 ensures 20 steps for each up/down, 40 for wave
  if type==:wave
    dt=duration/40.0 #adjust step interval to give correct total time
  else
    dt=duration/20.0
  end
  #puts l.length,type #uncomment for debugging to check number of steps
  puts "fadeControl #{min} #{max} #{duration} #{type}"
  in_thread do
    t=vt
    tick_reset
    l.length.times do
      control get(:lv),amp: l.tick,amp_slide: dt
      sleep dt
    end
  end
end


live_loop :test do
  with_fx :level,amp: 0 do |lv|
    set :lv,lv
    sample :loop_amen_full,beat_stretch: 8
    fadeControl(0,1,2,:up)  #fade up from 0 to 1 in 2 seconds
    sleep 2
    fadeControl(0.3,1,1,:down) #fade down from 1 to 0.3 in 1 second
    sleep 2
    fadeControl(0.3,1,1,:wave) #fade up and down from 0.3->1-> 0 in 1 second
    sleep 2
    fadeControl(0,0.3,1,:down) #fade down from 0.3 to 0 in 1 second
    sleep 2
    sample :loop_amen_full,beat_stretch: 8 #start another sample to give 16 second loop
    fadeControl(0,0.4,8,:wave) #fade up and down from 0->0.4->0 in 8 seconds
    sleep 8
  end  
end

I put the fx within the live_loop :test so that you could alter and rerun the loop without stopping.

Basically I put the control section in a function inside a thread. I saved the control pointer lv using set :lv and retrieved it where necessary.
The function has 4 parameters:
the min and max amplitude values, to type of fade and the overall time. I set the len parameter internally passed on to your fade function as 11 which gives 20 steps for :up and :down and 40 for :wave. I adjust the step time dt appropriately to give the correct total time required for the fade. If you only want a 10 step face the vaule to use is 6 in which case adjust the divides for the dt calculation to 20.0 and 10.0 rather than 40.0 and 20.0
By interspersing calls to fadeControl (which doesn’t consume time in the live loop, as it runs in a thread) with suitable sleep commands you can place fades where you want them in the live_loop with whatever duration time you wish making sure they don’t overlap subsequent fades. for subsequent fades set the range so that they follow on seamlessly from the previous one. This doesn;t ahve to be so, but you will get a sharp transition if not.
If you want to start the live_loop at full volume adjust the initial amp setting in the fx_level line to amp: 1

Finally if you are not going to reurn the program once started you can then put the with_fx :level outside the loop, plus the line set :lv,lv

EDIT ADDENDUM you can put these two lines outside the live loop, and then comment them out (plus the associated end after the live_loop) once the program is running. You can then re-run and the fx call will still be in force until you actually stop the whole program. Then of course you will have to uncomment the lines so that a new fx is set up again when you next start the program once more.

1 Like

I improved the parameter list for fadeControl to use start and finish, which makes it more logical when entering the data. So you get
fadeControl(1,0.5,2,:down) for example, which fades down from 1 to 0.5 in 2 seconds
and fadeControl(0.3,0.8,4,:up) which fades up from 0.3 to 0.8 in 4 seconds

The adjusted programs are below



define :fade do |min, max, len, type|
  case type
  when :up
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.ramp
  when :down
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.reverse.ramp
  when :wave
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.mirror
  end
end
define :fadeControl do |start,finish,duration,type|
  if type==:down
    min=finish
    max=start
  else
    min=start
    max=finish
  end
  l=fade min,max,11,type #11 ensures 20 steps for each up/down, 40 for wave
  if type==:wave
    dt=duration/40.0 #adjust step interval to give correct total time
  else
    dt=duration/20.0
  end
  #puts l.length,type #uncomment for debugging to check number of steps
  puts "fadeControl #{start} #{finish} #{duration} #{type}"
  in_thread do
    t=vt
    tick_reset
    l.length.times do
      control get(:lv),amp: l.tick,amp_slide: dt
      sleep dt
    end
  end
end

with_fx :level,amp: 0 do |lv|
  set :lv,lv
  live_loop :test do
    
    sample :loop_amen_full,beat_stretch: 8
    fadeControl(0,1,2,:up)  #fade up from 0 to 1 in 2 seconds
    sleep 2
    fadeControl(1,0.3,1,:down) #fade down from 1 to 0.3 in 1 second
    sleep 2
    fadeControl(0.3,1,1,:wave) #fade up and down from 0.3->1-> 0in 1 second
    sleep 2
    fadeControl(0.3,0,1,:down) #fade down from 0.3 to 0 in 1 second
    sleep 2 #there will be 1 second of silence here
    sample :loop_amen_full,beat_stretch: 8 #start another sample to give 16 second loop
    fadeControl(0,0.4,8,:wave) #fade up and down from 0->0.4->0 in 8 seconds
    sleep 8
  end
end

ADDENDUM EDIT
Finally (!!)
here is a much more compact way of writing the live loop above, making use of the at command. It is shown below with the fx wrapper.
The remainder of the program in my previous post remains the same.
Here is a much more compact version of the live loop using the at command. This replaces the live_loop :test and fx wrapper above. The remainder remains as it is.

with_fx :level,amp: 0 do |lv|
  set :lv,lv
  live_loop :test do
    at [0,2,4,6,8],[[0,1,2,:up],[1,0.3,1,:down],[0.3,1,1,:wave],[0.3,0,1,:down],[0,0.4,8,:wave]]do |param|
      fadeControl param[0],param[1],param[2],param[3]
    end
    sample :loop_amen_full,beat_stretch: 8
    sleep 8
    sample :loop_amen_full,beat_stretch: 8
    sleep 8
  end
end

Hi Robin,

this looks very promissing. I might have the opportunity at the weekend to have a closer look. Please give me some time for a feedback which I am eager to give. For now, thanks a lot that you took the time to look into it!

Martin

Fine Martin
I’m having fun playing with this. I’ve now changed things slightly adding the pointer :lv as a parameter inside the fadeControl function. This means that it is possible to have instances fo the function running in differnt live loops at the same time, each with their own fx :level wrapper.
So fade control is now

define :fadeControl do |start,finish,duration,type,pointer|
  if type==:down
    min=finish
    max=start
  else
    min=start
    max=finish
  end
  l=fade min,max,11,type #11 ensures 20 steps for each up/down, 40 for wave
  if type==:wave
    dt=duration/40.0 #adjust step interval to give correct total time
  else
    dt=duration/20.0
  end
  #puts l.length,type #uncomment for debugging to check number of steps
  puts "fadeControl #{start} #{finish} #{duration} #{type} #{pointer}"
  in_thread do
    t=vt
    tick_reset
    l.length.times do
      control get(pointer),amp: l.tick,amp_slide: dt
      sleep dt
    end
  end
end

and a typical call would be
fadeControl(0.3,0.8,2,:up,:lv2)
which would control the fx :level reference by :lv2 fading it up from 0.3 to 0.8 over 2 seconds

So if you had three controlled loops one would be on :lv1 a second on :lv2 and the third on:lv3

Hi Robin, this sounds exactly like what I need. Will get back to you …

Great. I’ve Made further progress. Can now choose which type of parameter to alter so can deal for example with with_fx :pan or with_fx ;reverb. I now have an example with three nested fx for pan, level and reverb and all are controlled independently within the loop at different times.

EDIT ADD
I’m also working on a touchOSC input controller for this. Initial experiments working OK, and so far I’m controlling the amp of a live_loop with fades up and down and “wave” remotely. Now working to allow several controllers together.

I now have a complete working system with two large scale examples.
which I will post separately. One controls five with_fx wrappers associated with two live loops, with sliding sequences built into the code. Each slide starts from the end point of a previous one.

The second uses four wrappers, but this time they are controlled by aTouchOSC screen.

The two fundamental routines fadeSteps (essentially your fade function) and fadeControl are shown below, in a simple example program.

1 Like

Hi Robin, you are very industrious (hope that’s the correct word, never heard or used it but the dictionary told me …). I still need some time to process what you’ve written but will definitely get back to you. I have some questions and some remarks …

The TouchOSC version has a video

code is available at

1 Like

Hi @robin.newman,

I finally had some time to look at your code in more detail. This is nice work! Thanks for looking into that and thanks for the inspiration!

There are however two obstacles which I find using this in the context of live coding:

  1. Due to the nature of live coding I will (hopefully) never stop the code unless I have finished the tune. This means I will place an fx only very rarely outside of a live_loop because I almost always want it to be adjustable by reevaluation.
  2. The other thing I found (unless I tested the wrong code or my SP installation behaves differently than yours) is: Once the fading has been done I want e. g. the volume to stay on the level of the finish value. As far as I checked your examples my own very simple ones behaves in this way: the fading will start again as soon as the live_loop starts again (which is obvious, because the sample’s amp is not set to the finish value and tick_reset is called every new run). Am I right? See this example:
define :fade do |min, max, len, type|
  case type
  when :up
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.ramp
  when :down
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.reverse.ramp
  when :wave
    b = (line min, max, steps: len, inclusive: true).stretch(2).drop(1).butlast.mirror
  end
end
define :fadeControl do |start,finish,duration,type|
  if type==:down
    min=finish
    max=start
  else
    min=start
    max=finish
  end
  l=fade min,max,11,type #11 ensures 20 steps for each up/down, 40 for wave
  if type==:wave
    dt=duration/40.0 #adjust step interval to give correct total time
  else
    dt=duration/20.0
  end
  #puts l.length,type #uncomment for debugging to check number of steps
  puts "fadeControl #{start} #{finish} #{duration} #{type}"
  in_thread do
    t=vt
    tick_reset
    l.length.times do
      control get(:lv),amp: l.tick,amp_slide: dt
      sleep dt
    end
  end
end

with_fx :level,amp: 0 do |lv|
  set :lv,lv
  live_loop :test do
    sample :loop_amen_full,beat_stretch: 8
    fadeControl(0,1,2,:up)  #fade up from 0 to 1 in 2 seconds
    sleep 8
    sample :elec_beep
    sleep 2
  end
end

Now I messed with your code, firstly to understand how it is working and secondly to try to build on it and find a solution to the outlined issues. I did not succeed yet (due to lack of knowledge and experience) but I have at least a better grasp of what’s going on as well as some ideas.

Here are some notes on my ideas (please read this with respect to the following code sketch):

  • You are very right, that the direction of the fade is defined by the min and max values. I build on that and skipped the ‘type’ thing completely; at least for me the type :wave is not necessary or better: this should better be coded as a separate function (actually it could be serving as a sort of LFO).
  • I’d rather would like to set the initial amp value within the live_loop and tried something like: amp: (set :vol, 1) but this does not work.
  • Actually it is my idea that I have to set the initial value just once (hence, the name :wink: ), because
  • I then would like to do something like: if [the current volume of s] != finish then continue with faceControl else set the [current volume of s] to finish (which then would be the starting point for the next fade)
  • I think it migt be a solution to know the current value of :s's amp because if it has reached the desired fading finish value fadeControl does not have to get called. Only if the current :amp differs from the finish value …

It might well be that I am thinking to complicated or into a very wrong direction. It’ll be nice if you had a look at that. I am definitely stuck.

Here is my unfinished framing which although not working might show what I am aming at:

define :fadeControl do | start, finish, duration, pointer |
  l = (line start, finish, steps: 11, inclusive: true).stretch(2).drop(1).butlast.ramp
  dt = duration / 20.0
  in_thread do
    tick_reset
    l.length.times do
      control get(pointer), amp: l.tick, amp_slide: dt
      sleep dt
    end
  end
end

set :vol, 0

live_loop :test2 do
  set :s, (sample :loop_amen, beat_stretch: 4, amp: get(:vol))
  fadeControl(get(:vol), 1, 2, :s) # if current_vol of :s != finish go on 
  sleep 4
end

Hi Martin
I am very busy this weekend so haven’t time to look at your answer in detail till Monday. However you can use it with the fx inside the loop. I have done do with three nested loops. Also you can do the control using at function or manually so it can happen independently of the loop and shouldn’t keep resetting.

Will give fuller answer later.

@robin.newman, thanks, whenever you have time and leisure to do so …

Hi Martin
I have had another look at things and I think the code below does what you want. The fadeControl function can be used to control other opts, but here is set to control the volume (:amp) of the sample in loop :test

The fade is controlled by the function setVol which needs two parameters: the final amp level (normal range 0->1 (although you can go higher) It always starts from the current volume level (held in :vol) so the fade is smooth without any jumps. It runs independently of the live_loop :test duration. (8 beats) and can for example last for 16 beats, or 2 beats. The initial volume the first time the program runs is set to 0 in the defonce :setup code, although this could be set to 1 if you wish. Note for such a change to take place, you would have to quit and restart SP or use the override option for defonce.

To test, just run the program. (The initial fade setup 0->0.5 will happen. When it has finished, just set a new fade eg setVol 1,6 and press run again, and so on.

#function returns step values for the fade
define :fade do |start, finish, len|
  b = (line start, finish, steps: len, inclusive: true).stretch(2).drop(1).butlast.ramp
  return b #cam be omitted but makes explicit what happens
end

define :fadeControl do |start,finish,duration,pointer,opt|
  return if start==finish
  l=fade start,finish,11 #11 ensures 20 steps for each up/down
  dt=duration/20.0
  puts "fadeControl #{start} #{finish} #{duration} #{pointer} #{opt}"
  in_thread do
    t=vt
    tick_reset
    l.length.times do
      #note that amp: is equivalent to :amp=> This enables use of :amp stored in a variable
      # similarly for pan: or phase: or any other similar
      # note also how the corresponding _slide is created.
      control get(pointer),opt=> l.tick,(opt.to_s+"_slide").to_sym => dt
      sleep dt
      set :vol,l.look #update current volume at end of each step
    end
  end
end

#set inital volume value on first run
defonce :setup do
  set :vol,0
end

live_loop :test do
  lv=sample :loop_amen_full,beat_stretch: 8,amp: get(:vol) #sample volume will be controlled
  set :lv,lv
  sleep 8
end

#defines setvol so that only final vol and duration need to be specified for each fade
define :setVol do |value,duration|
  fadeControl get(:vol),value,duration,:lv,:amp
end

setVol 0.5,4 #This sets and carries out the fade

EDIT I also have a version that will control volume and pan independently at the same time if you want that.

1 Like

Hi Robin, again thanks, did a quick check. Something seems to be wrong (might be my installation). I will need some time to check again an give some usefull feedback but will definitely look into this because it is on my live coding todo list.

Martin