Skip to content

Instantly share code, notes, and snippets.

@emlyn
Last active November 18, 2023 02:53
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save emlyn/33f72346e8f6abb25dd34ad4e85ed4c9 to your computer and use it in GitHub Desktop.
Save emlyn/33f72346e8f6abb25dd34ad4e85ed4c9 to your computer and use it in GitHub Desktop.
Strumming guitar chords in Sonic Pi
# Guitar Strumming - by Emlyn
# This tries to work out the guitar (or ukulele etc.) fingering for arbitrary chords (and tuning).
# It seems to work reasonably well for basic chords, but is quite naive and probably makes many mistakes.
# Ideas, bug reports, fixes etc. gratefully received, just comment below, or tweet @emlyn77.
# Feel free to make use of this code as you like (with attribution if you feel like it, but you don't have to).
# Thanks to @Project_Hell_CK for fixing the tuning, and spotting that it gets chord(:f, :major) not quite right.
# Next note higher or equal to base note n, that is in the chord c
define :next_note do |n, c|
# Make sure n is a number
n = note(n)
# Get distances to each note in chord, add smallest to base note
n + (c.map {|x| (note(x) - n) % 12}).min
end
ukulele = [:g, :c, :e, :a]
guitar_standard = [:e2, :a2, :d3, :g3, :b3, :e4]
# Return ring representing the chord chrd, as played on a guitar with given tuning
define :guitar do |tonic, name, tuning=guitar_standard|
chrd = (chord tonic, name)
# For each string, get the next higher note that is in the chord
c = tuning.map {|n| next_note(n, chrd)}
# We want the lowest note to be the root of the chord
root = note(chrd[0])
first_root = c.take_while {|n| (n - root) % 12 != 0}.count
# Drop up to half the lowest strings to make that the case if possible
if first_root > 0 and first_root < tuning.count / 2
c = (ring :r) * first_root + c.drop(first_root)
end
# Display chord fingering
#puts tonic, name, c.to_a.zip(tuning).map {|n, s| if n == :r then 'x' else (n - note(s)) end}.join, c
c
end
# Strum a chord with a certain delay between strings
define :strum do |c, d=0.1|
in_thread do
play_pattern_timed c.to_a.drop_while{|n| [nil,:r].include? n}, d
end
end
use_debug false
use_bpm 120
chords = ring((guitar :a, :m), (guitar :c, :M), (guitar :d, :M), (ring :r, :r, 53, 57, 60, 65),
(guitar :a, :m), (guitar :c, :M), (guitar :e, :M), (guitar :e, '7'))
live_loop :guit do
with_fx :reverb, room: 0.95 do
with_fx :lpf, cutoff: 115 do
with_synth :pluck do
tick
"D.DU.UDU".split(//).each do |s|
if s == 'D' # Down stroke
strum chords.look, 0.05
elsif s == 'U' # Up stroke
with_fx :level, amp: 0.5 do
strum chords.look.reverse, 0.03
end
end
sleep 0.5
end
end
end
end
end
@stefan-hoehn
Copy link

pretty good!

@Lorddirt
Copy link

Amazing! Been looking for this for quite a while!

@reyork2010
Copy link

thanks, it's amazing

@FalcoGer
Copy link

fails for me at

first_root = c.take_while {|n| (n - root) % 12 != 0}.count

with

Runtime Error: [buffer 2, line 26] - NoMethodError
Thread death +--> :live_loop_guit
 undefined method `take_while' for (ring 40, 45, 52, 57, 60, 64):SonicPi::Core::RingVector
Did you mean?  take_last

@emlyn
Copy link
Author

emlyn commented Oct 30, 2020

@FalcoGer thanks for the heads up. It looks like take_while no longer works on rings in recent versions of Sonic Pi. I've taken off the .ring to leave c as an Array, and that seems to work again.

@boardkeystown
Copy link

I've been spending hours with your little script here. Any idea on how could get it to generate different chord forms?

For example: https://www.all-guitar-chords.com/
Notice on a C Major chord you have 5 different forms?

I've been trying to modify it to work out different forms. For example I noticed that in the function

next_note

n + (c.map {|x| (note(x) - n) % 12}).max

for a c major chord you will get very close to the 2 second form of that guitar chord. I've been playing around with it and am stuck here.

There must be a way to pull this off. Because it would be super awesome.

@boardkeystown
Copy link

UPDATE:
I've discovered something very interesting with your algorithm!
At least for the major and minor chords.
If you shift the tuning by the fret that the chord is supposed start on it seems to generate it correctly.

For example:

c = (guitar :c, :major)

image

s = 3
guitar_standard = [:e2+s, :a2+s, :d3+s, :g3+s, :b3+s, :e4+s]

# Return ring representing the chord chrd, as played on a guitar with given tuning
define :guitar do |tonic, name, tuning=guitar_standard|
  chrd = (chord tonic, name)
 
  # For each string, get the next higher note that is in the chord
  c = tuning.map {|n| next_note(n, chrd)}
  
  # We want the lowest note to be the root of the chord
  root = note(chrd[0])
  first_root = c.take_while {|n| (n - root) % 12 != 0}.count
  # Drop up to half the lowest strings to make that the case if possible
  if first_root > 0 and first_root < tuning.count / 2
    c = (ring :r) * first_root + c.drop(first_root)
  end
  # Display chordt fingering
  ##| puts c.zip(tuning).map {|n, s| if n == :r then 'x' else (n - note(s)) end}.join, c
  c = c.ring
end

OUTPUT

 ├─ "Strum"
 ├─ "----------------------------x index 0"
 ├─ "#<SonicPi::Note :C3> midi: 48 index 1"
 ├─ "#<SonicPi::Note :G3> midi: 55 index 2"
 ├─ "#<SonicPi::Note :C4> midi: 60 index 3"
 ├─ "#<SonicPi::Note :E4> midi: 64 index 4"
 ├─ "#<SonicPi::Note :G4> midi: 67 index 5"
 └─ "Len: 6"

I just don't know enough about music theory to pull this off perfectly for every chord. But there must be a way.

@emlyn
Copy link
Author

emlyn commented Aug 3, 2021

Hey @boardkeystown,
That's really cool, I'm glad you're having fun with my code!
The way the algorithm works, is that for each string it picks the lowest fret whose note appears in the chord. So from that perspective, playing a barre chord is equivalent to tuning all the strings up that many semitones. I think that's why your change allows it to find the different forms.
Maybe it would make sense to add a barre parameter so that it works without having to redefine the tuning:

# Return ring representing the chord chrd, as played on a guitar with given tuning
define :guitar do |tonic, name, tuning=guitar_standard, barre=0|
  chrd = (chord tonic, name)
  # For each string, get the next higher note that is in the chord
  c = tuning.map {|n| next_note(n+barre, chrd)}
  # We want the lowest note to be the root of the chord
  root = note(chrd[0])
  first_root = c.take_while {|n| (n - root) % 12 != 0}.count
  # Drop up to half the lowest strings to make that the case if possible
  if first_root > 0 and first_root < tuning.count / 2
    c = (ring :r) * first_root + c.drop(first_root)
  end
  # Display chord fingering
  #puts c.zip(tuning).map {|n, s| if n == :r then 'x' else (n - note(s)) end}.join, c
  c
end

Then you could get the other forms with (guitar :c, :major, barre=3), (guitar :c, :major, barre=5) etc.
Although it's still not obvious in advance which values you can use for barre.

@boardkeystown
Copy link

Yeah I like that it better that way adding to the note.

Although it's still not obvious in advance which values you can use for barre

At least for major scale the next feat starts at then lowest root note (I think). I've testing out a few ways to iterate n by the lowest root for n times. It kind of works but it seriously does not work for other chord types. So yeah you are right it's not clear what barre values could be.

I'm not even sure it's possible to algorithmically write out any guitar chord. Well maybe but I just don't know how to yet. I know you can build a chord based on a chord formula following a scale. In the lib portion of sonic pi all the chord formulas are there which is how sonic pi builds chords. This could save time needing to gather every formula since in theory the class Chord could be modified to GChord and called to generate a ring of guitar chords.

I've also been looking into maybe even scraping guitar chords from somewhere but I can't find a data base that just writes them out in midi notes or something that I could massage with python.

Thoughts?

@emlyn
Copy link
Author

emlyn commented Aug 4, 2021

I think it should be possible to algorithmically generate guitar chords if you know enough music theory. However I also think there would be multiple solutions for each chord, and choosing the most normal/standard one may well involve a certain amount of subjective judgement that might be difficult to encode in an algorithm. But I'm not an expert on this, maybe it would actually be easy for someone who knows enough.
It would also be possible to scrape a list of known chords - I had a bit of a look and found chords-db.
I wanted to keep this version algorithmic, even if it doesn't get every chord completely "correct", so that it will work with non-standard tunings, and even other instruments (such as the ukulele). But I might take that list of chords and use it to evaluate how accurate this algorithm is, and how I can improve it (when I have time).

@boardkeystown
Copy link

OH I LOVE THAT DB YOU FOUND!

And yeah I agree with you. I rather it be algorithmically generated. It's proving to be tricky for every chord. I don't know enough music theory either to pull it off perfectly. I will let you know if I make progress.

@tsmcgrath
Copy link

This is awesome. Thanks for helping me to learn. One minor issue was that there was one thread death (line 39) and the suggested fix in Sonic Pi v3.3.1 was to change the line from drop_last to drop_while. This worked. Thanks very much again. Now I have something to study to get to what I'd like to do.

@tsmcgrath
Copy link

Actually drop_while still killed the thread at line 39.

@emlyn
Copy link
Author

emlyn commented Dec 12, 2021

Hi @tsmcgrath, I'm glad you find it useful!
I'm not sure exactly what you mean about changing drop_last to drop_while - I don't see drop_last used anywhere.

In any case, you could simplify line 39 to just play_pattern_timed c, d and it will work almost as well.
It will just delay the start slightly for chords that don't include the lowest strings, but if that is an issue, you could also change line 29 to c = c.drop(first_root) so that the unplayed strings are not included at the start.

@jonnyhotchkiss
Copy link

hi, I'm interested in this, but not sure I exactly understand what's being proposed, or the challenges!
Are you proposing an extension of the built-in chord library ?

Is it specifically a stringed-instrument library, with all the voiced notes, perhaps with alternate chords voicings
(if sonicpi can even differentiate)

thanks for the shares!

@emlyn
Copy link
Author

emlyn commented Sep 8, 2023

Hi @jonnyhotchkiss, thanks for your interest!
At the moment there is no plan to actually include this in Sonic Pi (although if Sam was interested in adding something based on this I'd be happy to help out).
But for now, you should be able to copy and paste the guitar.rb file into a Sonic Pi buffer and play around with it (e.g. try changing the chords and strum pattern), and incorporate it into other pieces.

@rbnpi
Copy link

rbnpi commented Sep 17, 2023

There are some problems with newer version of Sonic Pi with your code. You can't apply .drop_while to a Sonic Pi scale. You need to convert the scale to a list first. so you need c.to_a.drop_while which will work. Same applies for some other operations eg .zip

@emlyn
Copy link
Author

emlyn commented Sep 17, 2023

Thanks @rbnpi, I hadn't looked at this code for a while, but I've updated now it so it should work again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment