A little rust program for the C major scale

A little rust program for the C major scale

·

0 min read

Picture is from Ahmed Rhizkaan

Every year for the last 6 years during the summer I am on the mountain at the 'Turracher Höhe'. 'Participating' at the Thomas Leeb Bootcamp playing guitar for 1 week.

I am composing a song every year for the last 3 years in row, that's my personal challenge. Always pushing myself outside of my comfort zone to some, but still stimulating my curiosity enough to keep going.

Till this year I avoided music theory like leper. I rather explore the different tunings, than actually build them. Who cares what

I ii iii IV V VI vii I

means ... but this years challenge was to go down the rabbits hole. So a friend of mine (Adriane from France) who studies jazz in Leeds (England) took about one hour of his precious time to talk me through some minor parts of music theory.

Also I listened to some professional composers and other topics and I want to retain and apply these information to get a meaningful connection to it.

But how to go about it?

Well I am a professional programmer for over a decade now, so my brain is trained to remember algorithmic thinking. But it's not trained to retain music theory ;D ....

Which leads me to my beginning life-hack of music theory .... write a music theory shell application in rust to help me understanding the underlying mathematical structures by implementing them.

I will try to make this an ongoing regular exercise to combine of my two currently favorite things: Rust and Guitar playing / Music.

Scale alphabet C-Major

We will start our TDD with our core assumption.

I use western scales and the english notation so we have

const NOTE_ALPHABET: &str = "ABCDEFG";

Personally I think the system is stupid esp the more we get into different scales and 'perspectives' but this is our legacy and our standard so we build around it.

  1. rule of an Scale every letter of our alphabet has to be in there
  2. rule is the Scale starts with the Key.

About rule 1: This will get funny as soon as we have F## and other weird constructs just to stay within this rule. Which is one reason why I think there should be better expression where we don't have to do certain calculations in our head to actually understand that F + 2 halfsteps up is? G at least in this scale ...

About rule 2: A key is the so called root note that defines the starting point and the end of an octave. So C major will have 2 Cs.

Intervals

In general every scale has 12 semitones. These are distances of frequencies to each other. Those are relative to each other, I will not get to much into details but rather recommend watching this:

youtube.com/watch?v=mdEcLQ_RQPY

Important for us is the rule of semitonintervals of a Major scale

2 2 1 2 2 2 1

which translates to a more 'complete' picture like this

   2    2    1    2    2    2    1 
C    D    E    F    G    A    B    C
I   ii   iii  IV    V    VI  Vii   I

I will leave out the other theory this is already a bit much for a program that currently basically does the following. It just helps to remember it better to explain it ;D ... my dear rubber duckies

get_scale(C) -> CDEFGABC

back to our code:

we have our alphabet now we want our test:

#[cfg(test)]
mod tests {
    use crate::{get_scale, ScaleTypes};

    #[test]
    fn get_c_major_scale() {
        let scale = get_scale_alphabet_for_key("C".parse().unwrap());

        assert_eq!(scale.unwrap_or("".parse().unwrap()), "CDEFGABC".to_string())
    }
}

it just returns the current alphabet of our Scale with our letters in the right order

to our implementation

fn get_scale_alphabet_for_key(key: char) -> Result<String, ScaleError> {
    let index = NOTE_ALPHABET.chars().position(|c| {
        key == c
    });

    if index.is_none() {
        return Err(ScaleError::InvalidKey)
    }

    let mut scale = String::new();
    let key_idx = index.unwrap();
    let len = NOTE_ALPHABET.len();

    scale.push_str(NOTE_ALPHABET[key_idx..len].as_ref());
    scale.push_str(NOTE_ALPHABET[before_key_idx..key_idx].as_ref());
    scale.push(key);

    scale
}

lets look at our code base on logical 'tasks'.

we have our const with our alphabet, this is a &str str is just a 'slice' which can be seen as an array.

so basically our task is the following:

  • find the index of our 'key' in the array
  • push the part starting with our 'key' on the new string (vector of characters)
  • push the part before our key onto the new string
  • add the key at the last position to finish the octave

Now some of you more familiar with music theory probably guessed why I picked the C major scale which has no sharps and flats in it ;). Yes I start lazy.

so our index of our key is found by the following code block

let index = NOTE_ALPHABET.chars().position(|c| {
        key == c
    });

it can be seen as 'obvious' but just to be sure I will try to explain what's happening.

NOTE_ALPHABET.chars()

will get an iterator for the characters contained by our slice (&str)

NOTE_ALPHABET.chars().position();

will return the position (usize) where ever our closure return true.

|c| { key == c }

compares every character handed by our iterator to our function with our key and as soon as our key matches we return true and this will give use our index

to avoid problem like using a non existing key we add an error type to our program

enum ScaleError {
    InvalidKey
}

It's common in rust to use enums for ErrorHandling and I really learned to enjoy this. It allows us to 'catch all' in the end. Which I will show another time.

the next things are our initialization and positioning

    let mut scale = String::new();
    let key_idx = index.unwrap();
    let len = NOTE_ALPHABET.len();

our new scale

    let mut scale = String::new();

we need to write to the string so it's mutable.

since we could return 'None' as an option on position we need to unwrap our index

    let key_idx = index.unwrap();

and since we're going to need the length of our slice of characters

   let len = NOTE_ALPHABET.len();

we could solve this in different ways but I find range selectors the least overhead of meaning. it's basically how we would think as people 'from here to there'

So we will start from our key:

scale.push_str(NOTE_ALPHABET[key_idx..len].as_ref());

and push the &str 'CDEFG' on our new scale string

After that we append the 'AB'

scale.push_str(NOTE_ALPHABET[0..key_idx].as_ref());

so our current string is 'CDEFGAB'

We need our final key for the complete octave

scale.push(key);

now we have our 'CDEFGABC' complete C-Major scale

and we can return our Result

Ok(scale)

in the end lets extend our unitest with our error case

#[test]
    fn get_major_scale_by_invalid_key() {
        match get_scale_alphabet_for_key("X".parse().unwrap()).unwrap_err() {
            ScaleError::InvalidKey => assert_eq!(true, true),
            _ => assert_eq!(true, false)
        }
    }

the _ match is like a switch default and I just put it there so even if I add new error types I don't have to touch this test anymore.

So as always thanks for reading :) and feedback is always welcome :)