tonal-pcset
Version:
Create and manipulate pitch class sets
213 lines (201 loc) • 6.85 kB
JavaScript
/**
* [](https://www.npmjs.com/package/tonal-pcset)
* [](https://www.npmjs.com/browse/keyword/tonal)
*
* `tonal-pcset` is a collection of functions to work with pitch class sets, oriented
* to make comparations (isEqual, isSubset, isSuperset)
*
* This is part of [tonal](https://www.npmjs.com/package/tonal) music theory library.
*
* You can install via npm: `npm i --save tonal-pcset`
*
* ```js
* // es6
* import PcSet from "tonal-pcset"
* var PcSet = require("tonal-pcset")
*
* PcSet.isEqual("c2 d5 e6", "c6 e3 d1") // => true
* ```
*
* ## API documentation
*
* @module PcSet
*/
import { chroma as notechr } from "tonal-note";
import { chroma as ivlchr } from "tonal-interval";
import { rotate, range, compact } from "tonal-array";
const chr = str => notechr(str) || ivlchr(str) || 0;
const pcsetNum = set => parseInt(chroma(set), 2);
const clen = chroma => chroma.replace(/0/g, "").length;
/**
* Get chroma of a pitch class set. A chroma identifies each set uniquely.
* It"s a 12-digit binary each presenting one semitone of the octave.
*
* Note that this function accepts a chroma as parameter and return it
* without modification.
*
* @param {Array|String} set - the pitch class set
* @return {string} a binary representation of the pitch class set
* @example
* PcSet.chroma(["C", "D", "E"]) // => "1010100000000"
*/
export function chroma(set) {
if (isChroma(set)) return set;
if (!Array.isArray(set)) return "";
var b = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
set.map(chr).forEach(i => {
b[i] = 1;
});
return b.join("");
}
let all = null;
/**
* Get a list of all possible chromas (all possible scales)
* More information: http://allthescales.org/
* @return {Array} an array of possible chromas from '10000000000' to '11111111111'
*
*/
export function chromas(n) {
all = all || range(2048, 4095).map(n => n.toString(2));
return typeof n === "number"
? all.filter(chroma => clen(chroma) === n)
: all.slice();
}
/**
* Given a a list of notes or a pcset chroma, produce the rotations
* of the chroma discarding the ones that starts with "0"
*
* This is used, for example, to get all the modes of a scale.
*
* @param {Array|String} set - the list of notes or pitchChr of the set
* @param {Boolean} normalize - (Optional, true by default) remove all
* the rotations that starts with "0"
* @return {Array<String>} an array with all the modes of the chroma
*
* @example
* PcSet.modes(["C", "D", "E"]).map(PcSet.intervals)
*/
export function modes(set, normalize) {
normalize = normalize !== false;
var binary = chroma(set).split("");
return compact(
binary.map(function(_, i) {
var r = rotate(i, binary);
return normalize && r[0] === "0" ? null : r.join("");
})
);
}
var REGEX = /^[01]{12}$/;
/**
* Test if the given string is a pitch class set chroma.
* @param {string} chroma - the pitch class set chroma
* @return {Boolean} true if its a valid pcset chroma
* @example
* PcSet.isChroma("101010101010") // => true
* PcSet.isChroma("101001") // => false
*/
export function isChroma(set) {
return REGEX.test(set);
}
var IVLS = "1P 2m 2M 3m 3M 4P 5d 5P 6m 6M 7m 7M".split(" ");
/**
* Given a pcset (notes or chroma) return it"s intervals
* @param {String|Array} pcset - the pitch class set (notes or chroma)
* @return {Array} intervals or empty array if not valid pcset
* @example
* PcSet.intervals("1010100000000") => ["1P", "2M", "3M"]
*/
export function intervals(set) {
if (!isChroma(set)) return [];
return compact(
set.split("").map(function(d, i) {
return d === "1" ? IVLS[i] : null;
})
);
}
/**
* Test if two pitch class sets are identical
*
* @param {Array|String} set1 - one of the pitch class sets
* @param {Array|String} set2 - the other pitch class set
* @return {Boolean} true if they are equal
* @example
* PcSet.isEqual(["c2", "d3"], ["c5", "d2"]) // => true
*/
export function isEqual(s1, s2) {
if (arguments.length === 1) return s => isEqual(s1, s);
return chroma(s1) === chroma(s2);
}
/**
* Create a function that test if a collection of notes is a
* subset of a given set
*
* The function can be partially applied
*
* @param {Array|String} set - an array of notes or a chroma set string to test against
* @param {Array|String} notes - an array of notes or a chroma set
* @return {boolean} true if notes is a subset of set, false otherwise
* @example
* const inCMajor = PcSet.isSubsetOf(["C", "E", "G"])
* inCMajor(["e6", "c4"]) // => true
* inCMajor(["e6", "c4", "d3"]) // => false
*/
export function isSubsetOf(set, notes) {
if (arguments.length > 1) return isSubsetOf(set)(notes);
set = pcsetNum(set);
return function(notes) {
notes = pcsetNum(notes);
return notes !== set && (notes & set) === notes;
};
}
/**
* Create a function that test if a collectio of notes is a
* superset of a given set (it contains all notes and at least one more)
*
* @param {Array|String} set - an array of notes or a chroma set string to test against
* @param {Array|String} notes - an array of notes or a chroma set
* @return {boolean} true if notes is a superset of set, false otherwise
* @example
* const extendsCMajor = PcSet.isSupersetOf(["C", "E", "G"])
* extendsCMajor(["e6", "a", "c4", "g2"]) // => true
* extendsCMajor(["c6", "e4", "g3"]) // => false
*/
export function isSupersetOf(set, notes) {
if (arguments.length > 1) return isSupersetOf(set)(notes);
set = pcsetNum(set);
return function(notes) {
notes = pcsetNum(notes);
return notes !== set && (notes | set) === notes;
};
}
/**
* Test if a given pitch class set includes a note
* @param {Array|String} set - the base set to test against
* @param {String|Pitch} note - the note to test
* @return {Boolean} true if the note is included in the pcset
* @example
* PcSet.includes(["C", "D", "E"], "C4") // => true
* PcSet.includes(["C", "D", "E"], "C#4") // => false
*/
export function includes(set, note) {
if (arguments.length > 1) return includes(set)(note);
set = chroma(set);
return function(note) {
return set[chr(note)] === "1";
};
}
/**
* Filter a list with a pitch class set
*
* @param {Array|String} set - the pitch class set notes
* @param {Array|String} notes - the note list to be filtered
* @return {Array} the filtered notes
*
* @example
* PcSet.filter(["C", "D", "E"], ["c2", "c#2", "d2", "c3", "c#3", "d3"]) // => [ "c2", "d2", "c3", "d3" ])
* PcSet.filter(["C2"], ["c2", "c#2", "d2", "c3", "c#3", "d3"]) // => [ "c2", "c3" ])
*/
export function filter(set, notes) {
if (arguments.length === 1) return n => filter(set, n);
return notes.filter(includes(set));
}