👩‍💻 chrismanbrown.gitlab.io

Re: Guitar Driven Development

writing code for making music

2024-09-29

screenshot of a desktop showing vim and a pdf viewer side by side. vim is split into a buffer showing a recfile full of chord data, and a buffer showing a groff source file with recfmt slots. the pdf viewer shows guitar chord diagrams generated the code to the left.

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

...
snippet of a recfile full of chord data

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
proof-of-concept groff file full of pic(1) preprocessor instructions to make a guitar chord diagram. it contains a single recfmt slot, “{{name}}”, but everything else is hard coded because recfmt has no logic. this template will print the correct name of each chord, but will always display a diagram of a G chord irregardless.

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

a good looking diagram of a G chord

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
array_set(`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
.\"-------------
.\"-- m4 funs --
.\"-------------
define(`for',`ifelse($#,0,``$0'',`ifelse(eval($2<=$3),1,
  `pushdef(`$1',$2)$4`'popdef(`$1')$0(`$1',incr($2),$3,`$4')')')')
.\" 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',
`regexp($1, `\(.\)\(.\)\(.\)\(.\)\(.\)\(.\)', `\1, \2, \3, \4, \5, \6')')dnl
snippet of the next iteration of the groff template that contains a bunch of m4 definitions

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
snippet of the groffy part of the template that now defers to m4 functions for template logic. every word in {{double curly brackets}} is a recfmt slot.

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:


  1. Well technically I acquired it two years ago at the beginning of the COVID-19 lockdown, but I didn’t start learning to play it until now. I decided I needed some new hobbies to enjoy while stuck at home. I also decided I needed some new hobbies to enjoy outdoors. So I also got a skateboard and some rollerblades. I rode the skateboard maybe twice, but I grew to love the skates.↩︎

  2. Learn guitar with Andy: https://www.youtube.com/watch?v=BBz-Jyr23M4&list=PL-RYb_OMw7GfqsbipaR65GDDzA1rP5deq&pp=iAQB↩︎

  3. Learn guitar with Ellen: https://www.youtube.com/watch?v=5rcCiXqAShY&list=PLj0QZIx4bc7xPFgQsLy1bb0mv1ta-4hKR&pp=iAQB↩︎