Generate chord charts for ukulele

6

2

Your task is to write a function which outputs a ukulele chord chart in the following format for a given chord symbol. For instance, for input "G7", it must output:

 G C E A
---------
| | |#| |
---------
| |#| |#|
---------
| | | | |
---------
| | | | |
---------
| | | | |
---------

It must support all chords of these forms: X Xm X7 Xm7 Xmaj7 Xsus4 Xdim Xdim7

where X is any of A A# B C C# D D# E F F# G G#

(Supporting flats, such as Bb and Eb, is not required.)

The rest of the challenge description (until "Output and scoring") is purely explanatory: if you know ukulele chords already, you don't need it.

There are three steps parts to producing the chart for a chord.

  1. First, work out which notes are in the chord.
  2. Choose the correct fret for each string to match the notes.
  3. Draw the chart.

Work out which notes are in the chord

There are 12 notes in the cyclical music scale, each a semitone apart: A, A#, B, C, C#, D, D#, E, F, F#, G, G#, then A, A#....

(If you're not into music, just mentally translate every "A" into a 0, every "A#" into a 1, etc, and everything will work out just fine.)

A chord is a set of 3 or 4 notes, according to a defined pattern of intervals starting from the base note:

  • X: 0 + 4 + 3 ("major"). Example: C, C+E+G
  • Xm: 0 + 3 + 4 ("minor"). Example: Cm, C+D#+G
  • X7: 0 + 4 + 3 + 3 ("seven"). Example: C7, C+E+G+A#
  • Xm7: 0 + 3 + 4 + 3 ("minor seven"). Example: Cm7, C+D#+G+A#
  • Xmaj7: 0 + 4 + 3 + 4 ("major seven") . Example: Cmaj7, C+E+G+B
  • Xsus4: 0 + 5 + 2 ("sus four"): Example: Csus4, C+F+G
  • Xdim: 0 + 3 + 3 ("diminished"). Example: Cdim, C+D#+F#
  • Xdim7: 0 + 3 + 3 + 3 ("diminished seven"). Example: C+D#+F#+A#. [You may find it helpful to know that this is the same chord as A#dim7, D#dim7 and F#dim7].

(The name in parentheses is just for your music education, it's not relevant to the challenge.)

For instance: "Am" is a chord, where "A" is the base note, and "m" is the chord type. Looking up the table ("Xm: 0 + 3 + 4"), we see it has 3 notes:

  • the base note (A),
  • the note 3 semitones above (C, counting A# and B), and
  • the note 4 semitones above that (E).

(Chords that have 3 notes will by necessity be repeating one of the notes across the 4 strings. That's fine.)

Choose the correct frets for the notes

Ukulele is an instrument with four strings, that play the notes G, C, E and A. Pressing the string against a fret makes it sound higher, one semitone per fret. (Pressing fret 1 on the A string turns it to A#, fret 2 turns it to B, etc).

Chord charts

The chord chart represents the first few frets, played by the left hand, read from top to bottom ("higher" frets are lower down the page). The strings are the columns separated by | symbols. The frets are the rows separated by - symbols. The symbol # means that the string is pressed against that fret ("is fretted"), while the right hand strums.

So in the chord chart at the start:

  • The G string is not fretted (it is "open"), so it sounds like G.
  • The C string is fretted on fret 2 (two semitones up), so it sounds like D.
  • The E string is on fret 1 (one semitone up), and sounds like F.
  • The A string is on fret 2, so sounds like B.

So this chord has notes: G, D, F, B. (It's G7, G+B+D+F - the order of the pitches doesn't matter.)

Calculating chords

To calculate which fret to play for a given string, for a given chord:

  • if the string's base note (G, E, A or C) is not already in the chord, play the lowest fret that belongs to the chord
  • repeat for each of the four strings.

Example #1

For instance, let's play E7 (consisting of E + G# + B + D).

  1. The G string is not in the chord. Raise it to G# (fret 1).
  2. The C string is not in the chord. Raise it to D (fret 2).
  3. The E string is in the chord. Don't fret it. (Leave the string open.)
  4. The A string is not in the chord. Raise it to B (fret 2).

So we are playing: G#, D, E, B.

So we output:

 G C E A
---------
|#| | | |
---------
| |#| |#|
---------
| | | | |
---------
| | | | |
---------
| | | | |
---------

Example #2

Another example: G# (consisting of G# + B# + D#):

  1. The G string is not in the chord, raise it to G# (fret 1).
  2. The C string is in the chord (B#), don't fret it.
  3. The E string is not in the chord, raise it to G# (fret 4).
  4. The A string is not in the chord, raise it to B#(C) (fret 3)

So we are playing G#, C, G#, C

 G C E A
---------
|#| | | |
---------
| | | | |
---------
| | | |#|
---------
| | |#| |
---------
| | | | |
---------

(You may notice that sometimes not every note in the chord ends up getting played. Don't worry about it. "Proper" ukulele chords are slightly more complicated than this basic algorithm.)

Output and scoring

Your output must closely match the above. (You can use other characters instead of |, - and #, such as em-dashes or ASCII line-drawing characters, if they are visually superior. An unfilled circle character would be better than #). Trailing linespace on each line is ok. Whitespace before the first line or after the last line is not ok. Final linefeed is optional.

Normal forms of input and output. You will receive one chord name, guaranteed to be in the table above, in the correct case.

Standard loopholes disallowed. Online services are disallowed. If your language has music functions in its standard library, you may use them (but not if they're plugins, packages, etc.)

This is . Lowest score (in bytes) wins.

Extra credit

This simple algorithm sometimes produces useless chord fingerings that don't include all the important notes of the chord. (For instance, for Dm, it produces ACFA, the same as Fm.)

For extra credit, modify the algorithm so that:

  • For all chords, the root and third (first two notes, usually 0 and 3 or 0 and 4) are played on at least one string each.
  • For 4-note chords (X7, Xm7, Xmaj7, Xdim7), the root, third and seventh (first, second and fourth notes) are played on at least one string each.

Steve Bennett

Posted 2017-05-28T00:27:02.750

Reputation: 1 558

G# is G# B# D# (B# is the same as C, which is why this chord is more often written as Ab). That said, I assume in this challenge, we only have to support sharps in the input and not flats? – Level River St – 2017-05-28T01:34:14.743

Thanks, sorry I really messed that example up - kept redoing it. Flats aren't required - clarified there. – Steve Bennett – 2017-05-28T07:07:18.117

Answers

5

Javascript 280 282 287 bytes

(Non-meaningful linebreaks inserted for readability)

f=

a=>' G C E A'+[1,2,3,4,5].map(y=>
(l=`
---------
`)+[-2,3,7,0].map(
x=>(z=a.match(/(.#?)(.*)/),
[...'9'+{'':14,7:147,m:40,m7:740,maj7:148,sus4:24,dim:30,dim7:360}[z[2]]]
.map(i=>(i-x+'D EF G A BC'.search(z[1][0])+!!z[1][1]+8)%12)
.sort((a,b)=>a-b)[0]-y?'| ':'|#'
)).join``+'|').join``+l



document.getElementById("mysubmit").onclick=function() {
  document.getElementById("out").innerHTML=f(document.getElementById("myinput").value)
}
<input type="text" value="G7" id="myinput">
<input type="submit" id="mysubmit">
<pre id="out"></pre>

Commentary

I've never been so happy at Javascript's weird type conversions. This is probably the first time I've ever wanted 40 + 9 to equal 409 :) It's also really fortunate that integers are acceptable as keys in Objects.

There's nothing too clever going on here. I subtract 3 from each chord note in order to keep the range within 0-9, so that I can temporarily express a chord as a 3-digit number. Luckily, the order of notes isn't important, so we can express 036 as 360.

Steve Bennett

Posted 2017-05-28T00:27:02.750

Reputation: 1 558

FYI, if anyone wants to use this program, I've put it in a JSFiddle for easy access: https://jsfiddle.net/o5z28vbr/embedded/result/

– Beta Decay – 2017-05-28T08:23:19.350

Sadly it's not that useful for actual chord charts, because of the simplification of the algorithm. Maybe I'll add an extra credit section. – Steve Bennett – 2017-05-28T10:09:57.233

I dunno, it seems pretty accurate when I've tested it – Beta Decay – 2017-05-28T10:33:32.747

Try C, Dm7, G#, F7, B7. They're all missing a pretty important note or two. – Steve Bennett – 2017-05-28T10:37:30.127

Huh, that's the way I usually play them – Beta Decay – 2017-05-28T10:44:13.867

(err, that should be "Cm", not "C".) – Steve Bennett – 2017-05-28T11:07:06.467

I think [...'9'+[14,740,30,148,24,360,40,147][~~parseInt(z[2],30)%47&7]] saves 14 bytes. – Arnauld – 2017-05-28T16:09:45.930

Wow, never seen that trick with parseint before. How did you come up with %47&7? Just trial and error? Can you always find a combination that yields a sequence of different numbers like that? – Steve Bennett – 2017-06-02T05:17:53.100

Also, what does the ~~ do? It doesn't seem necessary? @Arnauld – Steve Bennett – 2017-06-02T05:20:27.737

Yes, it's just trial and error. Provided that the original list contains distinct strings matching [0-9a-z] and provided that these strings do not have a too long prefix in common, then yes: there exists at least one base b <= 36 such that parseInt(x,b) produces a list of distinct integers, and such a list can usually be reduced significantly by applying successive modulo operations. – Arnauld – 2017-06-02T07:43:01.520

You're correct about this ~~. I think my original approach was parseInt(s,30)%47‌​%8 which produces NaN if s is an empty string. The ~~ was there to force it to 0 instead. But &7 is now taking care of that. – Arnauld – 2017-06-02T07:45:51.960

3

Python 2, 518, 448 bytes

Thanks to Wheat Wizard for the coding suggestions!

S='GCEA' 
N=['A','A#','B','C','C#','D','D#','E','F','F#','G','G#']
C={'':(0,4,7),'m':(0,3,7),'7':(0,4,7,10),'m7':(0,3,7,10),'maj7':(0,4,7,11),'sus4':(0,5,7),'dim':(0,3,6),'dim7':(0,3,6,9)}
x=raw_input()
j=1+('#'in x)
c=[]
for s in S:c.append(min((N.index(i)-N.index(s))%12for i in [N[(a+N.index(x[:j]))%12]for a in C[x[j:]]]))
print' '+''.join(a+' 'for a in S)
for f in range(5):f=['|o'if i==f+1else'| 'for i in c];print'-'*9,'\n',''.join(f)+'|'



Try it online!

Sample Output

G
 G C E A 
--------- 
| | | | |
--------- 
| |o| |o|
--------- 
| | |o| |
--------- 
| | | | |
--------- 
| | | | |

Commentary

Thanks for a fun challenge. My daughter and I are off to buy a uke this weekend. I've never played the uke, but I play mandolin, and the theory is similar.

For this submission (my first), I addressed the original challenge without the extra credit components. I fear addressing the extra credit questions will add a lot of of bytes to the code. I hope to submit again with the extra credit bits, time permitting.

The logic is pretty simple if you trace your way back through the successive functions: 1. Parse the chord description. 2. Identify what notes are in that chord. 3. Figure out what frets on each string would fit 4. Assemble a chord by selecting the lowest fret that works on each string 5. Print out the chart fret by fret.

The core of the golfed code lies in the gnarly mess of nested list comprehensions in the eighth -- now seventh -- line of the code. The best way I can explain what's going on is by presenting the ungolfed version.

(Wheat Wizard suggested a shift from list comprehensions to generators that was not reflected in this ungolfed code)

STRINGS = 'GCEA' # Ukelele
#STRINGS = 'GDAE' # Mandolin
NOTES=['A','A#','B','C','C#','D','D#','E','F','F#','G','G#']
TYPES = {'':(0,4,7),
     'm':(0,3,7),
     '7':(0,4,7,10),
     'm7':(0,3,7,10 ),
     'maj7':(0,4,7,11),
     'sus4':(0,5,7),
     'dim':(0,3,6),
     'dim7':(0,3,6,9)}

def parse(x):
    # parses the chord into the root and the chord type
    i=2 if '#' in x else 1
    return x[:i],x[i:]

def strng(string,notes):
    # Return a list of possible fret locations based on notes in a chord
    # This will return all possible fret locations, including those 
    # beyond the fifth fret, where this code stops drawing.
    return sorted([(NOTES.index(i)-NOTES.index(string))%12 for i in notes])

def chordnotes(note,typ):
    #return the list of Notes that belong to the chord
    return  [NOTES[(a+NOTES.index(note))%12] for a in TYPES[typ]]

def chord((note,typ)):
    # Returns a list of four fret locations, in order of the strings.
    # This is the heart of the musical logic of the code. Here I use a naive
    # approach, and simply grab the lowest note on each string that fits the
    # chord.  This function needs to be rewritten to address the extra
    # credit parts of the challenge.
    # The extra parentheses in the function declaration allow us to pass in 
    # the results of the parse function directly.
    x = []
    for s in STRINGS:
        x.append(min(strng(s,chordnotes(note,typ))))
    return x

def printchart(x):
    # Do the heavy lifting of formatting the output
    # I calculate the layout of each fret, and print them in turn.
    ch = chord(parse(x))
    print  ' '+''.join([a+ ' ' for a in STRINGS])
    fret = '-'*9
    for f in range(5):
        note = ['|o' if i==f+1 else '| ' for i in ch]
        print fret + '\n' + ''.join(note)+'|'

def main():
    x = raw_input('Chord?')
    printchart(x)

if __name__ == '__main__':
    main()

CCB60

Posted 2017-05-28T00:27:02.750

Reputation: 159

Hello and welcome to our site! This post looks good except for one thing, you should not prompt Chord? before input, its not in the specifications and thus is extra output. This is good because not prompting saves more bytes anyway. You also have a lot of whitespace you can trim off that is not required by python. – Post Rock Garf Hunter – 2017-05-31T03:33:06.560

Here is a link with most of the extra whitespace trimmed away: Try it online!

– Post Rock Garf Hunter – 2017-05-31T03:33:35.880

Our site rules also allow you to use input() instead of raw_input() even though its not really good python practice. If you have any more questions about the site, or golfing python I'd be happy to answer them so just ping me in a comment or in chat! – Post Rock Garf Hunter – 2017-05-31T03:35:59.773

I don't want to bother you anymore so one last thing, but you use a bunch of list comprehensions in this code, which is a good idea. However you can save bytes by using generators instead in some situations. Built in functions like .join can use generators instead of lists meaning that you can get rid of the [...] inside the function. My next comment will include a link showing how this might be implemented in your case. – Post Rock Garf Hunter – 2017-05-31T03:45:31.247

Try it online! – Post Rock Garf Hunter – 2017-05-31T03:45:40.967

Many thanks! Just found the site a few days ago, and have been enjoying it a lot. Lots to learn. It's clear I need to be more aggressive about the golfing. It goes against my training to obfuscate the code so! I'll adopt your changes into the post and then go ponder how you did it. – CCB60 – 2017-05-31T03:57:43.840