Sonic LP -- Introduction

Literate Music Programming with Sonic Pi and Yarner

In this project, I documents my progress in music programming with Sonic Pi.

The project uses the Literate Programming tool Yarner, which extracts all the code from the book into files that can be opened and played in Sonic Pi. Extracted files can be found in the GitHub repository, directory code. Literate Programming sources in Markdown can be found in directory lp.

First Steps

House bounce

After discovering Sonic Pi a week before, and some playing with it in Live Coding, I decided to code a first complete track.

Starting from a simple bass line, it developed into a bouncy House tune. Listen here:

The track uses only built-in samples and synths of Sonic Pi, so the code should work without any additional setup.

The extracted code for direct use in Sonic Pi can be found in the GitHub repository: code/House/HouseBounce.rb.

Contents

Tempo

With 125 BMP, I decided to go for the upper range of typical House tracks.

#- Setup
use_bpm 125

Usage: House/HouseBounce.rb

Instruments

The track is played by a rather small combo of instruments: drums, a bass line, and playful bell or xylophone-like melody.

Notes and Sonic Pi code for each of the instruments are presented and explained in the following sections.

Drums

I used a standard House drums arrangement with the bass drum playing a 4-to-the-floor rhythm. The snare plays on every second beat, while cymbals play between beats.

X: 1 M: 4/4 L: 1/16 K: perc U:n=!style=x! V: BD name="Bass drum" V: SN name="Snare" V: CY name="Cymbal" [V:BD] F4 F4 F4 F4| [V:SN] z4 nG4 z4 nG4| [V:CY] z2 nf2 z2 nf2 z2 nf2 z2 nf2|

In the code, I use the function play_rhythm_sample, which allows me to easily define drum patterns as strings of e.g. 8th notes. The bass drum pattern is commented out, as we trigger beats individually (see below).

#- Drum rhythms
# bd_rhythm =    "x-x-x-x-"
drum_rhythm =    "--x---x-"
cymbals_rhythm = "-x-x-x-x"

Usage: House/HouseBounce.rb

Next, I present the code that actually plays these patterns. I use a live_loop for each instrument.

#- Drums
# ==> Bass drum.
# ==> Snare.
# ==> Cymbal.

Usage: House/HouseBounce.rb
Macros: Bass drum Snare Cymbal

At the start of each live loops, you will see lines like amp = bd_amp[tick + offset]. Here, we get parameters like the volume of the instrument over the course of the track. Section Title structure presents how this is actually implemented.

For the bass drum, I use the sample :bd_haus. It is played every quarter note, with a low pass filter. The filter is set to a higher cutoff on every second beat.

Additionally, a high pass filter is used to prevent overload in combination with the bass line.

#- Bass drum
live_loop :bd, sync: :main do
  amp = bd_amp[tick + offset]
  8.times do
    sample :bd_haus, lpf: 90, hpf: 45, amp: amp
    sleep 1
    sample :bd_haus, lpf: 110, hpf: 45, amp: amp
    sleep 1
  end
end

Usage: Drums

As a snare replacement, I use the sample :elec_twip, and play the pattern presented above. To make the sound more snappy, I cut off the release part of the sample (with finish: 0.15)

#- Snare
live_loop :drums, sync: :main do
  amp = drums_amp[tick + offset]
  4.times do
    use_sample_defaults finish: 0.15
    play_rhythm_sample :elec_twip, 0.5, drum_rhythm, amp: amp
  end
end

Usage: Drums

Finally, I used the closed cymbal sample :drum_cymbal_closed to play the respective pattern (i.e. between beats). Again, I cut the release part for a more snappy sound. Further, I added a high pass filter.

#- Cymbal
live_loop :cymbal, sync: :main do
  amp = cymbal_amp[tick + offset]
  4.times do
    use_sample_defaults hpf: 100, finish: 0.15, amp: amp
    play_rhythm_sample :drum_cymbal_closed, 0.5, cymbals_rhythm, amp: amp
  end
end

Usage: Drums

Bass

After some playing with the F minor pentatonic scale, I came up with this base melody for the bass line:

X: 1 M: 4/4 L: 1/4 E4|B4|d3B1|G2A2|\ E4|B4|d3B1|A2G2|

To make the track more interesting, I modified the above sequence to have two alternatives to play. First, I added an octave jump to the two half notes in every 4th bar:

X: 1 M: 4/4 L: 1/4 E4|B4|d3B1|GgAa|\ E4|B4|d3B1|AaGg|

For a 3rd melody, I added similar jumps to the whole notes:

X: 1 M: 4/4 L: 1/4 E3e|B3b|d3B|GgAa|\ E3e|B3b|d3B|AaGg|

Here, the melodies are expressed in Sonic Pi notes and durations (in beats):

#- Bass notes
bass_delay = 0.06
bass_notes = [
  [
    [[:e1, :b1, :d2, :b1, :g1, :a1],     [4 - bass_delay, 4, 3, 1, 2, 2]],
    [[:e1, :b1, :d2, :b1, :a1, :g1],     [4 - bass_delay, 4, 3, 1, 2, 2]]
  ],
  [
    [[:e1, :b1, :d2, :b1, :g1, :g2, :a1, :a2],     [4 - bass_delay, 4, 3, 1, 1, 1, 1, 1]],
    [[:e1, :b1, :d2, :b1, :a1, :a2, :g1, :g2],     [4 - bass_delay, 4, 3, 1, 1, 1, 1, 1]]
  ],
  [
    [[:e1, :e2, :b1, :b2, :d2, :b1, :g1, :g2, :a1, :a2],     [3 - bass_delay, 1, 3, 1, 3, 1, 1, 1, 1, 1]],
    [[:e1, :e2, :b1, :b2, :d2, :b1, :a1, :a2, :g1, :g2],     [3 - bass_delay, 1, 3, 1, 3, 1, 1, 1, 1, 1]]
  ]
]

Usage: House/HouseBounce.rb

Note the bass_delay. This was necessary as, for some reason, changing the note of the bass synth (see below) is delayed by approximately 0.03 seconds (or 0.06 beats). To compensate for that, the first note of the bass melody is slightly shortened.

I used the :dsaw synth for the bass line, routed through a slicer FX.

#- Bass
live_loop :bass, sync: :main do
  t = tick * 2 + offset
  amp = bass_amp[t]
  amp_end = bass_amp[t + 1]

  amp_min = bass_min_amp[t]
  shift = bass_shift[t]
  cutoff = bass_cutoff[t]
  melody = bass_melody[t]

  with_fx :slicer, phase: 0.5, smooth_down: 0.05, amp_min: amp_min do |sn|
    with_synth :dsaw do
      use_synth_defaults amp: amp, detune: 12, cutoff_min: 45 #, attack: 0.02

      m = bass_notes[melody][0]
      syn = play m[0][0] + shift, cutoff: 80, res: 0.75, sustain: 16, release: 0
      at do
        sleep m[1][0]
        slide_timed_synth syn, m[0][1..], m[1][1..], shift: shift
      end

      sleep 0.25
      control syn, cutoff: 92, cutoff_slide: 16, amp: amp_end, amp_slide: 31.75
      sleep 15.75


      m = bass_notes[melody][1]
      syn = play m[0][0] + shift, cutoff: 80, res: 0.75, sustain: 16, release: 0
      at do
        sleep m[1][0]
        slide_timed_synth syn, m[0][1..], m[1][1..], shift: shift
      end

      sleep 0.25
      control syn, cutoff: 80, cutoff_slide: 0
      control syn, cutoff: cutoff, cutoff_slide: 16
      sleep 7.75

      control sn, phase: 0.25
      sleep 6
      control sn, phase: 0.333
      sleep 2
    end
  end
end

Usage: House/HouseBounce.rb

After each melody part is triggered and released to its own thread, I modulate the cutoff value of the synth, as well as the slicer's phase. Using phases of 0.5, 0.25 and 0.333 beats (8th, 16th and 12th notes, respectively), the bass line turns into something like this:

X: 1 M: 4/4 L: 1/16 E2E2 E2E2 E2E2 E2E2|B2B2 B2B2 B2B2 B2B2|d2d2 d2d2 d2d2 B2B2|G2G2 G2G2 A2A2 A2A2| E2E2 E2E2 E2E2 E2E2|B2B2 B2B2 B2B2 B2B2|dddd dddd dddd BBBB|AAAA AAAA ((3GGG) ((3GGG)|

With the slicer's amp_min parameter, we can switch between the continuous and the "wobbled" melodies during the track.

Bells

To add some interest in the upper tonal range, and to make the track more playful, I decided to add some bell- or xylophone-like sounds. After experimenting with some random melodies from the same scale as the bass line (F minor pentatonic), I came up with this one:

X: 1 M: 4/4 L: 1/16 e2eg ege2 e2ge e2d2|e2g2 ege2 e2ge e2d2|

While playing with the bass wobble and the bells, I realized that the triplet wobble at the end of each bass loop conflicts with the quarter structure of the bell melody. Thus, I built a variation of the pattern with a simplified end:

X: 1 M: 4/4 L: 1/16 e2eg ege2 e2ge e2d2|e2g2 ege2 e4 g4|

Here are the bell melodies expressed for Sonic Pi:

#- Bell notes
bell_notes = [
  [
    [[:e5,  :e5,  :g5,  :e5,  :g5, :e5], [0.5, 0.25, 0.25, 0.25, 0.25, 0.5]],
    [[:e5,  :g5,  :e5,  :e5,  :d5],      [0.5, 0.25, 0.25,  0.5,  0.5]],
    [[:e5,  :g5,  :e5,  :g5,  :e5],      [0.5, 0.5, 0.25, 0.25, 0.5]],
    [[:e5,  :g5,  :e5,  :e5,  :d5],      [0.5, 0.25, 0.25,  0.5,  0.5]]
  ],
  [
    [[:e5,  :e5,  :g5,  :e5,  :g5, :e5], [0.5, 0.25, 0.25, 0.25, 0.25, 0.5]],
    [[:e5,  :g5,  :e5,  :e5,  :d5],      [0.5, 0.25, 0.25,  0.5,  0.5]],
    [[:e5,  :g5,  :e5,  :g5,  :e5],      [0.5, 0.5, 0.25, 0.25, 0.5]],
    [[:e5, :g5],      [1, 1]]
  ]
]

Usage: House/HouseBounce.rb

I used the :pretty_bell synth, with an echo effect that is modulated over the course of the track. I use the function play_timed_synth to conveniently play the above melody.

See section Title structure to see how the melody to play, as well as shift and echo, are modulated through the track.

#- Bells
live_loop :bell, sync: :main do
  amp = bell_amp[tick + offset]

  if amp > 0
    shift = bell_shift[look + offset]
    echo = bell_echo[look + offset]
    melody = bell_melody[look + offset]
    with_fx :echo, phase: 0.25, decay: 0.5, mix: echo, amp: 1 do |fx|
      with_synth :pretty_bell do
        use_synth_defaults amp: amp, release: 0.2
        for idx in melody
          for m in bell_notes[idx]
            play_timed_synth m[0], m[1], shift: shift
          end
        end
      end
    end
  else
    sleep 16
  end
end

Usage: House/HouseBounce.rb

Please note that the condition if amp > 0 is necessary here due to a bug in the :pretty_bell synth, which should be fixed in the next Sonic Pi release (3.3.2).

Title structure

To keep everything in sync, I use the loop :main, which triggers syncing of all other loops every 16 beats, or 4 bars. The offset variable can be used to start the track at a certain point, for tweaking.

The :main loop starts with a short delay to allow the other loops which are synchronized with it to start before the first cue.

#- Main loop
offset = 0

live_loop :main, delay: 0.01 do
  t = tick(:loop)
  puts t + offset
  sleep 4 * 4
end

Usage: House/HouseBounce.rb

To conveniently steer different parameters of the instruments, like volume, effects, etc., I implemented the functions str_scale and str_select, which convert strings to arrays and allow for the compact representation shown below.

Each character corresponds to one iteration of loop :main, i.e. 4 bars. Each section of 8 characters corresponds to one whole "arc of suspense" of 32 bars. These sections can also be seen as the building blocks of the track.

For the lines with str_scale, the characters -, 1, 2, ..., X stand for 0%, 10%, 20%, ..., 100% of the maximum value. As an example, bell_amp is used to regulate the volume of the bell melody, starting with 30% of the maximum. Here, the maximum is 0.3, so the bell starts with amp: 0.09 (0.3 * 0.3).

For the lines with str_select, the characters -, 1, ... correspond to zero-based indices into the array given as additional function parameter.

#- Title structure
##########################  0        8        16       24       32       40       48       56       64
bell_amp     =  str_scale("|33334343|----6655|----6666|----6688|XX668854|XXXX8876|8888XX76|X7X7X786|6666----|", max: 0.3 )
bell_shift   = str_select("|22111-11|----11--|11111111|11111111|1111----|1111----|----11--|1-1-1-1-|--------|", [0, -12, -24])
bell_echo    =  str_scale("|13375355|11111111|33333333|33333358|35353535|35353535|35355555|55555555|5688----|")
bell_melody  = str_select("|--------|-------1|-----1-1|-----1-1|-1-1-1-1|-1-1-1-1|-1-1-1-1|-------1|---1----|", [[0, 0], [0, 1]])

bd_amp       =  str_scale("|------11|XXXXXXX7|XXXXXXX7|--223456|XXXXXXX6|XXXXXXXX|XXXXXXXX|XXXX9988|--------|", max: 0.8)
drums_amp    =  str_scale("|--112222|33333333|33333333|11111112|55555555|55555555|55555555|55555555|--------|")

cymbal_amp   =  str_scale("|----2233|44444444|44444443|------22|44444444|44444444|44444444|33333333|--------|")

bass_amp     =  str_scale("|----1111|33333333|88888886|55555555|99999999|99999999|99999999|88667765|41------|", max: 0.7)
bass_cutoff  = str_select("|------11|------11|--------|------11|------11|------11|--11--11|--------|--------|", [92, 104])
bass_min_amp =  str_scale("|XXXXXXXX|99997777|33334477|XXXXXX77|22332244|22443377|22443377|88889999|XXXXXXXX|")
bass_shift   = str_select("|11112211|--------|--------|------12|--------|--------|----11--|--11--11|--------|", [12, 24, 36])
bass_melody  = str_select("|--------|------11|------22|--------|------11|--22--22|------22|--------|--------|", [0, 1, 2])

Usage: House/HouseBounce.rb

File structure

The code blocks of this document are arranged to a complete Sonic Pi program by inserting them in the output file in this order:

#- file:House/HouseBounce.rb
# ==> Setup.
# ==> Functions.
# ==> Main loop.
# ==> Title structure.
# ==> Drum rhythms.
# ==> Bass notes.
# ==> Bell notes.
# ==> Drums.
# ==> Bass.
# ==> Bells.

Macros: Setup Functions Main loop Title structure Drum rhythms Bass notes Bell notes Drums Bass Bells

This is done by the Literate Programming tool Yarner, by running it in the project's root directory. Alternatively, the extracted can be found in the GitHub repository: code/House/HouseBounce.rb.

Functions

A number of functions are required, to play melodies as well as to steer instruments using a compact string representation.

#- Functions
# ==> play_rhythm_sample.
# ==> play_timed_synth.
# ==> slide_timed_synth.
# ==> str_scale.
# ==> str_select.

Usage: House/HouseBounce.rb
Macros: play_rhythm_sample play_timed_synth slide_timed_synth str_scale str_select

play_rhythm_sample

Plays a rhythm from a string pattern.

#- play_rhythm_sample
define :play_rhythm_sample do |samp, duration, rhythm, amp: 1|
  for i in 0..rhythm.length-1
    s = rhythm[i]
    if (s == "|") or (s == " ")
      # Bar line, do nothing
    elsif s == "-"
      sleep duration
    elsif (s == "x") or (s == "X")
      sample samp, amp: amp
      sleep duration
    else
      a = amp * Integer(s) / 10.0
      sample samp, amp: a
      sleep duration
    end
  end
end

Usage: Functions

play_timed_synth

Plays a melody from an array of notes and an array sleep times.

#- play_timed_synth
define :play_timed_synth do |notes, times, shift: 0|
  for i in 0..notes.length-1
    play notes[i] + shift
    sleep times[i]
  end
end

Usage: Functions

slide_timed_synth

Plays a melody from an array of notes and an array sleep times, controlling the synth's note rather then playing multiple times.

#- slide_timed_synth
define :slide_timed_synth do |syn, notes, times, shift: 0|
  amp = (current_synth_defaults || {amp: 1})[:amp] || 1
  for i in 0..notes.length-1
    control syn, note: notes[i] + shift
    sleep times[i]
  end
end

Usage: Functions

str_scale

Creates an array of floats from a sting pattern.

#- str_scale
define :str_scale do |str, min: 0, max: 1|
  str.chars.filter_map{|s|
    if (s == "|") or (s == " ")
      # Bar line, do nothing
    elsif s == "-"
      min
    elsif (s == "x") or (s == "X")
      max
    else
      min + (max - min) * Integer(s) / 10.0
    end
  }.ring
end

Usage: Functions

str_select

Creates an array of values from a sting pattern.

#- str_select
define :str_select do |str, values|
  str.chars.filter_map{|s|
    if (s == "|") or s == " "
      # Bar line, do nothing
    elsif s == "-"
      values[0]
    else
      values[Integer(s)]
    end
  }.ring
end

Usage: Functions