Back in crazy land again -- please help me figure this one out

Okay, I’m stumped once again by strange behavior.
I’m working on an ambitious project that loads two long files with eval_file. Due to size limits, I can’t put them in the main messages, but one (called YummyFillings.rb) contains a bunch of useful methods that allow me to create envelopes and lfos on any slideable param, trancegates, easily transpose samples, etc.
The other long file (called arrange_buggy.rb) will allow me to build arrangements of multiple instruments, each of which can have its own rhythm, chords and melody, as well as arbitrary defaults, effects, envelopes, lfos and trancegates.
As you can imagine, it’s a beast. I’m in the midst of testing new features I just added (the envelopes, lfos and trancegates), when I ran into something that crashes sonic pi.
Here’s the code that works just fine:

eval_file "C:/Users/harry/Desktop/Scripting/SonicPi/YummyFillings.rb"
eval_file "C:/Users/harry/Desktop/Scripting/SonicPi/arrange_buggy.rb"


debugmode = true

whole = 4.0
half =2.0
quarter =1.0
eighth =0.5
sixteenth =0.25
dotted =1.5
triplet =2.0 / 3

use_bpm 120

voice = :blade

arrangement = {voice => "4w e4"}
lfos = {voice => ["lfo amp, 4*whole, quarter"]}
defaults = {voice => "cutoff: 40"}
effects = {voice => [":krush",":slicer"]}
envelopes = {voice => ["cutoff,half*dotted,quarter,whole*2,whole,40,100,70"]}

arrange arrangement, defaults, effects, nil

But when I change that last “nil” to “envelopes” on the last line, sonic pi crashes! It even crashes after a fresh reboot:

eval_file "C:/Users/harry/Desktop/Scripting/SonicPi/YummyFillings.rb"
eval_file "C:/Users/harry/Desktop/Scripting/SonicPi/arrange_buggy.rb"


debugmode = true

whole = 4.0
half =2.0
quarter =1.0
eighth =0.5
sixteenth =0.25
dotted =1.5
triplet =2.0 / 3

use_bpm 120

voice = :blade

arrangement = {voice => "4w e4"}
lfos = {voice => ["lfo amp, 4*whole, quarter"]}
defaults = {voice => "cutoff: 40"}
effects = {voice => [":krush",":slicer"]}
envelopes = {voice => ["cutoff,half*dotted,quarter,whole*2,whole,40,100,70"]}

arrange arrangement, defaults, effects, effects

WTF?!?
What’s going on here? How do I handle this?
Is it because I’m defining so many methods? But if so, why would nil work and envelopes crash?
I’m stumped!
Any ideas how to address this?
Thanks!
PS Long files will be posted in replies.
PPS Feel free to use YummyFillings.rb for your own use. And if you find any bugs let me know. I plan to do a more public rollout once I have arrange working.

Here’s the content for YummyFillings.rb.

##| YummyFillings.rb
##| Useful constants and functions for sonic pi
##| Harry LeBlanc 2024, gnu public license
##| HarryLeBlancLPCC@gmail.com




##| useful constants for specifying time intervals in notes
##| assumes one beat is a quarter note
##| allows combinations like dotted * eighth or quarter * triplet


whole = 4.0
half =2.0
quarter =1.0
eighth =0.5
sixteenth =0.25
dotted =1.5
triplet =2.0 / 3


debugmode = true

#scrub
#small utility function to clean nils with the specified value (defaults to "")
define :scrub do |value, cleanvalue = ""|
  (value == nil ? cleanvalue : value)
end


##| debugprint is a utility function to optionally print out debugging messages,
##|   controlled by the debugmode variable. If not set, defaults to false and prints nothing.
##|   label: a text string to explain what the value means.
##|   value: the value being displayed for debugging purposes. If nil, just displays the label.

define :debugprint do |label, value=nil|
  if !local_variables.include? "debugmode".to_sym
    debugmode = false
  end #if debugmode defined
  
  if debugmode
    puts scrub(label).to_s  + scrub(value).to_s
  end #if debug
end #define


##| samplebpm -- utility to return the bpm of any sample loop.
##|   thissample: the sample to extract the bpm from.
##|   beats: the number of beats used to calculate bpm. Defaults to 4
##| example:
##| puts samplebpm :loop_amen
##| puts samplebpm :loop_amen_full, 16



define :samplebpm do |thissample, beats=4|
  with_sample_bpm thissample, num_beats: beats do
    current_bpm
  end
end






##| transposesample
##| transposes a sample up or down by specified rpitch, while pitch_stretching to keep tempo.
##| You may need to fiddle with time_dis, window_size and pitch_dis to tweak the sound.
##| example:
##| mysample = "D:\\Loops\\Afroplug - Soul and Jazz Guitar Loops\\looperman-l-6258600-0353860-spilled-coffee.wav"
##| [90, 120, 150].each do |thisbpm|
##|   [0, -5, 3, 7].each do |thispitch|
##|     use_bpm thisbpm
##|     transposesample mysample, 16, thispitch
##|     sleep 16
##|   end
##|   sleep 2
##| end


define :transposesample do |thissample, pitch_stretch, rpitch, \
    time_dis=0.01, \
    window_size=0.1, \
    pitch_dis=0.01|
  ratio = midi_to_hz(60 + rpitch) / midi_to_hz(60)
  puts "ratio: " + ratio. to_s
  sample thissample \
    , pitch_stretch: pitch_stretch * ratio \
    , rpitch: rpitch \
    , time_dis: time_dis \
    , window_soze: window_size \
    , pitch_dis: pitch_dis
end


##| envelope -- applies an adsr envelope to any slideable param on any synth note or sample.
##|   best results when you set the sample/note's modulated value to the startlevel when playing the sample/note,
##|   otherwise you'll hear an audible glitch at the beginning of the sound.
##|   handle -- the node returned by sample/play commands.
##|   param -- the parameter being modulated by the envelope.
##|   attack -- attack time, in beats.
##|   decay -- decay time, in beats.
##|   sustain -- sustain time, in beats.
##|   relase -- release time, in beats.
##|   startlevel -- the level at the bottom of the attack phase. Scaled to what the param expects.
##|   peaklevel -- the level reached at the top of the attack phase, before gliding down to the sustain phase.
##|   sustainlevel -- the level sustained during the sustain phase
##| Example:
##| use_bpm 60
##| use_synth :bass_highend
##| handle = play 60, sustain: 8, decay: 8,res: 0.7
##| puts "handle: " + handle.to_s
##| env(handle, "drive", 1, 1, 3, 3, 0, 5, 3)




define :env do |handle, param, attack=0.25, decay=0, sustain=1, release=0.25, startlevel=0, peaklevel=1, sustainlevel=0.5|
  
  
  slideparam = param + "_slide"
  
  in_thread do
    
    #attack phase
    puts "attack phase"
    cmd = "control handle, " + slideparam + ": " + attack.to_s + ", " + param + ":" + startlevel.to_s
    puts cmd
    eval cmd
    sleep attack
    
    #decay phase
    puts "decay phase"
    if decay > 0
      puts "got decay time"
      
      cmd = "control handle, " + slideparam + ": " + decay.to_s + ", " + param + ":" + peaklevel.to_s
      puts cmd
      eval cmd
      sleep decay
    end #if decay > 0
    
    #sustain phase
    puts "sustain phase"
    cmd = "control handle, " + param + ":" + sustainlevel.to_s
    puts cmd
    eval cmd
    sleep sustain
    
    #decay phase
    puts "decay phase"
    cmd = "control handle, " + slideparam + ": " + decay.to_s + ", " + param + ": " + sustainlevel.to_s
    puts cmd
    eval cmd
    sleep decay
    
    #post-decay phase
    puts "post-decay phase"
    cmd = "control handle, " +  param + ": " + startlevel.to_s
    puts cmd
    eval cmd
    sleep decay
    
    
  end #thread
  
end #define


##| lfo -- provides an all-purpose lfo for any slideable param for any synth note or sample.
##|   best results when you set the sample/note's modulated value to the startlevel when playing the sample/note,
##|   otherwise you'll hear an audible glitch at the beginning of the sound.
##|   handle: the node returned when playing a note or sample.
##|   param: the parameter being modulated by the lfo.
##|   duration: how long the lfo effect will last.
##|   period: the period(s) of the lfo cycle.
##|   Can be a single value, a list/ring, or a comma-delimited string with symbolic values.
##|   e.g. "w,dq,ht,4s".
##|   w for whole.
##|   h for half.
##|   q for quarter.
##|   e for eighth.
##|   s for sixteenth.
##|   d for dotted (can be stacked).
##|   t for triplet.
##|   [0-9]* for how many reps (4w is four whole notes).
##|   one beat is a quarter note.
##|   span: the lower and upper limits of the lfo sweep. Can be a pair of values, or a ring/list, or a comma-delimited list.
##|   examples:
##| use_bpm 120
##| use_synth :bass_highend
##| handle = play 60, sustain: 8, decay: 0,res: 0.7, amp: 0
##| puts "handle: " + handle.to_s
##| lfo handle, "amp", 10, "q,q,e,e,e,e", "0,1,0,0.5,0,0.5", "square"
##| handle = sample :ambi_drone, pitch_stretch: 4
##| lfo handle, "amp", 4, "e,e,s,s,s,s", "0,1,0,0.5,0,0.5", "square"




define :lfo do |handle, param, duration, period=[0.5], span=(ring 0, 1), lfotype="triangle",  delay=0, rampupperiods=0, rampdowntime=0, lfocurve=0|
  
  #force args to rings
  lfotype = [lfotype].flatten.ring
  lfocurve = [lfocurve].flatten.ring
  puts "span: " + span.to_s
  puts "lfotype: " + lfotype.to_s
  puts "lfocurve: " + lfocurve.to_s
  
  slideparam = param + "_slide"
  shapeparam = slideparam + "_shape"
  curveparam = slideparam + "_curve"
  loops = 1
  downramp = 0.0
  rampratio = 1.0
  timetorampdown = duration
  lastspan = 0.0
  
  
  if period.is_a? String
    debugprint "period is a string"
    mylist = []
    period.split(",").each do |item|
      debugprint "item: ", item
      triplet = 1
      dots = 1
      thisnumber = ""
      notetime = 0
      restsign = 1
      item.chars.each do |letter|
        debugprint "letter: ", letter
        case letter
        when "r"
          restsign = -1
        when "w"
          notetime += 4.0
          debugprint "w ", notetime
        when "h"
          notetime += 2.0
          debugprint "h ", notetime
        when "q"
          notetime += 1.0
          debugprint "q ", notetime
        when "e"
          notetime += 0.5
          debugprint "e ", notetime
        when "s"
          notetime += 0.25
          debugprint "s ", notetime
        when "d"
          dots *= 1.5
          debugprint "d ", dots
        when "t"
          triplet = 2.0 / 3
          debugprint "t ", triplet
        when /\d/, "."
          thisnumber = thisnumber + letter
          debugprint "thisnumber: " + thisnumber
        else
          debugprint "garbage letter, ignoring"
        end #case letter
      end #each letter
      notetime *= restsign
      debugprint "notetime: " , notetime
      debugprint "thisnumber: " , thisnumber
      debugprint "triplet: " ,triplet
      debugprint "dots: ", dots
      thisnumber = ( thisnumber == "" ? 1 : thisnumber.to_f )
      notetime *= dots * triplet * thisnumber
      debugprint "final notetime: " , notetime
      mylist << notetime
      debugprint "mylist: ", mylist
      
    end #each item
    period = mylist.ring
    debugprint "period: ", period
    
  else
    debugprint "period is not a string"
    period = [period].flatten.ring
  end #if period is a string
  debugprint "period: ", period
  
  if span.is_a? String
    debugprint "span is a string"
    mylist = []
    span.split(",").each do |item|
      mylist << item.to_f
    end #each item
    span = mylist.ring
  else
    debugprint "gutter is not a string"
    span = [span].flatten.ring
  end #if span a string
  debugprint "span ", span
  
  
  if rampdowntime > 0
    puts "calculating ramp ratio"
    tempduration = duration
    tempperiod = period.to_a.ring #to force new object
    while tempduration > 0 do
      puts "tempduration: " + tempduration.to_s
      if tempduration <= rampdowntime
        timetorampdown -= tempperiod.tick
        downramp += 1
      end #if
    end #while
    rampdownratio = 1 / rampdownloops
  end #if calc rampdowntime
  
  
  
  
  
  
  in_thread do
    
    #initialize param at first span
    ##| puts "initial setting of param"
    ##| shape = 1 #stub it out, makes no difference
    ##| cmd = "control handle, " + param + ": " + span.look.to_s + ", " + slideparam.to_s + ": " + period.look.to_s + ", " + shapeparam.to_s + ": " + shape.to_s + ", " + curveparam.to_s + ": " + lfocurve.look.to_s
    ##| puts cmd
    ##| eval cmd
    sleep delay
    
    duration -= delay
    
    
    while duration > 0 do
      print "loop " + loops.to_s
      puts "look: " + look.to_s
      
      case lfotype.look[0..2].downcase
      when "tri"
        puts "triangle"
        shape = 1 #linear
      when "saw"
        puts "saw"
        shape = 1 #linear
      when "sin"
        puts "sine"
        shape = 3 #sine
      when "smo"
        puts "smooth random"
        shape = 3 #sine
      when "ran"
        puts "random"
        shape = 3 #sine
      when "ste"
        puts "step random"
        shape = 0 #step
      when "squ"
        puts "square"
        shape = 0 #step
      when "cus"
        puts "custom"
        shape = 5 #custom
      else
        puts "garbage, defaulting to triangle"
        shape = 1 #for garbage
      end
      
      
      
      
      if rampupperiods > 0 and loops <= rampupperiods
        rampratio = loops  / rampupperiods
        rampup -= 1
      end #if rampup
      
      if duration <= rampdowntime
        puts "time to ramp down"
        rampratio = downramp * rampdownratio
        downramp -= 1
      end #if ramping down
      
      
      case lfotype.look[0..2].downcase
      when "ran", "smo", "ste"
        puts "random style lfo"
        thisvalue = rrand(lastspan, span.look)
      else
        puts "toggle style lfo"
        thisvalue = span.look
      end #case lfo type
      puts "thisvalue: " + thisvalue.to_s
      puts "rampratio: " + rampratio.to_s
      
      
      thisvalue *= rampratio
      puts "adjusted thisvalue: " + thisvalue.to_s
      puts "this period: " + period.look.to_s
      
      cmd = "control handle, " + slideparam + ": " + period.look.to_s + ", " + shapeparam + ": " + shape.to_s + ", " + param + ": " + thisvalue.to_s + ", " + curveparam + ": " + lfocurve.look.to_s
      puts cmd
      eval cmd
      
      if lfotype.look == "saw"
        puts "saw, jumping to next span"
        cmd = "control handle, " + slideparam + ": " + period.tick.to_s + ", " + shapeparam + ": 0, " + param + ": " + span.tick.to_s + ", " + curveparam + ": " + lfocurve.look.to_s
        puts cmd
        eval cmd
      else
        puts "not saw"
      end #if saw
      puts "about to sleep " + period.look.to_s
      sleep period[loops -3]
      
      
      lastspan = span.look
      duration -= period[loops -3]
      loops += 1
      tick
      puts "bottom of while loop"
    end #while
    
  end #thread
  
end #define



##| spreadtobeats -- a utility function designed to take a spread, 
##| and convert it to a string of comma-delimited beat values to feed into arrange.
##| thisspread: the ring of booleans produced by the spread function, mapping the beats.to_s
##| beatvalue: duration of each beat, defaults to sixteenth
##| Example:
##| spreadtobeats spread(3, 8, 2), 0.5 


define :spreadtobeats do |thisspread, beatvalue=sixteenth|
  puts "thisspread: " + thisspread.to_s
  puts "beatvalue: " + beatvalue.to_s
  beats = ""
  isnote = false
  duration = 0
  comma=""
  
  whole = 4.0
  half =2.0
  quarter =1.0
  eighth =0.5
  sixteenth =0.25
  dotted =1.5
  triplet =2.0 / 3
  firstrest = true
  puts "beatvalue: " + beatvalue.to_s
  puts "sixteenth: " + sixteenth.to_s
  chunk = (beatvalue / sixteenth).to_i
  puts "chunk: " + chunk.to_s
  
  
  thisspread.each do |thisoneisnote|
    if thisoneisnote #got a new note, finish old note
      puts "got a new note"
      duration *= chunk #in case overrode beatvalue
      beats += comma
      comma = ","
      if firstrest
        beats += "r"
      end
      firstrest = false
      {whole=>"w", half=>"h", quarter=>"q", eighth=>"e", sixteenth=>"s"}.each do |size,code|
        puts "size: "
        puts size
        puts "code: "
        puts code
        (duration / size).times do
          beats += code
        end #looping on size
        duration = duration % size
        puts "beats: " + beats
      end #each beat size
      duration = 0
    end #if got a new note
    
    duration += 1
  end #each note
  if duration > 0
    beats += comma
    if firstrest
      beats += "r"
    end
    
    duration *= chunk
    {whole=>"w", half=>"h", quarter=>"q", eighth=>"e", sixteenth=>"s"}.each do |size,code|
      puts "size: "
      puts size
      puts "code: "
      puts code
      (duration / size).times do
        beats += code
      end #looping on size
      duration = duration % size
      puts "beats: " + beats
    end #each beat size
  end #if got a last note
  beats
end #define


##| eucliciate: a utility function wrapping spreadtobeats, bypasses need to create spread.
##| beats: how many beats to play. 
##| duration: how many beats in the whole cycle. 
##| rotations: how many offsets for the euclidean rhythm. 
##| chunk: how big is each beat; defaults to sixteenth (0.25)
##| Example:
##| euclidiate 3, 8, 2, 0.5 


define :euclidiate do |beats,duration,rotations=0,chunk=sixteenth|
  spreadtobeats spread(beats,duration).rotate(rotations), chunk
end



##| trancegate -- a trancegate that manipulates the volume up and down. Defaults to square wave, but you can use other lfo shapes.
##| note that the trancegate does not work in the release section, so arrange your sounds accordingly.
##| also, please set your initial amp: setting to match the maxvol param, to avoid glitches.
##|   handle: the node returned by sample or play commands.
##|   duration: how long the effect lasts. Should line up with sustain of played sound.
##|   Please note that the effect does not work on the decay phase.
##|   period: how long the gate lasts. Can be a single value, a ring/list, or a comma-delimited list.
##|   maxvol: the max amplitude when the gate is open. Defaults to 1.
##|   minvol: the min amplitude when the gate is closed. Defaults to 0. 
##|   gutter: how long the silence lasts between chunks. Can be a single value, list/ring or comma-delimited list.
##|   lfotype: defaults to square, but supports all lfotypes.
##|   curve: lfo type curve param. Used for custom lfo types
##| Examples:
##| use_bpm 120
##| use_synth :bass_highend
##| handle = play 60, sustain: 16, decay: 1,res: 0.7, amp: 0
##| puts "handle: " + handle.to_s
##| trancegate handle, 16, euclidiate("s", 16, 5)
##| handle = sample :ambi_drone, 16
##| trancegate handle, 16, euclidiate("s", 16, 5)

define :trancegate do |handle, duration, period=[0.5], gutter=[0.1], delay=0, maxvol= [1], minvol=[0], lfotype="square",  curve=0|
  
  #cook args to rings
  
  debugprint "handle: ", handle
  debugprint " duration: " , duration
  debugprint "period: ", period
  debugprint "delay: ", delay
  debugprint "maxvol: ", maxvol
  debugprint "minvol: ", minvol
  debugprint "lfotype: " , lfotype
  debugprint "curve: " , curve
  
  puts "top of trancegate function, cooking args"
  debugmode = true
  debugprint "are we printing in debug?"
  if period.is_a? String
    debugprint "period is a string"
    mylist = []
    period.split(",").each do |item|
      debugprint "item: ", item
      triplet = 1
      dots = 1
      thisnumber = ""
      notetime = 0
      restsign = 1
      item.chars.each do |letter|
        debugprint "letter: ", letter
        case letter
        when "r"
          restsign = -1
        when "w"
          notetime += 4.0
          debugprint "w ", notetime
        when "h"
          notetime += 2.0
          debugprint "h ", notetim
        when "q"
          notetime += 1.0
          debugprint "q ", notetime
        when "e"
          notetime += 0.5
          debugprint "e ", notetime
        when "s"
          notetime += 0.25
          debugprint "s ", notetime
        when "d"
          dots *= 1.5
          debugprint "d ", dots
        when "t"
          triplet = 2.0 / 3
          debugprint "t ", triplet
        when /\d/, "."
          thisnumber = thisnumber + letter
          debugprint "thisnumber: " + thisnumber
        else
          debugprint "garbage letter, ignoring"
        end #case letter
      end #each letter
      notetime *= restsign
      debugprint "notetime: " , notetime
      debugprint "thisnumber: " , thisnumber
      debugprint "triplet: " ,triplet
      debugprint "dots: ", dots
      thisnumber = ( thisnumber == "" ? 1 : thisnumber.to_f )
      notetime *= dots * triplet * thisnumber
      debugprint "final notetime: " , notetime
      mylist << notetime
      debugprint "mylist: ", mylist
      
    end #each item
    period = mylist.ring
    debugprint "period: ", period
    
  else
    debugprint "period is not a string"
    period = [period].flatten.ring
  end #if period is a string
  debugprint "period: ", period
  
  if gutter.is_a? String
    debugprint "gutter is a string"
    mylist = []
    gutter.split(",").each do |item|
      mylist << item.to_f
    end #each item
    gutter = mylist.ring
  else
    debugprint "gutter is not a string"
    gutter = [gutter].flatten.ring
  end #if span a string
  debugprint "gutter ", gutter
  
  if maxvol.is_a? String
    debugprint "maxvol is string"
    maxvol = maxvol.split(",").map do |x| x.to_f end
  else
    debugprint "maxvol not a string"
    maxvol = maxvol.flatten.ring
  end #if maxvols is string
  debugprint "maxvol: ", maxvol
  
  if minvol.is_a? String
    debugprint "minvol is string"
    minvol = minvol.split(",").map do |x| x.to_f end
  else
    debugprint "minvol not a string"
    minvol = minvol.flatten.ring
  end #if maxvols is string
  debugprint "maxvol: ", maxvol
  
  
  if lfotype.is_a? String
    debugprint "lfotype is a string"
    lfotype = lfotype.split(",").ring
  else
    debugprint "lfotype not a string"
    lfotype = [lfotype].flatten.ring
  end #if lfotype a string
  debugprint "lfotype: ",lfotype
  
  if curve.is_a? String
    debugprint "curve is a string"
    curve = curve.split(",").ring
  else
    debugprint "curve not a string"
    curve = [curve].flatten.ring
  end #if lfocurve a string
  puts "curve " + curve.to_s
  
  slideparam = "amp_slide"
  shapeparam = "amp_shape"
  curveparam = "amp_curve"
  
  
  
  in_thread do
    
    
    sleep delay
    duration -= delay
    
    while duration > 0 do
      debugprint "top of while loop, duration ", duration
      tick
      
      if period.look > 0
        
        
        
        case lfotype.look[0..2].downcase
        when "tri"
          debugprint "triangle"
          shape = 1 #linear
        when "saw"
          debugprint "saw"
          shape = 1 #linear
        when "sin"
          debugprint "sine"
          shape = 3 #sine
        when "smo"
          debugprint "smooth random"
          shape = 3 #sine
        when "ran"
          debugprint "random"
          shape = 3 #sine
        when "ste"
          debugprint "step random"
          shape = 0 #step
        when "squ"
          debugprint "square"
          shape = 0 #step
        when "cus"
          puts "custom"
          shape = 5 #custom
        else
          debugprint "garbage, defaulting to square"
          shape = 0 #step
        end
        
        debugprint "maxvol: ", maxvol.look
        debugprint "period: ", period.look
        debugprint "gutter: ", gutter.look
        debugprint "shape: ", shape
        debugprint "curve: ", curve
        
        
        
        
        cmd = "control handle, amp: " + maxvol.look.to_s + ", amp_slide: " + (period.look - gutter.look).to_s + ", amp_slide_shape: " + shape.to_s +  ", amp_slide_curve: " + curve.look.to_s
        debugprint "up cmd: ", cmd
        eval cmd
        sleep period.look - (gutter.look)
        
        cmd = "control handle, amp: " + minvol.look.to_s + ", amp_slide: " + (gutter.look).to_s + ", amp_slide_shape: " + shape.to_s +  ", amp_slide_curve: " + curve.look.to_s
        debugprint "down cmd: ", cmd
        eval cmd
        sleep gutter.look
        
        
        
      else
        sleep period.look.abs
      end #if period is positive
      
      duration -= (period.look.abs + gutter.look)
      debugprint "bottom of while loop"
      debugprint "duration: ", duration
    end #while
    
  end #thread
  
end #define









And here’s the contents of arrange_buggy.rb:

whole = 4.0
half =2.0
quarter =1.0
eighth =0.5
sixteenth =0.25
dotted =1.5
triplet =2.0 / 3





##| arrange
##| allows user to arrange multiple samples/synths to play in time with each other in a single function
##| uses a hashtable where the key is the sample and the value is a comma-delimited list of times.
##|   w,h,q,e,s for time divisions, d for dotted, t for triplet. Supports any number of dots.
##|   Supports either individual samples or lists of samples (rings or arrays). If a ring or array,
##|   a sound is chosen randomly from the list.
##| effects: a hash of effects to apply to each synth or sample, where the key is the instrument,
##|   and the value is the string that goes between "with_fx " and " do "
##| e.g. {bass=>["echo", "flanger"]} 
##| defaults: a hash of default settings per voice, where the key to the hash is the sample or synth e.g. {bass => "note_slide_curve: 3"}
##| nvelopes: a hash of envelopes (calls to the nv method), where the key to the hash is the sample or the synth,
##|   and the key is a list of strings, one per effect, which are the arguments (except the node handle) to the function.
##|   e.g. {bass => ["cutoff, quarter, sixteenth, quarter, quarter, 24, 96, 84", "note, quarter, sixteenth, whole, quarter, 36, 48, 43"]}
##| lfos: a hash of lfos, similar to envelopes. See lfo function for args.
##|   e.g. {lead => ["cutoff, quarter, [24,84],sine", "note, quarter, [36,48], square"]}
##| trancegates: a hash of trancegates, similar to envelopes. See trancegate function for args.
##|   e.g. {pad => ["whole * 4, 0.5"]}





define :arrange do |arrangement, defaults=nil, effects=nil, envelopes=nil, lfos=nil, trancegates=nil|
  timeline = {} #for the timing of notes
  melody = {} #for specified notes
  extraargs = {} #for initial values to match envelopes, lfos and trancegates
  # melody[nil] = nil #initialize with stub to simplify code
  defaults = (defaults == nil ? {} : defaults)
  effects = ( effects == nil ? {} : effects)
  envelopes = ( envelopes == nil ? {} : envelopes )
  lfos = ( lfos == nil ? {} : lfos )
  trancegates = (trancegates == nil ? {} : trancegates )


  debugprint "quoting first args in envelopes, initializing extra args for envelopes"
  if envelopes != nil
    envelopes.each do |key, valuelist|
      debugprint "key: ", key
      debugprint "valuelist: ", valuelist
      valuelist = [valuelist] if valuelist.is_a? String
      valuelist.each do |value|
        mylist = value.split(",")
        bareparam = mylist[0].gsub('"', '').gsub("'", '') #strip out quotes
        if mylist[0] !~ /["'].*["']/
          debugprint "no quotes"
          mylist[0] = '"' + mylist[0] + '"'
        end #if no quote
        envelopes[key] = ( envelopes[key] == nil ? [mylist.join(",")] : envelopes[key] << mylist.join(",") )
        startvalue = ( mylist.length > 5 ? mylist[5] : "0")
        debugprint "startvalue: ", startvalue
        debugprint "bareparam: ", bareparam
        extraargs[key] = "" if extraargs[key] == nil
        extraargs[key] += ", " + bareparam + ": " + startvalue + ", " + bareparam + "_slide_shape: 3"
        debugprint "extraargs: ", extraargs
      end #each value
    end #each key value pair
    debugprint "envelopes: ", envelopes
  else
    envelopes = {}
  end #if envelopes not null



  debugprint "quoting first args in lfos, initializing extra args for lfos"
  if lfos != nil
    lfos.each do |key, valuelist|
      valuelist.each do |value|
        mylist = value.split(",")
        bareparam = mylist[0].gsub('"', '').gsub("'", '') #strip out quotes
        if mylist[0] !~ /["'].*["']/
          debugprint "no quotes"
          mylist[0] = '"' + mylist[0] + '"'
        end #if no quote
        lfos[key] = ( lfos[key] == nil ? [mylist.join(",")] : lfos[key] << mylist.join(",") )
        startvalue = ( mylist.length > 3 ? mylist[3] : "0")
        case startvalue
        when nil
          startvalue = "0"
        else
          #will either be array literal [0,1] or ring literal (ring 0,1) or a variable
          #in any case, we want first value, so we'll play a game w/eval
          cmd = "startvalue = " + startvalue + "[0]"
          debugprint "cmd: ", cmd          eval cmd 
        end
        extraargs[key] = "" if extraargs[key] == nil
        extraargs[key] += ", " + bareparam + ": " + startvalue + ", " + bareparam + "_slide_shape: 3"
      end #each value
    end #each valuelist
    debugprint "lfos: ", lfos
  else 
    lfos = {}
  end #if lfos not null


 debugprint "quoting first args in trancegates, initializing extra args for trancegates"
  if trancegates != nil
    trancegates.each do |key, value|
      #can't have multiple trancegates per voice, not an array
      mylist = value.split(",")
      if mylist[0] !~ /["'].*["']/
        debugprint "no quotes"
        mylist[0] = '"' + mylist[0] + '"'
        trancegates[key] = mylist.join(",")
      end #if no quote
      startvalue = ( mylist.length > 4 ? mylist[1] : "1")
      extraargs[key] = "" if extraargs[key] == nil
      extraargs[key] += ", amp: " + startvalue 
    end #each
    debugprint "trancegates: ", trancegates.to_s
  else
    trancegates = {}
  end #if trancegates not null

  if defaults == nil
    defaults = {}
  end #if defaults nil


  debugprint "defaults", defaults
  debugprint "effects", effects
  debugprint "envelopes", envelopes
  debugprint "lfos", lfos
  debugprint "trancegates", trancegates
  debugprint "extraargs", extraargs



  
  
  
  instruments = nil
  arrangement.each do |synthorsample, instr_times|
    debugprint " "
    debugprint " "
    debugprint "synthorsample ", synthorsample.to_s
    debugprint "instr_times: ", instr_times.to_s
    timetillnext = 0
    thistime = 0
    dots = 0
    triplets = 1
    takerest = false
    tonelist = Array.new
    
    
    thistone = nil
    thischord = nil
    thismode = nil
    oldtone = nil
    oldchord = nil
    oldmode = nil
    chordtone = nil
    
    instr_times.split(",").each_with_index do |thisnote, howmanytimes| 
      duration = ""
      debugprint "thisnote: ", thisnote.to_s
      debugprint "thistime: ", thistime.to_s
      oldtone = thistone
      oldchord = thischord
      oldmode = thismode
      thisdur, thistone, thischord, thismode = thisnote.split(" ")
      if thischord == nil and oldmode != nil and ["arp", "asc", "des", "ran"].include? oldmode[0..2]
        debugprint "grabbing old tone, chord, mode"
        thistone = oldtone
        thischord = oldchord
        thismode = oldmode
      end #if subbing in old chord and mode
      
      tonemode = thistone != nil
      chordmode = thischord != nil
      modemode = thismode != nil
      firsttone = nil
      
      debugprint "thisdur: ", thisdur.to_s
      debugprint "thistone: ", thistone.to_s
      debugprint "thischord: ", thischord.to_s
      debugprint "thismode: ", thismode.to_s
      debugprint "tonemode: ", tonemode.to_s
      debugprint "chordmode: ", chordmode.to_s
      debugprint "modemode: ", modemode.to_s
      
      thisdur.each_char do |letter|
        debugprint letter
        case letter.downcase
        when "r"
          takerest = true
        when "w"
          timetillnext += 4.0
        when "h"
          timetillnext += 2.0
        when "q"
          timetillnext += 1.0
        when "e"
          timetillnext += 0.5
        when "s"
          timetillnext += 0.25
        when "d"
          dots = dots + 1
        when "t"
          triplets = 2.0 / 3
        when /\d/
          debugprint "duration digit"
          duration += letter
        else
          debugprint letter + " is garbage, ignored"
        end #case letter
      end #each letter
      
      
      debugprint "raw duration: ", duration.to_s
      if duration.length > 0
        duration = duration.to_i
      else
        duration = 1
      end #if duration length > 0
      debugprint "cooked duration: ", duration.to_s
      
      timetillnext = timetillnext * duration * triplets * (2 - (0.5 ** dots))
      debugprint "timetillnext: ", timetillnext.to_s
      
      if chordmode
        debugprint "chordmode"
        debugprint "thistone: ", thistone.to_s
        if chordtone == nil
          debugprint "first tone!"
          chordtone = thistone
        end #if firsttone nil
        
        chordtone = ( chordtone[0] == ":" ? chordtone.delete_prefix(":").to_sym : chordtone.to_i )
        debugprint "chordtone: ", chordtone.to_s
        if chord_names.to_a.include? thischord
          debugprint "chord"
          thistone = chord chordtone, thischord
        else
          debugprint "scale"
          thistone = scale chordtone, thischord
        end #if chord or scale
        debugprint "thistone: ", thistone.to_s
        if modemode
          debugprint "modemode"
          debugprint "mode ", thismode[0..2]
          case thismode[0..2].downcase
          when "ran"
            debugprint "random"
            thistone = thistone.to_a.shuffle[howmanytimes]
          when "asc"
            debugprint "ascending arpeggio"
            thistone = thistone[howmanytimes]
          when "arp"
            debugprint "ascending arpeggio"
            thistone = thistone[howmanytimes]
          when "des"
            debugprint "descending arpeggio"
            thistone = thistone.to_a.reverse[howmanytimes]
          else
            debugprint "chord mode, leave chord intact"
          end #if in random mode
        end #if mode mode
        debugprint "thistone: ", thistone.to_s
      end #if chordmode
      
      
      
      
      
      
      if !takerest
        debugprint "scheduling note"
        if timeline.has_key?(thistime)
          debugprint "found key ", thistime.to_s
          timeline[thistime] << [synthorsample, timetillnext]
        else
          debugprint "did not find key ", thistime.to_s
          timeline[thistime] = [[synthorsample, timetillnext]]
        end #if haskey
        debugprint "timeline: ", timeline.to_s
        debugprint "thistone: ", thistone.to_s
        if tonemode
          tonelist  << thistone
          debugprint "tonelist: ", tonelist.to_s
          tonelist.each_with_index do |x, i|
            debugprint "tonelist[" + i.to_s + "]: ", x\
          end #each
        # else
        #   debugprint "this is a rest, carry thistime to next item in loop to delay start of note"
        end #if tonemode
        
      end #if !takerest
      
      
      
      
      thistime = thistime + timetillnext
      timetillnext = 0
      dots = 0
      triplets = 1
      takerest = false
    end #each note for this instrument



    if tonelist.length > 0
      debugprint "got tones, adding to melody hash"
      debugprint "melody: ", melody
      debugprint "synthorsample: ", synthorsample
      debugprint "tonelist: ", tonelist
      melody[synthorsample] = tonelist
      debugprint "melody: ", melody
    end
    
  end #each instrument
  
 #  #now traverse keys of the timeline, which are times, in order, and play samples at that time_warp
  debugprint " "
  debugprint " "
  debugprint " "
  debugprint " "
  
  
  debugprint "timeline keys sorted: ", timeline.keys.sort
  debugprint "melody: ", melody
  nextloop = 0
  maxtime = 0
  timeline.keys.sort.each do |thiskey|
    debugprint "thiskey: ", thiskey.to_s
    nextloop = nextloop + 1
    debugprint "next loop: ", nextloop.to_s 
    debugprint ", timeline length: ", timeline.length.to_s
    sleepytime = 0
    if timeline.length > nextloop then
      sleepytime = timeline.keys.sort[nextloop] - thiskey
      debugprint "not last loop."
      debugprint "next key: ", timeline.keys.sort[nextloop]
      debugprint "sleepytime: ", sleepytime
      #the amount to rest is the duration between this time and next time
      #this even accounts for triplets vs 4s vs dots
    end #if
    
    
    thistime = timeline[thiskey]
    debugprint "processing time array, thistime: ", thistime
    maxtime = 0
    thistime.each do |thissound|
      debugprint "traversing thistime list"
      debugprint "thissound[0]: ", thissound[0]
      chosensound = thissound[0]
      if chosensound.is_a? Enumerable or chosensound.is_a? SonicPi::Core::RingVector
        chosensound = chosensound.choose
      end #if ring or array
      debugprint "chosensound: ",chosensound

      duration = thissound[1]
      debugprint "duration: ", duration
      
      handle = nil #init here so it's scoped to be visible where needed
      
      
      if synth_names.to_a.include? chosensound
        debugprint "synth mode, getting note to play"
        thisnote = melody[thissound[0]]
        debugprint "thisnote: ", thisnote
        debugprint "melody[thissound[0]]: ", melody[thissound[0]]
        melody[thissound[0]] = melody[thissound[0]].rotate #rotate note just played
        debugprint "thisnote: ", thisnote
        debugprint "thisnote[0]: ", thisnote[0]
        if thisnote[0].is_a? String
          debugprint "thisnote[0] is a string"
          if thisnote[0] =~ /:.*/
            debugprint "got a colon, turning to symbol"
            thisnote[0].delete_prefix(":").to_sym
            cooknote = thisnote[0]
            debugprint "thisnote: ", thisnote
            debugprint "cooknote:", cooknote
          else
            debugprint "no colon, turning to int"
            cooknote = thisnote[0]
            debugprint "cooknote: ", cooknote
          end
        else
          debugprint "thisnote[0] is a list/ring"
          cooknote = thisnote[0]
        end #if thisnote[0] is a string
        
        debugprint "converted thisnote: ", thisnote
        debugprint "cooknote: ", cooknote

        cmd = "handle = play cooknote, duration: " + duration.to_s
        cmd += scrub(extraargs[thissound])

        if defaults[thissound[0]] != nil
          debugprint "got synth defaults"
          cmd = "with_merged_synth_defaults " + defaults[thissound[0]] + " do " + cmd + " end"
          debugprint " defaults cmd: ", cmd 
        else
          debugprint "no synth defaults"
        end


        cmd = "with_synth chosensound do " + cmd + " end "

        if effects[thissound[0]] != nil
          if !effects[thissound[0]].is_a? String
            debugprint "effects are in a list or ring, converted to semicolon-delimited string"
            effects[thissound[0]] = effects[thissound[0]].join(";")
          end


          effects[thissound[0]].split(";").each do |effect|
            debugprint "adding effect ", effect
            cmd = "with_fx " + effect + " do " + cmd + " end "
          end #each effect
        else
          debugprint "no effects"
        end #if effects

        debugprint "cmd: ", cmd
        eval cmd
        

        debugprint "cmd: ", cmd
        eval cmd 

        debugprint "testing for envelopes, lfos, trancegates"
        debugprint "thissound: ", thissound
        debugprint "chosensound: ", chosensound
        debugprint "envelopes[thissound.to_sym]: ", envelopes[thissound[0]]
        debugprint "lfos[thissound.to_sym]: ", lfos[thissound[0]]
        debugprint "trancegates[thissound.to_sym]: ", trancegates[thissound[0]]
        
        
 #        if envelopes[thissound[0]] != nil
 #          debugprint "got at least one envelope for ", thissound[0]
 #          envelopes[thissound[0]].each do |envelope|
 #            debugprint "envelope: ", envelope
 #            cmd = "env handle ," + envelope
 #            debugprint "cmd: ", cmd
 #            eval cmd
 #          end #each envelope
 #        else
 #          debugprint "no envelopes"
 #        end #if there's an envelope
        
 #        if lfos[thissound[0]] != nil
 #          debugprint "got at least one lfo for ", chosensound
 #          lfos[thissound[0]].each do |lfo|
 #            debugprint "lfo: ", lfo
 #            cmd = "lfo handle ," + lfo
 #            debugprint "cmd: ", cmd
 #            eval cmd
 #          end #each lfo
 #        else
 #          debugprint "no lfos"
 #        end #if there's an envelope
       
 #         if trancegates[thissound[0]] != nil
 #          debugprint "got a trancegate for ", chosensound
 #          cmd = "trancegate handle ," + trancegates[thissound[0]]
 #          debugprint cmd
 #          eval cmd
 #        else 
 #          debugprint "no trancegates"
 #        end #if there's an envelope
        
      else
 #        debugprint "sample mode"
 #        debugprint "melody: ", melody
 #        debugprint "thissound: ", thissound
 #        debugprint "melody[thissound]: ", melody[thissound[0]]
 #        if melody[thissound[0]] != nil
 #          debugprint "got a melody, picking thisnote"
 #          thisnote = melody[thissound[0]]
 #          debugprint "thisnote: ", thisnote
 #          debugprint "melody[thissound[0]]: ", melody[thissound[0]]
 #          melody[thissound[0]] = melody[thissound[0]].rotate #rotate note just played
 #          #need to add smarts for rings with fewer entries than notes played
 #          debugprint "thisnote: ", thisnote
 #          debugprint "thisnote[0]: ", thisnote[0]
          
 #          if defaults[thisnote[0]] != nil
            
 #            cmd = "use_sample_defaults " + defaults[thisnote[0]]
 #            debugpring "cmd: ", cmd
 #            eval cmd
 #          end
          
          
 #          if thisnote[0].is_a? String
 #            debugprint "thisnote[0] is a string"
 #            if thisnote[0] =~ /:.*/
 #              debugprint "got a colon, turning to symbol"
 #              thisnote = thisnote[0].delete_prefix(":").to_sym
 #            else
 #              debugprint "no colon, turning to int"
 #              thisnote = thisnote[0].to_i
 #            end
            
 #            pitch_stretch_ratio = midi_to_hz(60 + thisnote) / midi_to_hz(60)

 #            cmd = "handle = sample chosensound, pitch_stretch: thissound[1].to_i * pitch_stretch_ratio, rpitch: thisnote"
 #            cmd += scrub(extraargs[thissound])
 #            debugprint "cmd: ", cmd

 #            if effects[thissound[0]] != nil
 #              effects[thissound[0]].split(";").each do |effect|
 #                debugprint "adding effect ", effect
 #                cmd = "with_fx " + effect + " do " + cmd + " end "
 #              end #each effect
 #            else
 #              debugprint "no effects"
 #            end #if effects
 #            debugprint "cmd: ", cmd

 #            eval cmd 


 #            if envelopes[thissound[0]] != nil
 #              debugprint "got at least one envelope for ", thissound
 #              envelopes[thissound[0]].each do |envelope|
 #                cmd = "env handle ," + envelope
 #                debugprint "cmd: ", cmd
 #                eval cmd
 #              end #each envelope
 #            else
 #              debugprint "no envelopes"
 #            end #if there's an envelope
            
 #            if lfos[thissound[0]] != nil
 #              debugprint "got at least one lfo for ", thissound[0]
 #              lfos[thissound[0]].each do |lfo|
 #                cmd = "lfo handle ," + lfo
 #                debugprint "cmd: ", cmd
 #                eval cmd
 #              end #each lfo
 #            else
 #              debugprint "no lfos"
 #            end #if there's an envelope
           
 #            if trancegates[thissound[0]] != nil
 #              debugprint "got a trancegate for ", chosensound
 #              cmd = "trancegate handle ," + trancegates[thissound[0]]
 #              debugprint "cmd: ", cmd
 #              eval cmd
 #            else
 #              debugprint "no trancegates"
 #            end #if there's an envelope
            
 #          else
 #            debugprint "thisnote[0] is a list/ring"
 #            thisnote[0].each do |onenote|
 #              debugprint "onenote: ", onenote
 #              pitch_stretch_ratio = midi_to_hz(60 + onenote) / midi_to_hz(60)

 #              cmd = "sample chosensound, pitch_stretch: thissound[1].to_i * pitch_stretch_ratio, rpitch: onenote"
 #              cmd += scrub(extraargs[thissound])
 #              if effects[thissound[0]] != nil
 #                effects[thissound[0]].split(";").each do |effect|
 #                  debugprint "adding effect ", effect
 #                  cmd = "with_fx " + effect + " do " + cmd + " end "
 #                end #each effect
 #                debugprint "cooked cmd with effects: ", cmd
 #              else
 #                debugprint "no effects"
 #              end #if effects
 #              debugprint "cmd: ", cmd
 #              eval cmd 

 #              if envelopes[thissound[0]] != nil
 #                debugprint "got at least one envelope for ", thissound
 #                envelopes[thissound[0]].each do |envelope|
 #                  cmd = "env handle ," + envelope
 #                  debugprint "cmd: ", cmd
 #                  eval cmd
 #                end #each envelope
 #              else
 #                debugprint "no envelopes"
 #              end #if there's an envelope
              
 #              if lfos[thissound[0]] != nil
 #                debugprint "got an lfo for ", thissound[0]
 #                lfos[thissound[0]].each do |lfo|
 #                  cmd = "lfo handle ," + lfos[thissound[0]]
 #                  debugprint "cmd: ", cmd
 #                  eval cmd
 #                end #each lfo
 #              else
 #                debugprint "no lfos"
 #              end #if there's an lfo
             
 #              if trancegates[thissound[0]] != nil
 #                debugprint "got a trancegate for ", thissound[0]
 #                cmd = "trancegate handle ," + trancegates[thissound[0]]
 #                debugprint cmd
 #                eval cmd
 #              else
 #                debugprint "no trancegates"
 #              end #if there's a trancegate

 #            end #each note
            
 #          end #if thisnote[0] is a string
        
 #        else
          
 #          debugprint "no note, play sample stark naked"
          
 #          #need to add code for pitch shifting samples
 #          cmd = "handle = sample chosensound"
 #          cmd += scrub(extraargs[thissound])
 #          if effects[thissound[0]] != nil
 #            effects[thissound[0]].split(";").each do |effect|
 #              debugprint "adding effect ", effect
 #              cmd = "with_fx " + effect + " do " + cmd + " end "
 #            end #each effect
 #            debugprint "cooked cmd with effects: ", cmd
 #          end #if effects
 #          debugprint "cmd: ", cmd
 #          eval cmd 

                  
 #          debugprint "testing for envelopes, lfos, trancegates"
 #          debugprint "thissound: ", thissound
 #          debugprint "chosensound: ", chosensound
 #          debugprint "envelopes[thissound[0]]: ", envelopes[thissound[0]]
 #          debugprint "lfos[thissound[0]]: ", lfos[thissound[0]]
 #          debugprint "trancegates[thissound[0]]: ", trancegates[thissound[0]]
          
 #          if envelopes[thissound[0]] != nil
 #            debugprint "got an envelope for ", thissound
 #            envelopes[thissound[0]].each do |envelope|
 #              cmd = "env handle ," + envelopes[thissound[0]]
 #              debugprint "cmd: ", cmd
 #              eval cmd
 #            end #each envelope
 #          else
 #            debugprint "no envelopes"
 #          end #if there's an envelope
          
          
 #          if lfos[thissound[0]] != nil
 #            debugprint "got an lfo for " + thissound[0]
 #            lfos[thissound[0]].each do |lfo|
 #              cmd = "lfo handle ," + lfo
 #              debugprint cmd
 #              eval cmd
 #            end
 #          else 
 #            debugprint "no lfos"
 #          end #if there's an envelope
          
          
         
 #          if trancegates[thissound[0]] != nil
 #            debugprint "got a trancegate for ", thissound[0]
 #            cmd = "trancegate handle ," + trancegates[thissound[0]]
 #            debugprint "cmd: ", cmd
 #            eval cmd
 #          else 
 #            debugprint "no trancegates"
 #          end #if there's an envelope
         
 #        end #if got a note
      end #if synth or sample
      
      debugprint "this sound time: ", thissound[1]
      maxtime = [thissound[1], maxtime].max
      debugprint "maxtime: ", maxtime
      sleepytime = maxtime if timeline.length == nextloop
    end #each thissound
    debugprint "sleepytime: ", sleepytime.to_s
    sleep sleepytime
  end #thistime/thiskey
  debugprint "end of arrange"
end #define






# kick = :bd_ada
# snare = [:sn_dolf, :sn_dub, :sn_generic, :sn_zome]
# hat = (ring :hat_bdu, :hat_cab, :hat_cats, :hat_gem, :hat_gnu)
# bass = :bass_foundation



Sorry, the last line in the crashing code should read:

arrange arrangement, defaults, effects, envelopes

OK, I’ve managed to work past whatever was causing the problem, but I still don’t understand what caused it.
It seems like there’s some kind of coding errors that cause sonic pi to just hang, but I don’t know how those differ from everyday just plain bugs.
Any ideas or insights?