I’ve been playing around with ECMAScript modules in node recently, and decided what better way to drive the concept home than creating a command line app to generate guitar fingerings for jazz scales? It may be totally pointless, but it’s definitely a lot of fun!
Let’s get started. You are probably asking yourself at this point, “Oh my here we go again. Why, Kevin, why?”. Well simply speaking, I find it much easier to organize my code using some type of module system.
Lin Clark has a great breakdown of ES modules here.
Currently, in order to use ES Modules in node you’ll need to use the --experimental-modules
flag.
node --experimental-modules your-file-here.mjs
This will allow you to add import statements for ES module files with the extension .mjs
:
import { Scale } from './Scale.mjs';
What’s all this nonsense about jazz scales?
First off, if we are going to generate a musical scale we need some notes to work with. Let’s create a new file named Note.mjs
:
// deltaC: number of semi-tones from middle C
// see: https://en.wikipedia.org/wiki/Scientific_pitch_notation
class Note {
constructor(deltaC) {
this.name = this.getName(deltaC);
this.octave = Math.floor(deltaC / 12) + 4;
this.frequency = this.getFrequency(deltaC);
this.numSemiTonesFromMiddleC = deltaC;
}
transpose(deltaC) {
return new Note(deltaC + this.numSemiTonesFromMiddleC);
}
getName(deltaC) {
let names = ['C', 'C♯/D♭', 'D', 'D♯/E♭', 'E', 'F', 'F♯/G♭', 'G', 'G♯/A♭', 'A', 'A♯/B♭', 'B'];
let namesReverse = ['C', 'B', 'A♯/B♭', 'A', 'G♯/A♭', 'G', 'F♯/G♭', 'F', 'E', 'D♯/E♭', 'D', 'C♯/D♭'];
let o8va = Math.floor(deltaC / 12) + 4;
if (deltaC === 0) {
return 'C4';
} else if (deltaC < 0) {
let index = Math.abs(deltaC) % 12;
return namesReverse[index] + o8va;
} else {
let index = Math.abs(deltaC) % 12;
return names[index] + o8va;
}
}
getFrequency(deltaC) {
return 440 * Math.pow(2, (deltaC - 9) / 12);
}
}
export { Note };
Now we can create Scale.mjs
to string the notes together as a musical scale:
import { Note } from './Note.mjs';
class Scale {
constructor(intervals, rootNote, range) {
this.rootNote = rootNote;
this.intervals = intervals;
this.scaleIntervals = this.getScaleIntervals(this.intervals);
this.range = range ? range : [new Note(0), new Note(12)];
this.notes = this.getNotesInRange(this.scaleIntervals, this.rootNote, this.range);
}
getScaleIntervals(intervals) {
let currentInterval = 0;
let result = [currentInterval];
intervals.forEach(function (interval) {
currentInterval += interval;
if (currentInterval < 12) {
result.push(currentInterval);
}
});
return result;
}
getNotesBy8va(scaleIntervals, rootNote) {
let results = [];
for (let i = 0; i < scaleIntervals.length; i++) {
let n = new Note(rootNote.numSemiTonesFromMiddleC + scaleIntervals[i]);
results.push(n);
}
return results;
}
getNotesInRange(scaleIntervals, rootNote, range) {
let results = [];
let currentRootNote = new Note(rootNote.numSemiTonesFromMiddleC);
// set scale root below starting note
while (currentRootNote.numSemiTonesFromMiddleC > range[0].numSemiTonesFromMiddleC) {
currentRootNote = currentRootNote.transpose(-12);
}
// continue building scale until we reach upper range limit
while (currentRootNote.numSemiTonesFromMiddleC < range[1].numSemiTonesFromMiddleC) {
let _notes = this.getNotesBy8va(scaleIntervals, currentRootNote);
_notes.forEach(function(n){
results.push(n);
});
currentRootNote = currentRootNote.transpose(12);
}
// clamp results within range
return results.filter((r)=>{
return r.numSemiTonesFromMiddleC >= range[0].numSemiTonesFromMiddleC && r.numSemiTonesFromMiddleC <= range[1].numSemiTonesFromMiddleC;
});
}
toString() {
return this.notes.map(function (n) { return n.name; }).join(',');
}
}
export { Scale };
And one more class to generate the scale fingerings for guitar. Save this one as FretBoard.mjs
.
import { Note } from './Note.mjs';
class FretBoard {
constructor() {
this.strings = [
{ name: 'E3', note: new Note(-8) },
{ name: 'A3', note: new Note(-3) },
{ name: 'D4', note: new Note(2) },
{ name: 'G4', note: new Note(7) },
{ name: 'B4', note: new Note(11) },
{ name: 'E5', note: new Note(16) },
];
}
setNotes(notes) {
this.notes = notes;
}
toString() {
let result = '\n';
result += ' E A D G B E\n';
result += ` ===========\n`;
for (let i = 0; i < 12; i++) {
let fretNum = ('0' + (i + 1)).slice(-2);
if (this.notes) {
let f = [];
for (let s = 0; s < this.strings.length; s++) {
let S = this.strings[s].note.transpose(i + 1);
let x = this.notes.filter(function (n) {
return n.numSemiTonesFromMiddleC === S.numSemiTonesFromMiddleC;
}).length > 0 ? 'x' : '|';
f[s] = x;
}
result += `${fretNum} ${f[0]} ${f[1]} ${f[2]} ${f[3]} ${f[4]} ${f[5]}\n`;
} else {
result += `${fretNum} | | | | | |\n`;
}
result += ` -----------\n`;
}
return result;
}
}
export { FretBoard };
Time to Rock ’n Roll
That’s everything! Let’s wire it all together in scales.mjs
:
//node --experimental-modules scales.mjs
import { Scale } from './Scale.mjs';
import { Note } from './Note.mjs';
import { FretBoard } from './FretBoard.mjs';
// set root note to middle C
let rootNote = new Note(0);
console.log("Root", rootNote);
console.log("---------------");
// create constants for the scale intervals, in this case half-step and whole-step
const W = 2;
const H = 1;
// define some common scales as arrays of intervals
const MAJOR_SCALE = [W, W, H, W, W, W, H];
const LYDIAN_SCALE = [W, W, W, H, W, W, H];
const LYDIAN_AUGMENTED_SCALE = [W, W, W, W, H, W, H];
const DIMISHED_SCALE = [H, W, H, W, H, W, H, W];
const DIMISHED_WHOLE_TONE_SCALE = [H, W, H, W, W, W, W];
// let's see what we get
console.log("Scales in the key of C");
console.log("Major:", new Scale(MAJOR_SCALE, rootNote).toString());
console.log("Lydian:", new Scale(LYDIAN_SCALE, rootNote).toString());
console.log("Lydian Augmented:", new Scale(LYDIAN_AUGMENTED_SCALE, rootNote).toString());
console.log("Diminished:", new Scale(DIMISHED_SCALE, rootNote).toString());
console.log("Diminished Whole Tone:", new Scale(DIMISHED_WHOLE_TONE_SCALE, rootNote).toString());
console.log("---------------");
// generate diminisehd whole tone scale with custom range covering guitar fretboard
let dimWholeTone = new Scale(DIMISHED_WHOLE_TONE_SCALE, rootNote, [new Note(-8), new Note(28)]);
console.log("Diminished Whole Tone:", dimWholeTone.toString());
// show me where to put my fingers please
let fretboard = new FretBoard();
fretboard.setNotes(dimWholeTone.notes);
console.log("Diminished Whole Tone Scale - Fretboard");
console.log(fretboard.toString());
You can now run scales.mjs
from the command line to see the magic happen:
> node --experimental-modules scales.mjs
(node:31500) ExperimentalWarning: The ESM module loader is experimental.
Root Note {
name: 'C4',
octave: 4,
frequency: 261.6255653005986,
numSemiTonesFromMiddleC: 0 }
---------------
Scales in the key of C
Major: C4,D4,E4,F4,G4,A4,B4
Lydian: C4,D4,E4,F♯/G♭4,G4,A4,B4
Lydian Augmented: C4,D4,E4,F♯/G♭4,G♯/A♭4,A4,B4
Diminished: C4,C♯/D♭4,D♯/E♭4,E4,F♯/G♭4,G4,A4,A♯/B♭4
Diminished Whole Tone: C4,C♯/D♭4,D♯/E♭4,E4,F♯/G♭4,G♯/A♭4,A♯/B♭4
---------------
Diminished Whole Tone: E3,F♯/G♭3,G♯/A♭3,A♯/B♭3,C4,C♯/D♭4,D♯/E♭4,E4,F♯/G♭4,G♯/A♭4,A♯/B♭4,C5,C♯/D♭5,D♯/E♭5,E5,F♯/G♭5,G♯/A♭5,A♯/B♭5,C6,C♯/D♭6,D♯/E♭6,E6
Diminished Whole Tone Scale - Fretboard
E A D G B E
===========
01 | x x x x |
-----------
02 x | x | x x
-----------
03 | x | x | |
-----------
04 x x x | x x
-----------
05 | | | x x |
-----------
06 x x x x | x
-----------
07 | x | | x |
-----------
08 x | x x | x
-----------
09 x x | x x x
-----------
10 | | x | | |
-----------
11 x x x x x x
-----------
12 x | | | | x
-----------