forked from tonaljs/tonal
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
101 lines (96 loc) · 3.3 KB
/
index.ts
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
99
100
101
import Chord from "@tonaljs/chord";
import Note from "@tonaljs/note";
import Range from "@tonaljs/range";
import Interval from "@tonaljs/interval";
import VoicingDictionary from "@tonaljs/voicing-dictionary";
import VoiceLeading from "@tonaljs/voice-leading";
const defaultRange = ["C3", "C5"];
const defaultDictionary = VoicingDictionary.all;
const defaultVoiceLeading = VoiceLeading.topNoteDiff;
function get(
chord: string,
range: string[] = defaultRange,
dictionary = defaultDictionary,
voiceLeading = defaultVoiceLeading,
lastVoicing?: string[],
) {
const voicings = search(chord, range, dictionary);
if (!lastVoicing || !lastVoicing.length) {
// notes = voicings[Math.ceil(voicings.length / 2)]; // pick middle voicing..
return voicings[0]; // pick lowest voicing..
} else {
// calculates the distance between the last note and the given voicings top note
// sort voicings with differ
return voiceLeading(voicings, lastVoicing);
}
}
function search(
chord: string,
range = defaultRange,
dictionary = VoicingDictionary.triads,
): string[][] {
const [tonic, symbol] = Chord.tokenize(chord);
const sets = VoicingDictionary.lookup(symbol, dictionary);
// find equivalent symbol that is used as a key in dictionary:
if (!sets) {
return [];
}
// resolve array of interval arrays for the wanted symbol
const voicings = sets.map((intervals) => intervals.split(" "));
const notesInRange = Range.chromatic(range); // gives array of notes inside range
return voicings.reduce((voiced: string[][], voicing: string[]) => {
// transpose intervals relative to first interval (e.g. 3m 5P > 1P 3M)
const relativeIntervals = voicing.map(
(interval) => Interval.substract(interval, voicing[0]) || "",
);
// get enharmonic correct pitch class the bottom note
const bottomPitchClass = Note.transpose(tonic, voicing[0]);
// get all possible start notes for voicing
const starts = notesInRange
// only get the start notes:
.filter((note) => Note.chroma(note) === Note.chroma(bottomPitchClass))
// filter out start notes that will overshoot the top end of the range
.filter(
(note) =>
(Note.midi(
Note.transpose(
note,
relativeIntervals[relativeIntervals.length - 1],
),
) || 0) <= (Note.midi(range[1]) || 0),
)
// replace Range.chromatic notes with the correct enharmonic equivalents
.map((note) => Note.enharmonic(note, bottomPitchClass));
// render one voicing for each start note
const notes = starts.map((start) =>
relativeIntervals.map((interval) => Note.transpose(start, interval)),
);
return voiced.concat(notes);
}, []);
}
function sequence(
chords: string[],
range = defaultRange,
dictionary = defaultDictionary,
voiceLeading = defaultVoiceLeading,
lastVoicing?: string[],
) {
const { voicings } = chords.reduce<{
voicings: string[][];
lastVoicing: string[] | undefined;
}>(
({ voicings, lastVoicing }, chord) => {
const voicing = get(chord, range, dictionary, voiceLeading, lastVoicing);
lastVoicing = voicing;
voicings.push(voicing);
return { voicings, lastVoicing };
},
{ voicings: [], lastVoicing },
);
return voicings;
}
export default {
get,
search,
sequence,
};