Gfret Gtk4 Port Part 4

I took some time away from coding entirely over the past several months for various reasons including, but not limited to, a bout with Covid and it's after effects, spending more time with family, and a general burnout feeling.

Current status

Everything is Broken!

Well, not entirely and it's certainly not accidental. I've been through the gui,ui file and made changes to get ready for supporting reverse (left handed) instruments, along with some extensive cleanup. Along the way the prefs.ui file was prepared for supporting a choice between Metric and Imperial units, as previously only Metric output was supported.

Most of the work has been in the backend crate, fretboard-layout. Right now at this moment, left handed output has been implemented but not tested, while the implementation for changing units is only partially complete. The crate compiles and the tests all pass, but there need to be more tests written.

The code

Rather than just using a boolean to decide whether our neck is going to be a simple Monoscale or a Multiscale, and then having another struct field which holds the value for the treble scale (hey, I was still pretty new to Rust when I started this project) I've decided to leverage Rust's enum type here.

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Handedness {
    Right,
    Left,
}

#[derive(Debug, PartialEq)]
pub enum Variant {
    Monoscale,
    Multiscale(f64, Handedness),
}

For those who need an explanation, our new enum Variant can be either Monoscale or Multiscale. If it is Multiscale then it further stores the associated data in the form of a tuple, which contains both the treble scale and another enum selecting between Right or Left handedness. This is nice. The Specs struct now has a single field which determines whether we have a Monoscale or Multiscale instrument, and if Multiscale, whether it is left or right handed. We could use three separate fields and it would work, but by doing things this way we get several benefits.

  • If it's a Monoscale, the treble scale is always the same as the bass scale, making that field redundant
  • Similarly, a Monoscale neck will have the frets laid out exactly the same whether it is right or left handed. The only difference will be the direction of the headstock and how the strings are mounted, both of which are out of scope for this tool.
  • We're basically encoding right into the types that a Monoscale neck will never have a value for those two items, making it simpler to reason about program state.

I believe that it's good practice, so far as possible at least, to design one's custom data types in such a way as to make it more difficult to represent an invalid program state. This way, it's much easier to account for all possible program states and correspondingly easier to be sure that your logic is sound. There is a very good blog post on this subject by Pablo Mansanet which even has a corresponding crate enabling bounds conditions on custom types. While I would leave it up to the individual whether or not going this far is necessary, if we can eliminate some confusion (and potential bugs) just by careful usage of the type system, then we should.

The fact that we're using a modern language here as opposed to something like C means that enums are treated like first class citizens. In C, an enum is just an integer in disguise. In Rust, my enum can have methods.

impl Variant {
    fn handedness(&self) -> Option<Handedness> {
        match self {
            Variant::Monoscale => None,
            Variant::Multiscale(_, x) => Some(*x),
        }
    }
}

So now later, in the impl block for the Specs struct, we can set our multiscale related data like so...

pub fn set_multi(&mut self, scale: Option<f64>) {
    match scale {
        Some(s) => {
            if let Some(hand) = self.variant.handedness() {
                self.variant = Variant::Multiscale(s, hand);
            } else {
                self.variant = Variant::Multiscale(s, Handedness::Right);
            };
        },
        None => self.variant = Variant::Monoscale,
    }
}

Of course our handedness property is easily dug out of the Specs struct, so why bother with creating a method in the first place? In C it's not unusual to find functions stretching on over hundreds of lines, with indentation of 8-10 layers deep being all too common. Does that mean it's good C code? It might run, but I really pity the person who has to maintain it. Better to keep each function small, and any block of code that we're likely to write more than once we might as well write only once and just call it as needed.

So we've complicated matters a bit by adding some functionality. But it turns out that it's not really that bad. Let's take a look at how we're plotting positions for each fret.

/// Distance from bridge to fret along each side of the fretboard.
pub struct Lengths {
    pub length_bass: f64,
    pub length_treble: f64,
}

/// A 2-dimensional representation of a point
pub struct Point(pub f64, pub f64);

/// 2 Points which form a line
pub struct Line {
    pub start: Point,
    pub end: Point,
}

Pretty simple so far. I've omitted the calculations for how we get the bass and treble Lengths, but those figures represent the distance from the bridge to the fret along the outer two strings. We're working in two dimensions, so wind up with exactly the same type of Point struct that you would see in thousands of other programs. And a Line is just drawn between two points. So how do we figure out where to plot our x and y coordinates? Here's the original code before we were even worried about handedness.

// backend.rs
/// This struct contains multiplication factors used to convert the raw lengths
/// from bridge to fret into x,y coordinates. It also contains an offset distance
/// used to correctly orient the two scales in a multiscale design so that the
/// desired fret is perpendicular to the centerline.
pub struct Factors {
    pub x_ratio: f64,
    pub y_ratio: f64,
    pub treble_offset: f64,
}
// further down, in the impl block for the Specs struct, is where we calculate
// the above factors

    /// Uses trigonometry to place the fret ends, based on visualizing their
    /// locations as a triangle where the hypotenuse is the string, and the
    /// opposite is the distance from the bridge parallel to the centerline.
    fn get_factors(&self) -> Factors {
        let height = (self.bridge - self.nut) / 2.0;
        let y_ratio = height / self.scale;
        let x_ratio = y_ratio.acos().sin();
        let factor = 2.0_f64.pow(self.pfret / 12.0);
        let length_bass = self.scale / factor;
        let length_treble = if self.multi {
            self.scale_treble / factor
        } else {
            length_bass
        };
        let bass_pfret = x_ratio * length_bass;
        let treble_pfret = x_ratio * length_treble;
        let treble_offset = bass_pfret - treble_pfret;
        Factors {
            x_ratio,
            y_ratio,
            treble_offset,
        }
    }

// fretboard.rs
impl Lengths {
    /// Plots the end of a fret, nut or bridge along the bass side of the scale
    fn get_point_bass(&self, factors: &Factors, config: &Config) -> Point {
        let x = (factors.x_ratio * self.length_bass) + config.border;
        let y = (factors.y_ratio * self.length_bass) + config.border;
        Point(x, y)
    }
    /// Plots the end of a fret, nut or bridge along the treble side of the scale
    fn get_point_treble(&self, factors: &Factors, specs: &Specs, config: &Config) -> Point {
        let x = factors.treble_offset + (factors.x_ratio * self.length_treble) + config.border;
        let y = specs.bridge - (factors.y_ratio * self.length_treble) + config.border;
        Point(x, y)
    }

All that winds up changing is the impl block for Lengths

    /// Plots the end of a fret, nut or bridge along the bass side of the scale
    fn get_point_bass(&self, factors: &Factors, specs: &Specs, config: &Config) -> Point {
        let x = (factors.x_ratio * self.length_bass) + config.border;
        let hand = specs.variant.handedness();
        let opposite = factors.y_ratio * self.length_bass;
        let y = match hand {
            Some(Handedness::Left) => specs.bridge - opposite + config.border,
            _ => opposite + config.border,
        };
        Point(x, y)
    }
    /// Plots the end of a fret, nut or bridge along the treble side of the scale
    fn get_point_treble(&self, factors: &Factors, specs: &Specs, config: &Config) -> Point {
        let x = factors.treble_offset + (factors.x_ratio * self.length_treble) + config.border;
        let hand = specs.variant.handedness();
        let opposite = factors.y_ratio * self.length_treble;
        let y = match hand {
            Some(Handedness::Left) => specs.bridge - opposite + config.border,
            _ => opposite + config.border,
        };
        Point(x, y)
    }

All that we're doing is checking for Handedness::Left, and if we have it then we're switching the treble side from the bottom of the image to the top. The local var opposite is so named because it's the opposite side of an imaginary right triangle formed where the string between the bridge and the fret is the hypotenuse. For the hand loca var we're again re-using our code to pull the handedness property out of the Specs struct using our enum method.