-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
151 lines (144 loc) · 5.85 KB
/
index.js
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// shorthand tonal notation (with quality after number)
var IVL_TNL = '([-+]?)(\\d+)(d{1,4}|m|M|P|A{1,4})'
// standard shorthand notation (with quality before number)
var IVL_STR = '(AA|A|P|M|m|d|dd)([-+]?)(\\d+)'
var COMPOSE = '(?:(' + IVL_TNL + ')|(' + IVL_STR + '))'
var IVL_REGEX = new RegExp('^' + COMPOSE + '$')
/**
* Parse a string with an interval in shorthand notation (https://en.wikipedia.org/wiki/Interval_(music)#Shorthand_notation)
* and returns an object with interval properties.
*
* @param {String} str - the string with the interval
* @param {Boolean} strict - (Optional) if its false, it doesn't check if the
* interval is valid or not. For example, parse('P2') returns null
* (because a perfect second is not a valid interval), but
* parse('P2', false) it returns { num: 2, dir: 1, q: 'P'... }
* @return {Object} an object properties or null if not valid interval string
* The returned object contains:
* - `num`: the interval number
* - `q`: the interval quality string (M is major, m is minor, P is perfect...)
* - `simple`: the simplified number (from 1 to 7)
* - `dir`: the interval direction (1 ascending, -1 descending)
* - `type`: the interval type (P is perfectable, M is majorable)
* - `alt`: the alteration, a numeric representation of the quality
* - `oct`: the number of octaves the interval spans. 0 for simple intervals.
* - `size`: the size of the interval in semitones
* @example
* var parse = require('interval-notation').parse
* parse('M3')
* // => { num: 3, q: 'M', dir: 1, simple: 3,
* // type: 'M', alt: 0, oct: 0, size: 4 }
*/
export function parse (str, strict) {
if (typeof str !== 'string') return null
var m = IVL_REGEX.exec(str)
if (!m) return null
var i = { num: +(m[3] || m[8]), q: m[4] || m[6] }
i.dir = (m[2] || m[7]) === '-' ? -1 : 1
var step = (i.num - 1) % 7
i.simple = step + 1
i.type = TYPES[step]
i.alt = qToAlt(i.type, i.q)
i.oct = Math.floor((i.num - 1) / 7)
i.size = i.dir * (SIZES[step] + i.alt + 12 * i.oct)
if (strict !== false) {
if (i.type === 'M' && i.q === 'P') return null
}
return i
}
var SIZES = [0, 2, 4, 5, 7, 9, 11]
var TYPES = 'PMMPPMM'
/**
* Get the type of interval. Can be perfectavle ('P') or majorable ('M')
* @param {Integer} num - the interval number
* @return {String} `P` if it's perfectable, `M` if it's majorable.
*/
export function type (num) {
return TYPES[(num - 1) % 7]
}
function dirStr (dir) { return dir === -1 ? '-' : '' }
function num (simple, oct) { return simple + 7 * oct }
/**
* Build a shorthand interval notation string from properties.
*
* @param {Integer} simple - the interval simple number (from 1 to 7)
* @param {Integer} alt - the quality expressed in numbers. 0 means perfect
* or major, depending of the interval number.
* @param {Integer} oct - the number of octaves the interval spans.
* 0 por simple intervals. Positive number.
* @param {Integer} dir - the interval direction: 1 ascending, -1 descending.
* @example
* var interval = require('interval-notation')
* interval.shorthand(3, 0, 0, 1) // => 'M3'
* interval.shorthand(3, -1, 0, -1) // => 'm-3'
* interval.shorthand(3, 1, 1, 1) // => 'A10'
*/
export function shorthand (simple, alt, oct, dir) {
return altToQ(simple, alt) + dirStr(dir) + num(simple, oct)
}
/**
* Build a special shorthand interval notation string from properties.
* The special shorthand interval notation changes the order or the standard
* shorthand notation so instead of 'M-3' it returns '-3M'.
*
* The standard shorthand notation has a string 'A4' (augmented four) that can't
* be differenciate from 'A4' (the A note in 4th octave), so the purpose of this
* notation is avoid collisions
*
* @param {Integer} simple - the interval simple number (from 1 to 7)
* @param {Integer} alt - the quality expressed in numbers. 0 means perfect
* or major, depending of the interval number.
* @param {Integer} oct - the number of octaves the interval spans.
* 0 por simple intervals. Positive number.
* @param {Integer} dir - the interval direction: 1 ascending, -1 descending.
* @example
* var interval = require('interval-notation')
* interval.build(3, 0, 0, 1) // => '3M'
* interval.build(3, -1, 0, -1) // => '-3m'
* interval.build(3, 1, 1, 1) // => '10A'
*/
export function build (simple, alt, oct, dir) {
return dirStr(dir) + num(simple, oct) + altToQ(simple, alt)
}
/**
* Get an alteration number from an interval quality string.
* It accepts the standard `dmMPA` but also sharps and flats.
*
* @param {Integer|String} num - the interval number or a string representing
* the interval type ('P' or 'M')
* @param {String} quality - the quality string
* @return {Integer} the interval alteration
* @example
* qToAlt('M', 'm') // => -1 (for majorables, 'm' is -1)
* qToAlt('P', 'A') // => 1 (for perfectables, 'A' means 1)
* qToAlt('M', 'P') // => null (majorables can't be perfect)
*/
export function qToAlt (num, q) {
var t = typeof num === 'number' ? type(num) : num
if (q === 'M' && t === 'M') return 0
if (q === 'P' && t === 'P') return 0
if (q === 'm' && t === 'M') return -1
if (/^A+$/.test(q)) return q.length
if (/^d+$/.test(q)) return t === 'P' ? -q.length : -q.length - 1
return null
}
function fillStr (s, n) { return Array(Math.abs(n) + 1).join(s) }
/**
* Get interval quality from interval type and alteration
*
* @function
* @param {Integer|String} num - the interval number of the the interval
* type ('M' for majorables, 'P' for perfectables)
* @param {Integer} alt - the interval alteration
* @return {String} the quality string
* @example
* altToQ('M', 0) // => 'M'
*/
export function altToQ (num, alt) {
var t = typeof num === 'number' ? type(Math.abs(num)) : num
if (alt === 0) return t === 'M' ? 'M' : 'P'
else if (alt === -1 && t === 'M') return 'm'
else if (alt > 0) return fillStr('A', alt)
else if (alt < 0) return fillStr('d', t === 'P' ? alt : alt + 1)
else return null
}