As a follow-up on isosceles' question, I have created a small API to generate the notes of a scale given the pitch class set and key note. I've added the reinventhing-the-wheel tag because I know there exist far superior frameworks that deal with notes, chords, scales, musical notations.
Description
A pitch class set describes a collection of pitch classes in steps relative to the reference note, which has pitch set class 0 and corresponds to the specified key note. For instance, the major scale is identified by pitch class set
[0, 2, 4, 5, 7, 9, 11]
.
Usage
The main purpose of the API is to generate scale notes as follows:
console.log(getScale('C', [0, 2, 4, 5, 7, 9, 11]).map(i => i.NameClass));
printing the following to the console:
["C" ,"D" ,"E" ,"F" ,"G" ,"A" ,"B"]
Notes can be manipulated:
let scale = {};
scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.NameClass);
scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.enharmonic().NameClass);
scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.ascendingEnharmonic().NameClass);
scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.descendingEnharmonic().NameClass);
scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.transpose(-2).NameClass);
scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.transpose(new Interval(1, 0)).NameClass);
scale = getScale('D##', [0, 2, 4, 5, 7, 9, 11]).map(i => i.invert().NameClass);
Questions
- Have I written idiomatic javascript or what can I improve?
- Is my API useful and reusable for more complex operations involving notes and scales?
- Are my methods self-describing or are comments required?
Code
(function() {
'use strict';
function getScale(key, pitchClassSet) {
const keyNote = Note.FromName(key);
return pitchClassSet.reduce(function (scale, scaleStep, i) {
const note = keyNote.clone().transpose(new Interval(i, scaleStep));
scale.push(note);
return scale;
}, []);
}
class Interval {
constructor(di, pi) {
this.di = di;
this.pi = pi;
}
get DegreeInterval() {
return this.di;
}
get PitchInterval() {
return this.pi;
}
}
class Note {
static FromName(name) {
const note = new Note(0, 0);
note.Name = name;
return note;
}
static FromDiatonicPitchClass(degree) {
const note = new Note(degree, 0);
note.PitchClass = Note.DiatonicPitchClassSet[note.DegreeClass];
return note;
}
static get DiatonicPitchClassSet() {
return (new function() {
const pcs = [0,2,4,5,7,9,11];
return function () {
return pcs;
}
})();
}
static get ScientificPitchClassSet() {
return (new function() {
const pcs = ['C','D','E','F','G','A','B'];
return function () {
return pcs;
}
})();
}
constructor(degree, pitch) {
this.d = Coil.FromDegree(degree);
this.p = Coil.FromPitch(pitch);
}
get DegreeClass() {
return this.d.Class;
}
set DegreeClass(n) {
this.d.Class = n;
}
get PitchClass() {
return this.p.Class;
}
set PitchClass(n) {
this.p.Class = n;
}
get Pitch() {
return this.p.Value;
}
set Pitch(n) {
this.p.Value = n;
this.d.Group = this.p.Group;
}
get Octave() {
return this.p.Group;
}
set Octave(n) {
this.p.Group = n;
this.d.Group = n;
}
get Accidentals() {
const diatonic = Note.FromDiatonicPitchClass(this.DegreeClass);
const coil = Coil.FromPitch(this.PitchClass - diatonic.PitchClass);
return coil.Delta;
}
set Accidentals(n) {
const diatonic = Note.FromDiatonicPitchClass(this.DegreeClass);
const coil = Coil.FromPitch(diatonic.PitchClass + n);
this.PitchClass = coil.Class;
}
get Name() {
const octaveName = 5 + this.Octave;
return this.NameClass + octaveName;
}
get NameClass() {
const degreeName = Note.ScientificPitchClassSet[this.DegreeClass];
const accidentals = this.Accidentals;
const accidentalsToken = accidentals === 0 ? ''
: (accidentals < 0 ? 'b' : '#' ).repeat(Math.abs(this.Accidentals));
return degreeName + accidentalsToken;
}
set Name(name) {
const regexp = /(?<degree>[a-gA-G])(?<accidentals>[b#]*)(?<octave>[-]?\d*)/
const result = regexp.exec(name);
const degreeToken = result.groups.degree.toUpperCase();
const accidentalsToken = result.groups.accidentals;
const octaveToken = result.groups.octave;
const degree = Note.ScientificPitchClassSet.indexOf(degreeToken);
const octave = octaveToken.length > 0 ? parseInt(octaveToken) : 5;
const accidentals = accidentalsToken.split('')
.map(c => c == 'b' ? -1 : c == '#' ? 1 : 0)
.reduce((a, b) => a + b, 0);
this.DegreeClass = degree;
this.Octave = octave - 5;
this.Accidentals = accidentals;
}
copy(other) {
this.d = other.d.clone();
this.p = other.p.clone();
return this;
}
clone() {
return new Note(0, 0).copy(this);
}
equals(other) {
if (typeof other === "undefined") { return false; }
return other.d.Equals(this.d)
&& other.p.Equals(this.p);
}
isEnharmonicEquivalent(other) {
return other.Pitch === this.Pitch;
}
isInversionalEquivalent(other) {
return other.p.clone().invert().Class === this.p.Class;
}
isOctaveEquivalent(other) {
return other.DegreeClass === this.DegreeClass
&& other.PitchClass === this.PitchClass;
}
normalize() {
this.Octave = 0;
return this;
}
enharmonic(preference, force) {
if (typeof force === "undefined") {
force = false;
}
if (typeof preference === "undefined") {
preference = 0;
}
let degree = Note.DiatonicPitchClassSet.indexOf(this.PitchClass);
if (degree === -1) {
degree = Note.DiatonicPitchClassSet.indexOf(this.PitchClass - 1);
const degreeAlt = Coil.FromDegree(degree + 1).Class;
if (force || (this.DegreeClass !== degree && this.DegreeClass !== degreeAlt)) {
this.DegreeClass = (preference < 0) ? degreeAlt : degree;
}
}
else {
this.DegreeClass = degree;
}
return this;
}
ascendingEnharmonic() {
return this.enharmonic(1, true);
}
descendingEnharmonic() {
return this.enharmonic(-1, true);
}
transpose(interval) {
let di = 0;
let pi = 0;
if (interval instanceof Note) {
di = interval.d.Value;
pi = interval.p.Value;
}
else if (interval instanceof Interval) {
di = interval.DegreeInterval;
pi = interval.PitchInterval;
}
else {
pi = interval;
}
this.d.translate(di);
this.p.translate(pi);
return this;
}
invert() {
this.d.invert();
this.p.invert();
return this;
}
reflect(pivot, pivot2) {
if (typeof pivot2 === "undefined") {
pivot2 = pivot;
}
this.d.reflect(pivot.d.Value, pivot2.d.Value);
this.p.reflect(pivot.p.Value, pivot2.p.Value);
return this;
}
negate() {
this.d.negate();
this.p.negate();
return this;
}
setDegreeClass(n) {
this.DegreeClass = n;
return this;
}
setPitchClass(n) {
this.PitchClass = n;
return this;
}
setPitch(n) {
this.Pitch = n;
return this;
}
setOctave(n) {
this.Octave = n;
return this;
}
setAccidentals(n) {
this.Accidentals = n;
return this;
}
setName(n) {
this.Name = n;
return this;
}
}
class Coil {
static FromDegree(n) {
return new Coil(n, 7);
}
static FromPitch(n) {
return new Coil(n, 12);
}
constructor(value, size) {
this.Value = value;
this.Size = size;
}
get Value() {
return this.value;
}
set Value(n) {
this.value = n;
}
get Size() {
return this.size;
}
set Size(n) {
this.size = Math.max(1, Math.abs(n));
}
get Class() {
return this.modulo(this.Value);
}
set Class(n) {
this.Value = this.Group * this.Size + this.modulo(n);
}
get Group() {
return Math.floor((this.Value) / this.Size);
}
set Group(n) {
this.Value = n * this.Size + this.Class;
}
get Delta() {
const d1 = this.Class;
const d2 = this.clone().invert().Class;
if (d1 > d2) {
return d2 === 0 ? d2 : -d2;
}
return d1;
}
get Distance() {
return Math.abs(this.Delta);
}
copy(other) {
this.Value = other.Value;
this.Size = other.Size;
return this;
}
clone() {
return new Coil(0, 0).copy(this);
}
equals(other) {
if (typeof other === "undefined") { return false; }
return other.Value === this.Value
&& other.Size == this.Size;
}
normalize() {
this.Value = this.Class;
return this;
}
normalizeUnordered() {
this.Value = this.Distance;
return this;
}
translate(n) {
let d = 0;
if (n instanceof Coil) {
d = n.Value;
} else {
d = n;
}
this.Value += d;
return this;
}
invert() {
this.Class = this.Size - this.Class;
return this;
}
reflect(pivot, pivot2) {
if (typeof pivot2 === "undefined") {
pivot2 = pivot;
}
this.Value = pivot + pivot2 - this.Value;
return this;
}
negate() {
this.Value *= -1;
return this;
}
add(other) {
return this.translate(other.Value);
}
addScalar(n) {
return this.translate(n);
}
subtract(other) {
return this.translate(-other.Value);
}
subtractScalar(n) {
return this.translate(-n);
}
modulo(n) {
return ((n % this.Size) + this.Size) % this.Size;
}
}
})();