I was looking for some kind of drum machine that can be controlled in some way while playing an instrument. Inspired by a smartphone app which allows you to control drum sequences through a MIDI controller (e.g. a pedal board), I wondered if something similar could be implemented with Sonic Pi. Surprisingly enough, it seems like that I have managed to!
I’m sharing here what I’ve done, hoping that can be useful for someone.
Starting from the script about drum machines I did years ago that I posted here, I reviewed it to make the drum parts more manageable and introduced some logic to handle MIDI events.
The operation is based on four midi notes to be mapped at the beginning of the script, that represent:
-
A button to start and stop the drums. When pressed for the first time it executes an “intro” drum part and then pass to the “section_a” drum part. Pressed again, it executes the “outro” drum part and stops the drums.
-
A button to change from “sequence_a” to “sequence_b” drum parts and viceversa. Sequences keep looping once started.
-
A “trans” button, that like the previous changes the sequence, but between the two it plays a transition drum part (“trans_a” to pass from “sequence_a” to “sequence_b”, “trans_b” to pass from “sequence_b” to “sequence_a”)
-
A “fill” button, that plays a fill bar for the section currently playing (“fill_a” is the fill for “section_a”, “fill_b” the analogous for “section_b”)
The drum parts start only at the end of the measure that is being executed at the moment: so when you press a button, you command what sequence play in the next measure.
And here’s the code:
use_bpm 120
### MIDI mappings ###
midi_note_command = "/midi:mpk_mini_3:10/note_on" # change this with your midi controller signature
# customize the values with the buttons of your midi controller that you want to map
midi_note_start_stop = 40 # button to start/stop the drums
midi_note_a_b = 41 # button to switch from section a to section b and viceversa
midi_note_trans = 42 # button to execute the transition from one section to another
midi_note_fill = 43 # button to play a fill in the current section
### Drum sounds ###
drum_sounds = {
"open_hh" => :drum_cymbal_open,
"closed_hh" => :drum_cymbal_closed,
"tom_hi" => :drum_tom_hi_soft,
"tom_mi" => :drum_tom_mid_soft,
"tom_lo" => :drum_tom_lo_soft,
"snare" => :drum_snare_soft,
"kick" => :drum_heavy_kick
}
### Drum parts ###
drum_parts = {
"intro" => {
"open_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"closed_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_hi" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_mi" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_lo" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"snare" => [0,0,1,0, 1,0,1,0, 1,0,0,1, 1,0,1,0],
"kick" => [1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0]
},
"section_a" => {
"open_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"closed_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"snare" => [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
"kick" => [1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0]
},
"trans_a" => {
"open_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0],
"closed_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_hi" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_mi" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 1,0,0,0],
"tom_lo" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,1,0,0],
"snare" => [0,0,1,1, 1,0,1,0, 0,0,1,0, 0,0,0,0],
"kick" => [1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0]
},
"fill_a" => {
"open_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0],
"closed_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_hi" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_mi" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_lo" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"snare" => [0,0,1,0, 1,1,1,0, 0,0,1,1, 1,0,0,0],
"kick" => [1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0]
},
"section_b" => {
"open_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"closed_hh" => [1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0],
"snare" => [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
"kick" => [1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0]
},
"trans_b" => {
"open_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 1,0,0,0],
"closed_hh" => [1,1,1,1, 1,1,1,1, 1,1,1,1, 1,0,0,0],
"snare" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 1,0,0,0],
"kick" => [1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0]
},
"fill_b" => {
"open_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0],
"closed_hh" => [1,0,1,1, 1,0,1,1, 1,0,1,0, 0,0,0,0],
"tom_hi" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 1,0,0,0],
"tom_mi" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_lo" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,1,0,0],
"snare" => [0,0,0,0, 1,0,0,0, 0,0,1,0, 1,0,0,0],
"kick" => [1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0]
},
"outro" => {
"open_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,0],
"closed_hh" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_hi" => [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
"tom_mi" => [1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0],
"tom_lo" => [0,0,1,0, 0,0,0,0, 0,1,1,0, 1,0,0,0],
"snare" => [0,0,0,0, 1,0,0,0, 0,0,0,0, 0,0,0,0],
"kick" => [1,0,0,0, 0,0,0,0, 1,0,0,0, 0,0,0,0]
}
}
### LOGIC ###
# Loop that reacts when a mapped midi note is pressed.
# When it happens, it sets the `is_new_command_sent` global variable to `true`.
live_loop :midi_commands_listener do
use_real_time
sync midi_note_command
command, velocity = get midi_note_command
if ([midi_note_start_stop, midi_note_a_b, midi_note_trans, midi_note_fill].include? command) then
set("is_new_command_sent", true)
end
end
# Loop that keeps track of the measures
measure = 1;
live_loop :conductor do
cue :beat
# every 4 bars, a :measure message is sent (notifying the beginning of a new measure)
if (tick % 4 == 0) then
cue :measure, measure
measure += 1
end
sleep 1
end
# Loop that manages the sounds to play, triggered at every beginning of measure
live_loop :drums do
# loop synced with :measure
# :measure carries the current measure of the loop
measure = (sync :measure)[0]
puts measure
# On start, resets all the used global variable values (last midi note sent included)
if (tick == 0) then
set(midi_note_command, nil)
set("now_playing", nil)
set("is_new_command_sent", false)
end
command, velocity = get midi_note_command # gets the last midi note sent
now_playing = get "now_playing" # gets the drum part name that is currently playing
is_new_command_sent = get "is_new_command_sent" # if it is true, it means that the midi note was sent during the previous loop execution (so it still has to be processed)
# If the global variable `is_new_command_sent` is set to true,
# determine which drum part play given the sent command
if (is_new_command_sent) then
set("is_new_command_sent", false)
if (command == midi_note_start_stop) then
if (now_playing == nil) then
# If it was pressed `midi_note_start_stop` and nothing is playing,
# plays the `intro` drum_part...
play_drum_part("intro")
# ... and from the next measure play `section_a`
set("now_playing", "section_a")
else
# if it was pressed `midi_note_start_stop` and something is playing,
# play the `outro` part...
play_drum_part("outro")
# ... and from the next measure play nothing
set("now_playing", nil)
end
elsif (command == midi_note_a_b) then
if (now_playing != "section_a") then
# If it was pressed `midi_note_a_b` and `section_a` is not playing,
# play it...
play_drum_part("section_a")
# ... and keep it playing
set("now_playing", "section_a")
else
# If it was pressed `midi_note_a_b` and `section_a` is playing,
# play `section_b`...
play_drum_part("section_b")
# ... and keep it playing
set("now_playing", "section_b")
end
elsif (command == midi_note_trans) then
if (now_playing == "section_a") then
# If it was pressed `midi_note_trans` and `section_a` is playing,
# play `trans_a`...
play_drum_part("trans_a")
# ... and from the next measure play `section_b`
set("now_playing", "section_b")
elsif (now_playing == "section_b") then
# If it was pressed `midi_note_trans` and `section_b` is playing,
# play `trans_b`...
play_drum_part("trans_b")
# ... and from the next measure play `section_a`
set("now_playing", "section_a")
end
elsif (command == midi_note_fill) then
if (now_playing == "section_a") then
# If it was pressed `midi_note_fill` and `section_a` is playing,
# play `fill_a`...
play_drum_part("fill_a")
# ... and from the next measure come back playing `section_a`
elsif (now_playing == "section_b")
# If it was pressed `midi_note_fill` and `section_b` is playing,
# play `fill_b`...
play_drum_part("fill_b")
# ... and from the next measure come back playing `section_b`
end
end
else
if (now_playing != nil) then
# If nothing was pressed during the last execution,
# play the drum part stored in `now_playing` variable.
# (if `now_playing` is nil, plays nothing)
play_drum_part(now_playing)
end
end
end
### UTILITY FUNCTIONS ###
define :play_drum_part do |drum_part_name|
drum_part = drum_parts[drum_part_name]
play_drum_type(drum_part, "kick")
play_drum_type(drum_part, "snare")
play_drum_type(drum_part, "tom_lo")
play_drum_type(drum_part, "tom_mi")
play_drum_type(drum_part, "tom_hi")
play_drum_type(drum_part, "closed_hh")
play_drum_type(drum_part, "open_hh")
end
define :play_drum_type do |drum_part, drum_type_name|
if (drum_part.key?(drum_type_name)) then
play_drum_pattern(drum_part[drum_type_name], drum_sounds[drum_type_name])
end
end
# pattern: is an array of 1s and 0s. 1 = beat played, 0 = no beat
# drum_sample: the sample to play
define :play_drum_pattern do |pattern, drum_sample|
# Every time this function is called a new thread is created.
in_thread do
pattern.each do |p|
if p == 1 then
sample drum_sample
end
sleep 0.25
end
end
end
I’m quite happy to have achieved what I had in mind: I thought it would be frustrating but instead it was more simple than i feared and most of all I had fun
Sonic Pi looks so versatile!