Chord progression tool

Cool. I made some experiments expanding Sonic Pi’s scales and chords using monkey patching.

Here’s the idea. Create a new file extending the scales and chords via monkey patching:

#scale_patch.rb
class SonicPi::Scale
  def self.patch(scales)
    scales.each { |name, intervals|
      self::SCALE[name] = intervals unless self::SCALE.key? name
    }
  end
end

class SonicPi::Chord
  def self.patch(chords)
    chords.each { |name, intervals|
      unless self::CHORD.key? name.to_sym
        self::CHORD[name.to_sym] = intervals
        self::CHORD_LOOKUP[name.to_sym] = intervals
        self::CHORD_NAMES.append(name)
      end
    }
  end
end

#Missing scales based on: https://www.newjazz.dk/Compendiums/scales_of_harmonies.pdf
scales = lambda {
  ionian1s = [1,2,1,2,2,2,2]
  ionian5s = [2,2,1,3,1,2,1]
  ionian6b = [2,2,1,2,1,3,1]
  {
    # Family 2
    :ionian1s=>ionian1s,
    :dorian7s=>ionian1s.rotate(1),
    :phrygian6s=>ionian1s.rotate(2),
    :lydian5s=>ionian1s.rotate(3),
    :mixolydian4s=>ionian1s.rotate(4),
    :aeolian3s=>ionian1s.rotate(5),
    :locrian2s=>ionian1s.rotate(6),
    # Family 3
    :ionian5s=>ionian5s,
    :dorian4s=>ionian5s.rotate(1),
    :phrygian3s=>ionian5s.rotate(2),
    :lydian2s=>ionian5s.rotate(3),
    :mixolydian1s=>ionian5s.rotate(4),
    :aeolian7s=>ionian5s.rotate(5),
    :locrian6s=>ionian5s.rotate(6),
    # Family 4
    :ionian6b=>ionian6b,
    :dorian5b=>ionian6b.rotate(1),
    :phrygian4b=>ionian6b.rotate(2),
    :lydian3b=>ionian6b.rotate(3),
    :mixolydian2b=>ionian6b.rotate(4),
    :aeolian1b=>ionian6b.rotate(5),
    :locrian7b=>ionian6b.rotate(6),
  }
}.call

chords = {
  # https://en.wikipedia.org/wiki/Minor_major_seventh_chord
  'mM7'=> [0, 3, 7, 11],
  # https://en.wikipedia.org/wiki/Augmented_major_seventh_chord
  'maj7+5'=> [0, 4, 8, 11],
  '6+5'=> [0, 4, 8, 9],
  # Missing altered chords: https://en.wikipedia.org/wiki/Altered_chord
  '7-5-3'=>[0, 3, 6, 10],
  '7+5+9'=>[0, 4, 8, 10, 14],
  '7-5-9'=>[0, 4, 6, 10, 13],
  '7-5+9'=>[0, 4, 6, 10, 14]

}

SonicPi::Scale.patch(scales)
SonicPi::Chord.patch(chords)

EDIT: Fixed a bug, thanks to @Nechoj and added some missing altered 7th chords.

Then load it to the Sonic Pi and create mapping between chord types and those new scales:

load "~/patch.rb" # Load extra scales and chords from separate file

use_random_seed 31
use_bpm 90

harmonic_scales = {
  :M7=>[:ionian, :lydian, :lydian2s, :ionian6b, :augmented],
  :minor7=>[:dorian, :phrygian, :aeolian, :phrygian6s, :dorian4s, :phrygian4b],
  '7'=>[:mixolydian, :mixolydian4s, :aeolian3s, :phrygian3s, :mixolydian2b, :diminished],
  'm7-5'=>[:locrian, :locrian2s, :ionian1s, :locrian6s, :dorian5b],
  'mM7'=>[:dorian7s, :aeolian7s, :lydian3b],
  :dim7=>[:mixolydian1s, :locrian7b, :aeolian1b, :diminished],
  'maj7+5'=>[:lydian5s, :ionian5s, :aeolian1b],
  '7-5'=>[:whole_tone],
  '7+5'=>[:whole_tone],
  '6+5'=>[:augmented2],
  # Other altered chords
  '7-5-3'=>[:ionian1],
  'm7+5'=>[:ionian1],
  '9'=>[:ionian1]
}

prog = [{tonic: :D, type: 'm7-5', invert: -1}, {tonic: :G, type: '7', invert: -1},{tonic: :C, type: 'mM7', invert: 1}]

live_loop :chords do
  with_fx :flanger, feedback: 0.5 do
    prog.each do |c|
      synth :hollow, note: chord(c[:tonic], c[:type], invert: c[:invert] ? c[:invert] : 0), sustain: 3, amp: 2
      sleep 4
    end
  end
end

live_loop :impro do
  with_synth :tri do
    with_synth_defaults sustain: 0.20, env_curve: 2, cutoff: 40, decay: 0.05, decay_level: 0.5, sustain_level: 0.5 do
      prog.each do |c|
        harmonic_scale = harmonic_scales[c[:type]].choose
        notes = scale(c[:tonic], harmonic_scale).shuffle
        4.times do
          s = [0.25,0.5,1.0].choose
          (1/s).times do
            if s>0.25 and rand>0.85
              2.times do
                play notes.tick
                sleep s/2
              end
            else
              play notes.tick
              sleep s
            end
          end
        end
      end
    end
  end
end

live_loop :bass do
  with_fx :reverb, damp: 0.9, room: 0.8  do
    with_synth :fm do
      use_synth_defaults depth: -1, divisor: 1, release: [0.25,0.5].choose, amp: 0.5
      use_octave -2
      prog.each_with_index do |c,i|
        harmonic_scale = harmonic_scales[c[:type]].choose
        notes = scale(c[:tonic], harmonic_scale).shuffle
        play chord(c[:tonic], c[:type], invert: c[:invert] ? c[:invert] : 0)[0]
        sleep 0.5
        play chord(c[:tonic], c[:type], invert: c[:invert] ? c[:invert] : 0)[2]
        sleep 0.5
        3.times do
          if rand>0.7
            2.times do
              play notes.tick
              sleep 0.25
            end
          else
            play notes.tick
            sleep 0.5
          end
        end
        peek = prog.ring[i+1]
        play chord(peek[:tonic], peek[:type], invert: peek[:invert] ? peek[:invert] : 0)[0] - [-1,1,].choose
        sleep 0.5
      end
    end
  end
end

live_loop :swing do
  with_fx :reverb, amp: 2 do
    sample :drum_cymbal_closed, amp: 0.2, beat_stretch: [0.1,0.2,0.3].choose
    sample :drum_cymbal_soft, amp: 0.1
    sleep 1
    sample :drum_cymbal_soft, amp: 0.1
    sleep 0.8
    sample :drum_cymbal_soft, amp: 0.1
    sleep 0.2
  end
end

EDIT EDIT: Added variations to walking bass

Sounds quite nice, even for random serialist approach. Creating arpeggios and filling in the gaps should create even nicer outcome.

3 Likes