Jazz Scales with NodeJS

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
   -----------