How do I transpose music chords using JavaScript?

2019-03-25 17:17发布

I was wondering how would one create a javascript function for transposing music chords.

Since I don't expect everyone to be a musician here, I'll try to explain how it works in music theory. I hope I don't forget something. If yes, musicians, please, correct me.

1) The simple chords

The simple chords are almost as simple as an alphabet and it goes like this:

C, C#, D, D#, E, F, F#, G, G#, A, A# B

From B it loops all over again to C. Therefore, If the original chord is E and we want to transpose +1, the resulting chord is F. If we transpose +4, the resulting chord is G#.

2) Expanded chords

They work almost like the simple chords, but contain a few more characters, which can safely be ignored when transposing. For example:

Cmi, C#7, Dsus7, Emi, Fsus4, F#mi, G ...

So again, as with the simple chords, if we transpose Dsus7 + 3 = Fsus7

3) Non-root bass tone

A problem arises when the bass plays a different tone than the chord root tone. This is marked by a slash after the chord and also needs to be transposed. Examples:

C/G, Dmi/A, F#sus7/A#

As with examples 1 and 2, everything is the same, but the part after the slash needs transpose too, therefore:

C/G + 5 = F/C

F#sus7/A# + 1 = Gsus7/B

I think this should be all, unless I forgot something.

So basically, imagine you have a javascript variable called chord and the transpose value transpose. What code would transpose the chord?

Example:

var chord = 'F#sus7/C#';
var transpose = 3; // remember this value also may be negative, like "-4"
... code here ...
var result; // expected result = 'Asus7/E';

7条回答
女痞
2楼-- · 2019-03-25 17:38

Define your keys with an object:

var keys = ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"];

Parse your chord with a regular expression:

var matches = /([A-G]#?)([^\/]*)(?:\/([A-G]#?))?/.exec(chord);
var key = matches[1];
var descriptor = matches[2];
var bass = matches[3];

Do a little math to get the new key:

var newKey = keys[(keys.indexOf(key) + transpose) % keys.length];
var newBass = keys[(keys.indexOf(bass) + transpose) % keys.length];

Put it all back together again:

var newChord = newKey + descriptor;
if (newBass) {
    newChord += "/" + newBass;
}
return newChord;
查看更多
beautiful°
3楼-- · 2019-03-25 17:49
function transpose(chord, increment)
{
    var cycle = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
    var el = chord.charAt(0);
    if(chord.length > 1 && chord.charAt(1) == '#')
    {
        el += "#";   
    }
    var ind = cycle.indexOf(el);
    var newInd = (ind + increment + cycle.length) % cycle.length;
    var newChord = cycle[newInd];
    return newChord + chord.substring(el.length);
}

I'll let you figure out the bass part, since it's really just calling the function twice.

Also, you can add the code here before the function for old browsers that don't support indexOf.

I put a demo on jsFiddle.

EDIT: The issue was with negative modulus. The above will work as long as long as the negative isn't more than the length (e.g. you can't transpose 100 steps down).

查看更多
Root(大扎)
4楼-- · 2019-03-25 17:51
function transposechord(chord, amount){
   var scale = ["C","Cb","C#","D","Db","D#","E","Eb","E#","F","Fb","F#","G","Gb","G#",
         "A","Ab","A#","B","Bb","B#"];
   var transp = ["Cb","C","C#","Bb","Cb","C","C","C#","D","Db","D","D#","C","Db","D",
                 "D","D#","E","Eb","E","F","D","Eb","E", "E","E#","F#", "E","F","F#",
                 "Eb","Fb","F","F","F#","G","Gb","G","G#","F","Gb","G", "G","G#","A", 
                 "Ab","A","A#","G","Ab","A","A","A#","B","Bb","B","C","A","Bb","B", 
                 "B","B#","C#"];
   var subst = chord.match(/[^b#][#b]?/g);
   for(var ax in subst){
      if(scale.indexOf(subst[ax])!==-1){
         if(amount>0){
            for(ix=0;ix<amount;ix++){
                var pos = scale.indexOf(subst[ax]);
                var transpos = 3*pos-2+3;
                subst[ax] = transp[transpos+1];
            }
         }
         if(amount<0){
            for(ix=0;ix>amount;ix--){
                var pos = scale.indexOf(subst[ax]);
                var transpos = 3*pos-2+3;
                subst[ax] = transp[transpos-1];
                }
            }
       } 
   }
   chord=subst.join("");
}

chord = C/B, amount = 1: C#/C or chord = Gm7, amount 2: Am7

查看更多
叛逆
5楼-- · 2019-03-25 17:54

Ok, so I've thought about this a fair bit now, and I have a functional answer. It's in standard Western Scale tones (sorry Northern Europeans).

To truly transpose a chord you need 3 pieces of information: 1) the CHORD name, 2) the OLDKEY, and 3) the NEWKEY. It's not always enough to modulate by an AMOUNT (is UP-2 from the key of E an F# or a Gb?).

Basically you need to preserve two distances – the distance in pitch between the CHORD and the OLDKEY, and the distance in 'letters' between the CHORD and the OLDKEY – when mapping the CHORD to the NEWKEY.

function transposeChord(chord, oldKey, newKey) {}

To simplify(?) this, I've predefined every scale and every possible note (and some impossible ones) in relation to that scale root.

    var scales = {
    // scale_form = [1-7, #1-7, b1-7, *1-7, bb1-7]
    "CScale": ["C", "D", "E", "F", "G", "A", "B", "C#", "D#", "E#", "F#", "G#", "A#", "B#", "Cb", "Db", "Eb", "Fb", "Gb", "Ab", "Bb", "C*", "D*", "E*", "F*", "G*", "A*", "B*", "Cbb", "Dbb", "Ebb", "Fbb", "Gbb", "Abb", "Bbb"],
    "GScale": ["G", "A", "B", "C", "D", "E", "F#", "G#", "A#", "B#", "C#", "D#", "E#", "F*", "Gb", "Ab", "Bb", "Cb", "Db", "Eb", "F", "G*", "A*", "B*", "C*", "D*", "E*", "F#*", "Gbb", "Abb", "Bbb", "Cbb", "Dbb", "Ebb", "Fb"],
    "DScale": ["D", "E", "F#", "G", "A", "B", "C#", "D#", "E#", "F*", "G#", "A#", "B#", "C*", "Db", "Eb", "F", "Gb", "Ab", "Bb", "C", "D*", "E*", "F#*", "G*", "A*", "B*", "C#*", "Dbb", "Ebb", "Fb", "Gbb", "Abb", "Bbb", "Cb"],
    "AScale": ["A", "B", "C#", "D", "E", "F#", "G#", "A#", "B#", "C*", "D#", "E#", "F*", "G*", "Ab", "Bb", "C", "Db", "Eb", "F", "G", "A*", "B*", "C#*", "D*", "E*", "F#*", "G#*", "Abb", "Bbb", "Cb", "Dbb", "Ebb", "Fb", "Gb"],
    "EScale": ["E", "F#", "G#", "A", "B", "C#", "D#", "E#", "F*", "G*", "A#", "B#", "C*", "D*", "Eb", "F", "G", "Ab", "Bb", "C", "D", "E*", "F#*", "G#*", "A*", "B*", "C#*", "D#*", "Ebb", "Fb", "Gb", "Abb", "Bbb", "Cb", "Db"],
    "BScale": ["B", "C#", "D#", "E", "F#", "G#", "A#", "B#", "C*", "D*", "E#", "F*", "G*", "A*", "Bb", "C", "D", "Eb", "F", "G", "A", "B*", "C#*", "D#*", "E*", "F#*", "G#*", "A#*", "Bbb", "Cb", "Db", "Ebb", "Fb", "Gb", "Ab"],
    "F#Scale": ["F#", "G#", "A#", "B", "C#", "D#", "E#", "F*", "G*", "A*", "B#", "C*", "D*", "E*", "F", "G", "A", "Bb", "C", "D", "E", "F#*", "G#*", "A#*", "B*", "C#*", "D#*", "E#*", "Fb", "Gb", "Ab", "Bbb", "Cb", "Db", "Eb"],
    "C#Scale": ["C#", "D#", "E#", "F#", "G#", "A#", "B#", "C*", "D*", "E*", "F*", "G*", "A*", "B*", "C", "D", "E", "F", "G", "A", "B", "C#*", "D#*", "E#*", "F#*", "G#*", "A#*", "B#*", "Cb", "Db", "Eb", "Fb", "Gb", "Ab", "Bb"],
    "G#Scale": ["G#", "A#", "B#", "C#", "D#", "E#", "F*", "G*", "A*", "B*", "C*", "D*", "E*", "F#*", "G", "A", "B", "C", "D", "E", "F#", "G#*", "A#*", "B#*", "C#*", "D#*", "E#*", "F**", "Gb", "Ab", "Bb", "Cb", "Db", "Eb", "F"],
    "D#Scale": ["D#", "E#", "F*", "G#", "A#", "B#", "C*", "D*", "E*", "F#*", "G*", "A*", "B*", "C#*", "D", "E", "F#", "G", "A", "B", "C#", "D#*", "E#*", "F**", "G#*", "A#*", "B#*", "C**", "Db", "Eb", "F", "Gb", "Ab", "Bb", "C"],
    "A#Scale": ["A#", "B#", "C*", "D#", "E#", "F*", "G*", "A*", "B*", "C#*", "D*", "E*", "F#*", "G#*", "A", "B", "C#", "D", "E", "F#", "G#", "A#*", "B#*", "C**", "D#*", "E#*", "F**", "G**", "Ab", "Bb", "C", "D#", "Eb", "F", "G"],
    // E#Scale:
    // B#Scale: 
    "FScale": ["F", "G", "A", "Bb", "C", "D", "E", "F#", "G#", "A#", "B", "C#", "D#", "E#", "Fb", "Gb", "Ab", "Bbb", "Cb", "Db", "Eb", "F*", "G*", "A*", "B#", "C*", "D*", "E*", "Fbb", "Gbb", "Abb", "Bbbb", "Cbb", ,"Dbb", ,"Ebb"],
    "BbScale": ["Bb", "C", "D", "Eb", "F", "G", "A", "B", "C#", "D#", "E", "F#", "G#", "A#", "Bbb", "Cb", "Db", "Ebb", "Fb", "Gb", "Ab", "B#", "C*", "D*", "E#", "F*", "G*", "A*", "Bbbb", "Cbb", "Dbb", "Ebbb", "Fbb", "Gbb", "Abb"],
    "EbScale": ["Eb", "F", "G", "Ab", "Bb", "C", "D", "E", "F#", "G#", "A", "B", "C#", "D#", "Ebb", "Fb", "Gb", "Abb", "Bbb", "Cb", "Db", "E#", "F*", "G*", "A#", "B#", "C*", "D*", "Ebbb", "Fbb", "Gbb", "Abbb", "Bbbb", "Cbb", "Dbb"],
    "AbScale": ["Ab", "Bb", "C", "Db", "Eb", "F", "G", "A", "B", "C#", "D", "E", "F#", "G#", "Abb", "Bbb", "Cb", "Dbb", "Ebb", "Fb", "Gb", "A#", "B#", "C*", "D#", "E#", "F*", "G*", "Abbb", "Bbbb", "Cbb", "Dbbb", "Ebbb", "Fbb", "Gbb"],
    "DbScale": ["Db", "Eb", "F", "Gb", "Ab", "Bb", "C", "D", "E", "F#", "G", "A", "B", "C#", "Dbb", "Ebb", "Fb", "Gbb", "Abb", "Bbb", "Cb", "D#", "E#", "F*", "G#", "A#", "B#", "C*", "Dbbb", "Ebbb", "Fbb", "Gbbb", "Abbb", "Bbbb", "Cbb"],
    "GbScale": ["Gb", "Ab", "Bb", "Cb", "Db", "Eb", "F", "G", "A", "B", "C", "D", "E", "F#", "Gbb", "Abb", "Bbb", "Cbb", "Dbb", "Ebb", "Fb", "G#", "A#", "B#", "C#", "D#", "E#", "F*", "Gbbb", "Abbb", "Bbbb", "Cbbb", "Dbbb", "Ebbb", "Fbb"]
    // CbScale:
    // FbScale:
    // BbbFlatScale:
    //  ...
    }       

Then you assign your scales based on the OLDKEY and NEWKEY:

var oldKeyScale = scales[key + "Scale"]
var newKeyScale = scales[newKey + "Scale"]

Finally, some regex to find and replace all those chord-roots/flats/sharps/doubleflats/etc with their corresponding position in the NEWKEY scale.

var transposedChord
transposedChord = chord.replace(/(([CDEFGAB]#\*)|([CDEFGAB]#)|([CDEFGAB]b+)|([CDEFGAB]\**))/g, function(match) {
    var i = oldKeyScale.indexOf(match)
    return newKeyScale[i]
})
return transposedChord

There's definitely a better, more 'think-like-a-computer' way to do this, but this will take

transposeChord("Am7/G", "C", "A#")

and return

"F*m7/E#"
查看更多
地球回转人心会变
6楼-- · 2019-03-25 17:55

How about a little somethin' like this:

function transposeChord(chord, amount) {
  var scale = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
  return chord.replace(/[CDEFGAB]#?/g,
                       function(match) {
                         var i = (scale.indexOf(match) + amount) % scale.length;
                         return scale[ i < 0 ? i + scale.length : i ];
                       });
}

alert(transposeChord("Dm7/G", 2)); // gives "Em7/A"
alert(transposeChord("Fmaj9#11", -23)); // gives "F#maj9#11"

Note that I threw in the "F#maj9#11" example just to give you more to think about with regard to what makes up a valid chord name: you may find a "#" sharp symbol that doesn't follow a letter (in this case it belongs to the "11").

And, obviously, my function only understands sharps, not flats, and doesn't understand keys, so, e.g., transposeChord("C/E", 1) will give "C#/F" when it really should be "C#/E#".

查看更多
不美不萌又怎样
7楼-- · 2019-03-25 17:55

All of these solutions are lacking the fact that after transposing a note, it needs to be converted to a sharp or flat depending on the key or chord.

So the API must be:

transpose(note, semitones, useSharps)

Here is my implementation. It also handles multiple # and b modifiers.

function transposeNote(note, semitones, useSharps) {
  // parse root followed by modifiers (# and b)
  const rx = /^([a-gA-G])([#b]*)$/;
  const m = rx.exec(note);
  if (!m) {
    return null;
  }
  // convert note from 0 to 11 based off of A
  let root;
  switch (m[1].toUpperCase()) {
    case "A":
      root = 0;
      break;
    case "B":
      root = 2;
      break;
    case "C":
      root = 3;
      break;
    case "D":
      root = 5;
      break;
    case "E":
      root = 7;
      break;
    case "F":
      root = 8;
      break;
    case "G":
      root = 10;
      break;
  }
  // modify root
  let mods = m[2];
  if (mods) {
    for (var i = 0; i < mods.length; i++) {
      if (mods.charAt(i) === "#") {
        root++;
      } else {
        root--;
      }
    }
  }
  // transpose note
  root = (root + semitones) % 12;

  if (root < 0) {
    root += 12
  }

  // convert back to a note
  const sharps = [
    "A",
    "A#",
    "B",
    "C",
    "C#",
    "D",
    "D#",
    "E",
    "F",
    "F#",
    "G",
    "G#"
  ];
  const flats = [
    "A",
    "Bb",
    "B",
    "C",
    "Db",
    "D",
    "Eb",
    "E",
    "F",
    "Gb",
    "G",
    "Ab"
  ];
  const transposedNote = useSharps ? sharps[root] : flats[root];
  return transposedNote;
}

function transposeChord(chord, semitones, useSharps) {
  const rx = /^([a-gA-G][#b]*)\s*([^\/]*)(\/[a-gA-G][#b]*)?$/;
  const m = rx.exec(chord);
  if (!m) {
    return null;
  }
  const root = transposeNote(m[1], semitones, useSharps);
  const quality = m[2] || "";
  let bass = m[3] || "";
  if (bass.length > 0) {
    bass = "/" + transposeNote(bass.substring(1), semitones, useSharps);
  }
  return root + quality + bass;
}

Use it like this

console.log(transposeChord("Cmin7/Eb", 3, false));
查看更多
登录 后发表回答