ruby · 5 min read
Ruby and Sonic Pi: Build a Four-Beat Phrase You Can Inspect
Compose a four-beat Sonic Pi phrase and inspect its note schedule with seven Ruby tests, while keeping score checks separate from actual audio playback.
A loop can have the right notes and the wrong length. Eight notes separated by half a beat make a four-beat phrase. Add an extra sleep after the loop, and the next repetition arrives late. Add a bass line with a different cycle length, and the parts may realign somewhere other than where you expected.
This example treats a short Sonic Pi composition as a schedule you can inspect. It includes a playable buffer for Sonic Pi and a separate Ruby recorder that checks note and sample events. The recorder does not produce sound or emulate Sonic Pi's timing engine. Its job is smaller: catch mistakes in the score's structure.
Beats are the unit
In ordinary Ruby, sleep 0.5 waits half a second. In Sonic Pi, musical sleep advances virtual time by beats. At 120 BPM, one beat lasts half a second, so a half-beat step lasts a quarter of a second. The Sonic Pi tutorial on duration explains that distinction.
The buffer sets use_bpm 120, then repeats one named live_loop. Its eight melody notes use MIDI numbers: 60, 63, 67, 70, 67, 63, 65 and 67. Those values give C, E-flat, G, B-flat, G, E-flat, F and G in the melody's octave. Numbers keep this fixture independent of a test recorder's note-name parser; Sonic Pi also accepts named notes.
There is no claim that this sequence is an automatically good composition. It is a small, fixed phrase with enough variation to hear a repeated shape. Change one note or its octave and listen to the effect before adding more machinery.
Put the layers on one clock
Instead of starting several independent loops, this first version places melody, bass and percussion in one half-beat step loop:
notes.each_with_index do |note, step| play note, amp: 0.4, release: 0.4 play 36, amp: 0.3, release: 1.5 if step == 0 || step == 4 sample :bd_haus, amp: 0.6 if step == 0 || step == 4 sample :sn_dub, amp: 0.4 if step == 2 || step == 6 sample :drum_cymbal_closed, amp: 0.15 sleep 0.5 endAll events before a sleep share the same logical onset. The bass and kick begin at beats zero and two. The snare begins at beats one and three. A closed hi-hat marks every half beat. The melody plays one note per step. Eight sleeps of 0.5 bring the loop back around after four beats, or two seconds at the selected tempo.
The buffer selects :pluck for both melodic pitches and the lower bass pitches. The bass is the same instrument in a lower register, not an independently configured synthesizer. The percussion uses bundled samples, described in the samples tutorial.
This shared clock makes the first composition easy to inspect. Separate live loops become useful when parts need different edit boundaries or independent patterns. When making that change, decide how they start and synchronize rather than assuming that equal BPM alone defines their phase relationship.
Note onset and sound duration are different
release shapes the synth envelope; sleep controls when the next step begins. Increasing release does not automatically increase the gap between notes. A long release can overlap a later onset, which may be intentional.
The melody's release is 0.4 beats against a 0.5-beat step. The bass release is 1.5 beats against a two-beat gap. The tests check those numeric relationships. They do not model the pluck synth's complete envelope or sample tails, and they do not guarantee that the mix is free of clipping.
Sonic Pi's envelope tutorial is useful when a note seems too sharp, too long or too quiet. Start playback at a comfortable volume, change one parameter, and listen. A schedule assertion cannot evaluate timbre or balance.
Run Ruby tests; play inside Sonic Pi
Download the score and test files. bundle exec ruby test_score.rb runs the structural checks. To hear the composition, paste the entire sonic_pi.rb file into a Sonic Pi buffer and press Run. Press Stop to end the live loop. Running that buffer directly with the ordinary Ruby interpreter will not provide Sonic Pi's play, sample or live_loop methods.
The recorder supplies a deliberately small set of those method names. It evaluates one loop body, records each event at the current beat, and advances its counter when sleep is called. Seven tests passed on Ruby 3.3.2: bar duration, eight melody onsets, bass positions, snare positions, kick and hi-hat positions, release values, and loop/instrument identity. The score contains twenty-two note/sample events per iteration.
This checks the actual buffer's control flow rather than a separately copied list of expected events. It does not check Sonic Pi installation, synth availability in a particular release, audio scheduling, device latency or the audible result. No Sonic Pi session or listening test was performed for this article. The attachment keeps that limit visible.
Change the pattern with a question in mind
Move the second snare half a beat earlier. Does the phrase feel less settled? Then restore it and lower the bass an octave. Those are distinct experiments; changing everything at once makes it hard to explain what improved.
For repeatable variation, begin with an explicit note table like this one. If you later introduce random choices, use Sonic Pi's documented random seed and record it with the pattern. Seeding from the wall clock discards the ability to replay a particular run. The randomness tutorial describes its deterministic sequence behavior.
A live loop lets you revise a running pattern at loop boundaries, as the live-loop tutorial explains. It is still worth testing and listening after edits; a typo or an overloaded audio environment does not become harmless because the interface supports live coding.
The score recorder gives the phrase a measurable skeleton. Sonic Pi gives it a sound. Keep both feedback paths: a test can tell you the snare moved, while your ears decide whether it should have.
Found a mistake or tried a different approach?
Send Alex a note ↗