Skip to main content

Crate guitar_tab_generator

Crate guitar_tab_generator 

Source
Expand description

Β§Guitar Tab Generator

Build + Test Coverage

Guitar Tab Generator logo

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.

Web Assembly Rust


Β§Table of Contents

Β§Demo

Example web application πŸš€

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 guitar_tab_generator::{generate_arrangements, TabInput};

// Newline-separated pitches. Blank lines are rests; a line like "D4G4" is a chord.
let input = TabInput::new("E2\nA2\nD3", "standard", 18, 0, 1);

// Arrangements are ranked by difficulty, easiest first.
let set = generate_arrangements(input).expect("input is valid");
println!("{}", set.render(0, 30, 2, None).expect("arrangement 0 exists"));

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.

Β§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
RepresentationStringFret
G3 : 3 β‡’ 030
G3 : 4 β‡’ 545
G3 : 5 β‡’ 10510
G3 : 6 β‡’ 15615
Β§Beat 2 is a rest and therefore has no fingerings.
Β§Beat 3 pitch fingerings
RepresentationStringFret
B3 : 2 β‡’ 020
B3 : 3 β‡’ 434
B3 : 4 β‡’ 949
B3 : 5 β‡’ 14514
Β§Beat 4 pitch fingerings
RepresentationStringFretRepresentationStringFret
D4 : 2 β‡’ 323G4 : 1 β‡’ 313
D4 : 3 β‡’ 737G4 : 2 β‡’ 828
D4 : 4 β‡’ 12412G4 : 3 β‡’ 12312
D4 : 5 β‡’ 17517G4 : 4 β‡’ 17417

Β§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 FingeringG4 Fingering
D4 : 2 β‡’ 3G4 : 1 β‡’ 3
D4 : 2 β‡’ 3G4 : 2 β‡’ 8
D4 : 2 β‡’ 3G4 : 3 β‡’ 12
D4 : 2 β‡’ 3G4 : 4 β‡’ 17
D4 : 3 β‡’ 7G4 : 1 β‡’ 3
D4 : 3 β‡’ 7G4 : 2 β‡’ 8
D4 : 3 β‡’ 7G4 : 3 β‡’ 12
D4 : 3 β‡’ 7G4 : 4 β‡’ 17
D4 : 4 β‡’ 12G4 : 1 β‡’ 3
D4 : 4 β‡’ 12G4 : 2 β‡’ 8
D4 : 4 β‡’ 12G4 : 3 β‡’ 12
D4 : 4 β‡’ 12G4 : 4 β‡’ 17
D4 : 5 β‡’ 17G4 : 1 β‡’ 3
D4 : 5 β‡’ 17G4 : 2 β‡’ 8
D4 : 5 β‡’ 17G4 : 3 β‡’ 12
D4 : 5 β‡’ 17G4 : 4 β‡’ 17

Note: D4 : 2 β‡’ 3 with G4 : 1 β‡’ 3 is valid; while D4 : 2 β‡’ 3 with G4 : 2 β‡’ 8 is 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)

StructsΒ§

Arrangement
Arrangement is re-exported for direct Rust consumers. The canonical 2.x access path for per-arrangement metadata is ArrangementSet::difficulty(i) and ArrangementSet::max_fret_span(i); direct construction of Arrangement values is internal. A single ranked guitar arrangement: one fingering choice per beat, ordered by line.
ArrangementSet
Opaque handle holding the result of one generate_arrangements call.
Guitar
A guitar configuration: the playable fret count above the capo and the reachable pitch range for each string.
NumArrangements
Validated count of arrangements to compute. Construction enforces 1..=NumArrangements::MAX.
ParseError
One unparseable substring in the input, with its 1-indexed line number.
PitchFingering
The assignment of a single Pitch to a specific StringNumber and fret position.
StringNumber
A validated guitar string number in the range 1..=12.
TabInput
Configuration bundle for one tab-generation request.
UnplayablePitch
A pitch that could not be played on the configured guitar, with its 1-indexed line number.

EnumsΒ§

Line
Arrangement is re-exported for direct Rust consumers. The canonical 2.x access path for per-arrangement metadata is ArrangementSet::difficulty(i) and ArrangementSet::max_fret_span(i); direct construction of Arrangement values is internal. One logical line of a parsed or arranged composition.
NormalizedBeat
One beat in the normalized input echoed back from ArrangementSet::normalized_input.
Pitch
A musical pitch in the range C0 through B9.
TabError
Top-level error variant for the WASM boundary.
TuningName
Named tuning presets. Parsed case-insensitively from strings.

FunctionsΒ§

create_arrangements
Arrangement is re-exported for direct Rust consumers. The canonical 2.x access path for per-arrangement metadata is ArrangementSet::difficulty(i) and ArrangementSet::max_fret_span(i); direct construction of Arrangement values is internal.
create_string_tuning
Builds a tuning map from a slice of open-string pitches, numbering them from string 1 (highest) to string N (lowest).
generate_arrangements
Generates an ArrangementSet from a TabInput. Single entry point for both Rust callers and the WASM boundary; JS sees this as generateArrangements.
get_tuning_names
Returns the supported TuningName variants, typed for JS consumption via tsify.
parse_lines
render_tab
Renders an Arrangement’s lines as an ASCII guitar tab.

Type AliasesΒ§

BeatVec
Arrangement is re-exported for direct Rust consumers. The canonical 2.x access path for per-arrangement metadata is ArrangementSet::difficulty(i) and ArrangementSet::max_fret_span(i); direct construction of Arrangement values is internal. One beat’s worth of items (usually Pitch or PitchFingering).