Hi everyone!
I’ve been tinkering with Sonic Pi for a few days now, and I’m absolutely hooked. Recently, I accidentally left my headphones unplugged while testing some ideas. Someone nearby overheard the music and said: “Is that yours? It sounds great! Send it over, maybe I can come up with some lyrics.”
Inspired by that offer, I decided to become a bit more “professional” and design a proper song layout with a structured introduction, a main section, and an automatic fade-out system.
This is the result. I’m quite prone to typos, so I hope the code is clear enough! I am currently working on a YouTube channel (@technoabu) to share my learning process, and I wanted this to be my first official contribution here on in_thread.
I would love to hear your thoughts and objective feedback on the structure!
use_bpm 135
# We define a lambda function that will receive the list of samples matching the filter criteria.
# To achieve a predictable sample rotation sequence, we use the current position of the 'tick'
# instead of making a random selection (.choose). We use 'look' so we don't inadvertently
# advance the tick of the corresponding loop.
rsamp = lambda do |s|
s.look
end
# If we want to loop/repeat the track after its controlled completion, we must set
# 'reiniciar_tema' (reset_track) to true, press Alt+R, reset 'reiniciar_tema' back to false,
# and press Alt+R once more. The track will then replay from the initial intro.
reiniciar_tema = false
if reiniciar_tema == true then
set :samples, false
set :bajo, false
set :fx_mix, 0
set :volumen, 1.0
set :fin, false
set :live_coding, false
end
# This block will only execute the very first time we press Alt+R.
# This allows us to make live-coding modifications on the fly and re-run Alt+R,
# ensuring the track continues from its current state (without reverting to the intro
# as if we just booted the track for the first time).
# Once the first execution finishes, the only way to restart it without closing Sonic Pi
# is by using the manual override switch above.
defonce :solo_la_primera_vez do
set :samples, false
set :bajo, false
set :fx_mix, 0
set :volumen, 1.0
set :fin, false
# By default, the track will end automatically on a scheduled basis.
# If we intend to do a proper live coding session, we don't want this automated ending.
# We can disable it by setting ':live_coding' to true. This way, the track will loop
# indefinitely until we manually set ':fin' to true when we wish to trigger the fade-out.
set :live_coding, false
end
# Master Clock / Control Loop
live_loop :batuta do
compas = tick
compas_16 = compas % 16
compas_32 = compas % 32
compas_64 = compas % 64
compas_160 = compas % 160
set :bajo, true if compas_16 == 15
set :samples, true if compas_32 == 31
set :fx_mix, 1 if compas_64 == 63
set :fin, true if compas_160 == 159
# Since we are operating with floating-point numbers, an insignificant remaining volume
# or a tiny negative number might occur, which would trigger an out-of-range exception.
# This conditional safety check prevents it.
if get[:fin] == true and get[:live_coding] == false then
if get[:volumen] > 0.05 then
set :volumen, get[:volumen] - 0.05
else
set :volumen, 0
end
end
# The final fade-out of the percussion samples might coincide with an awkward cutoff point.
# Therefore, we trigger the same note used at the start (:C4 / 60) to provide a predictable,
# musically consistent ending.
if get[:volumen] == 0 then
play 60, amp: 0.05, sustain: 1, release: 1
sleep 3
stop
end
sleep 1
end
# Harmonic anchor / Driving thread from start to finish
live_loop :nota, sync: :batuta do
play 60
sleep 16
if get[:volumen] == 0 then
stop
end
end
# Percussion Samples Loop
live_loop :samples_aleatorios, sync: :batuta do
# Each sample will alternate panning between left and right channels
panoramica = [-0.25, 0.25].tick
# We filter internal samples containing "bd_". However, this would also include "tbd_"
# (which we want to exclude). To solve this, we add a Regular Expression (/reg exp/)
# featuring a negative lookbehind (?<!t) to ensure the character preceding "bd_" is NOT a "t".
# Then, it calls our lambda function (rsamp), passing the list of matching physical file paths.
# Note: Filter conditions apply to the physical file names, not their Sonic Pi aliases.
if get[:samples] == true then
with_fx :distortion, distort: 0.6, mix: get[:fx_mix] do
sample "bd_", /(?<!t)bd_/, rsamp, amp: (1.5 * get[:volumen]), pan: panoramica
end
end
sleep 1
if get[:volumen] == 0 then
stop
end
end
# Off-beat Snap Loop (syncopated against the main drum)
live_loop :snaps, sync: :batuta do
panoramica = (line, -0.5, 0.5, steps: 7).tick
sample "perc_snap", rsamp, pan: panoramica, amp: (0.5 * get[:volumen])
sleep 1
if get[:volumen] == 0 then
stop
end
end
# Bass Melody Loop
# Update: Fixed a potential naming conflict by changing the loop name to :melodia_bajo
# and added a dynamic second control movement to give the bassline a richer, three-dimensional modulation.
live_loop :melodia_bajo, sync: :batuta do
if get[:bajo] == true then
with_fx :gverb, dry: 0.2, spread: 1, room: 3, amp: 0.35 do
sinte = synth :chiplead,
note: (chord, :D2, :minor7).tick,
cutoff: rrand(40, 60),
release: 2,
amp: (0.4 * get[:volumen]),
pan: -1
sleep 0.25
control sinte,
pan: 1,
pan_slide: 0.5,
cutoff: rrand(100, 130)
sleep 0.5
control sinte,
note: (chord, :D2, :minor7).choose,
pan: 0,
pan_slide: 0.5,
cutoff: rrand(60, 90)
end
end
sleep 0.25
if get[:volumen] == 0 then
stop
end
end