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|
Sheet MusicBass drumSnareCymbal

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|
Sheet Music

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|
Sheet Music

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|
Sheet Music

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)|
Sheet Music33

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|
Sheet Music

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|
Sheet Music

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