4
\$\begingroup\$

I am currently writing an app that converts musical keys. In a nutshell the conversion part of the script, is one giant if / then statement (if the user selected A then display B, etc etc.).

While this works perfectly fine, I definitely feel like this is fairly crude, and extremely lengthy, with all of the keys that need to be converted.

The following is an excerpt of the conversion function. It should be fairly readable, but basically the function first checks to make sure they've selected two keys (in this example C and B♭) and then checks the note (and whether or not it's sharp or flat), and then puts the correct answer in a couple of divs I have on the page (.noteName and .supNote).

//C to Bb Conversion
                if (firstInstSelected == "C" && secondInstSelected == "Bb") {
                    if (firstNote == "A" && secondNote == undefined) {
                        $('#' + btnLabelSelected).find('.noteName').text("B");
                        $('#' + btnLabelSelected).find('.supNote').text("");
                        return false;
                    }
                    if (firstNote == "A" && secondNote == "sharp" || firstNote == "B" && secondNote == "flat") {
                        $('#' + btnLabelSelected).find('.noteName').text("C");
                        $('#' + btnLabelSelected).find('.supNote').text("");
                        return false;
                    }
                    if (firstNote == "B" && secondNote == undefined) {
                        $('#' + btnLabelSelected).find('.noteName').text("C");
                        $('#' + btnLabelSelected).find('.supNote').text("#");
                        return false;
                    }
                    if (firstNote == "C" && secondNote == undefined) {
                        $('#' + btnLabelSelected).find('.noteName').text("D");
                        $('#' + btnLabelSelected).find('.supNote').text("");
                        return false;
                    }
                    if (firstNote == "C" && secondNote == "sharp" || firstNote == "D" && secondNote == "flat") {
                        $('#' + btnLabelSelected).find('.noteName').text("E");
                        $('#' + btnLabelSelected).find('.supNote').text("b");
                        return false;
                    }
                    if (firstNote == "D" && secondNote == undefined) {
                        $('#' + btnLabelSelected).find('.noteName').text("E");
                        $('#' + btnLabelSelected).find('.supNote').text("");
                        return false;
                    }
                    if (firstNote == "D" && secondNote == "sharp" || firstNote == "E" && secondNote == "flat") {
                        $('#' + btnLabelSelected).find('.noteName').text("F");
                        $('#' + btnLabelSelected).find('.supNote').text("");
                        return false;
                    }
                    if (firstNote == "E" && secondNote == undefined) {
                        $('#' + btnLabelSelected).find('.noteName').text("F");
                        $('#' + btnLabelSelected).find('.supNote').text("#");
                        return false;
                    }
                    if (firstNote == "F" && secondNote == undefined) {
                        $('#' + btnLabelSelected).find('.noteName').text("G");
                        $('#' + btnLabelSelected).find('.supNote').text("");
                        return false;
                    } 
                    if (firstNote == "F" && secondNote == "sharp" || firstNote == "G" && secondNote == "flat") {
                        $('#' + btnLabelSelected).find('.noteName').text("A");
                        $('#' + btnLabelSelected).find('.supNote').text("b");
                        return false;
                    }
                    if (firstNote == "G" && secondNote == undefined) {
                        $('#' + btnLabelSelected).find('.noteName').text("A");
                        $('#' + btnLabelSelected).find('.supNote').text("");
                        return false;
                    }
                    if (firstNote == "G" && secondNote == "sharp" || firstNote == "A" && secondNote == "flat") {
                        $('#' + btnLabelSelected).find('.noteName').text("B");
                        $('#' + btnLabelSelected).find('.supNote').text("b");
                        return false;

                    } else {
                        $('#' + btnLabelSelected).find('.noteName, .supNote').text("");
                        $('#' + btnLabelSelected).find('.noteFont').removeClass('hide');
                    }
                }

As you can see, this is really lengthy, and the app needs to have a bunch of these, so as you might imagine, that's a seriously long / redundant function.

I thought about perhaps maybe putting the correct answers into an array, and trying to grab the right answer from the array, but I'm sure if that would really save me any code length? (because I still would need a bunch of conditions, if the person selected keys C and B♭ and then the note A, etc etc.)

I'm hoping some of you JavaScript / jQuery wizards out there might have an alternative solution for me to shorten this up and make it more compact.

\$\endgroup\$
2
  • \$\begingroup\$ you could use a switch statement instead. w3schools.com/js/js_switch.asp That way your script stops when it finds a match and doesn't have to check the other conditions. \$\endgroup\$
    – kasper Taeymans
    Sep 30 '13 at 16:35
  • \$\begingroup\$ I want to thank everyone for the very creative answers. There really is some great stuff here, which is why this is an awesome community, so thanks for everyone's suggestions. :) \$\endgroup\$
    – Nick
    Oct 1 '13 at 19:17
3
\$\begingroup\$

First of all, is there a good reason you are separating the sharps/flats from their owning note? Why wouldn't the firstNote be "F#" for example.. Not only would that immediately flatten and simplify some of the conditionals, it would more accurately represent the actual real world data (namely.. the first note IS an "F#" not an "F" followed by a "#" note. That doesn't make real world sense).

How you could do this is create an object with keys (haha) being your "from" musical key and values being your "to" musical key. Then you simply look up notes from that object.

Since musical keys are offset mathematically, though, you could also have an array of notes with an object that keeps track of keys' offsets from other keys. You'd then index into this array with the appropriate offset in the conversion function. This is slightly more complicated that the object approach, but means you wouldn't have to build an object for each conversion, which is actually a huge win, since 12 choose 2 is 66. So you'd have to hard code 66 objects rather than just 66 offsets from each key to each other key.

Edit2: Here's a fiddle: http://jsfiddle.net/FX5CU/3/

Edit: added array implementation.

var notes = ['A', 'A#/Bb', 'B', 'C', 'C#/Db', 'D', 'D#/Eb', 'E', 'F', 'F#/Gb', 'G', 'G#/Ab']
  , positions = { 'A': 0
                , 'A#':1
                , 'Bb':1
                , 'B': 2
                , 'C': 3
                , 'C#':4
                , 'Db':4    //This encodes the positions in the array for each note.
                , 'D': 5
                , 'D#':6
                , 'Eb':6
                , 'E': 7
                , 'F': 8
                , 'F#':9
                , 'Gb':9
                , 'G':10
                , 'G#':11
                , 'Ab':11
                }
  , keyOffsets={'AA#':1
               ,'ABb':1
               ,'AB':2
               ,'AC':3
                /**etc**/     // Every pair of offsets here. You could get tricky and only
               ,'FC': -5      // store one direction if you wanted
               ,'FC#':-4 
                /**etc**/
               }
  ;
function transpose( key1, key2, note){
  return notes[positions[note] + keyOffsets[key1 + key2]]
}

Pretty neat, eh? The function finds the array position of your starting note in your positions object and gets the offset between the two keys by looking in your keyOffsets object. Adding these together yields the position in your notes array of the note in the new key.

\$\endgroup\$
11
  • \$\begingroup\$ Well, yes, I suppose it would have helped for you to see how people are selecting the notes, so envision if you a will a telephone keypad, it's very similar. The redundancy is so that if a user presses the sharp or flat key and doesn't press an actual note first, the program can deal with this scenario adequately. Am I thinking about this improperly? Possibly, as it's easy for me to get "lost in the logic" of the thing. \$\endgroup\$
    – Nick
    Sep 30 '13 at 20:45
  • \$\begingroup\$ Hmm.. UX-wise I wonder if the keypad would be better implemented as a one button for each possible note (shared buttons for A#/Bb etc). Seems like it'd lower amount of clicks by a decent amount for just two additional buttons (8 notes + # and b for 10 vs. 12 notes). This would help simplify the logic as well. \$\endgroup\$
    – Wyatt
    Sep 30 '13 at 21:22
  • \$\begingroup\$ Because..err..umm.. yeah, um. I guess I didn't consider making a few extra buttons to integrate #'s /b's. At first the extra button press didn't seen like much of a big deal, however, that's actually a pretty good idea.. duh, I should have thought of that. Your offset'd array idea is very cool. I'm going to have to study it for a while to really grasp what your doing, at first glance I'm not entirely sure how to get it up and running, but I'm sure with a bit of study of study and possibly another question or two, this one really might be the way to go. Thanks for this. \$\endgroup\$
    – Nick
    Sep 30 '13 at 23:47
  • \$\begingroup\$ Wyatt, I'm pretty sure this is definitely going to be the accepted answer, would you be opposed to me asking you a question or two via email to implement this? Thanks! \$\endgroup\$
    – Nick
    Oct 1 '13 at 19:16
  • \$\begingroup\$ Not at all. Talk to you soon. \$\endgroup\$
    – Wyatt
    Oct 1 '13 at 19:49
6
\$\begingroup\$

yeah I'd use a data structure that encodes the "business logic" and then a simple lookup to update the UI:

var notes = {
    CBb: {
        A: { noteName: "B", supNote: "" },
        Asharp: { noteName: "C", supNote: "" },
        Bflat: { noteName: "C", supNote: "" },
        ...
    },
    CF: {
        A: { noteName: "B", supNote: "" },
        Asharp: { noteName: "C", supNote: "" },
        Bflat: { noteName: "C", supNote: "" },
        ...
    }
};

var findNote = function (firstInstSelected, secondInstSelected, firstNote, secondNote) {
    var outer = notes[firstInstSelected + (secondInstSelected || "")];
    return outer && outer[firstNote + (secondNote || "")];
};


//...your event handler...
var note = findNote(firstInstSelected, secondInstSelected, firstNote, secondNote);
if (note) {
    $("#" + btnLabelSelected).find(".noteName").text(note.noteName);
    $("#" + btnLabelSelected).find(".supNote").text(note.supNote);
    return false;
}

$('#' + btnLabelSelected).find('.noteName, .supNote').text("");
$('#' + btnLabelSelected).find('.noteFont').removeClass('hide');

This keeps your logic nice and tidy and easy to understand.

\$\endgroup\$
3
  • \$\begingroup\$ Love this solution. Almost easy to wrap my brain around, I think this will be the first one I attempt to impliment. Only thing I'm a bit confused on is this looks awesome for one set only, but if I need multiple versions of this, (CBb:, CF:, CEb, etc etc) I would just use a different var name (not notes) for each grouping, right? Or would I combine all the possible groupings into the one notes var (At the top of the answer) and then somehow in the event handler select the right array? \$\endgroup\$
    – Nick
    Sep 30 '13 at 21:03
  • \$\begingroup\$ Based only on your example, all would be done within the same notes variable. Notice it is a nested structure. The outer-level would be the groupings CBb, CF, CEb, ... and then the inner-level are the values for that grouping. I added a CF to my example \$\endgroup\$
    – Brandon
    Sep 30 '13 at 21:46
  • \$\begingroup\$ This solution would require an object for each combination of keys, or 12 choose 2 = 66 objects (or more if you construct objects separately for identical sharps and flats). Instead, it would be easier to recognize that keys are related mathematically and just store the offsets between keys in an object, then index into an array with these offsets. See my answer below with an implementation of such a function. \$\endgroup\$
    – Wyatt
    Sep 30 '13 at 21:57
1
\$\begingroup\$

Convert the keys (start and end) and the note into numbers that represent semitones (a value from 0 to 11) do the calculation and then go back from semitone number to standard notation.

A ---> 0

A# ---> 1

B ---> 2

C ---> 3

...

G# ---> 11

Do this with a switch statement

Once you have startNote, startKey, endKey

You get endNote this way:

endNote = (endKey - startKey) + startNote;
endNote = (endNote<0?endNote+12:endNote%12);

Hope it's clear enought

\$\endgroup\$
0
\$\begingroup\$

This is just a nebulous thought, but you could make each key an array ie Cmaj=(c,d,e,...), Dmaj=(d,e,f#,...) then take the position in the original key's array of the note to be transposed and find the note at that position in the target key's array. So for example if the keys were Cmaj and Dmaj as before and the note to be transposed was 'e' you find its position in the first array (2) and check the target array at position 2 and you get 'f#'.

\$\endgroup\$
1
  • \$\begingroup\$ That's a very interesting idea. It's definitely worth further exploration, although it might be a bit above my jquery / javascript abilities. \$\endgroup\$
    – Nick
    Sep 30 '13 at 20:46
0
\$\begingroup\$

Interesting question. Being notes within a musical scale, presumably theres some way of automatically generating the array. I'm too tired now to even consider such a proposition. However, here's roughly how I'd go throwing into an array: (each of the 3 very basic tests pass)

The output is:

$('#' + btnLabelSelected).find('.noteName').text('E');
$('#' + btnLabelSelected).find('.supNote').text('b');

$('#' + btnLabelSelected).find('.noteName').text('E');
$('#' + btnLabelSelected).find('.supNote').text('_flat_');

$('#' + btnLabelSelected).find('.noteName, .supNote').text('');
$('#' + btnLabelSelected).find('.noteFont').removeClass('hide'); 

Code:

<!DOCTYPE html>
<html>
<head>
<script>
function mInit()
{
    // expecting to see this data ---> noteName: "E", supNote: "b"},
    emitCode("C", "Bb", "D", "flat");

    // expecting to see this data ---> noteName: "E", supNote: "_flat_"},
    emitCode("C#", "Bb", "D", "flat");

    // expecting to see calls to  set supNote text to '' and to remove the hide class from noteFont
    emitCode("C", "Bb", "D", "garbage");
}


window.addEventListener('load', mInit, false);

var dataArray =
[
           {
               firstInst: "C", 
               secondInst: "Bb", 
               conditions: 
               [
                    { firstNote: "A", secondNote: undefined, noteName: "B", supNote: ""},
                    { firstNote: "A", secondNote: "sharp", noteName: "C", supNote: ""},
                    { firstNote: "B", secondNote: "flat", noteName: "C", supNote: ""},
                    { firstNote: "B", secondNote: undefined, noteName: "C", supNote: "#"},
                    { firstNote: "C", secondNote: undefined, noteName: "D", supNote: ""},
                    { firstNote: "C", secondNote: "sharp", noteName: "E", supNote: "b"},
                    { firstNote: "D", secondNote: "flat", noteName: "E", supNote: "b"},
                    { firstNote: "D", secondNote: undefined, noteName: "E", supNote: ""},
                    { firstNote: "D", secondNote: "sharp", noteName: "F", supNote: ""},
                    { firstNote: "E", secondNote: "flat", noteName: "F", supNote: ""},
                    { firstNote: "E", secondNote: undefined, noteName: "F", supNote: "#"},
                    { firstNote: "F", secondNote: undefined, noteName: "G", supNote: ""},
                    { firstNote: "F", secondNote: "sharp", noteName: "A", supNote: "b"},
                    { firstNote: "G", secondNote: "flat", noteName: "A", supNote: "b"},
                    { firstNote: "G", secondNote: undefined, noteName: "A", supNote: ""},
                    { firstNote: "G", secondNote: "sharp", noteName: "B", supNote: "b"},
                    { firstNote: "A", secondNote: "flat", noteName: "B", supNote: "b"}
                ]
             },
           {
               firstInst: "C#", 
               secondInst: "Bb", 
               conditions: 
               [
                    { firstNote: "A", secondNote: undefined, noteName: "B", supNote: ""},
                    { firstNote: "A", secondNote: "sharp", noteName: "C", supNote: ""},
                    { firstNote: "B", secondNote: "flat", noteName: "C", supNote: ""},
                    { firstNote: "B", secondNote: undefined, noteName: "C", supNote: "#"},
                    { firstNote: "C", secondNote: undefined, noteName: "D", supNote: ""},
                    { firstNote: "C", secondNote: "sharp", noteName: "E", supNote: "b"},
                    { firstNote: "D", secondNote: "flat", noteName: "E", supNote: "_flat_"},        // **
                    { firstNote: "D", secondNote: undefined, noteName: "E", supNote: ""},
                    { firstNote: "D", secondNote: "sharp", noteName: "F", supNote: ""},
                    { firstNote: "E", secondNote: "flat", noteName: "F", supNote: ""},
                    { firstNote: "E", secondNote: undefined, noteName: "F", supNote: "#"},
                    { firstNote: "F", secondNote: undefined, noteName: "G", supNote: ""},
                    { firstNote: "F", secondNote: "sharp", noteName: "A", supNote: "b"},
                    { firstNote: "G", secondNote: "flat", noteName: "A", supNote: "b"},
                    { firstNote: "G", secondNote: undefined, noteName: "A", supNote: ""},
                    { firstNote: "G", secondNote: "sharp", noteName: "B", supNote: "b"},
                    { firstNote: "A", secondNote: "flat", noteName: "B", supNote: "b"}
                ]
             },
];

function emitCode(firstInst, secondInst, firstNote, secondNote)
{
    var i, n;

    n = dataArray.length;
    for (i=0; i<n; i++)
    {
        if ((dataArray[i].firstInst == firstInst) && (dataArray[i].secondInst == secondInst))
        {
            var j, m = dataArray[i].conditions.length;
            for (j=0; j<m; j++)
            {
                if (firstNote == dataArray[i].conditions[j].firstNote && secondNote == dataArray[i].conditions[j].secondNote)
                {
                    console.log("$('#' + btnLabelSelected).find('.noteName').text('" + dataArray[i].conditions[j].noteName + "');");
                    console.log("$('#' + btnLabelSelected).find('.supNote').text('" + dataArray[i].conditions[j].supNote + "');");
                    return;
                }
            }
             console.log("$('#' + btnLabelSelected).find('.noteName, .supNote').text('');");
             console.log("$('#' + btnLabelSelected).find('.noteFont').removeClass('hide');");
             return;
        }
    }
}

</script>
<style>
</style>
</head>
<body>
</body>
</html>
\$\endgroup\$
1
  • \$\begingroup\$ Your solution "looks" very elegant, and it will take me some time to muddle through the understanding of it. It appears fairly complex in nature. If this is you coding when you're "wiped out" I'm scared to see what you'd come up with when you're firing on all cylinders. :) \$\endgroup\$
    – Nick
    Sep 30 '13 at 20:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.