saebekassebil / teoria

Javascript taught Music Theory
http://saebekassebil.github.io/teoria
MIT License
1.31k stars 114 forks source link

Transposing chords by a number of semitones #83

Closed namuol closed 8 years ago

namuol commented 9 years ago

I'm creating a simple ukulele composition tool and need a way to transpose chords by a numerical offset in semitones (this is common in lots of tablature sites/apps).

In order to transpose a Chord, you must pass an Interval object to the .transpose or .interval method, but I don't know how to create such an Interval that correctly represents the operation I'm trying to achieve.

I'm not very familiar with music theory, but I determined that I can achieve this with a lookup table of interval names that achieve what I want:

var INTERVAL_NAMES = [
  'P1',
  'm2',
  'M2',
  'm3',
  'M3',
  'P4',
  'A4',
  'P5',
  'm6',
  'M6',
  'm7',
  'M7',
  'P8',
];

// Handles modulo correctly for negative numbers:
function mod(n, m) {
  return ((n % m) + m) % m;
}

function intervalFromSemitoneOffset (n) {
  return teoria.interval(INTERVAL_NAMES[mod(n, 12)]);
}

This seems to work, but feels sloppy. What would you suggest? Personally, I'd love it if .transpose/.interval could simply take an integer value, but I'm probably misunderstanding something about how intervals work.

saebekassebil commented 9 years ago

Hi Louis, thanks for writing!

Well, you don't "misunderstand" how intervals work - it's just that they're ambiguous in music theory. You could argue that you can't hear any difference, but there are several intervals that represent the same amount of semitones (in western equal temperament). E.g. A "major" third and a "diminished fourth" "contains" the same amount of semitones, but are theoretically different.

Anyways. I'd probably do it in a different way, since it seems that you don't care whether you get a "C#" or a "Db". What about transposing the root by key number?

var root = teoria.note('E');
var chordE7 = teoria.chord(root, '7');
var chordF7 = teoria.note.fromKey(root.key() + 1).chord(chordE.symbol)
var chordG7 = teoria.note.fromKey(chordF.root.key() + 2).chord(chordF.symbol)

// general function
function transposeChord(chord, semitones) {
  return teoria.note.fromKey(chord.root.key() + semitones).chord(chord.symbol)
}

Here I'm just reusing the Chord.symbol property, and creating a new root by key number. It might not be ideal, and I'm open to suggestions on how to do it better.

namuol commented 9 years ago

Much better! However, I'm having issues with slashed chords. Should the expected behavior be such that the bass note remains the same when a slashed chord is transposed?

For instance:

var transposed_DF = transposeChord(teoria.chord('D/F'), 1);

console.log(transposed_DF.name); // "Eb/F"
console.log(transposed_DF.symbol); // "/F"

Shouldn't the transposed chord actually be Eb/F#?

Note: using .interval has the same problem, because the .symbol property is not considered in transposition methods. The slashed chord problem might be worth opening a separate issue for...

saebekassebil commented 9 years ago

Yes it should - an oversight :) Let's fix that soon.

namuol commented 9 years ago

(Whoops, didn't mean to close the issue there)

I'd be happy to write some tests for these edge-cases if that helps!

saebekassebil commented 9 years ago

Yes please do! It might need some rethinking on how we handle "slash chords".

My initial thought is if the voicing doesn't have the root in voiced lowest, then display as a "/ chord", and don't let the slash be part of the .symbol?

saebekassebil commented 8 years ago

Closing this instead of #92