I recently made chordgenerator.xyz - a web app using Rust which lets you create guitar chord diagrams. The source is on Github here. This started off as a weekend project to create diagrams on the command-line, then expanded a bit to become an online web app too.
Guitar chords can be represented as the fret number per string (or an indication that the string shouldn’t be played), along with suggested fingering for the note. It’s also quite handy to have a name so you can describe it, so my Chord
struct ended up looking like this:
pub struct Chord<'a> {
pub frets: Vec<i32>,
pub fingers: Vec<&'a str>,
pub title: &'a str,
}
An example chord struct would be
let chord = Chord {
title: "Hendrix",
frets: vec![-1, 7, 6, 7, 8, -1],
fingers: vec!["x", "2", "1", "3", "4", "x"],
};
which would create this chord, the ‘Hendrix’ E7♯9:
Representing data
Frets and fingers are vectors, which correspond to the guitar strings from the lowest (E, shown as uppercase) to highest (also e, shown as lowercase). Fingers are strings, since these are shown as text directly in the diagram, and can be an ‘x’ to show the string should not be played, ‘0’ for an open string, or a number to show which finger should be used for the note. These could both probably be arrays rather than vectors, although that would rule out creating diagrams for extended range guitars (eg ones with seven or more strings, rather than the usual six).
I created an enum to represent the guitar strings, which really is a bit unnecessary since you could just use the numbers 1-6 instead (or 0-5 since I was using the index of the vectors). However, it did make things clearer when trying to pass the string around different methods, especially in the prototyping stage.
enum GuitarString {
E = 0,
A = 1,
D = 2,
G = 3,
B = 4,
HighE = 5,
}
Rust made it really simple to implement a conversion from an integer into my GuitarString enum using TryFrom
:
enum GuitarString {
E = 0,
A = 1,
D = 2,
G = 3,
B = 4,
HighE = 5,
}
impl TryFrom<usize> for GuitarString {
type Error = ();
fn try_from(value: usize) -> std::result::Result<Self, Self::Error> {
match value {
1 => Ok(GuitarString::A),
2 => Ok(GuitarString::D),
3 => Ok(GuitarString::G),
4 => Ok(GuitarString::B),
5 => Ok(GuitarString::HighE),
_ => Ok(GuitarString::E),
}
}
}
which can then be used
for (i, note) in chord_settings.frets.iter().enumerate() {
let string: GuitarString = i.try_into().unwrap_or(GuitarString::E);
svg_draw_note(note, string);
}
If you look through the code then this transformation is basically superfluous, since the string is then parsed back to an integer to work out the placement of the note: let x = offset_left + guitar_string as i32 * string_space;
but it made things much clearer when I was working out what to do.
The frets are numbers rather than strings, since I needed to do a bit of checking to make sure the diagram showed the correct area of the fretboard. Chord diagrams usually show the five or so frets around the chord being played rather than the entire length of the fretboard for clarity, so I needed to check in several places if a fret number provided was higher or lower than another, or the total difference between the highest and lowest fret. Using numbers for the fret vector saved having to convert the input in several places.
A couple of images to show how diagrams only show part of the fretboard:
Web
This started off as a weekend project over Christmas, then expanded a bit into a website split into a frontend calling a Rust API backend which creates the images. The backend is written in Actix and Rust, and the frontend is vanilla Javascript using HTML5 boilerplate. The diagrams themselves are created as SVGs (more on this later), and can also be downloaded as PNGs. The web backend and frontend code is here.
The main backend Rust library uses a Tera template to create an SVG file with a basic grid layout for the frets and strings, and then the library draws circles for the notes and text for fingering over the top of the grid.
I decided to create a library crate (lib.rs
) so that I could call it from a web version as well as from the command line. I also added a binary (main.rs
) which uses Clap for command-line usage, which was really useful for development and testing locally.
Drawing
Initially I used Cairo to create PNG files rather than SVGs, but these didn’t look great, especially on retina screens. There were also annoying issues to work around with Cario, like making sure the Cairo libraries and fonts were installed correctly on the production server, otherwise I got odd errors between local dev and running in production. Additionally, the Cairo-Rust docs are not the best - they’re very sparse, seemingly generated from the original API, so good enough if you already know what you’re looking for, but otherwise slightly baffling.
Switching to SVGs meant that the chord diagrams could scale to any size without any resolution issues, and also made it really easy to test: SVGs are just text, so it’s really simple to verify output against a fixture file, compared with trying to visually compare images.
Fun stuff
Since the chord data is just a title (string), and a vec for frets and a vec for fingerings, it’s easy to implement hashing. In my case, I just needed to derive Hash
from std::hash
:
use std::hash::{Hash, Hasher};
#[derive(Hash)]
pub struct Chord<'a> {
pub frets: Vec<i32>,
pub fingers: Vec<&'a str>,
pub title: &'a str,
}
pub fn get_filename(chord: &Chord) -> u64 {
let mut s = DefaultHasher::new();
chord.hash(&mut s);
s.finish()
}
This allowed me to use the hash of the data as the filename for the chord, so on the web side I can just check if a file already exists and display that if it does, saving having to create duplicate files.
Conclusion
This started off as a fun little project drawing chord diagrams, and quickly expanded into a command-line app and a running web app. There are a huge number of quality libraries available for Rust, which even made it easy to switch from drawing PNGs with Cairo to creating SVGs with Tera, while Actix let me quickly create a web api to serve the app.
I also liked Rust’s ability to create library and binary files for the same crate so I could split the code shared between the command line and the web, as well as being able to use workspaces to reference the local library crate code when creating the web version.
Try it out at chordgenerator.xyz, or have a look at the library source on Github.