A sequencing / live looping utility lib

I’m new here and I’m having a lot of fun (thanks @samaaron :star_struck: ). I was wondering if any of you would be interested in a little lib / microframework called “seq” I made? It’s just a few functions that sort of automate and clean up things in 2 domains:

  • the loops as sequencer tracks (sync, mute / solo track, skip to time, generic ticking clock)
  • a meter utility to manipulate beats, time signatures and cue generation

I define my tracks and meters like this:

# defining a meter, 7/4 with custom onbeats (the bools map)
seq_meter_info :seven_quarter, (bools 1, 0, 0, 1, 0, 0, 0), period: 0.25

# storing all meters in a easy to access hash
meters = seq_map (seq_meter_info ...), (seq_meter_info ...)

# same pattern, defining a track, it's on state and optional
# active sections (as an array of tick ranges) -> 7*8 or
# eight of my seven_quarters bars
seq_track_info :lead, true, sections: [(7*8..)]

# same pattern, storing them into a track map (for soloing the track)
tracks = seq_map (seq_track_info ...), (seq_track_info ...)

Then I can create a meter clock:

seq_meter_clock :main, # tick name and prefix for the cues
  meters[:seven_quarter], # the meter determines how the standard cues work
  [:beat, :onbeat, :offbeat] # custom cue selection prefixed here with main_
# available cues: beat, onbeat, offbeat/backbeat,
# downbeat/bars, upbeat, oddbars, evenbars, hyperbeat

I put it in the main loop and I add the commented skip! action (for easy triggering):

#skip! :main, 7*2 <- sets the :main tick at 2 bars from the start
live_loop :main do
  seq_meter_clock :main,
    meters[:seven_quarter],
    [:beat, :onbeat, :offbeat] # ticking and firing cues is taken care of
end

I can create a track and tie it to the clock:

# first arg is the cues it listens to, second is the track info
seq_track [:main_offbeat], tracks[:lead] do |message|
  play_chord chord(:a3, :m) # just some chord
end

The tracks are set in loops too, with mute! and solo! actions:

#solo! tracks, :lead
#mute! tracks[:lead]
live_loop :lead do
  seq_track ...
end

#solo! tracks, :thrill
#mute! tracks[:thrill]
live_loop :thrill do
  seq_track ...
end

Would any of you see a use for it? It’s difficult from where I stand to understand if I made something really cool or an unusable gimmick :yum:

1 Like

A little test track I made using it (30 lines of code, yay). It’s not Mozart.

It would be amazing for me to be able to access this library!

Oh, cool! Well, it still has some training wheels but I’m working out the kinks as I’m polishing my first Sonic Pi composition :partying_face: (well, second one, but first one that is not a joke).

I’m more used to traditional song composition than live looping, so mixing the 2 and hacking my way into the concepts has been a real pleasure!

It looks quite a bit different from what it looked like in the example though, because yeah the code is less than a week old :sweat_smile: So I’d rather stabilize it a bit before I sharing it, but that’s what my current song is about, so: soon!

Quick overview:

  • seq_map has been taken out as it’s basically a hash
  • generated cues also available as tests like play 65 if is_downbeat? time
  • seq_track includes the live_loop
  • apply_fx to allow fx chaining for a block
  • polyrhythms

I’ll be done for now once I get this composition out, I think. It’s swallowing the rest of my life pretty quick :smiling_face_with_tear:

Ok, track done, I’ll do a specific post about it I guess even if I feel quite ashamed of the final production :sweat_smile: Baby steps!

I’m currently revising my lib because there was a lot to be improved upon. Some of my “features” were just hindrances at a larger scale. The more you know.

Hey @vino.exe , here it is! It’s a lot more fun now and so I was pumped enough to make a fake ad for it :joy:

All your sequencing needs (just kidding) under 230 lines of code! Call now +XXX-YYY-ZZZ

code
# seq, a sequencer lib for sonic pi
# author: pebblepoison (https://soundcloud.com/briar-p-marten)
# v2, rework to fix the many many conceptual flaws :p

# SCORE NOTATION

def seq_meter pattern, precision: 1
  { pattern:, precision:, bar_count: pattern.length }
end

def seconds_to_ticks meter, seconds
  seconds * meter[:precision]
end

def ticks_to_seconds meter, ticks
  ticks * 1.0 / meter[:precision]
end

def seq_compose meters, sections
  playing = 0
  sections = sections.flatten
  tracks = sections.map { |s| s[:tracks] } .flatten.uniq
  allowed = tracks.map { |name| [name, true] } .to_h
  
  return { playing:, clocks: sections[0][:clocks], tracks:, allowed:, sections:, meters: }
end

def is_playing? composition, track_name
  return false unless composition[:allowed][track_name]
  
  composition[:sections][composition[:playing]][:tracks].include? track_name
end

def seq_section clocks, duration, tracks
  { clocks:, duration:, tracks: }
end

def refresh_score composition, ticks
  count = 0
  
  composition[:playing] = composition[:sections].find_index do |section|
    return if section[:duration] == -1
    
    count += section[:duration]
    
    count > ticks
  end
  
  stop if composition[:playing].nil?
  
  composition[:clocks] = composition[:sections][composition[:playing]][:clocks]
end

# TRACK BUILDING

def seq_track composition, name, signals
  signals.map do |signal|
    live_loop :"#{name}_track_#{signal}" do
      msg = sync signal
      
      stop if composition[:playing].nil?
      
      yield msg if is_playing? composition, name
    end
  end
end

def seq_effects *fx_list, controls: {}, &block
  fx = fx_list.shift
  
  unless fx_list.length.zero?
    with_fx *fx do |ctrl|
      controls = { **controls, "#{fx[0]}_control": ctrl }
      seq_effects *fx_list, controls: controls, &block 
    end
  else
    with_fx *fx do |ctrl|
      block.call ({ **controls, "#{fx[0]}_control": ctrl })
    end
  end
end

def seq_sampler *samples
  samples.each_with_index do |s, i|
    sample s[0], *(s.length > 2 ? s[2..] : [])
    sleep s[1] unless s[1] == 0 or samples.length - 1 == i
  end
end

# live commands

def solo! composition, name
  composition[:allowed].keys.each do |key|
    composition[:allowed][key] = key == name
  end
end

def mute! composition, name
  composition[:allowed][name] = false
end

# PERFORMANCE SETUP

def seq_clock name, signals: {beat: -> _ {true}}, precision: 1, composition: {}, &block
  live_loop :"#{name}_clock", delay: 1 do |old_msg|
    old_msg = {} if old_msg == 0 or old_msg.nil?
    
    play_head = play_head! name
    tick_set name, play_head.nil? ? look(name) : play_head
    
    msg = {
      **old_msg,
      "#{name}_ticks": look(name),
      clocks: composition[:clocks].dup.freeze,
      section: composition[:playing],
    }
    
    stop if composition[:playing].nil?
    
    if composition[:clocks].include? name
      refresh_score composition, look(name)
      
      msg = yield msg
      
      signals.each do |signal, test|
        cue signal, **msg if test.call look(name)
      end
    end
    
    tick_set name, look(name) + 1
    sleep ticks_to_seconds ({ precision: }), 1
    
    msg
  end
end

def seq_meter_clock meters, name, signal_names, composition: {}, &block
  seq_clock name,
    signals: (seq_meter_signals name, meters[name], signal_names),
    precision: meters[name][:precision],
  composition: composition do |msg|
    msg = msg == 0 ? {} : msg
    block.call ({ **msg, "#{name}_seconds": (ticks_to_seconds meters[name], look(name)) })
  end
end

def seq_meter_signals name, meter, signal_names
  signal_names.map do |signal_name|
    [:"#{name}_#{signal_name}", case signal_name
     when :beat
       -> _ { true }
     when :onbeat
       -> ticks { is_onbeat? meter, ticks }
     when :offbeat, :backbeat
       -> ticks { is_offbeat? meter, ticks }
     when :downbeat, :bar
       -> ticks { is_downbeat? meter, ticks }
     when :upbeat
       -> ticks { is_upbeat? meter, ticks }
     when :oddbar
       -> ticks { is_oddbar? meter, ticks  }
     when :evenbar
       -> ticks { is_evenbar? meter, ticks  }
     else
       -> _ { false }
    end]
  end.to_h
end

def seq_trigger msg, clock_name, ticks
  trigger! msg, clock_name, ticks if ticks == look(clock_name)
end

# live commands

def play! clock_name, ticks
  set :"#{clock_name}_play_head", ticks
end

def play_head! clock_name
  key = :"#{clock_name}_play_head"
  play_head = get[key]
  
  set key, nil unless play_head.nil?
  
  return play_head
end

def trigger! msg, clock_name, ticks
  cue :"#{clock_name}_trigger_#{ticks}", **msg
end

# signal tests

def is_onbeat? meter, ticks
  meter[:pattern][ticks]
end

def is_offbeat? meter, ticks
  not meter[:pattern][ticks]
end

def is_backbeat? meter, ticks
  is_offbeat? meter, ticks
end

def is_downbeat? meter, ticks
  ticks % meter[:bar_count] == 0
end

def is_bar? meter, ticks
  is_downbeat? meter, ticks
end

def is_upbeat? meter, ticks
  (ticks + 1) % meter[:bar_count] == 0
end

def is_oddbar? meter, ticks
  (ticks * 1.0 / meter[:bar_count]) % 2 == 0
end

def is_evenbar? meter, ticks
  (ticks * 1.0 / meter[:bar_count]) % 2 == 1
end


I’ll explain a bit so you hopefully know what to expect.
I’ll also include below a demo track I made today as this might be a bit dense :sweat:

  • score notation: seq_meter, seq_compose and seq_section to create scores with specific time signatures

    • as seq_meter has a precision: option, I have added ticks_to_seconds and seconds_to_ticks to ease the conversion between sequencer ticks and bpm-dependent sonic seconds
    • because sec_compose takes a potentially multi-dimensional array, repeatable sections can be written the array * operator (compose ... [ section 1, section 2, [ repeatable section ] * 4 ])
    • a live performance score can just be a section with a duration of -1 as it will loop forever
  • track building: seq_track, seq_effects and seq_sampler to quickly build and have access to your instruments, tracks and patterns while minimizing the clutter

    • solo! and mute! convenience functions that are easy to trigger live
    • seq_track checks whether it’s allowed to play depending on the section and stops at the end of the score
    • it includes the live loop and the cue binding
    • seq_effects is an fx chainer (use with caution :warning:) that collects its sliders into a hash following the convention fx_name + _control
    • seq_sampler chains samples, it’s like a very basic drum machine
  • performance setup: clocks, metered cue builders and play head positioning to configure you performance or development

    • seq_meter_clock is a live loop that binds together meter and composition
    • it generates standard cues based on your meter (simple beats, off beats, odd or even measures…) and prefixes the name with your clock name to avoid conflicts
    • by default, it just sends a basic beat for every tick, you have to expose the standard signals you want when calling the clock
    • all the cue tests are also available as functions (ex: is_onbeat? )
    • seq_meter_clock is based on the simpler seq_clock which doesn’t require a meter
    • play! function to position the play head at a specific tick on run start
    • seq_trigger and the live equivalent trigger! to send a cue at a specific beat (the live version just allows sending it whenever as it is not tied to tick progression), useful for long samples that should only happen once at a specific time

I still need to work on parallel clocks: currently you can theorically set them up but you’d be better off with a live loop. I want to set up 2 possibilities: a “snap” clock that has a meter start at the beginning of every section they’re involved in, and a “free” clock that just runs in parallel and doesn’t care about composition at all. Right now I’ve just got a very mid in-between. But hey, it’s a start :+1:

And here is the demo track.

code
run_file "<your path to>/seq2.rb"

# Millenium
# A demo for seq2

# SCORE

meters = {
  fq: (seq_meter (bools 1, 0, 1, 0), precision: 4),
  fh: (seq_meter (bools 1, 0, 1, 0), precision: 2),
}

# duration shortcuts
fq05 = ticks_to_seconds meters[:fq], 0.5
fq1 = ticks_to_seconds meters[:fq], 1
fq2 = ticks_to_seconds meters[:fq], 2
fq4 = ticks_to_seconds meters[:fq], 4
fq8 = ticks_to_seconds meters[:fq], 8

fh05 = ticks_to_seconds meters[:fh], 0.5
fh1 = ticks_to_seconds meters[:fh], 1
fh4 = ticks_to_seconds meters[:fh], 4

millenium_score = seq_compose meters, [
  (seq_section [:fq], 4*8, [:pads, :step_beat]),
  [
    (seq_section [:fq], 8, [:pads, :strong_beat, :wawa]),
    (seq_section [:fq], 8, [:pads, :strong_beat])
  ] * 2,
  (seq_section [:fq], 8, [:woosh, :crowd_call, :wawa]),
  (seq_section [:fq], 8, [:step_beat]),
  [
    (seq_section [:fq], 4*2, [:pads, :strong_beat]),
    (seq_section [:fq], 8, [:pads, :strong_beat, :wawa]),
  ] * 4,
  (seq_section [:fq], 8, [:woosh, :wawa_end]),
  (seq_section [:fh], 4*8, [:pads, :strong_beat]),
]

millenium_live = seq_compose meters, [
  (seq_section :fq, -1, [:pads, :wawa, :wawa_end, :woosh, :strong_beat, :step_beat, :crowd_call]),
]

# composition selection
millenium = millenium_score

# TRACKS

#solo! millenium, :pads
#mute! millenium, :pads
seq_track millenium, :pads, [:fq_downbeat, :fh_downbeat] do |msg|
  m4 = (msg[:clocks].include? :fq) ? fq4 : fh4
  
  seq_effects [:slicer, amp: 1.5] do
    play (chord msg[:root_note] + 48, :major), decay: 0, sustain: m4, release: 0.05
  end
end

#solo! millenium, :wawa
#mute! millenium, :wawa
seq_track millenium, :wawa, [:fq_oddbar] do |msg|
  seq_effects [:tremolo, phase: fq1, depth: 1] do
    play msg[:root_note] + 72, attack: 0, decay: 0, sustain: fq8, release: 0.05
  end
end

#solo! millenium, :wawa_end
#mute! millenium, :wawa_end
seq_track millenium, :wawa_end, [:fq_oddbar] do |msg|
  seq_effects [:tremolo, phase: fq1, depth: 1], [:octaver], [:echo], [:reverb], [:pitch_shift, pitch_slide: 1] do |sliders|
    play msg[:root_note] + 72, attack: 0, decay: 0, sustain: fq8, release: 0.05
    
    4.times do |i|
      control sliders[:pitch_shift_control], pitch: -i
      sleep fq2
      
      use_bpm_mul 0.5
    end
  end
end

#solo! millenium, :woosh
#mute! millenium, :woosh
seq_track millenium, :woosh, [:fq_oddbar] do |_|
  seq_effects [:flanger, feedback_slide: 1, amp: 0.2] do |sliders|
    use_synth :bnoise
    
    play 48, attack: fq1, decay: 0, sustain: fq4+fq1, release: fq2
    sleep fq4 + fq2
    control sliders[:flanger_control], feedback: 0.5
  end
end

#solo! millenium, :strong_beat
#mute! millenium, :strong_beat
seq_track millenium, :strong_beat, [:fq_downbeat, :fh_downbeat] do |msg|
  m05 = (msg[:clocks].include? :fq) ? fq05 : fh05
  m1 = (msg[:clocks].include? :fq) ? fq1 : fh1
  
  seq_sampler [:bd_haus, m1], [:bd_haus, m05, amp: 0.8], [:bd_haus, m05, amp: 0.8],
    [:hat_noiz, 0], [:sn_dolf, m1], [:bd_haus, m05, amp: 0.8], [:bd_haus, m05, amp: 0.8]
end

#solo! millenium, :step_beat
#mute! millenium, :step_beat
seq_track millenium, :step_beat, [:fq_onbeat] do |msg|
  seq_sampler [:bd_haus, fq1], [:hat_noiz, fq1, amp: 0.8]
end

#solo! millenium, :crowd_call
#mute! millenium, :crowd_call
seq_track millenium, :crowd_call, [:fq_trigger_66] do |_|
  # Large crowd outdoor screaming and yelling during a meeting in the street of Mexico City by felix.blume -- https://freesound.org/s/214147/ -- License: Creative Commons 0
  sample "<your path to>/crowd call.wav", amp: 0.5
end

# PERFORMANCE

use_bpm 60

#play! :fq, 80
seq_meter_clock meters, :fq, [:onbeat, :downbeat, :oddbar], composition: millenium do |msg|
  msg[:root_note] = [0, -2, 3, -4].ring.tick if is_downbeat? meters[:fq], msg[:fq_ticks]
  
  #trigger! msg, :fq, 66 if msg[:fq_beat] / 4.0 % 16 == 0
  seq_trigger msg, :fq, 66
  
  msg
end

#play! :fh, 40
seq_meter_clock meters, :fh, [:downbeat], composition: millenium do |msg|
  msg[:root_note] = [-2, -4, 1, -6].ring.tick if is_downbeat? meters[:fh], msg[:fh_ticks]
  msg
end

It can be fun as a live loop or a composed track so feel free to tweak and break it :wink:

Also, I wouldn’t recommend using the precision option for seq_meter as a way to change bpm, I did that because I couldn’t think of another use of a 2nd clock on this demo.