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.