Measuring Trumpet Fingering Difficulty in F#

In this previous blog, I talked about applying Gray Code to trumpet fingering to maybe improve the difficulty of fingering. I thought I’d take some time this evening to answer the question, “Is there even a problem here?”

img_20160917_183751121

(and yes, I know this is a cornet, but I just had it overhauled)

I did this by writing some code to evaluate all the common major scales based solely on difficulty of finger transitions. I chose to do this measurement in F#.

Before I get into the methodology, I want to explain my choice of F#. For the most part, the choice was arbitrary. I could have done this in a variety of different languages. If anything, my F# is a little rusty, so this was an exercise to flex those muscles. Taking time to explain the choice is due to an experience when I was in college. I sat in on a guest lecture from a researcher who did a similar study for guitar fingerings, which I saw as a fairly simple graph search problem and I was appalled that he had done it in Prolog, so of course being a righteous upstart, I asked him “why Prolog?”

OK? Good.

So I chose to represent trumpet fingerings by a discriminated union:

type fingering = 
    | NotANote
    | V000
    | V010
    | V100
    | V110
    | V101
    | V011
    | V111

I use NotANote as a placeholder when I start a sequence of transitions.

Next, I needed a way to measure the difficulty of any given fingering change. To do that, initially, I had a huge pattern match of the form:

match (vp1, vp2) with
| (V000, V000) -> 0.0
| (V010, V000) -> 1.0 // etc

Which required 49 cases and then afterward, I had to write unit tests to make sure that my arbitrary scores were reflexive. In thinking about it, the problem should be easy to break down into a couple rules:

  1. No valve changes are free (this is a lie, more on this later)
  2. Single valve changes are always cheap except for changing from V101 to V111, which is not quite as cheap.
  3. Double valve changes are more expensive that single, but V010 to V111 is bad.
  4. Triple valve changes are more expensive than double, but V101 to V010 is the worst.

Four rules struck me as more concise than 49 cases:

let fingeringDifficulty vp1 vp2 =
    // convert a note to a binary representation
    let noteToBinary note =
        match note with
        | NotANote -> 0b100000
        | V000 -> 0b000
        | V010 -> 0b010
        | V100 -> 0b100
        | V110 -> 0b110
        | V011 -> 0b011
        | V101 -> 0b101
        | V111 -> 0b111
    // naively count the number of bits set in a value
    let rec bitCountNaive n a =
        if n = 0 then a
        else bitCountNaive (n >>> 1) (a + if n &&& 1 = 0 then 0 else 1)
    let eitherIs a b c = (a = c) || (b = c)
    // rule implementation
    let binaryValveRules bp1 bp2 = 
        let changes = bp1 ^^^ bp2
        let nChanges = bitCountNaive changes 0
        match nChanges with
        | 0 -> 0.0
        | 1 -> if changes = 0b010 && (eitherIs bp1 bp2 0b101) then 2.0 else 1.0
        | 2 -> if changes = 0b101 then 2.0 else 1.5
        | 3 -> if (eitherIs bp1 bp2 0b101) then 4.0 else 3.0
        | _ -> raise(System.ArgumentOutOfRangeException())
    // filter out any NotANote elements
    match (vp1, vp1) with
    | (_, NotANote) | (NotANote, _) -> 0.0
    | _ -> binaryValveRules (noteToBinary vp1) (noteToBinary vp2)

Next, for want of a better standard, I use midi note numbers to represent pitch values. It’s good enough for what I need. Given that, we need a way to convert from midi note numbers to fingerings, so here is a programmatic trumpet fingering chart:

// the lowest practical note on a Bb trumpet is E2 or 52
// while you can have screaming notes at Bb5 or higher, these are
// really "party trick" notes. We'll stop at F5, which is about
// usually the upper limit for pro big band lead parts (by comparison,
// the highest note in the West Side Story lead part is E5, and that
// is a very challenging part.
// This also ignores "false" fingerings that are part and parcel with the
// instrument.
let midiNoteToFingering midinote =
    let minNote = 52 // E2
    let maxNote = 89 // F5
    let noteToFingering = [|
        V111 ; V101 ; V011 ; V110 ; V100 ; V010 ;
        V000 ; V111 ; V101 ; V011 ; V110 ; V100 ; V010 ; V000 ; V011 ; V110 ; V100 ; V010 ;
        V000 ; V110 ; V100 ; V010 ; V000 ; V100 ; V010 ; V000 ; V011 ; V110 ; V100 ; V010 ;
        V000 ; V110 ; V100 ; V010 ; V000 ; V100 ; V010 ; V000 |]
    if midinote < minNote || midinote> maxNote then NotANote
    else noteToFingering.[midinote - minNote]

this code is essentially a table lookup that filters out notes that are out of the instruments practical working range (discounting pedal tones and the nosebleed notes).

Given modeling and measurement, now we need to apply it.

type notePairMeasurer = (int*int) -> float
 
let simpleMeasurer (midinote1, midinote2) =
    fingeringDifficulty (midiNoteToFingering midinote1) (midiNoteToFingering midinote2)

This is a definition of a function type that measures the difficulty in transitioning between two notes, passed as a tuple of ints, returning a float. I’m calling 0.0 free with difficulty increasing. The simpleMeasurer applies the rules that I’ve laid out earlier to any pair of notes by looking them up in the fingering chart and then passing the fingering into the earlier 4 rule function.
With that, we can measure a sequence of notes:

let measureNotes (measurer:notePairMeasurer) notes =
    let measureNote (prev, currentScore) curr = (curr, currentScore + (measurer (prev, curr)))
    let (note, totalScore) = Seq.fold measureNote (-1, 0.0) notes
    totalScore

which is done with one nice fold.
I want to measure a whole bunch of major scales, but since I only want to type in the data for a major scale once, I need a way to transpose a sequence of notes:

let transpose semitones notes =
    Seq.map (fun note -> note + semitones) notes

And now you can see why F# is handy – that’s very simple code.
I also wanted a way to print the scale name, so I wrote this chunk of code to convert a given midi note number to its name:

let midiName note =
    let notenames = [| "C" ; "C#" ; "D" ; "Eb" ; "E" ; "F" ; "F#" ; "G" ; "Ab" ; "A" ; "Bb" ; "B" |]
    let octavenumber = note / 12 - 2
    let notename = notenames.[note % 12]
    notename + ((string)octavenumber)

And while I’m at it, here’s the C major scale starting on C3:

let majorScale = [| 60 ; 62 ; 64 ; 65 ; 67 ; 69 ; 71 ; 72 |] |> Array.toSeq

And at long last, I tie it all together. Yes, I could probably do this more efficiently, but it works well enough.

[<EntryPoint>]
let main argv = 
    let allScales =
        seq { for i in -8..11 do
                let score = majorScale |> transpose i |> measureNotes simpleMeasurer
                let scaleName = majorScale |> transpose i |> Seq.head |> midiName
                yield (score, score / (Seq.length majorScale |> float), scaleName) }
                |> Seq.sortBy (fun (score, avg, name) -> score)
    for (score, avg, name) in allScales do
        printfn "%s total score %f, average %f " name score avg
    0

And here is the output:

 Eb3 total score 7.500000, average 0.937500
 F3 total score 7.500000, average 0.937500
 Bb3 total score 7.500000, average 0.937500
 C3 total score 8.000000, average 1.000000
 D3 total score 8.000000, average 1.000000
 G3 total score 8.000000, average 1.000000
 Ab3 total score 8.500000, average 1.062500
 F2 total score 9.000000, average 1.125000
 Bb2 total score 9.000000, average 1.125000
 A3 total score 9.500000, average 1.187500
 G2 total score 10.500000, average 1.312500
 E3 total score 10.500000, average 1.312500
 F#3 total score 10.500000, average 1.312500
 B3 total score 10.500000, average 1.312500
 Ab2 total score 11.000000, average 1.375000
 A2 total score 11.500000, average 1.437500
 B2 total score 12.000000, average 1.500000
 C#3 total score 12.000000, average 1.500000
 E2 total score 13.000000, average 1.625000
 F#2 total score 13.500000, average 1.687500

And for the most part, I agree with the scoring. The top 3 are tied and they are pretty much the easiest scales. In general, the consistency of the average scores is encouraging in that there may not even be a problem that needs solving.

What’s missing? Well. The difficulty measurement doesn’t generalize beyond scales. And I alluded to this by naming the measurer I use ‘simpleMeasurer’. In reality, the difficulty of going from one note to another on a trumpet is more complicated. A trumpet is really 7 bugles bound together. You get scales by switching the bugles in and out, but they’re still bugles. Any given bugle can only play the harmonic series and skipping between harmonics is much more difficult than valve changes. So really, one needs to take that into account too. Putting those parts in, this could be used to measure the difficulty of a piece of music. For that, it should consume a stream of MIDI and, in addition to measuring fingering changes and skips between or across harmonics, it should also measure the relative amount of time spent actively playing and resting and the rough range center and relative time spent in the upper range.

Now with all of this in place, make the rules driven by the instrument. Then the piece’s difficulty can be measured across an entire ensemble.

One thought on “Measuring Trumpet Fingering Difficulty in F#”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.