How to do parameter tracks

I’m trying to figure out a neat way of tweaking individual sample/synth parameters in patterns without making it a big mess of case/if-then-else blocks. Let’s say I have a melody:

at(line(0, 4), [:c, :e, :g, :b]) do |n|
  play n
end

If I now want to add different velocities to the track I’d normally tweak the pattern:

at(line(0, 4), [[:c, 1], [:e, 0.5], [:g, 0.5], [:b, 1.0]]) do |(n, v)|
  play n, amp: v
end

That’s already pretty messy, imagine if I want to control more parameters - each note description will get quite complicated.

An alternative I’ve tried is to change the play body instead - especially if I only need a small number of notes to have special behavior:

at(line(0, 4), [:c, :e, :g, :b]) do |t, n, i| 
  play n, amp: case i                                                                                  
  when 0, 4 
    1 
  else 
    0.5 
  end 
end 

But you can see how this will also get complicated for multiple parameters. So I wanted to try separating these concerns by running a separate “track” on the side for each parameter, conceptually something like this:

at(line(0, 4), [:c, :e, :g, :b]) do |n|                                                                
  play n, amp: get(:amp) 
end 
 
at(line(0, 4), [1, 0.5, 0.5, 1]) do |a| 
  set :amp, a 
end 

I was hoping having these as separate track/layers like this would make multiple parameters easier to manage.

This does not work as-is though, because the order of set/get triggered from the at is not deterministic - the amplitudes will be all over the place for different runs. So I added a time_warp(-0.1) around the control track which fixed that problem. But I really want to run these in synced live loops, like this:

live_loop :tock do 
  sleep 4 
end 
 
live_loop :hey do 
  sync :tock 
  at(line(0, 4), [:c, :e, :g, :b]) do |n| 
    play n, amp: get(:amp) 
  end 
end 
 
live_loop :control do 
  time_warp(-0.001) do 
    sync :tock 
    at(line(0, 4), [1, 0.25, 1, 0.25]) do |a| 
      set :amp, a                                                                                      
    end 
  end 
end 

But however I sprinkle the time_warp in the :control loop, I can’t seem to get it to work correctly. Either it does not work on the parameter change at time 0 (due to the sync, I assume), it’s off-by one entirely or it causes threads to crash due to falling behind.

Does anyone know how to get something like the above to work?

Or alternatively, does anyone have a nice way of programming multiple parameter patterns to overlay the any note and time patterns?

Thanks!

2 Likes

Sounds like a really interesting question that requires a bit of deep thought. I’m currently super busy for the next few days, so if I haven’t replied by mid-week please ping me again so it comes back to my attention :slight_smile:

hi @siimphh,

why don’t use something like that :

riff_A_notes = [:c3, :d3, :r, :g3]
riff_A_velocity = [1, 0.5, 2, 0.5]
riff_A_duration = [2, 0.5, 0.5, 1]

at [0, 8] do
  use_synth :piano
  riff_A_notes.length.times do
    play riff_A_notes.tick('notes'), amp: riff_A_velocity.tick('velocity')
    sleep riff_A_duration.tick('sleep')
  end
end

at [0, 4, 8] do
  4.times do
    sample :drum_bass_soft
    sleep 1
  end
end

EDIT:
After reading martin’s code we can write this

at [0, 8] do
  use_synth :piano
  riff_A_notes.length.times do
    play riff_A_notes.tick, amp: riff_A_velocity.look
    sleep riff_A_duration.look
  end
end

Hi,

yeah, this a question (or rather belongs to a set of questions) I also have asked myself especially with respect to live coding: easy manipulation options without loosing the oversight.

So I do not have any ideal solution but can maybe some ideas (I have some examples in this context collected here, here and here).

Initially I think I prefer a ‘multiple ring solution’ (without at though at has the advantage that it somehow works like we would also notate). But this definitely is a matter of taste and also of the specific use case you are thinking of.

I am thinking of something like:

live_loop :three_rings do
  notes = (ring :g3, :c3, :r, :bb3, :c3, :bb3, :f3, :g3)
  
  # especially for live coding: I like this use of rings because you can 
 # easily extend or mess around with it ... 
  # notes = (ring :g3, :c3, :r, :bb3, :c3, :bb3, :f3, :g3) +
  #         (ring :g3, :c3, :r, :bb3, :c3, :bb3, :f3, :g3).reverse
  
  volume = (ring 1, 0.25, 0.15, 0.75, 0.25, 0.5, 0.75, 1)
  release = (ring 0.5, 0.25, 0.15, 0.5, 0.1, 0.15, 0.1, 0.25)
  
  play notes.tick, amp: volume.look, release: release.look
  
  # 'synth' might be useful to have more control
  #s = synth :fm, note: notes.tick, depth: 1, divisor: 1, release: release.look, amp: volume.look, cutoff: 130
  #control s, depth: 6, depth_slide: 0.25, cutoff: 70, cutoff_slide: 0.025
  
  sleep 0.25
end

Not really an answer to your question but IMHO an interesting idea is to combine the timing of at with the (shorter) runtime of a live loop:

live_loop :pattern do
  use_bpm 120
  nts = (ring :a3, [:c, :d].choose, :e, :g, [:a4, :r].choose).tick
  at (ring 0.5, 0.75, 1.25) do
    s = synth :fm, note: nts, release: 0.125, depth: 0.5, divisor: 0.5
    control s, depth_slide: 0.125, depth: 2
  end
  sleep 0.5
end

Similar to both Martin and nlb above, ticking through a set of rings, one for each opt, is also the best that I’ve been able to use at the moment… I have once or twice attempted to do the same where these rings are stored in and retrieved from the time-state and used between threads/live_loops, but that’s sometimes a little more fiddly :slight_smile:

I quite like the idea of using just one ring and apply ‘tick’ repeatedly but this does not really enhance readability but rather the opposite: makes things somehow obscure (but short).

yep that’s why i wrote with special tick for each parameter :slight_smile:

The issue with using full parameter tracks is that all tracks are fully specified at the same frequency as the notes/times track. But now writing this down, I suppose the way around this would be more clever about merging patterns, starting with something as simple as:

live_loop :chord do 
  at( 
    line(0, 4, steps:8), 
    [:c, :e, :g, :b, :g, :e, :g, :c].zip( 
      [1.0, 0.5] * 4, 
      stretch([1.0, 0.5], 4), 
    ), 
  ) do |(n, v, d)| 
    synth :fm, note: n, amp: v, sustain: d                                                                  
  end 
  sleep 4 
end 

Then the “heavy lifting” would be done directly on the patterns without dipping into the more fickle Sonic Pi timeline synchronization realm.

The only thing missing appears to be utility functions could join patterns in time/rythm domain.