tonal-note
Version:
Parse and manipulate music notes in scientific notation
437 lines (409 loc) • 14.4 kB
text/typescript
type NoteLetter = "A" | "B" | "C" | "D" | "E" | "F" | "G";
type NoteAccidental = string; // " " | "#" | "b" | " #" | " b" | "b " | "b#" | "#b" ;
type Note = string;
type Midi = number;
type Octave = number;
type OrNull<T> = T | null;
type NoteName = string;
type NoteProps = {
name: NoteName;
letter: NoteLetter;
acc: NoteAccidental; // {string}: the note accidentals
oct: OrNull<Octave>; // {Number}: the octave or null if not present
step: number; // {Number}: number equivalent of the note letter. 0 means C ... 6 means B.
pc: NoteName; //{string}: the pitch class (letter + accidentals)
alt: number; // {Number}: number equivalent of accidentals (negative are flats, positive sharps)
chroma: number; // {Number}: number equivalent of the pitch class, where 0 is C, 1 is C# or Db, 2 is D...
midi: OrNull<Midi>; // {Number}: the note midi number (IMPORTANT! it can be outside 0 to 127 range)
freq: OrNull<number>; // {Number}: the frequency using an equal temperament at 440Hz
};
type NoNoteProps = {
name: null;
// letter: null;
// acc: null; // {string}: the note accidentals
oct: null; // {Number}: the octave or null if not present
step: null; // {Number}: number equivalent of the note letter. 0 means C ... 6 means B.
pc: null; //{string}: the pitch class (letter + accidentals)
alt: null; // {Number}: number equivalent of accidentals (negative are flats, positive sharps)
chroma: null; // {Number}: number equivalent of the pitch class, where 0 is C, 1 is C# or Db, 2 is D...
midi: null; // {Number}: the note midi number (IMPORTANT! it can be outside 0 to 127 range)
freq: null;
};
/**
* [](https://www.npmjs.com/package/tonal-note)
* [](https://www.npmjs.com/browse/keyword/tonal)
*
* `tonal-note` is a collection of functions to manipulate musical notes in scientific notation
*
* This is part of [tonal](https://www.npmjs.com/package/tonal) music theory library.
*
* ## Usage
*
* ```js
* import * as Note from "tonal-note"
* // or const Note = require("tonal-note")
* Note.name("bb2") // => "Bb2"
* Note.chroma("bb2") // => 10
* Note.midi("a4") // => 69
* Note.freq("a4") // => 440
* Note.oct("G3") // => 3
*
* // part of tonal
* const Tonal = require("tonal")
* // or import Note from "tonal"
* Tonal.Note.midi("d4") // => 62
* ```
*
* ## Install
*
* [](https://npmjs.org/package/tonal-note/)
*
* ## API Documentation
*
* @module Note
*/
const NAMES = "C C# Db D D# Eb E F F# Gb G G# Ab A A# Bb B".split(
" "
) as NoteName[];
/**
* Get a list of note names (pitch classes) within a octave
*
* @param {string} accTypes - (Optional, by default " b#"). A string with the
* accidentals types: " " means no accidental, "#" means sharps, "b" mean flats,
* can be combined (see examples)
* @return {Array}
* @example
* Note.names(" b") // => [ "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B" ]
* Note.names(" #") // => [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" ]
*/
export const names = (accTypes?: NoteAccidental) =>
typeof accTypes !== "string"
? NAMES.slice()
: NAMES.filter(n => {
const acc = n[1] || " ";
return accTypes.indexOf(acc) !== -1;
});
const SHARPS = names(" #");
const FLATS = names(" b");
const REGEX = /^([a-gA-G]?)(#{1,}|b{1,}|x{1,}|)(-?\d*)\s*(.*)$/;
/**
* Split a string into tokens related to note parts.
* It returns an array of strings `[letter, accidental, octave, modifier]`
*
* It always returns an array
*
* @param {string} str
* @return {Array} an array of note tokens
* @example
* Note.tokenize("C#2") // => ["C", "#", "2", ""]
* Note.tokenize("Db3 major") // => ["D", "b", "3", "major"]
* Note.tokenize("major") // => ["", "", "", "major"]
* Note.tokenize("##") // => ["", "##", "", ""]
* Note.tokenize() // => ["", "", "", ""]
*/
export function tokenize(str?: Note | Midi) {
if (typeof str !== "string") str = "";
const m = REGEX.exec(str) as string[];
// Will never execute
// if (!m) return null;
return [m[1].toUpperCase(), m[2].replace(/x/g, "##"), m[3], m[4]] as [
NoteLetter | "",
string,
string,
string
];
}
const NO_NOTE = Object.freeze({
pc: null,
name: null,
step: null,
alt: null,
oct: null,
octStr: null,
chroma: null,
midi: null,
freq: null
} as NoNoteProps);
const SEMI = [0, 2, 4, 5, 7, 9, 11];
const properties = (str: Note | Midi) => {
const tokens = tokenize(str);
// Will never execute
// if (tokens === null) return NO_NOTE;
if (tokens[0] === "" || tokens[3] !== "") return NO_NOTE;
const [letter, acc, octStr] = tokens;
const p = {
letter,
acc,
octStr,
pc: letter + acc,
name: letter + acc + octStr,
step: (letter.charCodeAt(0) + 3) % 7,
alt: acc[0] === "b" ? -acc.length : acc.length,
oct: octStr.length ? +octStr : null,
chroma: 0,
midi: null,
freq: null
} as NoteProps;
p.chroma = (SEMI[p.step] + p.alt + 120) % 12;
p.midi = p.oct !== null ? SEMI[p.step] + p.alt + 12 * (p.oct + 1) : null;
p.freq = midiToFreq(p.midi);
return Object.freeze(p);
};
// function memo
const memo = <T extends string | number, V>(
fn: (s: T) => V,
cache = {} as { [key in T]: V }
) => (str: T) => cache[str] || (cache[str] = fn(str));
/**
* Get note properties. It returns an object with the following information:
*
* - name {string}: the note name. The letter is always in uppercase
* - letter {string}: the note letter, always in uppercase
* - acc {string}: the note accidentals
* - octave {Number}: the octave or null if not present
* - pc {string}: the pitch class (letter + accidentals)
* - step {Number}: number equivalent of the note letter. 0 means C ... 6 means B.
* - alt {Number}: number equivalent of accidentals (negative are flats, positive sharps)
* - chroma {Number}: number equivalent of the pitch class, where 0 is C, 1 is C# or Db, 2 is D...
* - midi {Number}: the note midi number (IMPORTANT! it can be outside 0 to 127 range)
* - freq {Number}: the frequency using an equal temperament at 440Hz
*
* This function *always* returns an object with all this properties, but if it"s
* not a valid note all properties will be null.
*
* The returned object can"t be mutated.
*
* @param {string} note - the note name in scientific notation
* @return {Object} an object with the properties (or an object will all properties
* set to null if not valid note)
* @example
* Note.props("fx-3").name // => "F##-3"
* Note.props("invalid").name // => null
* Note.props("C#3").oct // => 3
* Note.props().oct // => null
*/
export const props = memo(properties);
/**
* Given a note name, return the note name or null if not valid note.
* The note name will ALWAYS have the letter in upercase and accidentals
* using # or b
*
* Can be used to test if a string is a valid note name.
*
* @function
* @param {Pitch|string}
* @return {string}
*
* @example
* Note.name("cb2") // => "Cb2"
* ["c", "db3", "2", "g+", "gx4"].map(Note.name) // => ["C", "Db3", null, null, "G##4"]
*/
export const name = (str: Note) => props(str).name as NoteName;
/**
* Get pitch class of a note. The note can be a string or a pitch array.
*
* @function
* @param {string|Pitch}
* @return {string} the pitch class
* @example
* Note.pc("Db3") // => "Db"
* ["db3", "bb6", "fx2"].map(Note.pc) // => [ "Db", "Bb", "F##"]
*/
export const pc = (str: Note) => props(str).pc;
const isMidiRange = (m: Midi | number): m is Midi => m >= 0 && m <= 127;
/**
* Get the note midi number. It always return a number between 0 and 127
*
* @function
* @param {string|Number} note - the note to get the midi number from
* @return {Integer} the midi number or null if not valid pitch
* @example
* Note.midi("C4") // => 60
* Note.midi(60) // => 60
* @see midi.toMidi
*/
export const midi = (note: Note | Midi) => {
if (typeof note !== "number" && typeof note !== "string") {
return null;
}
const midi = props(note).midi;
const value = midi || midi === 0 ? midi : +note;
return isMidiRange(value) ? value : null;
};
/**
* Get the frequency from midi number
*
* @param {Number} midi - the note midi number
* @param {Number} tuning - (Optional) 440 by default
* @return {Number} the frequency or null if not valid note midi
*/
export const midiToFreq = (midi: Midi | Note | null, tuning = 440) =>
typeof midi === "number" ? Math.pow(2, (midi - 69) / 12) * tuning : null;
/**
* Get the frequency of a note
*
* @function
* @param {string|Number} note - the note name or midi note number
* @return {Number} the frequency
* @example
* Note.freq("A4") // => 440
* Note.freq(69) // => 440
*/
export const freq = (note: Note | Midi) => props(note).freq || midiToFreq(note);
const L2 = Math.log(2);
const L440 = Math.log(440);
/**
* Get the midi number from a frequency in hertz. The midi number can
* contain decimals (with two digits precission)
*
* @param {Number} frequency
* @return {Number}
* @example
* Note.freqToMidi(220)); //=> 57;
* Note.freqToMidi(261.62)); //=> 60;
* Note.freqToMidi(261)); //=> 59.96;
*/
export const freqToMidi = (freq: number) => {
const v = (12 * (Math.log(freq) - L440)) / L2 + 69;
return Math.round(v * 100) / 100;
};
/**
* Return the chroma of a note. The chroma is the numeric equivalent to the
* pitch class, where 0 is C, 1 is C# or Db, 2 is D... 11 is B
*
* @param {string} note - the note name
* @return {Integer} the chroma number
* @example
* Note.chroma("Cb") // => 11
* ["C", "D", "E", "F"].map(Note.chroma) // => [0, 2, 4, 5]
*/
export const chroma = (str: Note) => props(str).chroma;
/**
* Get the octave of the given pitch
*
* @function
* @param {string} note - the note
* @return {Integer} the octave or null if doesn"t have an octave or not a valid note
* @example
* Note.oct("C#4") // => 4
* Note.oct("C") // => null
* Note.oct("blah") // => undefined
*/
export const oct = (str: Note) => props(str).oct;
const LETTERS = "CDEFGAB";
/**
* Given a step number return it's letter (0 = C, 1 = D, 2 = E)
* @param {number} step
* @return {string} the letter
* @example
* Note.stepToLetter(3) // => "F"
*/
export const stepToLetter = (step: number) => LETTERS[step];
const fillStr = (s: string, n: number) => Array(n + 1).join(s);
const numToStr = (num: number | any, op: (a: number) => string) =>
typeof num !== "number" ? "" : op(num);
/**
* Given an alteration number, return the accidentals
* @param {Number} alt
* @return {string}
* @example
* Note.altToAcc(-3) // => "bbb"
*/
export const altToAcc = (alt?: number) =>
numToStr(alt, alt => (alt < 0 ? fillStr("b", -alt) : fillStr("#", alt)));
/**
* Creates a note name in scientific notation from note properties,
* and optionally another note name.
* It receives an object with:
* - step: the note step (0 = C, 1 = D, ... 6 = B)
* - alt: (optional) the alteration. Negative numbers are flats, positive sharps
* - oct: (optional) the octave
*
* Optionally it receives another note as a "base", meaning that any prop not explicitly
* received on the first parameter will be taken from that base note. That way it can be used
* as an immutable "set" operator for a that base note
*
* @function
* @param {Object} props - the note properties
* @param {string} [baseNote] - note to build the result from. If given, it returns
* the result of applying the given props to this note.
* @return {string} the note name in scientific notation or null if not valid properties
* @example
* Note.from({ step: 5 }) // => "A"
* Note.from({ step: 1, acc: -1 }) // => "Db"
* Note.from({ step: 2, acc: 2, oct: 2 }) // => "E##2"
* Note.from({ step: 7 }) // => null
* Note.from({alt: 1, oct: 3}, "C4") // => "C#3"
*/
export const from = (
fromProps = {} as Partial<NoteProps>,
baseNote: OrNull<Note> = null
) => {
const { step, alt, oct } = baseNote
? Object.assign({}, props(baseNote), fromProps)
: fromProps;
if (typeof step !== "number") return null;
// if (typeof alt !== "number") return null
const letter = stepToLetter(step);
if (!letter) return null;
const pc = letter + altToAcc(alt);
return oct || oct === 0 ? pc + oct : pc;
};
/**
* Deprecated. This is kept for backwards compatibility only.
* Use Note.from instead
*/
export const build = from;
/**
* Given a midi number, returns a note name. The altered notes will have
* flats unless explicitly set with the optional `useSharps` parameter.
*
* @function
* @param {number} midi - the midi note number
* @param {boolean} useSharps - (Optional) set to true to use sharps instead of flats
* @return {string} the note name
* @example
* Note.fromMidi(61) // => "Db4"
* Note.fromMidi(61, true) // => "C#4"
* // it rounds to nearest note
* Note.fromMidi(61.7) // => "D4"
*/
export function fromMidi(num: Midi, sharps: boolean | number = false) {
num = Math.round(num);
const pcs = sharps === true ? SHARPS : FLATS;
const pc = pcs[num % 12];
const o = Math.floor(num / 12) - 1;
return pc + o;
}
/**
* Simplify the note: find an enhramonic note with less accidentals.
*
* @param {string} note - the note to be simplified
* @param {boolean} useSameAccType - (optional, true by default) set to true
* to ensure the returned note has the same accidental types that the given note
* @return {string} the simplfiied note or null if not valid note
* @example
* Note.simplify("C##") // => "D"
* Note.simplify("C###") // => "D#"
* Note.simplify("C###", false) // => "Eb"
* Note.simplify("B#4") // => "C5"
*/
export const simplify = (note: Note, sameAcc: number | boolean = true) => {
const { alt, chroma, midi } = props(note);
if (chroma === null) return null;
const alteration = alt as number;
const useSharps = sameAcc === false ? alteration < 0 : alteration > 0;
return midi === null
? pc(fromMidi(chroma, useSharps))
: fromMidi(midi, useSharps);
};
/**
* Get the simplified and enhramonic note of the given one.
*
* @param {string} note
* @return {string} the enhramonic note
* @example
* Note.enharmonic("Db") // => "C#"
* Note.enhramonic("C") // => "C"
*/
export const enharmonic = (note: Note) => simplify(note, false);