A completely different way to use Sonic Pi with a midi controller

Recently I cam across a tweet from Phil Helliwell @kill9zombie in which he gave a link to a gist where he had described controlling Sonic Pi by means of an external midi controller. https://gist.github.com/kill9zombie/8b24389239891a5dbe3e This intrigued me, and I decided to investigate further. It turned out that it was very easy to set up, and I thought it would be useful to write up the procedure and give details of a simple program I have used to control Sonic Pi using 6 rotary potentiometers on my M-Audio Oxygen8 keyboard. The program should work with minor modification with other midi controllers, indeed Phil uses the program with an Akai lpd8 controller.

install prerequisites:

on both Mac and Pi

gem install micromidi

on a Pi

sudo apt-get update
sudo apt-get install libasound2-dev

The Midi Control system uses two ruby programs. A server, which you run from a terminal window which received midi input from your keyboard/controller and then sends processes and sends it on to a small client program which is incorporated in the program you are running in your Sonic Pi buffer.
The server program requires you to load in the gem micromidi which it uses to communicate with your keyboard/controller. The server also uses Ruby’s built in drb module. This DRuby distributed object system for Ruby, lets you control objects in one ruby program from another one. This excert form the DRuby documentation explains how it works.

dRuby allows methods to be called in one Ruby process upon a Ruby object located in another Ruby process, even on another machine. References to objects can be passed between processes. Method arguments and return values are dumped and loaded in marshalled format. All of this is done transparently to both the caller of the remote method and the object that it is called upon.

An object in a remote process is locally represented by a DRb::DRbObjectinstance. This acts as a sort of proxy for the remote object. Methods called upon this DRbObject instance are forwarded to its remote object. This is arranged dynamically at run time. There are no statically declared interfaces for remote objects, such as CORBA’s IDL.

dRuby calls made into a process are handled by a DRb::DRbServer instance within that process. This reconstitutes the method call, invokes it upon the specified local object, and returns the value to the remote caller. Any object can receive calls over dRuby. There is no need to implement a special interface, or mixin special functionality. Nor, in the general case, does an object need to explicitly register itself with a DRbServer in order to receive dRuby calls.

One process wishing to make dRuby calls upon another process must somehow obtain an initial reference to an object in the remote process by some means other than as the return value of a remote method call, as there is initially no remote object reference it can invoke a method upon. This is done by attaching to the server by URI. Each DRbServer binds itself to a URI such as ‘druby://example.com:8787’. A DRbServer can have an object attached to it that acts as the server’s front object. A DRbObject can be explicitly created from the server’s URI. This DRbObject’s remote object will be the server’s front object. This front object can then return references to other Ruby objects in the DRbServer’s process.

The server program is shown below. I have added some extra def k<n> functions to accommodate the numbering on my Oxygen-8 controller.

#!/usr/bin/env ruby
# encoding: utf-8

require 'drb/drb'
require 'micromidi'

# The URI we're using for RDb, we use unix sockets just because they're quicker.
DRB_URI="drbunix:/var/tmp/sonic-pi-midiconnector"

class SPIMidiConnector

  def initialize
    @input = UniMIDI::Input.use(0)
    @midithread = listen()
  end

  # Return the value of the pot.
  #
  # If we haven't moved a pot yet then just return 0.
  def get(key)
    @midithread[key] || 0
  end

  # Some convenience methods (they're shorter to type while livecoding).
  # These should return the midi value (0 to 127) of the potentiometer on the LPD8.
  def k1; get(:k1); end
  def k2; get(:k2); end
  def k3; get(:k3); end
  def k4; get(:k4); end
  def k5; get(:k5); end
  def k6; get(:k6); end
  def k7; get(:k7); end
  def k8; get(:k8); end
  def k9; get(:k9); end
  def k10; get(:k10); end
  def k25; get(:k25); end
  def k71; get(:k71); end
  def k91; get(:k91); end
 
  def thread
    @midithread
  end

private

  def listen
    midithread = Thread.new(@pads_map) do |pad_map|
      MIDI.using(@input) do

        # When we get a control change message, update a
        # thread attribute with the value.
        thru_except :control_change do |msg|
          key = "k#{msg.data[0]}".to_sym
          puts "key: #{key} value: #{msg.value}"
          midithread[key] = msg.value
        end

        join
      end
    end
    midithread
  end
end

# Then we start the DRb server, as per the DRb docs.

FRONT_OBJECT = SPIMidiConnector.new

$SAFE = 1

DRb.start_service(DRB_URI, FRONT_OBJECT)
DRb.thread.join

The client code which Phill supplies in the gist is added to your Sonic Pi program running in the current buffer is even easier.

require 'drb/drb'
DRB_URI="drbunix:/var/tmp/sonic-pi-midiconnector"
@lpd = DRbObject.new_with_uri(DRB_URI)

use_bpm 120

live_loop :kick do
  sample :bd_haus
  sleep 1
end

with_fx :bitcrusher do
  live_loop :saw do
    sync :kick
    use_synth :dsaw
    play chord(:c, :major, num_octaves: 4).choose,  cutoff: @lpd.k1, release: 2, attack: 1
    sleep 0.5
  end
end

This specimen program lets you control the cutoff value for the play command using rotary 1 on the lpd8 controller (or whichever one you are using). In fact the business part of the connection is all contained within the first three lines of code.

require 'drb/drb'
DRB_URI="drbunix:/var/tmp/sonic-pi-midiconnector"
@lpd = DRbObject.new_with_uri(DRB_URI)

Both of the above programs can be downloaded from https://gist.github.com/kill9zombie/8b24389239891a5dbe3e
either as a zip folder or by selecting the raw version of each file and saving them from your browser.

Once these are in place, you can access any of the available controllers using the construct: @lpd.k<n>    where <n> represents the controller number.

First you can test the connection between the midi controller and the server program. In a terminal window, navigate to the folder containing the server program and start it running by typing

ruby drb_server.rb

Now rotate a control knob on your midi controller and you should see its value and name output on the terminal screen. If the knobs that you choose to use have key names in the range k1-k8 then all is well. If you have a entry like k91 which is one of the values I get with my Oxygen-8 controller then you have to add an entry like

 def k91; get(:k91); end

to the server program as I have done in the listing above.

Now turning to the client program. Start Sonic Pi running and in an entry buffer window add the client program listed above. (You can either type it in, copy and paste it, or use the load key to select and load the file where it is saved.)

As supplied, it responds to changes in controller knob k1. If that is not available in the outputs you can see in the server terminal window, you may have to allocate it to an alternative number.

Having set the system up and tested it on both my Mac and my Pi2, I wrote a simple program which utilised 6 of the rotary controllers on my Oxygen-8, and I used the to control the parameters of a single note played repetitively in a live_loop.  I used k numbers 1,5,7,10,71 and 91 to match the codings on my knobs. These were used to select the synth and adjust the note value, duration, cutoff and amplitude of the note and its pan position. The Sonic Pi client program is shown below.

#demo of Sonic Pi controlled by three external midi-potentiometers

require 'drb/drb'
DRB_URI="drbunix:/var/tmp/sonic-pi-midiconnector"
@lpd = DRbObject.new_with_uri(DRB_URI)

live_loop :midi do
  
  k= @lpd.k7
  n=@lpd.k10
  c=@lpd.k5
  d=@lpd.k1
  d=0.05+0.4*d/127
  a=@lpd.k91/127.0*2 - 1
  fx=@lpd.k71/127.0
  with_fx :level,amp: fx do
    s=:beep  if range(110,128).include? k
    s=:tri   if range(88,110).include? k
    s= :pulse  if range(66,88).include? k
    s= :tb303   if range(44,66).include? k
    s= :fm   if range(22,44).include? k
    s= :zawa  if range(0,22).include? k
    puts "Synth "+s.to_s
    puts "Note "+n.to_s
    puts "Cutoff "+c.to_s
    puts "Duration "+d.to_s
    puts "Pan "+p.to_s
    puts "Level "+fx.to_s
    use_synth s
    play n,sustain: d,cutoff: c,pan: p
    sleep d
  end
end

The end effect is quite impressive. It brings a new dimension to live coding, especially if like me your typing is not very fluent. The program gives feedback as to the current settings via puts statements, and you can adjust the parameters as the program plays. As written it selects between 5 different synths, but you can also alter the choices and rerun the program to alter the live_loop whilst it is playing, getting the best of both ways of live coding. You may want to increase slightly the minimum duration value on a Raspberry Pi, to prevent breakup of the sound, especially with some of the more complex sysnths like zawa and tb303, but otherwise the program performs well on both Mac and Pi. Unfortunately I think there are issues in using it on Windows, and currently it doesn’t run on my Windows box.

You can hear a sample recorded with the above programs here

watch a visualised video produced from this program here

Advertisements

5 thoughts on “A completely different way to use Sonic Pi with a midi controller

  1. Impressive indeed! Transitions sound really smooth, much more so than expected from the code.

    Can’t wait to try it with a WX11 and/or some apps running “virtual MIDI” connections. Makes everything much clearer, so thanks a lot for that!

    Still a bit unsure how this specific example works in practice. Even the note values are controlled by a knob? But how come they’re diatonic? Because they only update once per loop?
    There might be something to be done with scales, in this case. Or patterns. The overall effect is quite neat, but sticking to a scale might make things more manageable.

    Haven’t really played with SPi’s `control` function, yet. Does it allow changes to happen during a given iteration of a loop?

    Thanks again for all your help!

    • The note values are effectively produced from midi numbers as the midi control will return integers from 1-127 depending on its position. Hence diatonal.
      I have experimented with starting a note with a long sustain outside the loop, and setting a note_slide time of 0.1. I then controlled the note pitch within a loop sing the midi control and could get a smoothly varying pitch. Of course you couldn’t then control the synth as the long sustained note was set with an initial synth which couldn’t be changed.
      e.g. something like

      require ‘drb/drb’
      DRB_URI=”drbunix:/var/tmp/sonic-pi-midiconnector”
      @lpd = DRbObject.new_with_uri(DRB_URI)
      use_synth :beep
      ct=play 72,sustain: 200,note_slide: 0.1
      live_loop :midi do
      n=@lpd.k10
      puts “Note “+n.to_s
      control ct, note: n,sustain: 0.1
      sleep 0.1
      end

    • enkerli, have you experimented with another program running virtual MIDI connections? It seems to be exactly what I need but that would be a really steep learning curve for me so if you’ve done some work on it already that would help a lot!

    • My previous comment doesn’t seem to have been posted.
      enkerli, have you given a try to a program running virtual MIDI? That seem to be the solution to what I’m trying to do, but that would be a really steep learning curve for me so if you have done some work already with that it would help immensely!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s