One thing that hasn’t been mentioned and is worth knowing early: in the two-loop version above, the bass loop and the chord loop each keep their own tick counter, and they only stay in agreement because the sleep times happen to line up (2 beats of chord equals 4 x 0.5 beats of bass). The moment you change one loop’s timing, or restart one of them while the other is running, they silently drift apart and the bass follows the wrong chord.
Since tick is local to each live_loop, the robust fix is to have a single authority for “which chord are we on”. The idiomatic Sonic Pi way is a cue: make the chord loop the boss and have it announce the chord, and have the bass loop wait for that announcement, so it can’t drift by construction.
triads = (ring :e3, :a3, :b3, :e3)
live_loop :harmony do
c = chord(triads.tick, :minor)
cue :chord_change, root: c[0]
use_synth :fm
play c + 12
sleep 2
end
live_loop :bassline do
info = sync :chord_change
use_synth :bass_foundation
4.times do
play info[:root] - 12, release: 0.6
sleep 0.5
end
end
Note the bass loop has no tick and no hardcoded chord list at all: it only knows what it was told. Change the progression, change the harmonic rhythm, reorder the ring, and the bass follows automatically. sync also blocks until the cue arrives, so the loops start in phase even if you evaluate the buffer mid-bar.
On the melodic side of your question, once you have the root you can walk around the chord rather than hammering one note. Indexing the chord keeps everything consonant for free:
c = chord(triads.look, :minor)
pattern = (ring 0, 0, 2, 1) # root, root, fifth, third
play c[pattern.tick] - 12
And as HarryLeBlanc says, notes are just numbers, so minus 12 is an octave down, plus 7 is a fifth up, and adding a passing note is arithmetic rather than another chord lookup. If you want something less rigid, picking from (scale :e3, :minor_pentatonic) with .choose on the weak beats while keeping chord tones on the strong beats gives you movement without ever landing out of key.