Friday, 25 November 2016

Hello SonicPi!

I recently discovered SonicPi, and it is awesome!
It is a programming framework in which you write Ruby code to control the Supercollider engine and produce music. It is aimed to “live” performance (live coding), but it can be also used to experiment with algorithmically generated music.
My first idea was that it should be not very difficult to “port” my piece “Bay“ from ngen to SonicPi, and it was very straightforward, indeed.
The same code can produce the two variations “Bay” and “Bay at night” simply uncommenting one line. If you don’t have SonicPi installed but want to know what does this sound like, here is a little excerpt.


Here is the code:

# Bay (2005) re-implemented in SonicPi (2016)

# The original Bay was created with ngen an rendered with Csound.
# The original ngen implementation is here
# https://dl.dropboxusercontent.com/u/2374752/csound/bay.gen
# and the csound rendition can be listened at
# https://archive.org/details/jld_bay

# The following algorithm description is extracted
# from the ngen version comments
#
# ; The algorithm is very simple, but leads to interesting results. The
# ; composer selects a note by name (eg. 'a2'), and other parameters
# ; governing the random process. The algorithm produces several
# ; activations of this note, separated by a interval of random
# ; length. The lenght of the note and its midi velocity are also
# ; random. The composer can specify the limits for these random
# ; quantities.
#
# ; The composer can call this algorithm several times, with different
# ; parameters each time.  Each invocation produces a random stream of
# ; repeating notes, all starting at zero. The ramdom length of the
# ; intervals leads to unexpected melodies, chords and dissonancies.

# This function generates a stream of repeated notes
define :generate do |start, reps, n, imin, imax, len, vmin, vmax|
  # Parameters:
  # start: initial number of notes to skip (will produce silence)
  # reps: number of notes the stream produces before dying
  # n:    Note to repeat. If it is an array, one random
  #       note from the array is selected in each iteration.
  #       Do not abuse of this feature
  # imin: Minimal interval between note repetitions
  # imax: Max interval between note repetitions
  # len:  Length of the note to produce in each repetition
  # vmin,vmax: range of "velocity" for the notes. In the original
  #       implementation, the values were MIDI velocities. In
  #       this implementation they are used to set amplitude
  #       and cutoff frequency

  reps.times do |r|
    if r<start                  # Wait the specified initial delay
      sleep rrand(imin, imax)
      next
    end
    # Choose a different synth in each iteration
    use_synth (ring :hollow, :prophet, :blade).tick
    # Adjust the volume, depending on the synth selected above
    # (:hollow is much quieter than :prophet, so I pump up the volume)
    vol_multiplier = (ring 1.2, 0.6, 0.7).look

    # Select the note to play in this iteration
    if n.is_a?(Array)
      chosen_note = n.choose
    else
      chosen_note = n
    end



    # Uncomment next line to get "Bay at night" :-)
    # chosen_note += -1 + rand_i(2)

    # Play the note. Several parameters are set to get a random
    # volume and length, between the limits specified
    play chosen_note, amp: vol_multiplier*rrand(vmin, vmax)/120,
      attack: len/2, decay: rrand(0.2,0.8)*len/2, 
      pan: choose((stretch rrand(-1, -0.6), 45, rrand(-0.2, 0.2), 10, rrand(0.6,1), 45)),
      cutoff: rrand(vmin, vmax),
      vibrato_delay: len/2, vibrato_onset:1,
      vibrato_rate: 2, vibrato_depth: 0.09

    # Wait a random time before triggering the note again
    sleep rrand(imin, imax)
  end
end

# The above function is the basic infrastructure. Next, I'll call several instances
# of the function, each one in a separated thread. For this, I'll define some
# arrays with the paramaters to be passed to the function in each invocation

use_bpm 40
use_random_seed = 1982
piece_length = 40

args = [
  [0, piece_length-3, :f2,9,12,9,60,90],
  [0, piece_length-3,:a2,9,12,9,60,90],
  [0, piece_length-3,:c3,9,12,9,60,90],
  [1, piece_length-2,:e3,9,12,9,60,90],
  [2, piece_length-3,:f3,9,12,9,60,90],
  [1, piece_length,:e4,9,12,9,60,90],
  [3, piece_length-2,:a4,8,12,9,60,70],
  [3, piece_length-2,:b4,8,10,9,50,70],
  [3, piece_length-2,[:c4,:cs4,:d4],10,20,3,60,90],
  [5, piece_length-4,:c5,8,10,9,50,80],
  [5, piece_length-5,:d5,8,10,9,50,90],
  [6, piece_length-5,:e5,8,10,9,50,70],
  [8, piece_length-5, [:b6,:c7,:d7],4,7,5,70,98]
]

# Now, I create another array containing the time at which each instance of the
# function has to be called. I fill the array with random values below 5, so that
# each call starts at a random instant, but all of them in the first 5 secs
instants = []
args.each do
  instants.push(rand(5))
end

# Using "at", I launch several instances of the function, each one at a
# random instant (specified in "instants" array) and with different
# parameters (specified in "args" array). All those functions share
# the same reverb and flanger effect
with_fx :reverb, spread: 0.9, mix: 0.8, room: 0.99 do
  with_fx :flanger do
    at instants, args do |params|
      generate *params
    end
  end
end

No comments:

Post a Comment