summaryrefslogtreecommitdiffstats
path: root/scale.mjs
blob: de25d14907d3f2813476fd7483ef2cb51f97cf84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
function notesBetween(a, b) {
    const [tonicA, tonicB] = [a[0], b[0]]
    const abs = tonicB.charCodeAt() - tonicA.charCodeAt();
    if (abs < 0) {
        return abs + 7;
    } else {
        return abs;
    }
}

const ringHandler = {
    get(target, prop) {
        return target[prop % target.length] || Reflect.get(...arguments);
    }
};

export const chromaticScale = new Proxy(
    ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B'],
    ringHandler);

// todo: handle tonics with a flat ‘♭’, so we can deal with [BE]♭.
function scaleFromIntervals(tonic, intervals) {
    const scaleIndex = chromaticScale.indexOf(tonic);
    if (scaleIndex < 0) {
        throw new Error(`tonic ${tonic} not found in scale ${chromaticScale.toString()}`);
    }

    let scale = [tonic];
    let steps = 0;
    let lastBase = tonic[0];
    for (let i = 0; i < intervals.length; i++) {
        steps += intervals[i];
        const note = chromaticScale[scaleIndex + steps];
        // don't display two base notes in a row by changing
        // accidentals.
        const delta = notesBetween(lastBase, note);
        if (delta === 0) {
            const nextBase = chromaticScale[scaleIndex + steps + 1][0];
            scale.push(`${nextBase}♭`)
        } else if (delta === 1) {
            scale.push(`${note}`)
        } else {
            // we can only be two away, so take the sharp of the
            // previous note.
            const nextBase = chromaticScale[scaleIndex + steps - 1][0];
            scale.push(`${nextBase}♯`)
        }
        lastBase = scale[scale.length-1];
    }
    return new Proxy(scale, ringHandler);
}

class Scale {
    constructor(tonic, intervals) {
        this.scale = scaleFromIntervals(tonic, intervals);
        return new Proxy(this, {
            get(target, prop) {
                if (prop in target) {
                    return target[prop];
                } else {
                    return target.scale[prop] || Reflect.get(...arguments);
                }
            }
        });
    }

    get tonic() {
        return this.scale[0];
    }

    get third() {
        return this.scale[2];
    }

    get fifth() {
        return this.scale[4];
    }

    get length() {
        return this.scale.length;
    }

    toString() {
        return `[${this.scale.join(", ")}]`;
    }
}

export function MajorScale(tonic) {
    console.debug(`MajorScale(${tonic})`);
    const intervals = [2, 2, 1, 2, 2, 2];
    return new Scale(tonic, intervals);
}

export function MinorScale(tonic) {
    console.debug(`MinorScale(${tonic})`);
    const intervals = [2, 1, 2, 2, 1, 2];
    return new Scale(tonic, intervals);
}