Re: Guitar Driven Development
writing code for making music
2024-09-29

This is a reply to Guitar driven development by my pal eli. (Hi, eli!)
https://eli.li/guitar-driven-development
I too recently acquired a guitar!1 I took like three days of lessons on youtube2 3 and then decided that I am so dedicated a student of the instrument that I ought to write a little program that will keep track of all the chords I have learned so far, and that will also export them as a good-lookin’ pdf.
To start, I wrote down all the chords I know in a little recutils database.
It looks like this:
%rec: chords
%desc: all the chords i know
%mandatory: name basefret frets fingers
%allowed: name basefret frets fingers
name: G
basefret: 1
frets: 320033
fingers: 210034
name: Cadd9
basefret: 1
frets: x32033
fingers: x21034
name: Dsus4
basefret: 1
frets: xx0233
fingers: xx0134
...
recfmt can easily get all the data into a template. But unfortunately it has no template logic. And not in the way that mustache templates are “logicless” but actually have conditionals and iteration.
.PS 1i
boxwid=1
boxht=1.5
circlerad=0.45
down.\"----------------
.\"-- CHORD NAME --
.\"----------------
.ps 18
.\" NOTE: HERE IS A RECFMT SLOT:
"{{name}}"
.ps 10
move 3.\"----------------
.\"-- DRAW BOXES --
.\"----------------
for i = 1 to 4 do
{
[ right; A: box; B: box; C: box; D: box; E: box; ]
}.\"--------------
.\"-- BASEFRET --
.\"--------------
.ps 80
line wid 2 ht 2 from 1st [].nw to 1st [].ne.ps 10
.\"---------------
.\"-- PLACEMENT --
.\"---------------
.\" FIXME: HARD CODED BECAUSE NO LOGIC:
circle fill 1 at 3rd [].A.w
circle fill 1 at 2nd [].A.e
circle fill 1 at 3rd [].D.e
circle fill 1 at 3rd [].E.e.\"---------------
.\"-- FINGERING --
.\"---------------
.ps 14
.\" FIXME: HARD CODED BECAUSE NO LOGIC:
circle fill 0 at 1st [].B.e - (0,-1.75)
circle fill 0 at 1st [].C.e - (0,-1.75).ps 10
.PE
This template creates a good looking diagram with this little invocation:
recsel chords.rec -e 'name="G"' \
| recfmt -f chord.tmpl \
| groff -ms -p -Tpdf \
> chord.pdf

rec is still a great way to represent and store data. recsel is still a great way to query that data. And recfmt is still a great way to iterate over that data. But I need more template logic!
Luckily, I have m4, the one true templating language. With m4 I get logical branching and constants. m4 doesn’t actually have arrays or loops. But you can fake it by writing your own.
------------------
.\"-- m4 constants --
.\"------------------
.\"define(`_first_string',`[].A.w')dnl
define(`_second_string',`[].A.e')dnl
define(`_third_string',`[].B.e')dnl
define(`_fourth_string',`[].C.e')dnl
define(`_fifth_string',`[].D.e')dnl
define(`_sixth_string',`[].E.e')dnl
define(`_offset_above',`- (0,-1.75)')dnl
define(`_offset_below',`- (0,1.75)')dnl
-----------------
.\"-- m4 "arrays" --
.\"-----------------
.\"define(`array_set', `define(`$1[$2]', `$3')')dnl
define(`array_get', `defn(`$1[$2]')')dnl
(`strings',1,_first_string)dnl
array_set(`strings',2,_second_string)dnl
array_set(`strings',3,_third_string)dnl
array_set(`strings',4,_fourth_string)dnl
array_set(`strings',5,_fifth_string)dnl
array_set(`strings',6,_sixth_string)dnl
array_set-------------
.\"-- m4 funs --
.\"-------------
.\"define(`for',`ifelse($#,0,``$0'',`ifelse(eval($2<=$3),1,
(`$1',$2)$4`'popdef(`$1')$0(`$1',incr($2),$3,`$4')')')')
`pushdef
.\" TODO: Figure out how to loop this in a way that works..define(`_dofingers', `
ifelse($1,0,`circle fill 0 at 1st array_get(`strings',1) _offset_above',$1,x, ,`"$1" at 4th array_get(`strings',1) _offset_below')
ifelse($2,0,`circle fill 0 at 1st array_get(`strings',2) _offset_above',$2,x, ,`"$2" at 4th array_get(`strings',2) _offset_below')
ifelse($3,0,`circle fill 0 at 1st array_get(`strings',3) _offset_above',$3,x, ,`"$3" at 4th array_get(`strings',3) _offset_below')
ifelse($4,0,`circle fill 0 at 1st array_get(`strings',4) _offset_above',$4,x, ,`"$4" at 4th array_get(`strings',4) _offset_below')
ifelse($5,0,`circle fill 0 at 1st array_get(`strings',5) _offset_above',$5,x, ,`"$5" at 4th array_get(`strings',5) _offset_below')
ifelse($6,0,`circle fill 0 at 1st array_get(`strings',6) _offset_above',$6,x, ,`"$6" at 4th array_get(`strings',6) _offset_below')
)dnl
'
.\" TODO: Figure out how to loop this in a way that works..define(`_dofrets',`
ifelse($1,0, , $1,x, `[ line up right; move down; line up left ] at 1st array_get(`strings',1) _offset_above', `circle fill 1 at $1th array_get(`strings',1)')
ifelse($2,0, , $2,x, `[ line up right; move down; line up left ] at 1st array_get(`strings',2) _offset_above', `circle fill 1 at $2th array_get(`strings',2)')
ifelse($3,0, , $3,x, `[ line up right; move down; line up left ] at 1st array_get(`strings',3) _offset_above', `circle fill 1 at $3th array_get(`strings',3)')
ifelse($4,0, , $4,x, `[ line up right; move down; line up left ] at 1st array_get(`strings',4) _offset_above', `circle fill 1 at $4th array_get(`strings',4)')
ifelse($5,0, , $5,x, `[ line up right; move down; line up left ] at 1st array_get(`strings',5) _offset_above', `circle fill 1 at $5th array_get(`strings',5)')
ifelse($6,0, , $6,x, `[ line up right; move down; line up left ] at 1st array_get(`strings',6) _offset_above', `circle fill 1 at $6th array_get(`strings',6)')
)dnl
'define(`_split',
($1, `\(.\)\(.\)\(.\)\(.\)\(.\)\(.\)', `\1, \2, \3, \4, \5, \6')')dnl `regexp
The hardest part of m4 is figuring out how to properly escape macros when using nested macros and functions. I haven’t figured it out for this section yet. So it’s still really verbose here. But hey it works!
.PS 1i
boxwid=1
boxht=1.5
circlerad=0.45
down.\"----------------
.\"-- CHORD NAME --
.\"----------------
.ps 18
"{{name}}"
.ps 10
move 3.\"----------------
.\"-- DRAW BOXES --
.\"----------------
for i = 1 to 4 do
{
[ right; A: box; B: box; C: box; D: box; E: box; ]
}.\"--------------
.\"-- BASEFRET --
.\"--------------
"{{basefret}}" at 1st _sixth_string + (1,0.75)'
`.\"---------------
.\"-- PLACEMENT --
.\"---------------
_dofrets(_split(`{{frets}}')).\"---------------
.\"-- FINGERING --
.\"---------------
.ps 14
_dofingers(_split(`{{fingers}}')).ps 10
.PE
So now that the m4 functions are performing logic on the rec data, the template will actually spit out a guitar chord diagram for each chord in the recfile!
Now the build command looks something like this:
recsel chords.rec \ # select chords
| recfmt -f chart.template \ # format chords
| m4 \ # template logic
| groff -ms -p -Tpdf \ # groff to pdf
| > chords.pdf # save to file
This has been fun to hack around on. It uses three of my favorite pieces of esoteric software: recutils, m4, and groff.
My ultimate plan is to make a small markup syntax for guitar tablature to keep track of songs as I learn them that can include one of these diagrams for each chord used in the tab. I think that will be fun!
Further reading: