Tips on simplifying/optimizing code?

Hey folks, I’ve been working on creating generative music compositions in Sonic Pi with the hopes of creating a generative “album” of “tracks” that play indefinitely. I’ve been using Sonic Pi to generate MIDI data, that is then paired up with a DAW (in my case Ableton) to synthesize the sound. You can find an example of one of these compositions here.

Now that I have the basic process down (tutorials coming soon!) I’ve started making more complex compositions. Right now, for instance I’m working on a track that has five instruments with different degrees of variability, MIDI CCs that act as LFOs, and this thing I’m calling a “phrase controller.” In most linear music, you’ll have different sections of a track that play in a particular order (intro, bridge, hook, etc), but with generative music, you don’t want the track to necessarily play the same sections over and over again in the same order, so I’m using this phrase controller to determine what parts are playing and with what variables at any given time.

I think the ideas behind what I’m creating are really interesting, and I’m really digging the sounds. However, its become obvious that the code is majorly overly complex and bloated. This has become particularly apparent given that when in Phrase 3, drum beats have started skipping and jumping. I’d love any tips and tricks you have on simplifying and optimizing the code!

# LUPIN TULPA

/
Beginning code:
- set BPM of track
- create :tick to sync the rest of the live_loops
/
use_bpm 88

live_loop :tick do
  sleep 1
end

/
Define instruments:
- use parameters in function that can be controlled by "phrase_controller"
- set midi channels
/

define :choir do
  use_midi_defaults channel: 1
  sus = (knit 4, 2, 8, 1, 16, 1).choose
  ##| sus = 1
  
  midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus*3
  sleep sus
  midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus*2
  sleep sus
  midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus
  sleep sus
end


define :bass do |sus|
  
  use_midi_defaults channel: 2
  bass_note = [:e1, :g1, :a1, :g1].tick
  
  midi bass_note, sustain: sus
  sleep sus
  midi bass_note, sustain: sus
  sleep sus
  midi :es1, sustain: sus
  sleep sus
  midi bass_note, sustain: sus
  sleep sus
end

define :guit do |prob1, prob2, prob3, speed|
  sus = (knit 0.25, 2, 0.5, 1).choose
  ##| sus = 1
  
  if one_in(prob1)
    midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus*speed, channel: 3
    sleep sus*speed
    midi (scale :e4, :major, num_octaves: 1).choose, sustain: sus*speed, channel: 3
    sleep sus*speed
    midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus*speed, channel: 3
    sleep sus*speed
    midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus*speed, channel: 3
    sleep sus*speed
  end
  
  if one_in(prob2)
    midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus*speed, channel: 4
    sleep sus*speed
    midi (scale :e4, :major, num_octaves: 1).choose, sustain: sus*speed, channel: 4
    sleep sus*speed
    midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus*speed, channel: 4
    sleep sus*speed
    midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus*speed, channel: 4
    sleep sus*speed
  end
  
  if one_in(prob3)
    midi (scale :e3, :major, num_octaves: 2).choose, sustain: sus*speed, channel: 3
    midi (scale :e3, :major, num_octaves: 2).choose, sustain: sus*speed, channel: 4
    sleep sus*2*speed
    midi (scale :e4, :major, num_octaves: 2).choose, sustain: sus*speed, channel: 3
    midi (scale :e4, :major, num_octaves: 2).choose, sustain: sus*speed, channel: 4
    sleep sus*2*speed
    midi (scale :e3, :major, num_octaves: 2).choose, sustain: sus*speed, channel: 3
    midi (scale :e3, :major, num_octaves: 2).choose, sustain: sus*speed, channel: 4
    sleep sus*2*speed
    midi (scale :e3, :major, num_octaves: 2).choose, sustain: sus*speed, channel: 3
    midi (scale :e3, :major, num_octaves: 2).choose, sustain: sus*speed, channel: 4
    sleep sus*2*speed
  end
  
  sleep 0.25
end

define :drum1 do
  use_midi_defaults channel: 5, sustain: 0.25
  midi :g2
  sleep 2
  midi :g2
  sleep 0.25
  midi :f2
  sleep 1.75
end

define :drum2 do
  use_midi_defaults channel: 5, sustain: 0.25
  midi :g2
  sleep 0.5
  midi :d2
  sleep 1.5
  midi :g2
  sleep 0.25
  midi :f2
  sleep 1.75
end

define :drum3 do
  use_midi_defaults channel: 5, sustain: 0.25
  midi :g2
  sleep 0.5
  midi :d2
  sleep 1.5
  midi :g2
  sleep 0.25
  midi :f2
  sleep 0.5
  midi :gs2
  sleep 0.25
  midi :a2
  sleep 0.25
  midi :d2
  sleep 0.75
end

define :drum4 do
  use_midi_defaults channel: 5, sustain: 0.25
  midi :g2
  sleep 0.5
  midi :d2
  sleep 0.5
  midi :gs2
  sleep 1
  midi :g2
  sleep 0.25
  midi :f2
  sleep 0.5
  midi :gs2
  sleep 0.25
  midi :a2
  sleep 0.25
  midi :d2
  sleep 0.75
  
  midi :g2
  sleep 0.5
  midi :d2
  sleep 0.75
  midi :e2
  sleep 0.75
  midi :g2
  sleep 0.25
  midi :f2
  sleep 0.5
  midi :gs2
  sleep 0.25
  midi :a2
  sleep 0.25
  midi :d2
  sleep 0.25
  midi :b2
  sleep 0.5
end

define :drum5 do
  use_midi_defaults channel: 5, sustain: 0.25
  midi :g2
  sleep 0.5
  midi :d2
  sleep 0.5
  
  midi :gs2
  sleep 0.25
  midi :e2
  sleep 0.5
  midi :b1
  sleep 0.25
  
  midi :g2
  sleep 0.25
  midi :f2
  sleep 0.25
  midi :cs3
  sleep 0.25
  midi :gs2
  sleep 0.25
  
  midi :a2
  sleep 0.25
  midi :d2
  midi :gs2
  sleep 0.75
  
  midi :g2
  sleep 0.5
  midi :d2
  sleep 0.5
  
  midi :gs2
  sleep 0.25
  midi :e2
  sleep 0.5
  midi :b2
  sleep 0.25
  
  midi :g2
  sleep 0.25
  midi :f2
  sleep 0.5
  midi :gs2
  sleep 0.25
  
  midi :a2
  sleep 0.25
  midi :d2
  sleep 0.25
  midi :b2
  sleep 0.5
end

define :drum6 do
  use_midi_defaults channel: 5, sustain: 0.25
  #1
  midi :g2
  sleep 0.5
  midi :d2
  sleep 0.5
  
  #2
  midi :gs2
  sleep 0.25
  midi :e2
  sleep 0.5
  midi :b1
  sleep 0.25
  
  #3
  midi :g2
  sleep 0.25
  midi :f2
  sleep 0.25
  midi :cs3
  sleep 0.25
  midi :gs2
  sleep 0.25
  
  #4
  midi :a2
  sleep 0.25
  midi :d2
  midi :gs2
  sleep 0.25
  midi :b1
  sleep 0.5
  
  #5
  midi :g2
  sleep 0.5
  midi :d2
  sleep 0.5
  
  #6
  midi :gs2
  midi :as2
  sleep 0.25
  midi :e2
  sleep 0.5
  midi :b2
  sleep 0.25
  
  #7
  midi :g2
  sleep 0.25
  midi :f2
  sleep 0.25
  midi :cs3
  sleep 0.25
  midi :gs2
  sleep 0.25
  
  #8
  midi :a2
  midi :d2
  sleep 0.25
  midi :gs2
  midi :d2
  sleep 0.25
  midi :b2
  midi :d2
  sleep 0.5
end

live_loop :cc_tuner do
  stop
  cc10 = (line 70, 50, steps: 100).mirror.tick
  midi_cc 20, cc10, channel: 1
  sleep 1
end

live_loop :cc_control, sync: :tick do
  ##| stop
  
  cc3 = (line 10, 30, steps: 40).mirror.tick
  midi_cc 13, cc3, channel: 1
  
  cc4 = (line 0, 127, steps: 200).mirror.tick
  midi_cc 14, cc4, channel: 1
  
  cc5 = (line 127, 0, steps: 200).mirror.tick
  midi_cc 15, cc5, channel: 1
  
  cc6 = (line 50, 70, steps: 100).mirror.tick
  midi_cc 16, cc6, channel: 1
  
  cc7 = (line 70, 50, steps: 100).mirror.tick
  midi_cc 17, cc7, channel: 1
  
  sleep 1
end


/
Phrase controller:
- Select which phrase will play
- Determine how many times a phrase will play before selecting next phrase
- Create live_loops that use phrase variable to determine the changes in the 
/
live_loop :phrase_control, sync: :tick do
  ##| stop
  
  phrase = 3
  repeats = 12
  
  if phrase == -3 then
    sleep 1
  end
  if phrase == -2 then
    sleep 1
  end
  if phrase == -1 then
    sleep 1
  end
  if phrase == 0 then
    ##| repeats = 4
    cc1 = (line 50, 90, steps: 400).mirror.tick
    midi_cc 11, cc1, channel: 1
    
    cc2 = (line 90, 70, steps: 400).mirror.tick
    midi_cc 12, cc2, channel: 1
    
    cc8 = (line 30, 60, steps: 100).mirror.tick
    midi_cc 18, cc8, channel: 1
    
    cc9 = (line 60, 90, steps: 100).mirror.tick
    midi_cc 19, cc9, channel: 1
    
    cc10 = (line 50, 100, steps: 200).mirror.tick
    midi_cc 20, cc10, channel: 1
    
    sleep 1
  end
  if phrase == 1 then
    ##| repeats = 4
    cc1 = (line 50, 70, steps: 400).mirror.tick
    midi_cc 11, cc1, channel: 1
    
    cc2 = (line 90, 70, steps: 400).mirror.tick
    midi_cc 12, cc2, channel: 1
    
    cc8 = (line 40, 80, steps: 50).mirror.tick
    midi_cc 18, cc8, channel: 1
    
    cc9 = (line 40, 70, steps: 100).mirror.tick
    midi_cc 19, cc9, channel: 1
    
    cc10 = (line 50, 80, steps: 200).mirror.tick
    midi_cc 20, cc10, channel: 1
    
    sleep 1
  end
  if phrase == 2 then
    ##| repeats = 4
    /
    cc1 = (line 30, 50, steps: 400).mirror.tick
    midi_cc 11, cc1, channel: 1

    cc2 = (line 90, 70, steps: 400).mirror.tick
    midi_cc 12, cc2, channel: 1
    /
    cc8 = (line 80, 100, steps: 50).mirror.tick
    midi_cc 18, cc8, channel: 1
    
    cc9 = (line 40, 90, steps: 100).mirror.tick
    midi_cc 19, cc9, channel: 1
    /
    cc10 = (line 50, 80, steps: 200).mirror.tick
    midi_cc 20, cc10, channel: 1/
    
    sleep 1
  end
  if phrase == 3 then
    ##| repeats = 4
    sleep 1
  end
  
  
  
  repeats.times do
    ##| stop
    
    live_loop :choir_phrase, sync: :tick do
      if phrase == -3 then
        choir
      end
      if phrase == -2 then
        choir
      end
      if phrase == -1 then
        choir
      end
      if phrase == 0
        choir
      end
      if phrase == 1 then
        choir
      end
      if phrase == 2 then
        choir
      end
      if phrase == 3 then
        choir
      end
    end
    
    live_loop :bass_phrase, sync: :tick do
      if phrase == -3 then
        bass 4
      end
      if phrase == -2 then
        bass 4
      end
      if phrase == -1 then
        bass 4
      end
      if phrase == 0
        bass 2
      end
      if phrase == 1 then
        bass 4
      end
      if phrase == 2 then
        bass 4
      end
      if phrase == 3 then
        bass 4
      end
    end
    
    live_loop :guit_phrase, sync: :tick do
      if phrase == -3 then
        guit 2, 2, 1, (knit 1, 2, 2, 1).choose
      end
      if phrase == -2 then
        guit 1, 1, 4, 2
        if one_in(2)
          sleep (knit 4, 1, 8, 2, 16, 1).choose
        end
      end
      if phrase == -1 then
        guit 4, 0, 0, (knit 4, 1, 8, 4).choose
        if one_in(1)
          sleep (knit 4, 1, 8, 2, 16, 4).choose
        end
      end
      if phrase == 0
        sleep 1
      end
      if phrase == 1 then
        sleep 1
      end
      if phrase == 2 then
        sleep 1
      end
      if phrase == 3 then
        sleep 1
      end
    end
    
    live_loop :drum_phrase do
      if phrase == -3 then
        sleep 1
      end
      if phrase == -2 then
        sleep 1
      end
      if phrase == -1 then
        sleep 1
      end
      if phrase == 0 then
        drum1
      end
      if phrase == 1 then
        drum2
      end
      if phrase == 2 then
        drum3
        ##| drum4
      end
      if phrase == 3 then
        drum5
        drum6
      end
    end
    
  end
  
  if phrase == -3 then
    phrase = (knit -3, 4, -2, 1).choose
  end
  if phrase == -2 then
    phrase = (knit -3, 1, -2, 4, -1, 1).choose
  end
  if phrase == -1 then
    phrase = (knit -2, 1, -1, 4, 0, 1).choose
  end
  if phrase == 0 then
    phrase = (knit -1, 1, 0, 4, 1, 1).choose
  end
  if phrase == 1 then
    phrase = (knit 0, 1, 1, 4, 2, 1).choose
  end
  if phrase == 2 then
    phrase = (knit 1, 1, 2, 4, 3, 1).choose
  end
  if phrase == 3 then
    phrase = (knit 2, 1, 3, 4).choose
  end
  
  print "phrase", phrase
  ##| sleep 1
end

I’ve also noticed that the phrase changes invariably after every tick rather than sticking to one phrase according to the “repeats” variable. I figure this is because of the placement of the following code, but I could be wrong.

if phrase == -3 then
    phrase = (knit -3, 4, -2, 1).choose
  end
  if phrase == -2 then
    phrase = (knit -3, 1, -2, 4, -1, 1).choose
  end
  if phrase == -1 then
    phrase = (knit -2, 1, -1, 4, 0, 1).choose
  end
  if phrase == 0 then
    phrase = (knit -1, 1, 0, 4, 1, 1).choose
  end
  if phrase == 1 then
    phrase = (knit 0, 1, 1, 4, 2, 1).choose
  end
  if phrase == 2 then
    phrase = (knit 1, 1, 2, 4, 3, 1).choose
  end
  if phrase == 3 then
    phrase = (knit 2, 1, 3, 4).choose
  end
1 Like

Hi! I love your description of the project. I haven’t tried running your code yet, as I’m just slightly too lazy to rig up a MIDI synth to play it on, but I can suggest a few idioms to shorten/simplify the code. (Apologies if you’ve already figured this out—it’s been more than a week since you posted this. I really should start checking in more often!)

  1. When the same value is used over and over, set it in a variable. For example in the :guit block there are a lot of lines like

    midi (scale :e3, :major, num_octaves: 1).choose, sustain: sus*speed, channel: 3
    

    Since they’re all drawing on the same scale, you can just declare it once:

    use_midi_defaults channel: 3
    my_scale = (scale :e3, :major, :num_octaves: 2)
    midi my_scale.choose sustain: sus*speed
    
  2. Instead of using a sequence of if statements to choose the current phrase, try putting them in an array. So instead of:

    if phrase == -3 then
        phrase = (knit -3, 4, -2, 1).choose
    end
    if phrase == -2 then
        phrase = (knit -3, 1, -2, 4, -1, 1).choose
    end
    if phrase == -1 then
    ## etc.
    

    You could do:

    phrase_sequence = [
        (knit -3, 4, 2, 1),
        (knit -3, 1, -2, 4, -1, 1),
        (knit -2, 1, -1, 4, 0, 1),
        ## etc.
        ]
    phrase = phrase_sequence[phrase].choose
    

    The current version is not just verbose; it’s also a little logically tricky: note that it’s possible for several of these if blocks to be triggered on a single pass. Do you think that’s what’s causing the phrase value to be reset too often?

  3. Some of the patterns are written with long sequences alternating between midi and sleep statements. There are a lot of ways to simplify these. My personal go-to is to use two rings, one for pitches and one for durations. So instead of:

    define :drum5 do
      use_midi_defaults channel: 5, sustain: 0.25
      midi :g2
      sleep 0.5
      midi :d2
      sleep 0.5
     
      midi :gs2
      sleep 0.25
      midi :e2
      sleep 0.5
      midi :b1
      sleep 0.25
    

    You could do:

    define :drum5 do
      use_midi_defaults channel: 5, sustain: 0.25
      notes = ring(
        :g2, :d2,
        :gs2, :e2, :b1,
        ## etc.
      )
      durs = ring(
        0.5, 0.5,
        0.25, 0.5, 0.25,
        ## etc.
      )
      22.times do
        tick
        midi notes.look
        sleep durs.look
      end
    

    If you were using the built-in synths rather than MIDI, the play_pattern_timed functionwould probably be even better. If you aren’t familiar with it, check it out in the documentation, in the Lang tab.

  4. In phrases 0, 1, and 2 I suspect the MIDI controllers are changing faster than you intend. Note that the .tick method increments the value for the entire thread, so this code actually advances each line five steps:

    if phrase == 0 then
      ##| repeats = 4
      cc1 = (line 50, 90, steps: 400).mirror.tick
      midi_cc 11, cc1, channel: 1
    
      cc2 = (line 90, 70, steps: 400).mirror.tick
      midi_cc 12, cc2, channel: 1 
    
      cc8 = (line 30, 60, steps: 100).mirror.tick
      midi_cc 18, cc8, channel: 1
    
      cc9 = (line 60, 90, steps: 100).mirror.tick
      midi_cc 19, cc9, channel: 1
    
      cc10 = (line 50, 100, steps: 200).mirror.tick
      midi_cc 20, cc10, channel: 1
    
      sleep 1
    end
    

    When I have more than one thing controlled by a tick, I usually put a single tick statement at the beginning, then use the .look method to get the value without changing it:

    if phrase == 0 then
      tick
      ##| repeats = 4
      cc1 = (line 50, 90, steps: 400).mirror.look
      midi_cc 11, cc1, channel: 1
    
      cc2 = (line 90, 70, steps: 400).mirror.look
      midi_cc 12, cc2, channel: 1
      ## etc.
    

    Alternatively you could use named ticks by providing a key (symbol) in an argument; check the Lang reference for examples of that! That would let you treat each MIDI CC independently of the others.

Whew, sorry that came out so long! I hope some of it is helpful. :smile: Best of luck!! And if you ever have a chance to get an audio recording, I’d love to hear how it sounds. [edit: never mind, I should have clicked the link in your post! Listening now.]

Hi @pashultz. Thank you so much for your comprehensive follow up. All of these tips are incredibly helpful! I’m reading through and it all makes a lot of sense. I’m going to implement these (both in the Lupin Tulpa track I’d posted here, as well as some of my other compositions) and report back.

  1. This makes a ton of sense. Disappointed I didn’t think of that :stuck_out_tongue:
  2. I’m not sure that I fully understand this.
    First, I don’t really understand how “it’s possible for several of these if blocks to be triggered on a single pass” (though I do understand that this could lead to the complication I’m experiencing here and in other tracks). My intention was to: check what phrase is currently assigned, run specific code based on what phrase is assigned, and then select another phrase based on the current phrase (making it possible to only stay within the same phrase or move up or down by one phrase). Could you help me understand how the current set up is allowing several if blocks to trigger on a single pass?
    Second, I don’t think I fully understand the alternate code you’ve offered, but let me try to explain it back to you and perhaps you can point out if/where I’m mistaken. Here, I’m defining an array with potential options, ordered by phrase (but they are not explicitly linked to a specific “phrase” variable, so in a sense where as before phrase == -3 can generate (knit -3, 4, -2, 1).choose, now that becomes phrase_sequence [0]). Then phrase is set at the end of the block by passing the current phrase into the array phrase_sequence and “running” choose. I think I understand the logic a bit more typing it out (sometimes that’s what it takes :stuck_out_tongue:) but I’m still concerned about one thing in particular. In this case, wouldn’t the phrase variable be remapped from -3 → 3 to 0 → 6? Not the biggest problem, I can rewrite the code to match this, but want to double check.
  3. Thank you for this! I’ve seen this structure around in-thread but never fully understood it, I’ll definitely implement it for more of the randomized melodies, though I’m tempted to keep the more precise compositions, like drum patterns, arranged in this way so that it’s easier to human-read timings.
  4. :exploding_head: This is huge, thank you for this! I had no idea, and this has been causing me some frustration in another track. This might be a basic question, but when you say check the Lang reference, can you explain what you mean? I’m currently using VS Code to use Sonic Pi, but I can still open up the default editor if need be.

Thanks again for all your help, I really appreciate it. I haven’t made any recordings of this specific track yet, but can post/send your way as I do more documentation. This is a more recent generative composition (a bit of ambient meets noise) where I’m also controlling Hydra Video Synth with the MIDI CCs coming out of Sonic Pi. The CC ticking thing in point 4 was causing a lot of issues here, so this is really helpful. The internet connection was pretty whack so the visuals drop a lot of frames, but I think the music is pretty consistent throughout (with a lot of intentional glitching).

Ah, I’m so glad some of that was helpful! I’m definitely not an expert, but I’ve had to learn a few lessons the hard way (like the tick/look behavior, and a specific thing about syncing live_loops—iykyk) so at this point they are especially salient to me. :smile:

Thanks for that YouTube link. I love it! I’ve never used Ableton (just Ardour on Linux), and these synths/textures are so rich and interesting. You’ve got a way with Hydra too; those scanline effects are :cook: :fire:

You guessed it: by “Lang reference” I meant the last tab of the built-in documentation browser in the Sonic Pi editor. The built-in documentation is super handy: you can just press ctrl-i (on Windows and Linux; not sure what it is on macOS) to jump to the reference for the thing at the cursor. I’m an Emacs diehard in the rest of my life, but for Sonic Pi this feature is compelling enough to keep me in the default editor. (Plus I never really figured out how to control it from Emacs :laughing: )

You’re absolutely right about my rewrite of the phrase thing! I should have adjusted the numbers in the knit structures to match the indices in the array, or maybe more simply changed phrase_sequence[phrase] to phrase_sequence[phrase + 3]. Sorry for the confusion! As for how it could trigger multiple if blocks: it checks every condition every time. If phrase is -3, it might get set to -2. Then it might get set to -1 immediately, then 0, then 1, all before it loops back and actually executes the code for phrase 1. That doesn’t seem like the expected behavior. To avoid this, you could use the elsif statement or perhaps better the case statement from Ruby (the language on which Sonic Pi is built). The array approach feels pretty natural to me, but they should all work the same.

Anyway, now I’m looking back at this, I think you were right that it has more to do with the way the code is arranged: I haven’t really tried nesting live_loops inside each other, and I’m having a hard time wrapping my head around what it does. My approach would be to put all the live_loops at the top level, and use the get/set mechanism to pass the value of phrase among all the loops. Sort of like this:

live_loop :phrase_control do
  # use the get/set syntax to update the phrase variable. I admit this is ugly!
  set :phrase, phrase_sequence[get[:phrase]].choose
  # this loop should fire only once per phrase. If you know the duration, you can sleep:
  sleep phrase_length
end
  
live_loop :choir_phrase do
  sync :tick
  # get the phrase from the global state
  if get[:phrase] == -3 then #etc.
end

live_loop :guit_phrase do
  sync :tick
  if get[:phrase] == -3 then #etc.
end

Does that make sense? This get/set business is one of the less intuitive parts of SP, syntax-wise. At first I always wanted to put parentheses after get, as if it were a function call. But once I got my head around it, it turned out to be awfully useful.

Thanks for that YouTube link. I love it! I’ve never used Ableton (just Ardour on Linux), and these synths/textures are so rich and interesting. You’ve got a way with Hydra too; those scanline effects are :cook: :fire:

I really appreciate the kind words! A lot of the synths that I use (especially for generative compositions) are using Surge XT, an open-source synth/VST. I’d definitely recommend it, and they have a Linux build.

As for how it could trigger multiple if blocks: it checks every condition every time. If phrase is -3, it might get set to -2. Then it might get set to -1 immediately, then 0, then 1, all before it loops back and actually executes the code for phrase 1. That doesn’t seem like the expected behavior. To avoid this, you could use the elsif statement or perhaps better the case statement from Ruby (the language on which Sonic Pi is built). The array approach feels pretty natural to me, but they should all work the same.

Great thanks, I’ll definitely take a look at these and report back.

Anyway, now I’m looking back at this, I think you were right that it has more to do with the way the code is arranged: I haven’t really tried nesting live_loop s inside each other, and I’m having a hard time wrapping my head around what it does. My approach would be to put all the live_loop s at the top level, and use the get/set mechanism to pass the value of phrase among all the loops.

Yeah, I’m definitely not wedded to the nest live_loops, and I actually just “discovered” get/set for another track* so I’ll give it a shot here. I think I’d just nested them in order to ensure that phrase could be set once and affect all of the nested loops.

*side note: I feel weird calling these “tracks” do we have another word to describe our coded music? Particularly when it’s generative and not confined to a set length of time?

@pashultz
Okay, I’ve gone ahead and implemented your changes. Thank you again for all of your suggestions! I actually went ahead and even implemented the rings() because I figured I won’t edit the pattern much more at this point. This is the new and improved code! (with a few questions to follow):

# LUPIN TULPA

/
Beginning code:
- set BPM of track
- create :tick to sync the rest of the live_loops
/

use_bpm 88

live_loop :tick do
  sleep 1
end

/
Define instruments:
- use parameters in function that can be controlled by "phrase_controller"
- set midi channels
/

define :choir do
  use_midi_defaults channel: 1
  sus = (knit 4, 2, 8, 1, 16, 1).choose
  choir_scale = (scale :e3, :major, num_octaves: 1)
  
  midi choir_scale.choose, sustain: sus*3
  sleep sus
  midi choir_scale.choose, sustain: sus*2
  sleep sus
  midi choir_scale.choose, sustain: sus
  sleep sus
end


define :bass do |sus|
  
  use_midi_defaults channel: 2
  bass_note = [:e1, :g1, :a1, :g1].tick
  
  midi bass_note, sustain: sus
  sleep sus
  midi bass_note, sustain: sus
  sleep sus
  midi :es1, sustain: sus
  sleep sus
  midi bass_note, sustain: sus
  sleep sus
end

define :guit do |prob1, prob2, prob3, speed|
  sus = (knit 0.25, 2, 0.5, 1).choose
  guit_scale3_1 = (scale :e3, :major, num_octaves: 1)
  guit_scale4_1 = (scale :e4, :major, num_octaves: 1)
  guit_scale3_2 = (scale :e3, :major, num_octaves: 2)
  guit_scale4_2 = (scale :e4, :major, num_octaves: 2)
  ##| sus = 1
  
  if one_in(prob1)
    midi guit_scale3_1.choose, sustain: sus*speed, channel: 3
    sleep sus*speed
    midi guit_scale3_1.choose, sustain: sus*speed, channel: 3
    sleep sus*speed
    midi guit_scale3_1.choose, sustain: sus*speed, channel: 3
    sleep sus*speed
    midi guit_scale3_1.choose, sustain: sus*speed, channel: 3
    sleep sus*speed
  end
  
  if one_in(prob2)
    midi guit_scale3_1.choose, sustain: sus*speed, channel: 4
    sleep sus*speed
    midi guit_scale4_1.choose, sustain: sus*speed, channel: 4
    sleep sus*speed
    midi guit_scale3_1.choose, sustain: sus*speed, channel: 4
    sleep sus*speed
    midi guit_scale3_1.choose, sustain: sus*speed, channel: 4
    sleep sus*speed
  end
  
  if one_in(prob3)
    midi guit_scale3_2.choose, sustain: sus*speed, channel: 3
    midi guit_scale3_2.choose, sustain: sus*speed, channel: 4
    sleep sus*2*speed
    midi guit_scale4_2.choose, sustain: sus*speed, channel: 3
    midi guit_scale4_2.choose, sustain: sus*speed, channel: 4
    sleep sus*2*speed
    midi guit_scale3_2.choose, sustain: sus*speed, channel: 3
    midi guit_scale3_2.choose, sustain: sus*speed, channel: 4
    sleep sus*2*speed
    midi guit_scale3_2.choose, sustain: sus*speed, channel: 3
    midi guit_scale3_2.choose, sustain: sus*speed, channel: 4
    sleep sus*2*speed
  end
  
  sleep 0.25
end

define :drum1 do
  use_midi_defaults channel: 5, sustain: 0.25

  notes = ring(
    :g2, :g2, :f2
  )

  duration = ring(
    2, 0.25, 1.75
  )

  3.times do
    tick
    midi notes.look
    sleep duration.look
  end
end

define :drum2 do
  use_midi_defaults channel: 5, sustain: 0.25

  notes = ring(
    :g2, :d2, :g2, :f2
  )

  duration = ring(
    0.5, 1.5, 0.25, 1.75
  )

  4.times do
    tick
    midi notes.look
    sleep duration.look
  end
end

define :drum3 do
  use_midi_defaults channel: 5, sustain: 0.25

  notes = ring(
    :g2, :d2, :g2, :f2, :gs2, :a2, :d2
  )

  duration = ring(
    0.5, 1.5, 0.25, 0.5, 0.25, 0.25, 0.75
  )

  7.times do
    tick
    midi notes.look
    sleep duration.look
  end
end

define :drum4 do
  use_midi_defaults channel: 5, sustain: 0.25

  notes = ring(
    :g2, :d2, :gs2, :g2, :f2, :gs2, :a2, :d2,
    :g2, :d2, :e2, :g2, :f2, :gs2, :a2, :d2, :b2
  )

  duration = ring(
    0.5, 0.5, 1, 0.25, 0.5, 0.25, 0.25, 0.75,
    0.5, 0.75, 0.75, 0.25, 0.5, 0.25, 0.25, 0.25, 0.5
  )

  17.times do
    tick
    midi notes.look
    sleep duration.look
  end
end

define :drum5 do
  use_midi_defaults channel: 5, sustain: 0.25

  notes = ring(
    :g2, :d2,
    :gs2, :e2, :b1,
    :g2, :f2, :cs3, :gs2,
    :a2, :d2, :gs2,
    :g2, :d2,
    :gs2, :e2, :b2,
    :g2, :f2, :gs2,
    :a2, :d2, :b2
  )

  duration = ring(
    0.5, 0.5, 
    0.25, 0.5, 0.25, 
    0.25, 0.25, 0.25, 0.25, 
    0.25, 0, 0.75, 
    0.5, 0.5, 
    0.25, 0.5, 0.25, 
    0.25, 0.5, 0.25, 
    0.25, 0.25, 0.5
  )

  23.times do
    tick
    midi notes.look
    sleep duration.look
  end

  /
    midi :g2
    sleep 0.5
    midi :d2
    sleep 0.5
    
    midi :gs2
    sleep 0.25
    midi :e2
    sleep 0.5
    midi :b1
    sleep 0.25
    
    midi :g2
    sleep 0.25
    midi :f2
    sleep 0.25
    midi :cs3
    sleep 0.25
    midi :gs2
    sleep 0.25
    
    midi :a2
    sleep 0.25
    midi :d2
    sleep 0
    midi :gs2
    sleep 0.75
    
    midi :g2
    sleep 0.5
    midi :d2
    sleep 0.5
    
    midi :gs2
    sleep 0.25
    midi :e2
    sleep 0.5
    midi :b2
    sleep 0.25
    
    midi :g2
    sleep 0.25
    midi :f2
    sleep 0.5
    midi :gs2
    sleep 0.25
    
    midi :a2
    sleep 0.25
    midi :d2
    sleep 0.25
    midi :b2
    sleep 0.5
  /
end

define :drum6 do
  use_midi_defaults channel: 5, sustain: 0.25

  notes = ring(
    :g2, :d2,
    :gs2, :e2, :b1,
    :g2, :f2, :cs3, :gs2,
    :a2, :d2, :gs2, :b1,
    :g2, :d2,
    :gs2, :as2, :e2, :b2,
    :g2, :f2, :cs3, :gs2,
    :a2, :d2, :gs2, :d2, :b2, :d2
  )

  duration = ring(
    0.5, 0.5,
    0.25, 0.5, 0.25,
    0.25, 0.25, 0.25, 0.25,
    0.25, 0, 0.25, 0.5,
    0.5, 0.5,
    0, 0.25, 0.5, 0.25,
    0.25, 0.25, 0.25, 0.25,
    0, 0.25, 0, 0.25, 0, 0.5
  )

  29.times do
    tick
    midi notes.look
    sleep duration.look
  end

  /
    #1
    midi :g2
    sleep 0.5
    midi :d2
    sleep 0.5
    
    #2
    midi :gs2
    sleep 0.25
    midi :e2
    sleep 0.5
    midi :b1
    sleep 0.25
    
    #3
    midi :g2
    sleep 0.25
    midi :f2
    sleep 0.25
    midi :cs3
    sleep 0.25
    midi :gs2
    sleep 0.25
    
    #4
    midi :a2
    sleep 0.25
    midi :d2
    midi :gs2
    sleep 0.25
    midi :b1
    sleep 0.5
    
    #5
    midi :g2
    sleep 0.5
    midi :d2
    sleep 0.5
    
    #6
    midi :gs2
    midi :as2
    sleep 0.25
    midi :e2
    sleep 0.5
    midi :b2
    sleep 0.25
    
    #7
    midi :g2
    sleep 0.25
    midi :f2
    sleep 0.25
    midi :cs3
    sleep 0.25
    midi :gs2
    sleep 0.25
    
    #8
    midi :a2
    midi :d2
    sleep 0.25
    midi :gs2
    midi :d2
    sleep 0.25
    midi :b2
    midi :d2
    sleep 0.5
  /
end

live_loop :cc_tuner do
  stop
  tick

  cc10 = (line 70, 50, steps: 100).mirror.look
  midi_cc 20, cc10, channel: 1
  sleep 1
end

live_loop :cc_control, sync: :tick do
  ##| stop
  tick

  cc3 = (line 10, 30, steps: 40).mirror.look
  midi_cc 13, cc3, channel: 1
  
  cc4 = (line 0, 127, steps: 200).mirror.look
  midi_cc 14, cc4, channel: 1
  
  cc5 = (line 127, 0, steps: 200).mirror.look
  midi_cc 15, cc5, channel: 1
  
  cc6 = (line 50, 70, steps: 100).mirror.look
  midi_cc 16, cc6, channel: 1
  
  cc7 = (line 70, 50, steps: 100).mirror.look
  midi_cc 17, cc7, channel: 1
  
  sleep 1
end


/
    Phrase controller:
    - Select which phrase will play
    - Determine how many times a phrase will play before selecting next phrase
    - Create live_loops that use phrase variable to determine the changes in the 
/

phrase = 0


live_loop :phrase_control, sync: :tick do
  ##| stop

  phrase_length = [20, 40, 60, 80, 100].choose*[1,2,4].choose
    # phrase_length = 5

  phrase_sequence = [
    (knit -3, 4, -2, 1),
    (knit -3, 1, -2, 4, -1, 1),
    (knit -2, 1, -1, 4, 0, 1),
    (knit -1, 1, 0, 4, 1, 1),
    (knit 0, 1, 1, 4, 2, 1),
    (knit 1, 1, 2, 4, 3, 1),
    (knit 2, 1, 3, 4)
    ]

  phrase = phrase_sequence[phrase - 3].choose
  set :phrase, phrase

  sleep phrase_length
end

live_loop :cc_phrase do
    if get[:phrase] == -3 then
        sleep 1
      end
      if get[:phrase] == -2 then
        sleep 1
      end
      if get[:phrase] == -1 then
        sleep 1
      end
      if get[:phrase] == 0 then
        tick
    
        cc1 = (line 50, 90, steps: 400).mirror.look
        midi_cc 11, cc1, channel: 1
        
        cc2 = (line 90, 70, steps: 400).mirror.look
        midi_cc 12, cc2, channel: 1
        
        cc8 = (line 30, 60, steps: 100).mirror.look
        midi_cc 18, cc8, channel: 1
        
        cc9 = (line 60, 90, steps: 100).mirror.look
        midi_cc 19, cc9, channel: 1
        
        cc10 = (line 50, 100, steps: 200).mirror.look
        midi_cc 20, cc10, channel: 1
        
        sleep 1
      end
      if get[:phrase] == 1 then
        tick 
    
        cc1 = (line 50, 70, steps: 400).mirror.look
        midi_cc 11, cc1, channel: 1
        
        cc2 = (line 90, 70, steps: 400).mirror.look
        midi_cc 12, cc2, channel: 1
        
        cc8 = (line 40, 80, steps: 50).mirror.look
        midi_cc 18, cc8, channel: 1
        
        cc9 = (line 40, 70, steps: 100).mirror.look
        midi_cc 19, cc9, channel: 1
        
        cc10 = (line 50, 80, steps: 200).mirror.look
        midi_cc 20, cc10, channel: 1
        
        sleep 1
      end
      if get[:phrase] == 2 then
        tick
    
        /
        cc1 = (line 30, 50, steps: 400).mirror.look
        midi_cc 11, cc1, channel: 1
    
        cc2 = (line 90, 70, steps: 400).mirror.look
        midi_cc 12, cc2, channel: 1
        /
        cc8 = (line 80, 100, steps: 50).mirror.look
        midi_cc 18, cc8, channel: 1
        
        cc9 = (line 40, 90, steps: 100).mirror.look
        midi_cc 19, cc9, channel: 1
        /
        cc10 = (line 50, 80, steps: 200).mirror.look
        midi_cc 20, cc10, channel: 1/
        
        sleep 1
      end
      if get[:phrase] == 3 then
        ##| repeats = 4
        sleep 1
      end
end

live_loop :choir_phrase, sync: :tick do

    if get[:phrase] == -3 then
      choir
    end
    if get[:phrase] == -2 then
      choir
    end
    if get[:phrase] == -1 then
      choir
    end
    if get[:phrase] == 0
      choir
    end
    if get[:phrase] == 1 then
      choir
    end
    if get[:phrase] == 2 then
      choir
    end
    if get[:phrase] == 3 then
      choir
    end
end
  
live_loop :bass_phrase, sync: :tick do
    if get[:phrase] == -3 then
      bass 4
    end
    if get[:phrase] == -2 then
      bass 4
    end
    if get[:phrase] == -1 then
      bass 4
    end
    if get[:phrase] == 0
      bass 2
    end
    if get[:phrase] == 1 then
      bass 4
    end
    if get[:phrase] == 2 then
      bass 4
    end
    if get[:phrase] == 3 then
      bass 4
    end
end
  
live_loop :guit_phrase, sync: :tick do
    if get[:phrase] == -3 then
      guit 2, 2, 1, (knit 1, 2, 2, 1).choose
    end
    if get[:phrase] == -2 then
      guit 1, 1, 4, 2
      if one_in(2)
        sleep (knit 4, 1, 8, 2, 16, 1).choose
      end
    end
    if get[:phrase] == -1 then
      guit 1, 0, 0, (knit 4, 1, 8, 4).choose
      if one_in(1)
        sleep (knit 4, 1, 8, 2, 16, 4).choose
      end
    end
    if get[:phrase] == 0
      sleep 1
    end
    if get[:phrase] == 1 then
      sleep 1
    end
    if get[:phrase] == 2 then
      sleep 1
    end
    if get[:phrase] == 3 then
      sleep 1
    end
end
  
live_loop :drum_phrase do
    if get[:phrase] == -3 then
      sleep 1
    end
    if get[:phrase] == -2 then
      sleep 1
    end
    if get[:phrase] == -1 then
      sleep 1
    end
    if get[:phrase] == 0 then
      drum1
    end
    if get[:phrase] == 1 then
      drum2
    end
    if get[:phrase] == 2 then
      drum3
      ##| drum4
    end
    if get[:phrase] == 3 then
      drum5
      drum6
    end
end

So the two places where I’m still encountering some issues are:

  1. Occasionally, when the phrase is -3 or 3, it will jump to the other end (-3 → 3, 3 → -3). I’m not sure what’s causing this… Could this be an absolute value thing? Something to do with the fact that the phrases pass into the negative (I don’t think I will do that for future code, but I do think it was useful for this particular track as a demonstration that there are two poles that are musically different ideas that the code is oscillating between).
  2. The drum patterns for drum5 and drum6 are a bit wonky, and I can’t tell why. Perhaps a second set of eyes could be useful! I left in the original timings commented out. I’m wondering if this has to do with the fact that there are at times notes that are suppose to play at the same time? In those cases, I simply added a 0 to the duration ring, but even as I was doing that, it felt off.

Thanks again for all your help. I’m going to make a video soon walking folks through the code and I’ll definitely shout you out! You’ll also be able to hear what the track sounds like in the background.

1 Like

Excellent! I’m pretty sure the phrase change problem is just because of a typo. There should be a + instead of a - here:

phrase = phrase_sequence[phrase - 3].choose
  set :phrase, phrase

As for drum5 and drum6, I think you’re right about the 0-duration sleeps. I believe that will cause the loop to try to tick twice at the same time, which would lead to undefined behavior—perhaps even wonky behavior. Hmm.

Unfortunately, I don’t see an elegant fix. I think the idiomatic SP way would be to put the two simultaneous notes into an array, since you can play [:d2, :gs2] with the built-in synths. Unfortunately you can’t midi [:d2, :gs2] in the same way. (I’d consider this a bug, but it’s been known for ages, so maybe they prefer it this way for some reason?) But you can work around it by writing a little handler for arrays:

define :drum5 do
  use_midi_defaults channel: 5, sustain: 0.25
  
  notes = ring(
    :g2, :d2,
    :gs2, :e2, :b1,
    :g2, :f2, :cs3, :gs2,
    :a2, [:d2, :gs2],
    :g2, :d2,
    :gs2, :e2, :b2,
    :g2, :f2, :gs2,
    :a2, :d2, :b2
  )
  
  duration = ring(
    0.5, 0.5,
    0.25, 0.5, 0.25,
    0.25, 0.25, 0.25, 0.25,
    0.25, 0.75,
    0.5, 0.5,
    0.25, 0.5, 0.25,
    0.25, 0.5, 0.25,
    0.25, 0.25, 0.5
  )
  
  23.times do
    tick
    n = notes.look
    if n.respond_to?('each')
      n.each do |x|
        midi x
      end
    else
      midi n
    end
    sleep duration.look
  end
end

Can’t wait to hear it! And thanks so much for the shout-out!

A lot of the synths that I use (especially for generative compositions) are using Surge XT, an open-source synth/VST. I’d definitely recommend it, and they have a Linux build.

Oh yeah! I’ve installed it and tried a couple of the presets but not really messed around with it. Need to do more, clearly.

*side note: I feel weird calling these “tracks” do we have another word to describe our coded music? Particularly when it’s generative and not confined to a set length of time?

Great question. Probably depends on how deterministic and interactive it is: I’ve used/heard “works,” “pieces,” “systems,” “processes,” (in descending order of determinism) maybe even “virtual ensembles” or “hyperinstruments” if they’re interactive and I’m feeling fanciful. Though when I’m talking to people who are NOT older and wealthier than me, they’re “things.” :person_shrugging:

There should be a + instead of a - here:

:person_facepalming:
Right, that makes sense.

Unfortunately, I don’t see an elegant fix. I think the idiomatic SP way would be to put the two simultaneous notes into an array, since you can play [:d2, :gs2] with the built-in synths. Unfortunately you can’t midi [:d2, :gs2] in the same way. (I’d consider this a bug, but it’s been known for ages, so maybe they prefer it this way for some reason?) But you can work around it by writing a little handler for arrays

Okay, I’m going to try this, I just want to understand it so I’m going to try to break it down:

if n.respond_to?('each')
      n.each do |x|
        midi x
      end
    else

Here, we’re using n instead of notes because in this if statement, n represents an array. .respond_to?('each') can only work with arrays so it’s essentially checking if it’s an array and saying “if there’s an array here with more than one item, then for each of these items (x) midi x.”

Is that about right?

1 Like

Exactly! Great explanation.
[edited to add:] Here’s how I got there. I had seen examples of midi_chord functions on this forum that used the .each method. But we want something here that can handle both arrays and single notes, so we have to know when we can safely use .each. I’m actually not especially familiar with Ruby, so I did a web search for “ruby array test.” The second-ranked answer in this StackOverflow thread asks,

Are you sure it needs to be an array? You may be able to use respond_to?(method) so your code would work for similar things that aren’t necessarily arrays…

… which got me thinking. I’ve suspected (but not really checked) that a lot of Sonic Pi’s types may be derived from the equivalent Ruby ones without necessarily being identical. We could probably use the kind_of? predicate suggested in the top answer to catch anything that inherits from Array, but here all we really need is something that can be .each-ed! So I went with that.

Yes you use the respond to method. I have done that often.
e.g.

define :pmidi do |n,sus=1,vf = 0.2| #default for sus and v can be used
  if n.respond_to?(:each)
    n.each do |nv|
      midi nv,sustain: sus, vel_f: vf # no sleep between
    end
  else
    midi n, sustain: sus, vel_f: vf
  end
  sleep sus #sleep duration of note or list here
end

This will work for single note values or for two or three notes together in a list eg

live_loop :test do
  pmidi [:c4,:e4] #default 1 beat
  pmidi 72,0.25,0.9 #one note, sustain 0.25,vel_f 0.9
end

Here is a fuller version I use when playing lsitrs of notes and duration which may include chords as well.

#function to play midi notes/duration lists with optional transpose
define :mplay do |notes,durs,channel,tr,vel=100|
  for x in 0..notes.length-1
    if notes[x].respond_to?(:each)
      notes[x].each do |n|
        midi note(n)+tr,sustain: durs[x],channel: channel,velocity: vel
      end
    else
      midi note(notes[x])+tr,sustain: durs[x],channel: channel,velocity: vel
    end
    sleep durs[x]
  end
end