Guitar Tab Generator
Generate fingerstyle guitar tabs based on the difficulty of different finger positions. Built with Rust. Designed for compilation to WebAssembly for use in web applications.
Table of Contents
- Guitar Tab Generator
Demo
Features
- Input pitch parsing
- Alternate tunings
- Capo consideration
- Any number of strings (not just 6 string guitars!)
- Configurable number of frets
- Tab width and padding formatting
- Playback indicator for playback applications
- Pathfinding via Yen's k-shortest-paths algorithm (built on Dijkstra) to rank arrangements from least to most difficult.
Quick start (2.0.0)
Rust:
use ;
// Newline-separated pitches. Blank lines are rests; a line like "D4G4" is a chord.
let input = new;
// Arrangements are ranked by difficulty, easiest first.
let set = generate_arrangements.expect;
println!;
TypeScript (after wasm-pack build):
import init, { generateArrangements } from "./pkg/wasm_guitar_tab_generator/guitar_tab_generator.js";
await init();
const set = generateArrangements({
input: "E2\nA2\nD3",
tuningName: "standard",
guitarNumFrets: 18,
guitarCapo: 0,
numArrangements: 1,
maxFretSpanFilter: undefined,
});
for (let i = 0; i < set.len; i++) {
console.log(set.render(i, 30, 2, null));
}
set.free(); // or `using set = generateArrangements(...)` in TS 5.2+
ArrangementSet is a wasm-bindgen opaque handle. Call set.free() when done (or use using in runtimes with explicit resource management). Without that, the underlying allocation only releases when FinalizationRegistry runs, which is not prompt on every runtime.
See MIGRATION.md for the 2.0.0 migration guide, CHANGELOG.md for the full change list, and types.md for the typed surface.
Previous versions
This project has been attempted numerous times with varying levels of success. This attempt utilizes Rust and WASM to overcome the previously-encountered roadblocks regarding performance, distribution, and developer ergonomics.
- Typescript version (2022)
- Java version (2019 - 2022)
Pathfinding Algorithm Visualization
The pathfinding calculation is initiated by generate_arrangements() (which calls create_arrangements() internally).
Let's look at an example with a standard guitar where we want to find the optimal arrangement to play a G3, then a rest, then a B3, then a D4 and G4 simultaneously. The pitch input for this example could look like this:
G3
B3
D4G4
Pitch Fingerings
The different fingerings are then calculated for each pitch. For example, a G3 can be played on string 3 at fret 0, or string 4 at fret 5, and so on:
Beat 1 pitch fingerings
| Representation | String | Fret |
|---|---|---|
| G3 : 3 ⇒ 0 | 3 | 0 |
| G3 : 4 ⇒ 5 | 4 | 5 |
| G3 : 5 ⇒ 10 | 5 | 10 |
| G3 : 6 ⇒ 15 | 6 | 15 |
Beat 2 is a rest and therefore has no fingerings.
Beat 3 pitch fingerings
| Representation | String | Fret |
|---|---|---|
| B3 : 2 ⇒ 0 | 2 | 0 |
| B3 : 3 ⇒ 4 | 3 | 4 |
| B3 : 4 ⇒ 9 | 4 | 9 |
| B3 : 5 ⇒ 14 | 5 | 14 |
Beat 4 pitch fingerings
| Representation | String | Fret | Representation | String | Fret | |
|---|---|---|---|---|---|---|
| D4 : 2 ⇒ 3 | 2 | 3 | G4 : 1 ⇒ 3 | 1 | 3 | |
| D4 : 3 ⇒ 7 | 3 | 7 | G4 : 2 ⇒ 8 | 2 | 8 | |
| D4 : 4 ⇒ 12 | 4 | 12 | G4 : 3 ⇒ 12 | 3 | 12 | |
| D4 : 5 ⇒ 17 | 5 | 17 | G4 : 4 ⇒ 17 | 4 | 17 |
Fingering combinations for each beat
For beat 1 and beat 3, the fingering combinations are identical to the pitch fingerings since those beats only play one pitch each. For beat 4, we calculate the cartesian product of the pitch fingerings to consider all of the fingerings combinations.
Beat 4 fingering combinations
| D4 Fingering | G4 Fingering | |
|---|---|---|
| D4 : 2 ⇒ 3 | G4 : 1 ⇒ 3 | |
| D4 : 2 ⇒ 3 | G4 : 3 ⇒ 12 | |
| D4 : 2 ⇒ 3 | G4 : 4 ⇒ 17 | |
| D4 : 3 ⇒ 7 | G4 : 1 ⇒ 3 | |
| D4 : 3 ⇒ 7 | G4 : 2 ⇒ 8 | |
| D4 : 3 ⇒ 7 | G4 : 4 ⇒ 17 | |
| D4 : 4 ⇒ 12 | G4 : 1 ⇒ 3 | |
| D4 : 4 ⇒ 12 | G4 : 2 ⇒ 8 | |
| D4 : 4 ⇒ 12 | G4 : 3 ⇒ 12 | |
| D4 : 5 ⇒ 17 | G4 : 1 ⇒ 3 | |
| D4 : 5 ⇒ 17 | G4 : 2 ⇒ 8 | |
| D4 : 5 ⇒ 17 | G4 : 3 ⇒ 12 | |
| D4 : 5 ⇒ 17 | G4 : 4 ⇒ 17 |
Note:
D4 : 2 ⇒ 3withG4 : 1 ⇒ 3is valid; whileD4 : 2 ⇒ 3withG4 : 2 ⇒ 8is invalid since multiple frets cannot be played on the same string.
Pathfinding nodes
To calculate the optimal set of fingering combinations of each beat, we construct a node for each fingering combination. The node contains underlying data that informs the calculation of the difficulty of progressing from one fingering combination to another including
- the average non-zero fret value
- the non-zero fret span
Additionally, each node must be unique so the beat index is included in the underlying data of the node.
flowchart TB
subgraph Beat1
direction TB
1.1("G3 : 3 ⇒ 0")
1.2("G3 : 4 ⇒ 5")
1.3("G3 : 5 ⇒ 10")
1.4("G3 : 6 ⇒ 15")
end
subgraph Beat2
direction TB
2.1("Rest")
end
subgraph Beat3
direction TB
3.1("B3 : 2 ⇒ 0")
3.2("B3 : 3 ⇒ 4")
3.3("B3 : 4 ⇒ 9")
3.4("B3 : 5 ⇒ 14")
end
subgraph Beat4
direction TB
4.1("D4 : 2 ⇒ 3 \nG4 : 1 ⇒ 3")
4.2("D4 : 2 ⇒ 3 \nG4 : 3 ⇒ 12")
4.3("D4 : 2 ⇒ 3 \nG4 : 4 ⇒ 17")
4.4("D4 : 3 ⇒ 7 \nG4 : 1 ⇒ 3")
4.5("D4 : 3 ⇒ 7 \nG4 : 2 ⇒ 8")
4.6("D4 : 3 ⇒ 7 \nG4 : 4 ⇒ 17")
4.7("D4 : 4 ⇒ 12\nG4 : 1 ⇒ 3")
4.8("D4 : 4 ⇒ 12\nG4 : 2 ⇒ 8")
4.9("D4 : 4 ⇒ 12\nG4 : 3 ⇒ 12")
4.10("D4 : 5 ⇒ 17\nG4 : 1 ⇒ 3")
4.11("D4 : 5 ⇒ 17\nG4 : 2 ⇒ 8")
4.12("D4 : 5 ⇒ 17\nG4 : 3 ⇒ 12")
4.13("D4 : 5 ⇒ 17\nG4 : 4 ⇒ 17")
end
Beat1 ~~~ Beat2 ~~~ Beat3 ~~~ Beat4
With the node edges, we can see the directed graph take shape:
%%{ init: { 'flowchart': { 'curve': 'basis' } } }%%
flowchart TB
subgraph Beat1
direction TB
1.1("G3 : 3 ⇒ 0")
1.2("G3 : 4 ⇒ 5")
1.3("G3 : 5 ⇒ 10")
1.4("G3 : 6 ⇒ 15")
end
1.1 & 1.2 & 1.3 & 1.4 --> 2.1
subgraph Beat2
direction TB
2.1("Rest")
end
2.1 --> 3.1 & 3.2 & 3.3 & 3.4
subgraph Beat3
direction TB
3.1("B3 : 2 ⇒ 0")
3.2("B3 : 3 ⇒ 4")
3.3("B3 : 4 ⇒ 9")
3.4("B3 : 5 ⇒ 14")
end
3.1 & 3.2 & 3.3 & 3.4 --> 4.1 & 4.2 & 4.3 & 4.4 & 4.5 & 4.6 & 4.7 & 4.8 & 4.9 & 4.10 & 4.11 & 4.12 & 4.13
subgraph Beat4
direction TB
4.1("D4 : 2 ⇒ 3 \nG4 : 1 ⇒ 3")
4.2("D4 : 2 ⇒ 3 \nG4 : 3 ⇒ 12")
4.3("D4 : 2 ⇒ 3 \nG4 : 4 ⇒ 17")
4.4("D4 : 3 ⇒ 7 \nG4 : 1 ⇒ 3")
4.5("D4 : 3 ⇒ 7 \nG4 : 2 ⇒ 8")
4.6("D4 : 3 ⇒ 7 \nG4 : 4 ⇒ 17")
4.7("D4 : 4 ⇒ 12\nG4 : 1 ⇒ 3")
4.8("D4 : 4 ⇒ 12\nG4 : 2 ⇒ 8")
4.9("D4 : 4 ⇒ 12\nG4 : 3 ⇒ 12")
4.10("D4 : 5 ⇒ 17\nG4 : 1 ⇒ 3")
4.11("D4 : 5 ⇒ 17\nG4 : 2 ⇒ 8")
4.12("D4 : 5 ⇒ 17\nG4 : 3 ⇒ 12")
4.13("D4 : 5 ⇒ 17\nG4 : 4 ⇒ 17")
end
Beat1 ~~~ Beat2 ~~~ Beat3 ~~~ Beat4
Algorithm choice
The number of fingering combinations grows exponentially with more beats and pitches so the choice of shortest path algorithm is critical. Yen's k-shortest-paths algorithm, built on Dijkstra, was chosen so the search returns several ranked arrangements rather than a single path, for the following reasons:
- The sequential nature of the musical arrangement problem results in a directed graph where only nodes representing consecutive beat fingering combinations have edges from one to the next.
- The edges between nodes are weighted with the difficulty of moving from one fingering combination to another so the graph above is already constructed with the only possible next nodes connected.
Contributing and Installation
Build from source
Requires:
git clone https://github.com/noahbaculi/guitar-tab-generator.git
cd guitar-tab-generator
Run examples
cargo run --example basic
cargo run --example advanced
Run WASM demo
wasm-pack build --target web --out-dir pkg/wasm_guitar_tab_generator
python3 -m http.server # then open http://localhost:8000/examples/wasm.html
Background code runner
bacon
Calculate code coverage
cargo tarpaulin --out Html --output-dir dev/tarpaulin-coverage
Screen for potentially unused feature flags
unused-features analyze --report-dir 'dev/unused-features-report'
unused-features build-report --input 'dev/unused-features-report/report.json'
Build WASM binary
wasm-pack build --target web --out-dir pkg/wasm_guitar_tab_generator
# check binary size
ls -l pkg/wasm_guitar_tab_generator/guitar_tab_generator_bg.wasm
Future Improvements
- Custom tuning support over the WASM boundary (today only the preset list crosses)
- Per-arrangement fingering inspector (read access without re-pathfinding) --
PitchFingering::{string_number, fret, pitch}getters - Arrangement export / import (serialize a set for offline replay)