Recursive 'melodic' thread creation

Hi there,

I’d like to share with you one of the first things I did in Sonic Pi. It’ll play for about 5 minutes before it stops. Not very engaging music, but nice in the background.

# Made 2025-03-09

define :play_complex do |n, v|
  return if n == -1

  weight_on_lower = (0.8)**((n-69)/12)  # extra weight on duration(release) and velocity in lower octaves
  release = weight_on_lower*1.66

  vel = v+0.1*rrand(0, 1)
  amp1 = 0.1*weight_on_lower*(0.5+vel)
  amp2 = amp1*(0.1+0.7*vel)
  amp3 = amp2*(0.1+0.5*vel)

  # each note is made into a complex: base-note and octave doubled with added octaves and higher 5ths
  notes = [n-0.05, n+0.05, n+12+0.02, n+12-0.02, n+19, n+24, n+36, n+43]
  amps = [amp1, amp1, amp2, amp2, amp3, amp3*0.5, 0.05*amp1*(0.7+vel), 0.02*amp1*(0.1+vel)]
  rel_release = [1, 1, 0.8, 0.8, 0.5, 0.4, 0.1, 0.02]
  pans = [-1, 1, -1, 1, -0.5, 0.5, -0.2, 0.2]

  notes.each_with_index do |note, i|
    play note, amp: amps[i], pan: pans[i], release: rel_release[i]*release
  end
end

define :play_and_sprout do |s, n, v, r|
  return if v < 0.012

  in_thread do
    play_complex n, v

    # more rythmic timing
    sleep (1+rand_i(3))*0.5*s
    # more dreamy timing
    # sleep (1+rrand(0, 2)*0.5)*s

    offspring = dice(6) < 6 ? 1 : 2  # sometimes sprout two notes
    offspring.times do
      next_chord_note = (chord r+12*[0,0,0,0,0,0,1,1,2].choose, :major).choose
      next_alt_note = (chord n + 12 * 12 * [0, 0, 0, 1].choose, :major).choose
      n_next = dice(12) < 12 ? next_chord_note : next_alt_note

      v_next = dice(10) < 10 ? 0.5*v : 1.4*v
      v_next = 0.9 if v_next > 0.9

      play_and_sprout s, n_next, v_next, r
    end
  end
end

chords = [:c2, :c3, :g3, :f3, :c3, :f2, :g2, :c3]
song_arc = [0.0, 0.2, 0.5, 0.7, 0.8, 0.7, 0.9, 0.3, 0.1, 0.0]
with_fx :gverb, room: 30 do
  use_bpm 96
  loop do
    bars_per_chord = 2
    root = chords.ring[tick/bars_per_chord]
    pt = chords.length*bars_per_chord  # define tempo of progression for calculating progression in arc
    arc_vel = (song_arc[look/pt]*(pt-look%pt) + song_arc[look/pt+1]*(look%pt))/pt  # no ring. song will end automatically

    play_and_sprout 1, root, 0.4*arc_vel, root
    sleep 2
    play_and_sprout 0.5, root+7, 0.33*arc_vel, root
    sleep 2
  end
end

I like how it is both random, as well as adhering to an idea of what the song should be like.

When it plays, some dissonant notes appear. No doubt because of the way the source for next_alt_note is picked. One can make the creation of this more consonant, but I quite like the fact some of these weird notes pop up.

The play_complex-function was inspired by a tune on dittytoy.net (the site that set me onto Sonic Pi).

As a programmer I notice that there are a lot of parameters, magic values if you like.

Love to hear you feedback

Recording on Soundcloud

3 Likes

I like it!
It never occurred to me that sonic pi could emulate a synth’s unison spread feature, but it’s a cool idea. I think I’m going to take a stab at that myself.
BTW, single-letter varibable names drive me crazy. They’re so cryptic, it makes it harder for other people to grok the intent. Just my pet peeve. My only exception is i for a loop index.

Thanks!

I’m very much a newbie in dealing with DAWs, Synths and other intersections of electronics and music. Is ‘a synth’s unison spread feature’ what is done in the play_complex-function? It’s the part of the code I didn’t come up with myself.

Any form of using synths for creating and desiging sounds - be it as standalone equipment, or in a computer - is something I need to learn more about.

You are right about the single-letter variables. I changed them, and hope that makes the code more readable at a glance. (Can no longer edit original post, probably because there is a reply now). (Only left the pt, otherwise the next line would explode in length). Furthermore I made the next_alt_note more consonant, and cleaned up code a bit more, moved the use_bpm into each thread. I don’t know if that is necessary.

# Made 2025-03-09

# play a complex of notes, based on note (n) and velocity (v) - derived from earlier rework of a program on dittynet
define :play_complex do |note_in, velocity|
  weight_on_lower = (0.8)**((note_in-69)/12)  # extra weight on duration(release) and velocity in lower octaves
  release = weight_on_lower*1.66

  vel = velocity+0.1*rrand(0, 1)
  amp1 = 0.1*weight_on_lower*(0.5+vel)
  amp2 = amp1*(0.1+0.7*vel)
  amp3 = amp2*(0.1+0.5*vel)

  # each note is made into a complex: base-note and octave doubled with added octaves and higher 5ths
  notes = [note_in-0.05, note_in+0.05, note_in+12+0.02, note_in+12-0.02, note_in+19, note_in+24, note_in+36, note_in+43]
  amps = [amp1, amp1, amp2, amp2, amp3, amp3*0.5, 0.05*amp1*(0.7+vel), 0.02*amp1*(0.1+vel)]
  relative_release = [1, 1, 0.8, 0.8, 0.5, 0.4, 0.1, 0.02]
  pans = [-1, 1, -1, 1, -0.5, 0.5, -0.2, 0.2]

  notes.each_with_index do |note, i|
    play note, amp: amps[i], pan: pans[i], release: relative_release[i]*release
  end
end

define :play_and_sprout do |timing, note, velocity, root|
  return if velocity < 0.012

  play_complex note, velocity
  # more rythmic timing
  sleep (1+rand_i(3))*0.5*timing
  # more dreamy timing
  # sleep (1+rrand(0, 2)*0.5)*timing

  in_thread do
    use_bpm 96
    offspring = dice(6) < 6 ? 1 : 2  # sometimes sprout two notes
    offspring.times do
      next_chord_note = (chord root+12*[0,0,0,0,0,0,1,1,2].choose, :major).choose
      next_alt_note = (chord root+12*[1, 1, 1, 2].choose, [:add2, :add4, :add9, :add11, :add13].choose).choose
      next_note = dice(12) < 12 ? next_chord_note : next_alt_note

      next_velocity = dice(10) < 10 ? 0.5*velocity : 1.4*velocity
      next_velocity = 0.9 if next_velocity > 0.9

      play_and_sprout timing, next_note, next_velocity, root
    end
  end
end

chords = [:c2, :c3, :g3, :f3, :c3, :f2, :g2, :c3]
song_arc = [0.0, 0.2, 0.5, 0.7, 0.8, 0.7, 0.9, 0.3, 0.1, 0.0]   # no ring. song will end automatically
with_fx :gverb, room: 30 do
  live_loop :sprouting do
    use_bpm 96
    bars_per_chord = 2
    root = chords.ring[tick/bars_per_chord]
    pt = chords.length*bars_per_chord  # tempo of progression, used for calculating progression in arc
    arc_vel = (song_arc[look/pt]*(pt-look%pt) + song_arc[look/pt+1]*(look%pt))/pt

    play_and_sprout 1, root, 0.4*arc_vel, root
    sleep 2
    play_and_sprout 0.5, root+7, 0.33*arc_vel, root
    sleep 2
  end
end
1 Like

Just tried your recursive melodic threads… works like a dream. Thanks for sharing. Interesting how you didn’t use a ring and got the tune to end automatically. Nice code :slight_smile:

1 Like

Really enjoying playing with this. Altering parametrers eg different chords, different offspring numbers, different bars per chord etc. There is a huge range to exploit.

Some really cool code.
EDIT also choosing different synths on each iteration of the :sprouting live_loop. Pluck gives some great base nortes.

1 Like