Virtual drummer controlled by MIDI inputs

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 :smiley:
Sonic Pi looks so versatile!

2 Likes