├── .npmignore ├── src ├── .editorconfig ├── primitives.ts ├── validators │ ├── interval.ts │ ├── key.ts │ ├── note.ts │ ├── abc-note.ts │ └── chord.ts ├── pattern.ts ├── regex.ts ├── math.ts ├── convert.ts ├── key.ts ├── index.ts ├── note-collection.ts ├── utilities.ts ├── interval.ts ├── abc.ts ├── README.md ├── note.ts ├── chord.ts ├── circles.ts └── palette.ts ├── tsconfig.json ├── rollup.config.js ├── test ├── motive_test.js ├── chord_test.js ├── abc_test.js ├── interval_test.js └── note_test.js ├── bower.json ├── .gitignore ├── package.json ├── LICENSE-MIT ├── README.md └── dist ├── motive.ems.js ├── motive.cjs.js └── motive.umd.js /.npmignore: -------------------------------------------------------------------------------- 1 | _site 2 | standalone 3 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | -------------------------------------------------------------------------------- /src/primitives.ts: -------------------------------------------------------------------------------- 1 | export const operators = { 2 | 'b': -1, 3 | '#': 1, 4 | 'x': 2 5 | }; 6 | 7 | export const steps = ['C','D','E','F','G','A','B']; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "target": "es5", 5 | "outDir": "tmp", 6 | "noImplicitAny": false, 7 | "sourceMap": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | 3 | 4 | export default { 5 | input: 'tmp/index.js', 6 | output: [ 7 | { file: pkg.main, format: 'cjs' }, 8 | { file: pkg.module, format: 'es' }, 9 | { file: pkg.browser, format: 'umd', name: 'motive' }, 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /test/motive_test.js: -------------------------------------------------------------------------------- 1 | var motive = require('../'); 2 | 3 | exports['motive'] = function(test) { 4 | test.expect(6); 5 | test.ok(motive); 6 | test.ok(motive.note); 7 | test.ok(motive.abc); 8 | test.ok(motive.chord); 9 | test.ok(motive.interval); 10 | test.ok(motive.constructors); 11 | test.done(); 12 | }; 13 | -------------------------------------------------------------------------------- /test/chord_test.js: -------------------------------------------------------------------------------- 1 | var chord = require('../').chord; 2 | 3 | exports['chord'] = function(test) { 4 | test.expect(4); 5 | test.ok(chord); 6 | test.equal(chord('Dm7').root.name, 'D'); 7 | test.equal(chord('A7#9').notes.contains('C#'), true); 8 | test.equal(chord('F#/A#').bass.name, 'A#'); 9 | test.done(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/validators/interval.ts: -------------------------------------------------------------------------------- 1 | import {makeValidation} from '../regex'; 2 | 3 | 4 | function validateIntervalName(intervalName: string) { 5 | 6 | var intervalRegex = /^(P|M|m|A+|d+)(\d+|U)$/; 7 | 8 | return makeValidation('interval', intervalRegex, function(captures){ 9 | return { 10 | quality: captures[1], 11 | size: parseInt(captures[2], 10) 12 | }; 13 | })(intervalName); 14 | }; 15 | 16 | export default validateIntervalName 17 | -------------------------------------------------------------------------------- /src/validators/key.ts: -------------------------------------------------------------------------------- 1 | import {makeValidation} from '../regex'; 2 | 3 | 4 | function validateKeyName(keyName: string){ 5 | 6 | var keyRegex = /^([A-G])(b+|\#+|x+)* ?(m|major|minor)?$/i; 7 | 8 | return makeValidation('key', keyRegex, function(captures){ 9 | return { 10 | step: captures[1], 11 | accidental: captures[2] ? captures[2] : '', 12 | quality: captures[3] ? captures[3] : '' 13 | }; 14 | })(keyName); 15 | }; 16 | 17 | export default validateKeyName; -------------------------------------------------------------------------------- /src/validators/note.ts: -------------------------------------------------------------------------------- 1 | import {makeValidation} from '../regex'; 2 | 3 | 4 | function validateNoteName(noteName: string) { 5 | 6 | var noteRegex = /^([A-G])(b+|\#+|x+)?(\-?[0-9]+)?$/; 7 | 8 | return makeValidation('note', noteRegex, function(captures) { 9 | return { 10 | step: captures[1], 11 | accidental: captures[2] ? captures[2] : '', 12 | octave: captures[3] ? parseInt(captures[3], 10) : null 13 | }; 14 | })(noteName); 15 | }; 16 | 17 | export default validateNoteName; -------------------------------------------------------------------------------- /src/validators/abc-note.ts: -------------------------------------------------------------------------------- 1 | import {makeValidation} from '../regex'; 2 | 3 | 4 | function validateAbcNoteName(abcNoteName: string) { 5 | var abcRegex = /((?:\_|\=|\^)*)([a-g]|[A-G])((?:\,|\')*)/; 6 | 7 | return makeValidation('abc-note', abcRegex, function(captures) { 8 | return { 9 | accidental: captures[1] ? captures[1] : '', 10 | step: captures[2], 11 | adjustments: captures[3] ? captures[3] : '' 12 | }; 13 | })(abcNoteName); 14 | }; 15 | 16 | export default validateAbcNoteName; -------------------------------------------------------------------------------- /test/abc_test.js: -------------------------------------------------------------------------------- 1 | var abc = require('../').abc; 2 | 3 | exports['abc'] = function(test) { 4 | test.expect(9); 5 | test.ok(abc); 6 | test.equals(abc('=A').name, 'A'); 7 | test.equals(abc('C,,').octave, 2); 8 | test.equals(abc('^^G').name, 'Gx'); 9 | test.equals(abc('__b\'\'').name, 'Bbb'); 10 | test.equals(abc('__b\'\'').octave, 7); 11 | test.equals(abc("__b''").octave, 7); 12 | test.equals(abc("^F,',").octave, 3); 13 | test.equals(abc("^F,',").parts.accidental, '#'); 14 | test.done(); 15 | }; 16 | -------------------------------------------------------------------------------- /test/interval_test.js: -------------------------------------------------------------------------------- 1 | var interval = require('../').interval; 2 | 3 | exports['interval'] = function(test) { 4 | test.expect(9); 5 | test.ok(interval); 6 | test.equal(interval('M2').semitones, 2); 7 | test.equal(interval('d5').quality, 'd'); 8 | test.equal(interval('M7').semitones, 11); 9 | test.equal(interval('m9').octaves, 1); 10 | test.equal(interval('m2').octaves, 0); 11 | test.equal(interval('A4').species, 'P'); 12 | test.equal(interval('m6').species, 'M'); 13 | test.equal(interval('M9').size, 9); 14 | test.done(); 15 | }; 16 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "motive", 3 | "main": "dist/motive.umd.js", 4 | "version": "1.0.1", 5 | "homepage": "https://github.com/jshanley/motivejs", 6 | "authors": [ 7 | "John Shanley " 8 | ], 9 | "description": "JavaScript music theory library", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "music", 17 | "theory" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "test", 25 | "tests", 26 | "tmp", 27 | "src", 28 | "index.js", 29 | "Makefile", 30 | "tsconfig.json" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/pattern.ts: -------------------------------------------------------------------------------- 1 | import {toObject, isString} from './utilities'; 2 | import Note from './note'; 3 | import NoteCollection from './note-collection'; 4 | 5 | 6 | class Pattern { 7 | 8 | intervalNames: string[]; 9 | 10 | constructor(intervals: string[]) { 11 | this.intervalNames = intervals; 12 | } 13 | 14 | from(item: Note|string) { 15 | const note = toObject(item, toNote) 16 | return new NoteCollection(this.intervalNames.map(function(d) { 17 | if (d === 'R') d = 'P1'; 18 | return note.up(d); 19 | })) 20 | } 21 | 22 | } 23 | 24 | function toNote(item: Note|string) { 25 | if (isString(item)) { 26 | return new Note(item); 27 | } else { 28 | return item; 29 | } 30 | } 31 | 32 | export default Pattern; 33 | 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Browser fiddling 2 | fiddle.html 3 | 4 | # Ignore test results 5 | actual 6 | results 7 | 8 | # Ignore compiled docs 9 | _gh_pages 10 | _gh-pages 11 | _site 12 | 13 | # Numerous always-ignore extensions 14 | *.csv 15 | *.dat 16 | *.diff 17 | *.err 18 | *.gz 19 | *.log 20 | *.log 21 | *.orig 22 | *.out 23 | *.pid 24 | *.rej 25 | *.ruby-version 26 | *.sass-cache 27 | *.seed 28 | *.swo 29 | *.swp 30 | *.vi 31 | *.zip 32 | *~ 33 | lib-cov 34 | 35 | # OS or Editor folders 36 | .DS_Store 37 | ._* 38 | Thumbs.db 39 | .cache 40 | .project 41 | .settings 42 | .tmproj 43 | *.esproj 44 | nbproject 45 | *.sublime-* 46 | .vscode 47 | 48 | # Komodo 49 | *.komodoproject 50 | .komodotools 51 | 52 | # Folders to ignore 53 | .hg 54 | .svn 55 | .CVS 56 | .idea 57 | tmp 58 | node_modules 59 | docpad 60 | pids 61 | logs 62 | 63 | # Files to ignore 64 | npm-debug.log -------------------------------------------------------------------------------- /test/note_test.js: -------------------------------------------------------------------------------- 1 | var note = require('../').note; 2 | 3 | exports['note'] = function(test) { 4 | test.equal(note('C').name, 'C'); 5 | test.equal(note('D').pitchClass, 2); 6 | test.equal(note('Fb').isEnharmonic('E'), true); 7 | test.equal(note('Fb').isEnharmonic('E#'), false); 8 | test.equal(note('G').intervalFrom('C'), 'P5'); 9 | test.equal(note('Bb').intervalTo('D'), 'M3'); 10 | test.done(); 11 | }; 12 | 13 | exports['pitch'] = function(test) { 14 | test.equal(note('G#4').name, 'G#'); 15 | test.equal(note('Db6').octave, 6); 16 | test.equal(note('A4').midi, 69); 17 | test.equal(note('C5').midi, 72); 18 | test.equal(note('Bbb').parts.accidental, 'bb'); 19 | test.equal(note('Ab').parts.step, 'A'); 20 | test.equal(note('C4').isEnharmonic('B#3'), true); 21 | test.equal(note('C4').isEnharmonic('B#4'), false); 22 | test.equal(note(69).name, 'A'); 23 | test.equal(note(70).octave, 4); 24 | test.done(); 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "motive", 3 | "description": "JavaScript music theory library", 4 | "version": "1.0.1", 5 | "homepage": "http://jshanley.github.io/motivejs/", 6 | "author": { 7 | "name": "John Shanley", 8 | "url": "https://github.com/jshanley" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/jshanley/motivejs.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/jshanley/motivejs/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/jshanley/motivejs/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "main": "dist/motive.cjs.js", 24 | "module": "dist/motive.ems.js", 25 | "browser": "dist/motive.umd.js", 26 | "engines": { 27 | "node": ">= 12.7.0" 28 | }, 29 | "scripts": { 30 | "build": "tsc -p tsconfig.json && rollup -c rollup.config.js", 31 | "test": "nodeunit" 32 | }, 33 | "devDependencies": { 34 | "nodeunit": "^0.11.3", 35 | "rollup": "^1.21.4", 36 | "typescript": "^3.6.3" 37 | }, 38 | "dependencies": {} 39 | } 40 | -------------------------------------------------------------------------------- /src/regex.ts: -------------------------------------------------------------------------------- 1 | // this makes a validation function for a string type defined by 'name' 2 | function makeValidation(name: string, exp: RegExp, parser: (a: RegExpExecArray) => T) { 3 | return function(input: string) { 4 | if (typeof input !== 'string') { 5 | throw new TypeError('Cannot validate ' + name + '. Input must be a string.'); 6 | } 7 | var validate = function() { 8 | return input.match(exp) ? true : false; 9 | }; 10 | return { 11 | valid: validate(), 12 | parse: function(){ 13 | if (!validate()) { 14 | return false; 15 | } 16 | var captures = exp.exec(input); 17 | return parser(captures); 18 | } 19 | }; 20 | }; 21 | }; 22 | 23 | function splitStringByPattern(str: string, pattern: RegExp): string[] { 24 | var output: string[] = []; 25 | while(pattern.test(str)) { 26 | var thisMatch = str.match(pattern); 27 | output.push(thisMatch[0]); 28 | str = str.slice(thisMatch[0].length); 29 | } 30 | return output; 31 | }; 32 | 33 | export { 34 | makeValidation, 35 | splitStringByPattern 36 | }; -------------------------------------------------------------------------------- /src/math.ts: -------------------------------------------------------------------------------- 1 | interface ICircle { 2 | array: T[]; 3 | size: number; 4 | indexOf: (item: U) => number; 5 | atIndex: (index: number) => T; 6 | } 7 | 8 | class Circle implements ICircle { 9 | array: any[]; 10 | size: number; 11 | 12 | constructor(array: any[]) { 13 | this.array = array; 14 | this.size = array.length; 15 | } 16 | 17 | // define functions for simple circular lookup 18 | // most instances will override these functions 19 | // with custom accessors 20 | indexOf(item: U) { 21 | return this.array.indexOf(item); 22 | } 23 | 24 | atIndex(index: number) { 25 | return this.array[modulo(index, this.size)]; 26 | } 27 | 28 | } 29 | 30 | function modulo(a: number, b: number): number { 31 | if (a >= 0) { 32 | return a % b; 33 | } else { 34 | return ((a % b) + b) % b; 35 | } 36 | } 37 | 38 | function mod7(a: number): number { 39 | return modulo(a, 7); 40 | } 41 | 42 | function mod12(a: number): number { 43 | return modulo(a, 12); 44 | } 45 | 46 | export { 47 | ICircle, 48 | Circle, 49 | modulo, 50 | mod7, 51 | mod12 52 | }; -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2017 John Shanley 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/validators/chord.ts: -------------------------------------------------------------------------------- 1 | import {makeValidation} from '../regex'; 2 | 3 | 4 | function validateChordName(chordName: string) { 5 | 6 | // lets split up this ugly regex 7 | var intro = /^/, 8 | root_note = /([A-G](?:b+|\#+|x+)?)/, 9 | species = /((?:maj|min|sus|aug|dim|mmaj|m|\-)?(?:\d+)?(?:\/\d+)?)?/, 10 | alterations = /((?:(?:add|sus)(?:\d+)|(?:sus|alt)|(?:\#|\+|b|\-)(?:\d+))*)/, 11 | bass_slash = /(\/)?/, 12 | bass_note = /([A-G](?:b+|\#+|x+)?)?/, 13 | outro = /$/; 14 | 15 | var chordRegex = new RegExp( 16 | intro.source + 17 | root_note.source + 18 | species.source + 19 | alterations.source + 20 | bass_slash.source + 21 | bass_note.source + 22 | outro.source 23 | ); 24 | 25 | return makeValidation('chord', chordRegex, function(captures){ 26 | return { 27 | root: captures[1], 28 | species: captures[2] ? captures[2] : '', 29 | alterations: captures[3] ? captures[3] : '', 30 | slash: captures[4] ? captures[4] : '', 31 | bass: captures[5] ? captures[5] : '' 32 | }; 33 | })(chordName); 34 | 35 | }; 36 | 37 | export default validateChordName; -------------------------------------------------------------------------------- /src/convert.ts: -------------------------------------------------------------------------------- 1 | import {operators} from './primitives' 2 | 3 | 4 | function accidentalToAlter(accidental: string): number { 5 | if (!accidental) { 6 | return 0; 7 | } 8 | var totalSymbolValue = 0; 9 | // look up the value of each symbol in the parsed accidental 10 | for (var a = 0; a < accidental.length; a++){ 11 | totalSymbolValue += operators[accidental[a]]; 12 | } 13 | // add the total value of the accidental to alter 14 | return totalSymbolValue; 15 | } 16 | 17 | function alterToAccidental(alter: number): string { 18 | if (typeof alter === 'undefined') { 19 | throw new Error('Cannot convert alter to accidental, none given.'); 20 | } 21 | if (alter === 0 || alter === null) { 22 | return ''; 23 | } 24 | let accidental = ''; 25 | while (alter < 0) { 26 | accidental += 'b'; 27 | alter += 1; 28 | } 29 | while (alter > 1) { 30 | accidental += 'x'; 31 | alter += -2; 32 | } 33 | while (alter > 0) { 34 | accidental += '#'; 35 | alter += -1; 36 | } 37 | return accidental; 38 | } 39 | 40 | function mtof(midi: number): number { 41 | return Math.pow(2, ((midi - 69) / 12)) * 440; 42 | } 43 | 44 | export { 45 | accidentalToAlter, 46 | alterToAccidental, 47 | mtof 48 | }; -------------------------------------------------------------------------------- /src/key.ts: -------------------------------------------------------------------------------- 1 | import validateKeyName from './validators/key'; 2 | import {fifths} from './circles'; 3 | 4 | 5 | class Key { 6 | 7 | name: string; 8 | fifths: number; 9 | mode: 'major'|'minor'; 10 | 11 | constructor(keyInput: string) { 12 | // run input through validation 13 | var parsed = validateKeyName(keyInput).parse(); 14 | if (!parsed) { 15 | throw new Error('Invalid key name: ' + keyInput.toString()); 16 | } 17 | // assign mode based on the parsed input's quality 18 | if (/[a-g]/.test(parsed.step) || parsed.quality === 'minor' || parsed.quality === 'm') { 19 | this.mode = 'minor'; 20 | } else { 21 | this.mode = 'major'; 22 | } 23 | // now that we have the mode, enforce uppercase for root note 24 | parsed.step = parsed.step.toUpperCase(); 25 | // get fifths for major key 26 | this.fifths = fifths.indexOf(parsed.step + parsed.accidental); 27 | // minor is 3 fifths less than major 28 | if (this.mode === 'minor') { 29 | this.fifths -= 3; 30 | this.name = parsed.step.toLowerCase() + parsed.accidental + ' minor'; 31 | } else { 32 | this.name = parsed.step + parsed.accidental + ' major'; 33 | } 34 | } 35 | 36 | } 37 | 38 | export default Key; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import _abc from './abc'; 2 | import {Circle} from './math'; 3 | import * as _circles from './circles'; 4 | 5 | import Key from './key'; 6 | import Note from './note'; 7 | import Chord from './chord'; 8 | import Interval from './interval'; 9 | import Pattern from './pattern'; 10 | import NoteCollection from './note-collection'; 11 | 12 | 13 | namespace motive { 14 | 15 | export const abc = _abc; 16 | 17 | export const key = function(keyInput) { 18 | return new Key(keyInput); 19 | }; 20 | 21 | export const note = function(noteInput) { 22 | return new Note(noteInput); 23 | }; 24 | 25 | export const chord = function(chordInput) { 26 | return new Chord(chordInput); 27 | }; 28 | 29 | export const interval = function(intervalInput) { 30 | return new Interval(intervalInput); 31 | }; 32 | 33 | export const pattern = function(patternInput) { 34 | return new Pattern(patternInput); 35 | }; 36 | 37 | export const noteCollection = function(noteCollectionInput) { 38 | return new NoteCollection(noteCollectionInput); 39 | }; 40 | 41 | export const circles = _circles; 42 | 43 | export const constructors = { 44 | Note: Note, 45 | Interval: Interval, 46 | Chord: Chord 47 | }; 48 | } 49 | 50 | export default motive; -------------------------------------------------------------------------------- /src/note-collection.ts: -------------------------------------------------------------------------------- 1 | import Note from './note'; 2 | import Interval from './interval'; 3 | import {toObject} from './utilities'; 4 | import Pattern from './pattern'; 5 | 6 | class NoteCollection { 7 | 8 | array: Note[] 9 | 10 | constructor(noteArray: (Note|string)[] = []) { 11 | this.array = noteArray.map(function(d) { 12 | return toObject(d, toNote); 13 | }); 14 | } 15 | 16 | contents() { 17 | return this.array; 18 | } 19 | 20 | each(fn: (value: Note, index: number, array: Note[]) => void) { 21 | this.array.forEach(fn); 22 | return this; 23 | } 24 | 25 | contains(item: Note|string): boolean { 26 | const note = toObject(item, toNote); 27 | var output = false; 28 | this.each(function(d) { 29 | if (d.isEquivalent(note)) output = true; 30 | }); 31 | return output; 32 | } 33 | 34 | add(item: Note|string) { 35 | const note = toObject(item, toNote); 36 | this.array.push(note); 37 | return this; 38 | } 39 | 40 | remove(item: Note|string) { 41 | const note = toObject(item, toNote); 42 | this.array = this.array.filter(function(d) { 43 | return !d.isEquivalent(note); 44 | }); 45 | return this; 46 | } 47 | 48 | map(fn: (value: Note, index: number, array: Note[]) => Note|string) { 49 | return new NoteCollection(this.array.map(fn)); 50 | } 51 | 52 | names() { 53 | return this.array.map(function(d) { 54 | return d.name; 55 | }) 56 | } 57 | 58 | patternFrom(item: Note|string) { 59 | const note = toObject(item, toNote); 60 | if (!this.contains(note)) return new Pattern([]); 61 | var intervals = []; 62 | this.each(function(d) { 63 | intervals.push(new Interval(d.intervalFrom(note))); 64 | }); 65 | intervals.sort(function(a,b) { 66 | return a.size - b.size; 67 | }); 68 | intervals = intervals.map(function(d) { 69 | var name = d.name !== 'P1' ? d.name : 'R'; 70 | return name; 71 | }); 72 | return new Pattern(intervals); 73 | }; 74 | 75 | } 76 | 77 | function toNote(string: string) { 78 | return new Note(string); 79 | } 80 | 81 | export default NoteCollection; -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | import {steps} from './primitives'; 2 | import validateNoteName from './validators/note'; 3 | import validateIntervalName from './validators/interval'; 4 | import {fifths, intervals} from './circles'; 5 | 6 | 7 | function transpose(note_name: string, direction: string, interval: string): string { 8 | if (direction !== 'up' && direction !== 'down') { 9 | throw new Error('Transpose direction must be either "up" or "down".'); 10 | } 11 | var parsed_n = validateNoteName(note_name).parse(); 12 | if (!parsed_n) { 13 | throw new Error('Invalid note name.'); 14 | } 15 | var parsed_i = validateIntervalName(interval).parse(); 16 | if (!parsed_i) { 17 | throw new Error('Invalid interval name.'); 18 | } 19 | 20 | var factor = direction === 'up' ? 1 : -1; 21 | 22 | var new_note_name = fifths.atIndex( 23 | fifths.indexOf(parsed_n.step + parsed_n.accidental) + 24 | (factor * intervals.indexOf(interval)) 25 | ); 26 | 27 | // check if octave adjustment is needed 28 | if (parsed_n.octave === null) { 29 | return new_note_name; 30 | } 31 | 32 | // octave adjustment 33 | var new_octave = parsed_n.octave + (factor * Math.floor(parsed_i.size / 8)); 34 | var normalized_steps = parsed_i.size > 7 ? (parsed_i.size % 7) - 1 : parsed_i.size - 1; 35 | if ((steps.indexOf(parsed_n.step) + normalized_steps) >= 7) { 36 | new_octave += factor; 37 | } 38 | return new_note_name + new_octave.toString(10); 39 | }; 40 | 41 | function isString(input: any): input is string { 42 | return typeof input === 'string'; 43 | } 44 | 45 | function isNumber(input: any): input is number { 46 | return typeof input === 'number'; 47 | } 48 | 49 | // ensures that a function requiring a note (or similar type of) object as input 50 | // gets an object rather than a string representation of it. 51 | // 'obj' will be the function used to create the object. 52 | function toObject(input: T|string, obj: (s: string) => T): T { 53 | if (isString(input)) { 54 | input = obj(input); 55 | } 56 | if (typeof input !== 'object') { 57 | throw new TypeError('Input must be an object or string.'); 58 | } 59 | return input; 60 | } 61 | 62 | export { 63 | transpose, 64 | isString, 65 | isNumber, 66 | toObject 67 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # motive [![NPM version](https://badge.fury.io/js/motive.png)](http://badge.fury.io/js/motive) 2 | 3 | > JavaScript music theory library 4 | 5 | ## Install and Use 6 | ### node: 7 | ``` bash 8 | npm install motive 9 | # OR 10 | yarn add motive 11 | ``` 12 | 13 | ``` javascript 14 | var motive = require('motive'); 15 | ``` 16 | ### browser: 17 | 18 | There is a UMD build of the library available in the [dist](dist) directory. 19 | ``` html 20 | 21 | ``` 22 | 23 | This will expose `motive` in the global namespace. 24 | 25 | alternatively, you can require `motive.umd.js` as an AMD module. 26 | 27 | If you still use Bower, you can also install motive this way: 28 | 29 | ``` bash 30 | bower install motive 31 | ``` 32 | 33 | ## Examples 34 | 35 | _**update 0.2.1** you now have direct access to the `Note`, `Interval` and `Chord` classes so they can be extended. These can be found under `motive.constructors`._ 36 | 37 | create a note: 38 | ```javascript 39 | var myNote = motive.note('Bb'); 40 | // you now have some info about your note 41 | myNote.type; // 'note' 42 | myNote.pitchClass; // 10 43 | myNote.isEnharmonic('A#'); // true 44 | myNote.intervalFrom('Eb'); // 'P5' 45 | myNote.intervalTo('C'); // 'M2' 46 | ``` 47 | 48 | set the note's octave to make it an exact pitch: 49 | ```javascript 50 | myNote.setOctave(3); 51 | // now that it's a pitch you have some additional info 52 | myNote.type; // 'pitch' 53 | myNote.midi; // 58 54 | myNote.frequency; // 233.0818.... 55 | ``` 56 | 57 | make a new note by transposing: 58 | ```javascript 59 | var otherNote = myNote.up('P5'); 60 | // this creates a new note up a perfect fifth from your first note 61 | otherNote.name; // 'F' 62 | otherNote.octave; // 4 63 | ``` 64 | 65 | create a chord: 66 | ```javascript 67 | var myChord = motive.chord('Dm7'); 68 | // the root is a motive.Note object 69 | myChord.root; // '[note D]' 70 | myChord.intervals; // [ 'R', 'm3', 'P5', 'm7' ] 71 | // this is an array of motive.Note objects representing the members 72 | myChord.notes; // [ '[note D]', '[note F]', '[note A]', '[note C]' ] 73 | ``` 74 | 75 | ## Contributing 76 | 77 | See the notes in the [README](src/README.md) for the **src** directory. 78 | 79 | ## License 80 | Copyright (c) 2014 John Shanley. 81 | 82 | Licensed under the [MIT license](LICENSE-MIT). 83 | 84 | Project created by [John Shanley](https://github.com/jshanley). 85 | -------------------------------------------------------------------------------- /src/interval.ts: -------------------------------------------------------------------------------- 1 | import validateIntervalName from './validators/interval'; 2 | 3 | 4 | class Interval { 5 | 6 | steps: number; 7 | name: string; 8 | type: string; 9 | quality: string; 10 | size: number; 11 | normalized: string; 12 | species: string; 13 | octaves: number; 14 | semitones: number; 15 | 16 | constructor(intervalName: string) { 17 | var parsed = validateIntervalName(intervalName).parse(); 18 | if (!parsed) { 19 | throw new Error('Invalid interval name.'); 20 | } 21 | 22 | this.steps = parsed.size - 1; 23 | var normalizedSize = parsed.size > 7 ? (this.steps % 7) + 1 : parsed.size; 24 | 25 | this.name = intervalName; 26 | this.type = 'interval'; 27 | this.quality = parsed.quality; 28 | this.size = parsed.size; 29 | this.normalized = this.quality + normalizedSize.toString(10); 30 | 31 | 32 | this.species = getIntervalSpecies(normalizedSize); 33 | 34 | // this is kinda ugly but it works... 35 | // dividing by 7 evenly returns an extra octave if the value is a multiple of 7 36 | this.octaves = Math.floor(this.size / 7.001); 37 | 38 | this.semitones = getIntervalSemitones(this.quality, normalizedSize, this.octaves, this.species); 39 | } 40 | } 41 | 42 | function getIntervalSemitones(quality, normalizedSize, octaves, species) { 43 | // semitones from root of each note of the major scale 44 | var major = [0,2,4,5,7,9,11]; 45 | 46 | // qualityInt represents the integer difference from a major or perfect quality interval 47 | // for example, m3 will yield -1 since a minor 3rd is one semitone less than a major 3rd 48 | var qualityInt = 0; 49 | var q1 = quality.slice(0,1); 50 | switch (q1) { 51 | case 'P': 52 | case 'M': 53 | break; 54 | case 'm': 55 | qualityInt -= 1; 56 | break; 57 | case 'A': 58 | qualityInt += 1; 59 | break; 60 | case 'd': 61 | if (species === 'M') { 62 | qualityInt -= 2; 63 | } else { 64 | qualityInt -= 1; 65 | } 66 | break; 67 | } 68 | // handle additional augmentations or diminutions 69 | for (var q = 0; q < quality.slice(1).length; q++) { 70 | if (quality.slice(1)[q] === 'd') { 71 | qualityInt -= 1; 72 | } else if (quality.slice(1)[q] === 'A') { 73 | qualityInt += 1; 74 | } 75 | } 76 | 77 | return major[normalizedSize - 1] + qualityInt + (octaves * 12); 78 | } 79 | 80 | // 1,4,5 are treated differently than other interval sizes, 81 | // this helps to identify them immediately 82 | function getIntervalSpecies(size) { 83 | if (size === 1 || size === 4 || size === 5) { 84 | return 'P'; 85 | } else { 86 | return 'M'; 87 | } 88 | } 89 | 90 | export default Interval; 91 | -------------------------------------------------------------------------------- /src/abc.ts: -------------------------------------------------------------------------------- 1 | import Note from './note'; 2 | import {accidentalToAlter, alterToAccidental} from './convert'; 3 | 4 | import validateNoteName from './validators/note'; 5 | import validateAbcNoteName from './validators/abc-note'; 6 | 7 | 8 | function abc(abcInput: string): Note { 9 | var sci = abcToScientific(abcInput); 10 | return new Note(sci); 11 | } 12 | 13 | const accidentals = { 14 | "_": -1, 15 | "=": 0, 16 | "^": 1 17 | }; 18 | 19 | // octave adjustments 20 | const adjustments = { 21 | ",": -1, 22 | "'": 1 23 | }; 24 | 25 | function abcToScientific(abcInput: string): string { 26 | var parsed = validateAbcNoteName(abcInput).parse(); 27 | if (!parsed) { 28 | throw new Error('Cannot convert ABC to scientific notation. Invalid ABC note name.'); 29 | } 30 | 31 | var step, 32 | alter = 0, 33 | accidental, 34 | octave; 35 | 36 | // if parsed step is a capital letter 37 | if (/[A-G]/.test(parsed.step)) { 38 | octave = 4; 39 | } else { // parsed step is lowercase 40 | octave = 5; 41 | } 42 | 43 | // get the total alter value of all accidentals present 44 | for (var c = 0; c < parsed.accidental.length; c++) { 45 | alter += accidentals[parsed.accidental[c]]; 46 | } 47 | 48 | // for each comma or apostrophe adjustment, adjust the octave value 49 | for (var d = 0; d < parsed.adjustments.length; d++) { 50 | octave += adjustments[parsed.adjustments[d]]; 51 | } 52 | 53 | step = parsed.step.toUpperCase(); 54 | accidental = alterToAccidental(alter); 55 | 56 | var output = step + accidental + octave.toString(10); 57 | if (!validateNoteName(output).valid) { 58 | throw new Error('Something went wrong converting ABC to scientific notation. Output invalid.'); 59 | } 60 | return output; 61 | }; 62 | 63 | function scientificToAbc(scientific: string): string { 64 | var parsed = validateNoteName(scientific).parse(); 65 | if (!parsed || parsed.octave === null) { 66 | throw new Error('Cannot convert scientific to ABC. Invalid scientific note name.'); 67 | } 68 | 69 | var abc_accidental = '', 70 | abc_step, 71 | abc_octave = ''; 72 | 73 | var alter = accidentalToAlter(parsed.accidental); 74 | 75 | // add abc accidental symbols until alter is consumed (alter === 0) 76 | while(alter < 0) { 77 | abc_accidental += '_'; 78 | alter += 1; 79 | } 80 | while(alter > 0) { 81 | abc_accidental += '^'; 82 | alter -= 1; 83 | } 84 | 85 | // step must be lowercase for octaves above 5 86 | // add apostrophes or commas to get abc_octave 87 | // to the correct value 88 | var o = parsed.octave; 89 | if (o >= 5) { 90 | abc_step = parsed.step.toLowerCase(); 91 | for( ; o > 5; o--) { 92 | abc_octave += '\''; 93 | } 94 | } else { 95 | abc_step = parsed.step.toUpperCase(); 96 | for( ; o < 4; o++) { 97 | abc_octave += ','; 98 | } 99 | } 100 | 101 | var output = abc_accidental + abc_step + abc_octave; 102 | if (!validateAbcNoteName(output).valid) { 103 | throw new Error('Something went wrong converting scientific to ABC. Output invalid.'); 104 | } 105 | return output; 106 | }; 107 | 108 | export default abc; 109 | export { 110 | abcToScientific, 111 | scientificToAbc 112 | } -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## Important Concepts 2 | 3 | ### Input Validation and Parsing 4 | 5 | The first step when creating an object such as `motive.note` or `motive.chord` is to run the input string through validation. 6 | 7 | Validation for each type is found at `regex.validation.`, for example chord names can be validated using `regex.validation.chordName('Am6')` 8 | 9 | The validation ensures that a properly formatted string was given as input. 10 | 11 | The validation process also splits the input string into component parts. Since these parts may also be useful later on, they can be sent back after validation. 12 | 13 | For an example, this is how validation is done for chord names: 14 | 15 | ``` javascript 16 | var parsed = regex.validate.chordName(chord_name).parse(); 17 | if (!parsed) { 18 | throw new Error('Invalid chord name.'); 19 | } 20 | ``` 21 | 22 | `regex.validate.chordName(chord_name)` returns an object with two methods: 23 | 24 | `.valid()` simply returns a boolean representing whether or not the string is valid. 25 | 26 | `.parse()` splits the input into its component parts, and returns them as an object, or else returns `false` if the input is not valid. 27 | 28 | By storing the return value from the `.parse()` method in the variable `parsed` we can access the properties: 29 | 30 | `parsed.root` - the root of the chord. 31 | 32 | `parsed.species` - the basic type of chord. 33 | 34 | `parsed.alterations` - alterations to the basic type. 35 | 36 | `parsed.slash` - either `'/'` or `''` depending on the presence of a slash chord. 37 | 38 | `parsed.bass` - either the bass note following the slash or `''`. 39 | 40 | ### Fifths 41 | 42 | Many objects created by this library have a property called `fifths`. This is an integer value that is used for consistent arithmetic within and between different types. 43 | 44 | #### for Note Names 45 | 46 | The simplest usage of fifths is for note names. Each note name has a specific fifths value, defined as the number of sharps (positive) or flats (negative) in that note's key signature. In other words, if a major scale was built from that root note, how many sharps or flats would it have? 47 | 48 | This is essential for keeping enharmonic notes from being confused, as the fifths values will be unique. 49 | 50 | For example, the fifths value of **E** is 4, because E-major has 4 sharps. The note **Fb** is enharmonically equivalent to **E**, but its fifths value is -8, because Fb-major contains 6 flats and one double flat, making 8. 51 | 52 | #### for Intervals 53 | 54 | Fifths can also be assigned as a value for simple intervals (those which are contained within an octave). In this case the value literally represents the number of fifths up (positive) or down (negative) it would take to span the generalized interval (generalized meaning disregarding octave displacement). 55 | 56 | Consider the interval of a major-3rd or M3: 57 | 58 | How many fifths are in a generalized M3? Lets begin at **C**, so our target note is the major-3rd **E**. 59 | 60 | Ascending in fifths from **C**, we get: **G**, **D**, **A**, **E** 61 | 62 | so the M3 is considered to have a value of 4 fifths. 63 | 64 | #### Putting them together... 65 | 66 | As you may have noticed, this is similar to the calculation we were making for Note Names. In fact, another way to define the fifths value for note names would be to determine the number of fifths the note is away from **C**, which turns out to be the same number as the number of flats or sharps in its key signature. 67 | 68 | In this way, the different types can be operated on arithmetically while keeping the musical spelling (eg. E vs Fb) consistent. 69 | -------------------------------------------------------------------------------- /src/note.ts: -------------------------------------------------------------------------------- 1 | import {mtof} from './convert'; 2 | import {isString, isNumber, transpose} from './utilities'; 3 | import {fifths, intervals, pitchNames} from './circles'; 4 | import validateNoteName from './validators/note'; 5 | import {scientificToAbc} from './abc'; 6 | 7 | 8 | interface INote { 9 | name: string; 10 | type: 'note'|'pitch'; 11 | pitchClass: number; 12 | parts: { 13 | step: string; 14 | accidental: string; 15 | } 16 | } 17 | interface IPitch { 18 | octave: number; 19 | scientific: string; 20 | abc: string; 21 | midi: number; 22 | frequency: number; 23 | } 24 | 25 | type UserInputNote = Note | string; 26 | 27 | class Note implements INote, IPitch { 28 | 29 | name: string; 30 | type: 'note'|'pitch'; 31 | pitchClass: number; 32 | parts: { 33 | step: string; 34 | accidental: string; 35 | } 36 | octave: number; 37 | scientific: string; 38 | abc: string; 39 | midi: number; 40 | frequency: number; 41 | 42 | constructor(noteName: string); 43 | constructor(midiNumber: number); 44 | constructor(noteInput: string|number) { 45 | let name; 46 | if (isString(noteInput)) { 47 | name = noteInput; 48 | } else if (isNumber(noteInput)) { 49 | name = pitchNames.atIndex(noteInput); 50 | } else { 51 | throw new TypeError('Note name must be a string or number.'); 52 | } 53 | 54 | const parsed = validateNoteName(name).parse(); 55 | if (!parsed) { 56 | throw new Error('Invalid note name.'); 57 | } 58 | 59 | this.name = name; 60 | this.type = 'note'; 61 | this.pitchClass = pitchNames.indexOf(parsed.step + parsed.accidental); 62 | 63 | this.parts = { 64 | step: parsed.step, 65 | accidental: parsed.accidental 66 | }; 67 | 68 | if (parsed.octave !== null) { 69 | this.setOctave(parsed.octave); 70 | } 71 | } 72 | 73 | setOctave(octave: number) { 74 | if (!isNumber(octave)) { 75 | throw new TypeError('Octave must be a number.'); 76 | } 77 | this.name = this.parts.step + this.parts.accidental; 78 | this.type = 'pitch'; 79 | this.octave = octave; 80 | this.scientific = this.name + octave.toString(10); 81 | this.abc = scientificToAbc(this.scientific); 82 | this.midi = pitchNames.indexOf(this.scientific); 83 | this.frequency = mtof(this.midi); 84 | } 85 | 86 | isEquivalent(other: UserInputNote) { 87 | other = toNote(other); 88 | if (this.name !== other.name) { 89 | return false; 90 | } 91 | if (this.type === 'pitch' && other.type === 'pitch' && this.octave !== other.octave) { 92 | return false; 93 | } 94 | return true; 95 | } 96 | 97 | isEnharmonic(other: UserInputNote) { 98 | const otherNote = toNote(other); 99 | if (this.pitchClass !== otherNote.pitchClass) { 100 | return false; 101 | } 102 | if (this.type === 'pitch' && otherNote.type === 'pitch' && (Math.abs(this.midi - otherNote.midi) > 11)) { 103 | return false; 104 | } 105 | return true; 106 | } 107 | 108 | transpose(direction: string, interval: string): Note { 109 | return new Note(transpose(this.type === 'pitch' ? this.scientific : this.name, direction, interval)); 110 | } 111 | 112 | intervalTo(note: UserInputNote): string { 113 | const otherNote = toNote(note); 114 | return intervals.atIndex(fifths.indexOf(otherNote.name) - fifths.indexOf(this.name)); 115 | } 116 | 117 | intervalFrom(note: UserInputNote): string { 118 | const otherNote = toNote(note); 119 | return intervals.atIndex(fifths.indexOf(this.name) - fifths.indexOf(otherNote.name)); 120 | } 121 | 122 | up(interval: string): Note { 123 | return this.transpose('up', interval); 124 | } 125 | 126 | down(interval: string): Note { 127 | return this.transpose('down', interval); 128 | } 129 | 130 | toString() { 131 | let name; 132 | if (this.type === 'note') { 133 | name = this.name; 134 | } else if (this.type === 'pitch'){ 135 | name = this.scientific; 136 | } 137 | return '[note ' + name + ']'; 138 | } 139 | 140 | } 141 | 142 | 143 | function toNote(input: UserInputNote): Note { 144 | if (isString(input)) { 145 | return new Note(input); 146 | } else { 147 | return input; 148 | } 149 | } 150 | 151 | export default Note; -------------------------------------------------------------------------------- /src/chord.ts: -------------------------------------------------------------------------------- 1 | import Note from './note'; 2 | import NoteCollection from './note-collection'; 3 | import validateChordName from './validators/chord'; 4 | import {transpose} from './utilities'; 5 | import {applyAlterations} from './palette'; 6 | 7 | 8 | class Chord { 9 | 10 | name: string; 11 | type: 'chord'; 12 | root: Note; 13 | formula: string; 14 | isSlash: boolean; 15 | bass: Note; 16 | intervals: string[]; 17 | notes: NoteCollection; 18 | 19 | constructor(chordName: string) { 20 | var parsed = validateChordName(chordName).parse(); 21 | if (!parsed) { 22 | throw new Error('Invalid chord name.'); 23 | } 24 | var speciesIntervals = getSpeciesIntervals(parsed.species); 25 | 26 | var memberIntervals = applyAlterations(speciesIntervals, parsed.alterations); 27 | 28 | this.name = chordName; 29 | this.type = 'chord'; 30 | this.root = new Note(parsed.root); 31 | this.formula = parsed.species + parsed.alterations; 32 | this.isSlash = parsed.slash === '/' ? true : false; 33 | this.bass = this.isSlash ? new Note(parsed.bass) : this.root; 34 | this.intervals = memberIntervals; 35 | this.notes = getChordNotes(this.intervals, this.root); 36 | } 37 | 38 | transpose(direction: string, interval: string) { 39 | const root = this.root.transpose(direction, interval); 40 | return new Chord(root.name + this.formula); 41 | } 42 | 43 | toString() { 44 | return '[chord ' + this.name + ']'; 45 | } 46 | } 47 | 48 | function getChordNotes(intervals, root) { 49 | var output = []; 50 | output.push(root); 51 | for (var i = 1; i < intervals.length; i++) { 52 | output.push(root.up(intervals[i])); 53 | } 54 | return new NoteCollection(output); 55 | } 56 | 57 | var getSpeciesIntervals = (function(){ 58 | 59 | var basic_types = { 60 | five: ['R','P5'], 61 | maj: ['R','M3','P5'], 62 | min: ['R','m3','P5'], 63 | aug: ['R','M3','A5'], 64 | dim: ['R','m3','d5'], 65 | sus2: ['R','M2','P5'], 66 | sus4: ['R','P4','P5'] 67 | }; 68 | 69 | var extensions = { 70 | nine: ['M9'], 71 | eleven: ['M9','P11'], 72 | thirteen: ['M9','P11','M13'] 73 | }; 74 | 75 | var species_regex = /^(maj|min|mmin|m|aug|dim|alt|sus|\-)?((?:\d+)|(?:6\/9))?$/; 76 | 77 | return function getSpeciesIntervals(species: string): string[] { 78 | 79 | // easy stuff 80 | if (species in basic_types) { 81 | return basic_types[species]; 82 | } 83 | if (species === '') { 84 | return basic_types.maj; 85 | } 86 | if (species === '5') { 87 | return basic_types.five; 88 | } 89 | if (species === 'm' || species === '-') { 90 | return basic_types.min; 91 | } 92 | if (species === 'sus') { 93 | return basic_types.sus4; 94 | } 95 | 96 | var output = []; 97 | 98 | var captures = species_regex.exec(species); 99 | 100 | var prefix = captures[1] ? captures[1] : '', 101 | degree = captures[2] ? captures[2] : ''; 102 | 103 | switch (prefix) { 104 | case '': 105 | if (degree === '6/9') { 106 | output = output.concat(basic_types.maj, ['M6','M9']); 107 | } else { 108 | output = output.concat(basic_types.maj, degree === '6' ? 'M6' : 'm7'); 109 | } 110 | break; 111 | case 'maj': 112 | output = output.concat(basic_types.maj, degree === '6' ? 'M6' : 'M7'); 113 | break; 114 | case 'min': 115 | case 'm': 116 | case '-': 117 | output = output.concat(basic_types.min, degree === '6' ? 'M6' : 'm7'); 118 | break; 119 | case 'aug': 120 | output = output.concat(basic_types.aug, degree === '6' ? 'M6' : 'm7'); 121 | break; 122 | case 'dim': 123 | output = output.concat(basic_types.dim, 'd7'); 124 | break; 125 | case 'mmaj': 126 | output = output.concat(basic_types.min, 'M7'); 127 | break; 128 | default: 129 | break; 130 | } 131 | 132 | switch (degree) { 133 | case '9': 134 | output = output.concat(extensions.nine); 135 | break; 136 | case '11': 137 | output = output.concat(extensions.eleven); 138 | break; 139 | case '13': 140 | output = output.concat(extensions.thirteen); 141 | break; 142 | default: 143 | break; 144 | } 145 | return output; 146 | }; 147 | 148 | })(); 149 | 150 | 151 | export default Chord; -------------------------------------------------------------------------------- /src/circles.ts: -------------------------------------------------------------------------------- 1 | import {Circle, modulo, mod12} from './math'; 2 | import {accidentalToAlter, alterToAccidental} from './convert'; 3 | import validateNoteName from './validators/note'; 4 | import validateIntervalName from './validators/interval'; 5 | 6 | 7 | let fifths: Circle = new Circle(['F','C','G','D','A','E','B']); 8 | fifths.indexOf = function(this: Circle, noteName: string) { 9 | var step = noteName[0], 10 | accidental = noteName.slice(1), 11 | alter = accidentalToAlter(accidental); 12 | var index = this.array.indexOf(step); 13 | index = index + (this.size * alter); 14 | return index - 1; 15 | }; 16 | fifths.atIndex = function(this: Circle, index: number) { 17 | index = index + 1; 18 | var alter = Math.floor(index / this.array.length), 19 | accidental = alterToAccidental(alter); 20 | index = modulo(index, this.size); 21 | return this.array[index] + accidental; 22 | }; 23 | 24 | // these values represent the size of intervals arranged by fifths. 25 | // Given 4, each value is value[i] = mod7(value[i-1] + 4) with 26 | // the exception that zero is avoided by setting mod7(7) = 7 27 | let intervals: Circle = new Circle([4,1,5,2,6,3,7]); 28 | intervals.indexOf = function(intervalName) { 29 | 30 | var parsed = validateIntervalName(intervalName).parse(); 31 | if (!parsed) { 32 | throw new Error('Invalid interval name.'); 33 | } 34 | 35 | var quality = parsed.quality, 36 | size = parsed.size; 37 | 38 | // string to integer, make 'unison' into size 1 39 | // size = size === 'U' ? 1 : parseInt(size, 10); 40 | 41 | // normalize large intervals 42 | size = size <= 7 ? size : modulo(size, this.size); 43 | 44 | // adjust by -1 since array starts with P4 which is index -1 45 | var size_index = this.array.indexOf(size) - 1; 46 | 47 | // now calculate the correct index value based on the interval quality and size 48 | var index, 49 | len_A, 50 | len_d; 51 | if (quality === 'P' || quality === 'M') { 52 | index = size_index; 53 | } 54 | else if (quality === 'm') { 55 | index = size_index - this.size; 56 | } 57 | else if (quality.match(/A+/)) { 58 | len_A = quality.match(/A+/)[0].length; 59 | index = size_index + (this.size * len_A); 60 | } 61 | else if (quality.match(/d+/)) { 62 | len_d = quality.match(/d+/)[0].length; 63 | if (size === 1 || size === 4 || size === 5) { 64 | index = size_index - (this.size * len_d); 65 | } else { 66 | index = size_index - (this.size + (this.size * len_d)); 67 | } 68 | } 69 | return index; 70 | }; 71 | intervals.atIndex = function(index) { 72 | 73 | // adjustment needed since array starts with P4 which is index -1 74 | var idx = index + 1; 75 | 76 | // factor represents the number of trips around the circle needed 77 | // to get to index, and the sign represents the direction 78 | // negative: anticlockwise, positive: clockwise 79 | var factor = Math.floor(idx / this.size); 80 | 81 | // mod by the size to normalize the index now that we know the factor 82 | idx = modulo(idx, this.size); 83 | 84 | // the size of the resultant interval is now known 85 | var size = this.array[idx].toString(10); 86 | 87 | // time to calculate the quality 88 | var quality = ''; 89 | if (factor > 0) { 90 | for (var f = 0; f < factor; f += 1) { 91 | quality += 'A'; 92 | } 93 | } else if (factor === 0) { 94 | quality = idx < 3 ? 'P' : 'M'; 95 | } else if (factor === -1) { 96 | quality = idx < 3 ? 'd' : 'm'; 97 | } else if (factor < -1) { 98 | for (var nf = -1; nf > factor; nf -= 1) { 99 | quality += 'd'; 100 | } 101 | quality += idx < 3 ? 'd' : ''; 102 | } 103 | return quality + size; 104 | }; 105 | 106 | let pitchNames: Circle = new Circle(['C','C#','D','Eb','E','F','F#','G','Ab','A','Bb','B']); 107 | pitchNames.indexOf = function(member) { 108 | var parsed = validateNoteName(member).parse(); 109 | if (!parsed) { 110 | throw new Error('Invalid pitch name.'); 111 | } 112 | var alter = accidentalToAlter(parsed.accidental); 113 | var step_index = this.array.indexOf(parsed.step); 114 | // return pitch class if no octave given 115 | if (parsed.octave === null) { 116 | return mod12(step_index + alter); 117 | } 118 | return step_index + alter + (this.size * (parsed.octave + 1)); 119 | }; 120 | pitchNames.atIndex = function(index) { 121 | var octave = Math.floor(index / this.size) - 1; 122 | var note_index = mod12(index); 123 | return this.array[note_index] + octave.toString(10); 124 | }; 125 | 126 | 127 | export { 128 | fifths, 129 | intervals, 130 | pitchNames 131 | }; -------------------------------------------------------------------------------- /src/palette.ts: -------------------------------------------------------------------------------- 1 | import validateIntervalName from './validators/interval'; 2 | import {splitStringByPattern} from './regex'; 3 | 4 | 5 | function piaCompare(a,b) { 6 | var qualities = ['d','m','P','M','A']; 7 | if (a.size < b.size) { 8 | return -1; 9 | } else if (a.size > b.size) { 10 | return 1; 11 | } else { 12 | if (qualities.indexOf(a.quality) < qualities.indexOf(b.quality)) { 13 | return -1; 14 | } else if (qualities.indexOf(a.quality) > qualities.indexOf(b.quality)) { 15 | return 1; 16 | } else { 17 | return 0; 18 | } 19 | } 20 | } 21 | 22 | function isFalse(thing: any): thing is false { 23 | return thing === false; 24 | } 25 | 26 | 27 | class ParsedIntervalArray { 28 | 29 | array: Array<{ 30 | quality: string, 31 | size: number 32 | }>; 33 | 34 | constructor(intervalArray) { 35 | this.array = []; 36 | for (var i = 0; i < intervalArray.length; i++) { 37 | if (intervalArray[i] === 'R') { 38 | this.array.push({quality: 'P', size: 1}); 39 | } else { 40 | const parsed = validateIntervalName(intervalArray[i]).parse(); 41 | if (!isFalse(parsed)) this.array.push(parsed); 42 | } 43 | } 44 | } 45 | 46 | sort() { 47 | return this.array.sort(piaCompare); 48 | } 49 | 50 | add(interval) { 51 | var pInterval = validateIntervalName(interval).parse(); 52 | if (!isFalse(pInterval)) { 53 | for (var i = 0; i < this.array.length; i++) { 54 | if (this.array[i].size === pInterval.size && this.array[i].quality === pInterval.quality) { 55 | return; 56 | } 57 | } 58 | this.array.push(pInterval); 59 | this.sort(); 60 | } 61 | } 62 | 63 | remove(size) { 64 | // alias is the octave equivalent of size, for instance 65 | // the alias of 2 is 9, alias of 13 is 6 66 | var alias = size <= 7 ? size + 7 : size - 7; 67 | var updated = []; 68 | // add all intervals that are not of the given size or its alias 69 | for (var i = 0; i < this.array.length; i++) { 70 | if (this.array[i].size !== size && this.array[i].size !== alias) { 71 | updated.push(this.array[i]); 72 | } 73 | } 74 | this.array = updated; 75 | } 76 | 77 | update(interval) { 78 | var pInterval = validateIntervalName(interval).parse(); 79 | if (!isFalse(pInterval)) { 80 | // remove any intervals of the same size 81 | this.remove(pInterval.size); 82 | // add the new interval 83 | this.array.push(pInterval); 84 | this.sort(); 85 | } 86 | } 87 | 88 | unparse() { 89 | this.sort(); 90 | var output = []; 91 | for (var i = 0; i < this.array.length; i++) { 92 | var str = this.array[i].quality + this.array[i].size; 93 | if (str === 'P1') { 94 | output.push('R'); 95 | } else { 96 | output.push(str); 97 | } 98 | } 99 | return output; 100 | } 101 | } 102 | 103 | const applyAlterations = (function() { 104 | 105 | var alteration_regex = /^(?:(?:add|sus|no)(?:\d+)|(?:sus|alt)|(?:n|b|\#|\+|\-)(?:\d+))/; 106 | 107 | // applies to alterations of the form (operation)(degree) such as 'b5' or '#9' 108 | var toInterval = function(alteration) { 109 | var valid = /(?:n|b|\#|\+|\-)(?:\d+)/; 110 | if (!valid.test(alteration)) { 111 | return false; 112 | } 113 | var operation = alteration.slice(0,1); 114 | var degree = alteration.slice(1); 115 | if (operation === '+') { operation = '#'; } 116 | if (operation === '-') { operation = 'b'; } 117 | if (operation === '#') { 118 | return 'A' + degree; 119 | } 120 | if (operation === 'b') { 121 | if (degree === '5' || degree === '11' || degree === '4') { 122 | return 'd' + degree; 123 | } else { 124 | return 'm' + degree; 125 | } 126 | } 127 | if (operation === 'n') { 128 | if (degree === '5' || degree === '11' || degree === '4') { 129 | return 'P' + degree; 130 | } else { 131 | return 'M' + degree; 132 | } 133 | } 134 | }; 135 | 136 | /* might want this later 137 | var intervalType = function(parsed_interval) { 138 | if (parsed_interval.quality === 'P' || parsed_interval.quality === 'M') { 139 | return 'natural'; 140 | } else { 141 | return 'altered'; 142 | } 143 | }; 144 | */ 145 | var alterationType = function(alteration) { 146 | if (/sus/.test(alteration)) { 147 | return 'susX'; 148 | } 149 | if (/add/.test(alteration)) { 150 | return 'addX'; 151 | } 152 | if (/no/.test(alteration)) { 153 | return 'noX'; 154 | } 155 | if (/alt/.test(alteration)) { 156 | return 'alt'; 157 | } 158 | return 'binary'; 159 | }; 160 | 161 | function getNaturalInterval(size) { 162 | var normalized = size < 8 ? size : size % 7; 163 | if (normalized === 1 || normalized === 4 || normalized === 5) { 164 | return 'P' + size.toString(10); 165 | } else { 166 | return 'M' + size.toString(10); 167 | } 168 | } 169 | 170 | return function(intervalArray, alterations) { 171 | var pia = new ParsedIntervalArray(intervalArray); 172 | var alterationArray = splitStringByPattern(alterations, alteration_regex); 173 | // for each alteration... 174 | for (var a = 0; a < alterationArray.length; a++) { 175 | var thisAlteration = alterationArray[a]; 176 | switch(alterationType(thisAlteration)) { 177 | case 'binary': 178 | var asInterval = toInterval(thisAlteration); 179 | pia.update(asInterval); 180 | break; 181 | case 'susX': 182 | pia.remove(3); 183 | pia.add('P4'); 184 | break; 185 | case 'addX': 186 | var addition = parseInt(thisAlteration.slice(3), 10); 187 | pia.add(getNaturalInterval(addition)); 188 | break; 189 | case 'noX': 190 | var removal = parseInt(thisAlteration.slice(2), 10); 191 | pia.remove(removal); 192 | break; 193 | case 'alt': 194 | pia.update('d5'); 195 | pia.add('A5'); 196 | pia.update('m9'); 197 | pia.add('A9'); 198 | pia.update('m13'); 199 | break; 200 | } 201 | } 202 | 203 | return pia.unparse(); 204 | }; 205 | })(); 206 | 207 | export { 208 | ParsedIntervalArray, 209 | applyAlterations 210 | }; -------------------------------------------------------------------------------- /dist/motive.ems.js: -------------------------------------------------------------------------------- 1 | var operators = { 2 | 'b': -1, 3 | '#': 1, 4 | 'x': 2 5 | }; 6 | var steps = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; 7 | 8 | function accidentalToAlter(accidental) { 9 | if (!accidental) { 10 | return 0; 11 | } 12 | var totalSymbolValue = 0; 13 | // look up the value of each symbol in the parsed accidental 14 | for (var a = 0; a < accidental.length; a++) { 15 | totalSymbolValue += operators[accidental[a]]; 16 | } 17 | // add the total value of the accidental to alter 18 | return totalSymbolValue; 19 | } 20 | function alterToAccidental(alter) { 21 | if (typeof alter === 'undefined') { 22 | throw new Error('Cannot convert alter to accidental, none given.'); 23 | } 24 | if (alter === 0 || alter === null) { 25 | return ''; 26 | } 27 | var accidental = ''; 28 | while (alter < 0) { 29 | accidental += 'b'; 30 | alter += 1; 31 | } 32 | while (alter > 1) { 33 | accidental += 'x'; 34 | alter += -2; 35 | } 36 | while (alter > 0) { 37 | accidental += '#'; 38 | alter += -1; 39 | } 40 | return accidental; 41 | } 42 | function mtof(midi) { 43 | return Math.pow(2, ((midi - 69) / 12)) * 440; 44 | } 45 | 46 | // this makes a validation function for a string type defined by 'name' 47 | function makeValidation(name, exp, parser) { 48 | return function (input) { 49 | if (typeof input !== 'string') { 50 | throw new TypeError('Cannot validate ' + name + '. Input must be a string.'); 51 | } 52 | var validate = function () { 53 | return input.match(exp) ? true : false; 54 | }; 55 | return { 56 | valid: validate(), 57 | parse: function () { 58 | if (!validate()) { 59 | return false; 60 | } 61 | var captures = exp.exec(input); 62 | return parser(captures); 63 | } 64 | }; 65 | }; 66 | } 67 | function splitStringByPattern(str, pattern) { 68 | var output = []; 69 | while (pattern.test(str)) { 70 | var thisMatch = str.match(pattern); 71 | output.push(thisMatch[0]); 72 | str = str.slice(thisMatch[0].length); 73 | } 74 | return output; 75 | } 76 | 77 | function validateNoteName(noteName) { 78 | var noteRegex = /^([A-G])(b+|\#+|x+)?(\-?[0-9]+)?$/; 79 | return makeValidation('note', noteRegex, function (captures) { 80 | return { 81 | step: captures[1], 82 | accidental: captures[2] ? captures[2] : '', 83 | octave: captures[3] ? parseInt(captures[3], 10) : null 84 | }; 85 | })(noteName); 86 | } 87 | 88 | function validateIntervalName(intervalName) { 89 | var intervalRegex = /^(P|M|m|A+|d+)(\d+|U)$/; 90 | return makeValidation('interval', intervalRegex, function (captures) { 91 | return { 92 | quality: captures[1], 93 | size: parseInt(captures[2], 10) 94 | }; 95 | })(intervalName); 96 | } 97 | 98 | var Circle = /** @class */ (function () { 99 | function Circle(array) { 100 | this.array = array; 101 | this.size = array.length; 102 | } 103 | // define functions for simple circular lookup 104 | // most instances will override these functions 105 | // with custom accessors 106 | Circle.prototype.indexOf = function (item) { 107 | return this.array.indexOf(item); 108 | }; 109 | Circle.prototype.atIndex = function (index) { 110 | return this.array[modulo(index, this.size)]; 111 | }; 112 | return Circle; 113 | }()); 114 | function modulo(a, b) { 115 | if (a >= 0) { 116 | return a % b; 117 | } 118 | else { 119 | return ((a % b) + b) % b; 120 | } 121 | } 122 | function mod12(a) { 123 | return modulo(a, 12); 124 | } 125 | 126 | var fifths = new Circle(['F', 'C', 'G', 'D', 'A', 'E', 'B']); 127 | fifths.indexOf = function (noteName) { 128 | var step = noteName[0], accidental = noteName.slice(1), alter = accidentalToAlter(accidental); 129 | var index = this.array.indexOf(step); 130 | index = index + (this.size * alter); 131 | return index - 1; 132 | }; 133 | fifths.atIndex = function (index) { 134 | index = index + 1; 135 | var alter = Math.floor(index / this.array.length), accidental = alterToAccidental(alter); 136 | index = modulo(index, this.size); 137 | return this.array[index] + accidental; 138 | }; 139 | // these values represent the size of intervals arranged by fifths. 140 | // Given 4, each value is value[i] = mod7(value[i-1] + 4) with 141 | // the exception that zero is avoided by setting mod7(7) = 7 142 | var intervals = new Circle([4, 1, 5, 2, 6, 3, 7]); 143 | intervals.indexOf = function (intervalName) { 144 | var parsed = validateIntervalName(intervalName).parse(); 145 | if (!parsed) { 146 | throw new Error('Invalid interval name.'); 147 | } 148 | var quality = parsed.quality, size = parsed.size; 149 | // string to integer, make 'unison' into size 1 150 | // size = size === 'U' ? 1 : parseInt(size, 10); 151 | // normalize large intervals 152 | size = size <= 7 ? size : modulo(size, this.size); 153 | // adjust by -1 since array starts with P4 which is index -1 154 | var size_index = this.array.indexOf(size) - 1; 155 | // now calculate the correct index value based on the interval quality and size 156 | var index, len_A, len_d; 157 | if (quality === 'P' || quality === 'M') { 158 | index = size_index; 159 | } 160 | else if (quality === 'm') { 161 | index = size_index - this.size; 162 | } 163 | else if (quality.match(/A+/)) { 164 | len_A = quality.match(/A+/)[0].length; 165 | index = size_index + (this.size * len_A); 166 | } 167 | else if (quality.match(/d+/)) { 168 | len_d = quality.match(/d+/)[0].length; 169 | if (size === 1 || size === 4 || size === 5) { 170 | index = size_index - (this.size * len_d); 171 | } 172 | else { 173 | index = size_index - (this.size + (this.size * len_d)); 174 | } 175 | } 176 | return index; 177 | }; 178 | intervals.atIndex = function (index) { 179 | // adjustment needed since array starts with P4 which is index -1 180 | var idx = index + 1; 181 | // factor represents the number of trips around the circle needed 182 | // to get to index, and the sign represents the direction 183 | // negative: anticlockwise, positive: clockwise 184 | var factor = Math.floor(idx / this.size); 185 | // mod by the size to normalize the index now that we know the factor 186 | idx = modulo(idx, this.size); 187 | // the size of the resultant interval is now known 188 | var size = this.array[idx].toString(10); 189 | // time to calculate the quality 190 | var quality = ''; 191 | if (factor > 0) { 192 | for (var f = 0; f < factor; f += 1) { 193 | quality += 'A'; 194 | } 195 | } 196 | else if (factor === 0) { 197 | quality = idx < 3 ? 'P' : 'M'; 198 | } 199 | else if (factor === -1) { 200 | quality = idx < 3 ? 'd' : 'm'; 201 | } 202 | else if (factor < -1) { 203 | for (var nf = -1; nf > factor; nf -= 1) { 204 | quality += 'd'; 205 | } 206 | quality += idx < 3 ? 'd' : ''; 207 | } 208 | return quality + size; 209 | }; 210 | var pitchNames = new Circle(['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']); 211 | pitchNames.indexOf = function (member) { 212 | var parsed = validateNoteName(member).parse(); 213 | if (!parsed) { 214 | throw new Error('Invalid pitch name.'); 215 | } 216 | var alter = accidentalToAlter(parsed.accidental); 217 | var step_index = this.array.indexOf(parsed.step); 218 | // return pitch class if no octave given 219 | if (parsed.octave === null) { 220 | return mod12(step_index + alter); 221 | } 222 | return step_index + alter + (this.size * (parsed.octave + 1)); 223 | }; 224 | pitchNames.atIndex = function (index) { 225 | var octave = Math.floor(index / this.size) - 1; 226 | var note_index = mod12(index); 227 | return this.array[note_index] + octave.toString(10); 228 | }; 229 | 230 | var _circles = /*#__PURE__*/Object.freeze({ 231 | fifths: fifths, 232 | intervals: intervals, 233 | pitchNames: pitchNames 234 | }); 235 | 236 | function transpose(note_name, direction, interval) { 237 | if (direction !== 'up' && direction !== 'down') { 238 | throw new Error('Transpose direction must be either "up" or "down".'); 239 | } 240 | var parsed_n = validateNoteName(note_name).parse(); 241 | if (!parsed_n) { 242 | throw new Error('Invalid note name.'); 243 | } 244 | var parsed_i = validateIntervalName(interval).parse(); 245 | if (!parsed_i) { 246 | throw new Error('Invalid interval name.'); 247 | } 248 | var factor = direction === 'up' ? 1 : -1; 249 | var new_note_name = fifths.atIndex(fifths.indexOf(parsed_n.step + parsed_n.accidental) + 250 | (factor * intervals.indexOf(interval))); 251 | // check if octave adjustment is needed 252 | if (parsed_n.octave === null) { 253 | return new_note_name; 254 | } 255 | // octave adjustment 256 | var new_octave = parsed_n.octave + (factor * Math.floor(parsed_i.size / 8)); 257 | var normalized_steps = parsed_i.size > 7 ? (parsed_i.size % 7) - 1 : parsed_i.size - 1; 258 | if ((steps.indexOf(parsed_n.step) + normalized_steps) >= 7) { 259 | new_octave += factor; 260 | } 261 | return new_note_name + new_octave.toString(10); 262 | } 263 | function isString(input) { 264 | return typeof input === 'string'; 265 | } 266 | function isNumber(input) { 267 | return typeof input === 'number'; 268 | } 269 | // ensures that a function requiring a note (or similar type of) object as input 270 | // gets an object rather than a string representation of it. 271 | // 'obj' will be the function used to create the object. 272 | function toObject(input, obj) { 273 | if (isString(input)) { 274 | input = obj(input); 275 | } 276 | if (typeof input !== 'object') { 277 | throw new TypeError('Input must be an object or string.'); 278 | } 279 | return input; 280 | } 281 | 282 | var Note = /** @class */ (function () { 283 | function Note(noteInput) { 284 | var name; 285 | if (isString(noteInput)) { 286 | name = noteInput; 287 | } 288 | else if (isNumber(noteInput)) { 289 | name = pitchNames.atIndex(noteInput); 290 | } 291 | else { 292 | throw new TypeError('Note name must be a string or number.'); 293 | } 294 | var parsed = validateNoteName(name).parse(); 295 | if (!parsed) { 296 | throw new Error('Invalid note name.'); 297 | } 298 | this.name = name; 299 | this.type = 'note'; 300 | this.pitchClass = pitchNames.indexOf(parsed.step + parsed.accidental); 301 | this.parts = { 302 | step: parsed.step, 303 | accidental: parsed.accidental 304 | }; 305 | if (parsed.octave !== null) { 306 | this.setOctave(parsed.octave); 307 | } 308 | } 309 | Note.prototype.setOctave = function (octave) { 310 | if (!isNumber(octave)) { 311 | throw new TypeError('Octave must be a number.'); 312 | } 313 | this.name = this.parts.step + this.parts.accidental; 314 | this.type = 'pitch'; 315 | this.octave = octave; 316 | this.scientific = this.name + octave.toString(10); 317 | this.abc = scientificToAbc(this.scientific); 318 | this.midi = pitchNames.indexOf(this.scientific); 319 | this.frequency = mtof(this.midi); 320 | }; 321 | Note.prototype.isEquivalent = function (other) { 322 | other = toNote(other); 323 | if (this.name !== other.name) { 324 | return false; 325 | } 326 | if (this.type === 'pitch' && other.type === 'pitch' && this.octave !== other.octave) { 327 | return false; 328 | } 329 | return true; 330 | }; 331 | Note.prototype.isEnharmonic = function (other) { 332 | var otherNote = toNote(other); 333 | if (this.pitchClass !== otherNote.pitchClass) { 334 | return false; 335 | } 336 | if (this.type === 'pitch' && otherNote.type === 'pitch' && (Math.abs(this.midi - otherNote.midi) > 11)) { 337 | return false; 338 | } 339 | return true; 340 | }; 341 | Note.prototype.transpose = function (direction, interval) { 342 | return new Note(transpose(this.type === 'pitch' ? this.scientific : this.name, direction, interval)); 343 | }; 344 | Note.prototype.intervalTo = function (note) { 345 | var otherNote = toNote(note); 346 | return intervals.atIndex(fifths.indexOf(otherNote.name) - fifths.indexOf(this.name)); 347 | }; 348 | Note.prototype.intervalFrom = function (note) { 349 | var otherNote = toNote(note); 350 | return intervals.atIndex(fifths.indexOf(this.name) - fifths.indexOf(otherNote.name)); 351 | }; 352 | Note.prototype.up = function (interval) { 353 | return this.transpose('up', interval); 354 | }; 355 | Note.prototype.down = function (interval) { 356 | return this.transpose('down', interval); 357 | }; 358 | Note.prototype.toString = function () { 359 | var name; 360 | if (this.type === 'note') { 361 | name = this.name; 362 | } 363 | else if (this.type === 'pitch') { 364 | name = this.scientific; 365 | } 366 | return '[note ' + name + ']'; 367 | }; 368 | return Note; 369 | }()); 370 | function toNote(input) { 371 | if (isString(input)) { 372 | return new Note(input); 373 | } 374 | else { 375 | return input; 376 | } 377 | } 378 | 379 | function validateAbcNoteName(abcNoteName) { 380 | var abcRegex = /((?:\_|\=|\^)*)([a-g]|[A-G])((?:\,|\')*)/; 381 | return makeValidation('abc-note', abcRegex, function (captures) { 382 | return { 383 | accidental: captures[1] ? captures[1] : '', 384 | step: captures[2], 385 | adjustments: captures[3] ? captures[3] : '' 386 | }; 387 | })(abcNoteName); 388 | } 389 | 390 | function abc(abcInput) { 391 | var sci = abcToScientific(abcInput); 392 | return new Note(sci); 393 | } 394 | var accidentals = { 395 | "_": -1, 396 | "=": 0, 397 | "^": 1 398 | }; 399 | // octave adjustments 400 | var adjustments = { 401 | ",": -1, 402 | "'": 1 403 | }; 404 | function abcToScientific(abcInput) { 405 | var parsed = validateAbcNoteName(abcInput).parse(); 406 | if (!parsed) { 407 | throw new Error('Cannot convert ABC to scientific notation. Invalid ABC note name.'); 408 | } 409 | var step, alter = 0, accidental, octave; 410 | // if parsed step is a capital letter 411 | if (/[A-G]/.test(parsed.step)) { 412 | octave = 4; 413 | } 414 | else { // parsed step is lowercase 415 | octave = 5; 416 | } 417 | // get the total alter value of all accidentals present 418 | for (var c = 0; c < parsed.accidental.length; c++) { 419 | alter += accidentals[parsed.accidental[c]]; 420 | } 421 | // for each comma or apostrophe adjustment, adjust the octave value 422 | for (var d = 0; d < parsed.adjustments.length; d++) { 423 | octave += adjustments[parsed.adjustments[d]]; 424 | } 425 | step = parsed.step.toUpperCase(); 426 | accidental = alterToAccidental(alter); 427 | var output = step + accidental + octave.toString(10); 428 | if (!validateNoteName(output).valid) { 429 | throw new Error('Something went wrong converting ABC to scientific notation. Output invalid.'); 430 | } 431 | return output; 432 | } 433 | function scientificToAbc(scientific) { 434 | var parsed = validateNoteName(scientific).parse(); 435 | if (!parsed || parsed.octave === null) { 436 | throw new Error('Cannot convert scientific to ABC. Invalid scientific note name.'); 437 | } 438 | var abc_accidental = '', abc_step, abc_octave = ''; 439 | var alter = accidentalToAlter(parsed.accidental); 440 | // add abc accidental symbols until alter is consumed (alter === 0) 441 | while (alter < 0) { 442 | abc_accidental += '_'; 443 | alter += 1; 444 | } 445 | while (alter > 0) { 446 | abc_accidental += '^'; 447 | alter -= 1; 448 | } 449 | // step must be lowercase for octaves above 5 450 | // add apostrophes or commas to get abc_octave 451 | // to the correct value 452 | var o = parsed.octave; 453 | if (o >= 5) { 454 | abc_step = parsed.step.toLowerCase(); 455 | for (; o > 5; o--) { 456 | abc_octave += '\''; 457 | } 458 | } 459 | else { 460 | abc_step = parsed.step.toUpperCase(); 461 | for (; o < 4; o++) { 462 | abc_octave += ','; 463 | } 464 | } 465 | var output = abc_accidental + abc_step + abc_octave; 466 | if (!validateAbcNoteName(output).valid) { 467 | throw new Error('Something went wrong converting scientific to ABC. Output invalid.'); 468 | } 469 | return output; 470 | } 471 | 472 | function validateKeyName(keyName) { 473 | var keyRegex = /^([A-G])(b+|\#+|x+)* ?(m|major|minor)?$/i; 474 | return makeValidation('key', keyRegex, function (captures) { 475 | return { 476 | step: captures[1], 477 | accidental: captures[2] ? captures[2] : '', 478 | quality: captures[3] ? captures[3] : '' 479 | }; 480 | })(keyName); 481 | } 482 | 483 | var Key = /** @class */ (function () { 484 | function Key(keyInput) { 485 | // run input through validation 486 | var parsed = validateKeyName(keyInput).parse(); 487 | if (!parsed) { 488 | throw new Error('Invalid key name: ' + keyInput.toString()); 489 | } 490 | // assign mode based on the parsed input's quality 491 | if (/[a-g]/.test(parsed.step) || parsed.quality === 'minor' || parsed.quality === 'm') { 492 | this.mode = 'minor'; 493 | } 494 | else { 495 | this.mode = 'major'; 496 | } 497 | // now that we have the mode, enforce uppercase for root note 498 | parsed.step = parsed.step.toUpperCase(); 499 | // get fifths for major key 500 | this.fifths = fifths.indexOf(parsed.step + parsed.accidental); 501 | // minor is 3 fifths less than major 502 | if (this.mode === 'minor') { 503 | this.fifths -= 3; 504 | this.name = parsed.step.toLowerCase() + parsed.accidental + ' minor'; 505 | } 506 | else { 507 | this.name = parsed.step + parsed.accidental + ' major'; 508 | } 509 | } 510 | return Key; 511 | }()); 512 | 513 | var Interval = /** @class */ (function () { 514 | function Interval(intervalName) { 515 | var parsed = validateIntervalName(intervalName).parse(); 516 | if (!parsed) { 517 | throw new Error('Invalid interval name.'); 518 | } 519 | this.steps = parsed.size - 1; 520 | var normalizedSize = parsed.size > 7 ? (this.steps % 7) + 1 : parsed.size; 521 | this.name = intervalName; 522 | this.type = 'interval'; 523 | this.quality = parsed.quality; 524 | this.size = parsed.size; 525 | this.normalized = this.quality + normalizedSize.toString(10); 526 | this.species = getIntervalSpecies(normalizedSize); 527 | // this is kinda ugly but it works... 528 | // dividing by 7 evenly returns an extra octave if the value is a multiple of 7 529 | this.octaves = Math.floor(this.size / 7.001); 530 | this.semitones = getIntervalSemitones(this.quality, normalizedSize, this.octaves, this.species); 531 | } 532 | return Interval; 533 | }()); 534 | function getIntervalSemitones(quality, normalizedSize, octaves, species) { 535 | // semitones from root of each note of the major scale 536 | var major = [0, 2, 4, 5, 7, 9, 11]; 537 | // qualityInt represents the integer difference from a major or perfect quality interval 538 | // for example, m3 will yield -1 since a minor 3rd is one semitone less than a major 3rd 539 | var qualityInt = 0; 540 | var q1 = quality.slice(0, 1); 541 | switch (q1) { 542 | case 'P': 543 | case 'M': 544 | break; 545 | case 'm': 546 | qualityInt -= 1; 547 | break; 548 | case 'A': 549 | qualityInt += 1; 550 | break; 551 | case 'd': 552 | if (species === 'M') { 553 | qualityInt -= 2; 554 | } 555 | else { 556 | qualityInt -= 1; 557 | } 558 | break; 559 | } 560 | // handle additional augmentations or diminutions 561 | for (var q = 0; q < quality.slice(1).length; q++) { 562 | if (quality.slice(1)[q] === 'd') { 563 | qualityInt -= 1; 564 | } 565 | else if (quality.slice(1)[q] === 'A') { 566 | qualityInt += 1; 567 | } 568 | } 569 | return major[normalizedSize - 1] + qualityInt + (octaves * 12); 570 | } 571 | // 1,4,5 are treated differently than other interval sizes, 572 | // this helps to identify them immediately 573 | function getIntervalSpecies(size) { 574 | if (size === 1 || size === 4 || size === 5) { 575 | return 'P'; 576 | } 577 | else { 578 | return 'M'; 579 | } 580 | } 581 | 582 | var Pattern = /** @class */ (function () { 583 | function Pattern(intervals) { 584 | this.intervalNames = intervals; 585 | } 586 | Pattern.prototype.from = function (item) { 587 | var note = toObject(item, toNote$1); 588 | return new NoteCollection(this.intervalNames.map(function (d) { 589 | if (d === 'R') 590 | d = 'P1'; 591 | return note.up(d); 592 | })); 593 | }; 594 | return Pattern; 595 | }()); 596 | function toNote$1(item) { 597 | if (isString(item)) { 598 | return new Note(item); 599 | } 600 | else { 601 | return item; 602 | } 603 | } 604 | 605 | var NoteCollection = /** @class */ (function () { 606 | function NoteCollection(noteArray) { 607 | if (noteArray === void 0) { noteArray = []; } 608 | this.array = noteArray.map(function (d) { 609 | return toObject(d, toNote$2); 610 | }); 611 | } 612 | NoteCollection.prototype.contents = function () { 613 | return this.array; 614 | }; 615 | NoteCollection.prototype.each = function (fn) { 616 | this.array.forEach(fn); 617 | return this; 618 | }; 619 | NoteCollection.prototype.contains = function (item) { 620 | var note = toObject(item, toNote$2); 621 | var output = false; 622 | this.each(function (d) { 623 | if (d.isEquivalent(note)) 624 | output = true; 625 | }); 626 | return output; 627 | }; 628 | NoteCollection.prototype.add = function (item) { 629 | var note = toObject(item, toNote$2); 630 | this.array.push(note); 631 | return this; 632 | }; 633 | NoteCollection.prototype.remove = function (item) { 634 | var note = toObject(item, toNote$2); 635 | this.array = this.array.filter(function (d) { 636 | return !d.isEquivalent(note); 637 | }); 638 | return this; 639 | }; 640 | NoteCollection.prototype.map = function (fn) { 641 | return new NoteCollection(this.array.map(fn)); 642 | }; 643 | NoteCollection.prototype.names = function () { 644 | return this.array.map(function (d) { 645 | return d.name; 646 | }); 647 | }; 648 | NoteCollection.prototype.patternFrom = function (item) { 649 | var note = toObject(item, toNote$2); 650 | if (!this.contains(note)) 651 | return new Pattern([]); 652 | var intervals = []; 653 | this.each(function (d) { 654 | intervals.push(new Interval(d.intervalFrom(note))); 655 | }); 656 | intervals.sort(function (a, b) { 657 | return a.size - b.size; 658 | }); 659 | intervals = intervals.map(function (d) { 660 | var name = d.name !== 'P1' ? d.name : 'R'; 661 | return name; 662 | }); 663 | return new Pattern(intervals); 664 | }; 665 | return NoteCollection; 666 | }()); 667 | function toNote$2(string) { 668 | return new Note(string); 669 | } 670 | 671 | function validateChordName(chordName) { 672 | // lets split up this ugly regex 673 | var intro = /^/, root_note = /([A-G](?:b+|\#+|x+)?)/, species = /((?:maj|min|sus|aug|dim|mmaj|m|\-)?(?:\d+)?(?:\/\d+)?)?/, alterations = /((?:(?:add|sus)(?:\d+)|(?:sus|alt)|(?:\#|\+|b|\-)(?:\d+))*)/, bass_slash = /(\/)?/, bass_note = /([A-G](?:b+|\#+|x+)?)?/, outro = /$/; 674 | var chordRegex = new RegExp(intro.source + 675 | root_note.source + 676 | species.source + 677 | alterations.source + 678 | bass_slash.source + 679 | bass_note.source + 680 | outro.source); 681 | return makeValidation('chord', chordRegex, function (captures) { 682 | return { 683 | root: captures[1], 684 | species: captures[2] ? captures[2] : '', 685 | alterations: captures[3] ? captures[3] : '', 686 | slash: captures[4] ? captures[4] : '', 687 | bass: captures[5] ? captures[5] : '' 688 | }; 689 | })(chordName); 690 | } 691 | 692 | function piaCompare(a, b) { 693 | var qualities = ['d', 'm', 'P', 'M', 'A']; 694 | if (a.size < b.size) { 695 | return -1; 696 | } 697 | else if (a.size > b.size) { 698 | return 1; 699 | } 700 | else { 701 | if (qualities.indexOf(a.quality) < qualities.indexOf(b.quality)) { 702 | return -1; 703 | } 704 | else if (qualities.indexOf(a.quality) > qualities.indexOf(b.quality)) { 705 | return 1; 706 | } 707 | else { 708 | return 0; 709 | } 710 | } 711 | } 712 | function isFalse(thing) { 713 | return thing === false; 714 | } 715 | var ParsedIntervalArray = /** @class */ (function () { 716 | function ParsedIntervalArray(intervalArray) { 717 | this.array = []; 718 | for (var i = 0; i < intervalArray.length; i++) { 719 | if (intervalArray[i] === 'R') { 720 | this.array.push({ quality: 'P', size: 1 }); 721 | } 722 | else { 723 | var parsed = validateIntervalName(intervalArray[i]).parse(); 724 | if (!isFalse(parsed)) 725 | this.array.push(parsed); 726 | } 727 | } 728 | } 729 | ParsedIntervalArray.prototype.sort = function () { 730 | return this.array.sort(piaCompare); 731 | }; 732 | ParsedIntervalArray.prototype.add = function (interval) { 733 | var pInterval = validateIntervalName(interval).parse(); 734 | if (!isFalse(pInterval)) { 735 | for (var i = 0; i < this.array.length; i++) { 736 | if (this.array[i].size === pInterval.size && this.array[i].quality === pInterval.quality) { 737 | return; 738 | } 739 | } 740 | this.array.push(pInterval); 741 | this.sort(); 742 | } 743 | }; 744 | ParsedIntervalArray.prototype.remove = function (size) { 745 | // alias is the octave equivalent of size, for instance 746 | // the alias of 2 is 9, alias of 13 is 6 747 | var alias = size <= 7 ? size + 7 : size - 7; 748 | var updated = []; 749 | // add all intervals that are not of the given size or its alias 750 | for (var i = 0; i < this.array.length; i++) { 751 | if (this.array[i].size !== size && this.array[i].size !== alias) { 752 | updated.push(this.array[i]); 753 | } 754 | } 755 | this.array = updated; 756 | }; 757 | ParsedIntervalArray.prototype.update = function (interval) { 758 | var pInterval = validateIntervalName(interval).parse(); 759 | if (!isFalse(pInterval)) { 760 | // remove any intervals of the same size 761 | this.remove(pInterval.size); 762 | // add the new interval 763 | this.array.push(pInterval); 764 | this.sort(); 765 | } 766 | }; 767 | ParsedIntervalArray.prototype.unparse = function () { 768 | this.sort(); 769 | var output = []; 770 | for (var i = 0; i < this.array.length; i++) { 771 | var str = this.array[i].quality + this.array[i].size; 772 | if (str === 'P1') { 773 | output.push('R'); 774 | } 775 | else { 776 | output.push(str); 777 | } 778 | } 779 | return output; 780 | }; 781 | return ParsedIntervalArray; 782 | }()); 783 | var applyAlterations = (function () { 784 | var alteration_regex = /^(?:(?:add|sus|no)(?:\d+)|(?:sus|alt)|(?:n|b|\#|\+|\-)(?:\d+))/; 785 | // applies to alterations of the form (operation)(degree) such as 'b5' or '#9' 786 | var toInterval = function (alteration) { 787 | var valid = /(?:n|b|\#|\+|\-)(?:\d+)/; 788 | if (!valid.test(alteration)) { 789 | return false; 790 | } 791 | var operation = alteration.slice(0, 1); 792 | var degree = alteration.slice(1); 793 | if (operation === '+') { 794 | operation = '#'; 795 | } 796 | if (operation === '-') { 797 | operation = 'b'; 798 | } 799 | if (operation === '#') { 800 | return 'A' + degree; 801 | } 802 | if (operation === 'b') { 803 | if (degree === '5' || degree === '11' || degree === '4') { 804 | return 'd' + degree; 805 | } 806 | else { 807 | return 'm' + degree; 808 | } 809 | } 810 | if (operation === 'n') { 811 | if (degree === '5' || degree === '11' || degree === '4') { 812 | return 'P' + degree; 813 | } 814 | else { 815 | return 'M' + degree; 816 | } 817 | } 818 | }; 819 | /* might want this later 820 | var intervalType = function(parsed_interval) { 821 | if (parsed_interval.quality === 'P' || parsed_interval.quality === 'M') { 822 | return 'natural'; 823 | } else { 824 | return 'altered'; 825 | } 826 | }; 827 | */ 828 | var alterationType = function (alteration) { 829 | if (/sus/.test(alteration)) { 830 | return 'susX'; 831 | } 832 | if (/add/.test(alteration)) { 833 | return 'addX'; 834 | } 835 | if (/no/.test(alteration)) { 836 | return 'noX'; 837 | } 838 | if (/alt/.test(alteration)) { 839 | return 'alt'; 840 | } 841 | return 'binary'; 842 | }; 843 | function getNaturalInterval(size) { 844 | var normalized = size < 8 ? size : size % 7; 845 | if (normalized === 1 || normalized === 4 || normalized === 5) { 846 | return 'P' + size.toString(10); 847 | } 848 | else { 849 | return 'M' + size.toString(10); 850 | } 851 | } 852 | return function (intervalArray, alterations) { 853 | var pia = new ParsedIntervalArray(intervalArray); 854 | var alterationArray = splitStringByPattern(alterations, alteration_regex); 855 | // for each alteration... 856 | for (var a = 0; a < alterationArray.length; a++) { 857 | var thisAlteration = alterationArray[a]; 858 | switch (alterationType(thisAlteration)) { 859 | case 'binary': 860 | var asInterval = toInterval(thisAlteration); 861 | pia.update(asInterval); 862 | break; 863 | case 'susX': 864 | pia.remove(3); 865 | pia.add('P4'); 866 | break; 867 | case 'addX': 868 | var addition = parseInt(thisAlteration.slice(3), 10); 869 | pia.add(getNaturalInterval(addition)); 870 | break; 871 | case 'noX': 872 | var removal = parseInt(thisAlteration.slice(2), 10); 873 | pia.remove(removal); 874 | break; 875 | case 'alt': 876 | pia.update('d5'); 877 | pia.add('A5'); 878 | pia.update('m9'); 879 | pia.add('A9'); 880 | pia.update('m13'); 881 | break; 882 | } 883 | } 884 | return pia.unparse(); 885 | }; 886 | })(); 887 | 888 | var Chord = /** @class */ (function () { 889 | function Chord(chordName) { 890 | var parsed = validateChordName(chordName).parse(); 891 | if (!parsed) { 892 | throw new Error('Invalid chord name.'); 893 | } 894 | var speciesIntervals = getSpeciesIntervals(parsed.species); 895 | var memberIntervals = applyAlterations(speciesIntervals, parsed.alterations); 896 | this.name = chordName; 897 | this.type = 'chord'; 898 | this.root = new Note(parsed.root); 899 | this.formula = parsed.species + parsed.alterations; 900 | this.isSlash = parsed.slash === '/' ? true : false; 901 | this.bass = this.isSlash ? new Note(parsed.bass) : this.root; 902 | this.intervals = memberIntervals; 903 | this.notes = getChordNotes(this.intervals, this.root); 904 | } 905 | Chord.prototype.transpose = function (direction, interval) { 906 | var root = this.root.transpose(direction, interval); 907 | return new Chord(root.name + this.formula); 908 | }; 909 | Chord.prototype.toString = function () { 910 | return '[chord ' + this.name + ']'; 911 | }; 912 | return Chord; 913 | }()); 914 | function getChordNotes(intervals, root) { 915 | var output = []; 916 | output.push(root); 917 | for (var i = 1; i < intervals.length; i++) { 918 | output.push(root.up(intervals[i])); 919 | } 920 | return new NoteCollection(output); 921 | } 922 | var getSpeciesIntervals = (function () { 923 | var basic_types = { 924 | five: ['R', 'P5'], 925 | maj: ['R', 'M3', 'P5'], 926 | min: ['R', 'm3', 'P5'], 927 | aug: ['R', 'M3', 'A5'], 928 | dim: ['R', 'm3', 'd5'], 929 | sus2: ['R', 'M2', 'P5'], 930 | sus4: ['R', 'P4', 'P5'] 931 | }; 932 | var extensions = { 933 | nine: ['M9'], 934 | eleven: ['M9', 'P11'], 935 | thirteen: ['M9', 'P11', 'M13'] 936 | }; 937 | var species_regex = /^(maj|min|mmin|m|aug|dim|alt|sus|\-)?((?:\d+)|(?:6\/9))?$/; 938 | return function getSpeciesIntervals(species) { 939 | // easy stuff 940 | if (species in basic_types) { 941 | return basic_types[species]; 942 | } 943 | if (species === '') { 944 | return basic_types.maj; 945 | } 946 | if (species === '5') { 947 | return basic_types.five; 948 | } 949 | if (species === 'm' || species === '-') { 950 | return basic_types.min; 951 | } 952 | if (species === 'sus') { 953 | return basic_types.sus4; 954 | } 955 | var output = []; 956 | var captures = species_regex.exec(species); 957 | var prefix = captures[1] ? captures[1] : '', degree = captures[2] ? captures[2] : ''; 958 | switch (prefix) { 959 | case '': 960 | if (degree === '6/9') { 961 | output = output.concat(basic_types.maj, ['M6', 'M9']); 962 | } 963 | else { 964 | output = output.concat(basic_types.maj, degree === '6' ? 'M6' : 'm7'); 965 | } 966 | break; 967 | case 'maj': 968 | output = output.concat(basic_types.maj, degree === '6' ? 'M6' : 'M7'); 969 | break; 970 | case 'min': 971 | case 'm': 972 | case '-': 973 | output = output.concat(basic_types.min, degree === '6' ? 'M6' : 'm7'); 974 | break; 975 | case 'aug': 976 | output = output.concat(basic_types.aug, degree === '6' ? 'M6' : 'm7'); 977 | break; 978 | case 'dim': 979 | output = output.concat(basic_types.dim, 'd7'); 980 | break; 981 | case 'mmaj': 982 | output = output.concat(basic_types.min, 'M7'); 983 | break; 984 | default: 985 | break; 986 | } 987 | switch (degree) { 988 | case '9': 989 | output = output.concat(extensions.nine); 990 | break; 991 | case '11': 992 | output = output.concat(extensions.eleven); 993 | break; 994 | case '13': 995 | output = output.concat(extensions.thirteen); 996 | break; 997 | default: 998 | break; 999 | } 1000 | return output; 1001 | }; 1002 | })(); 1003 | 1004 | var motive; 1005 | (function (motive) { 1006 | motive.abc = abc; 1007 | motive.key = function (keyInput) { 1008 | return new Key(keyInput); 1009 | }; 1010 | motive.note = function (noteInput) { 1011 | return new Note(noteInput); 1012 | }; 1013 | motive.chord = function (chordInput) { 1014 | return new Chord(chordInput); 1015 | }; 1016 | motive.interval = function (intervalInput) { 1017 | return new Interval(intervalInput); 1018 | }; 1019 | motive.pattern = function (patternInput) { 1020 | return new Pattern(patternInput); 1021 | }; 1022 | motive.noteCollection = function (noteCollectionInput) { 1023 | return new NoteCollection(noteCollectionInput); 1024 | }; 1025 | motive.circles = _circles; 1026 | motive.constructors = { 1027 | Note: Note, 1028 | Interval: Interval, 1029 | Chord: Chord 1030 | }; 1031 | })(motive || (motive = {})); 1032 | var motive$1 = motive; 1033 | 1034 | export default motive$1; 1035 | -------------------------------------------------------------------------------- /dist/motive.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var operators = { 4 | 'b': -1, 5 | '#': 1, 6 | 'x': 2 7 | }; 8 | var steps = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; 9 | 10 | function accidentalToAlter(accidental) { 11 | if (!accidental) { 12 | return 0; 13 | } 14 | var totalSymbolValue = 0; 15 | // look up the value of each symbol in the parsed accidental 16 | for (var a = 0; a < accidental.length; a++) { 17 | totalSymbolValue += operators[accidental[a]]; 18 | } 19 | // add the total value of the accidental to alter 20 | return totalSymbolValue; 21 | } 22 | function alterToAccidental(alter) { 23 | if (typeof alter === 'undefined') { 24 | throw new Error('Cannot convert alter to accidental, none given.'); 25 | } 26 | if (alter === 0 || alter === null) { 27 | return ''; 28 | } 29 | var accidental = ''; 30 | while (alter < 0) { 31 | accidental += 'b'; 32 | alter += 1; 33 | } 34 | while (alter > 1) { 35 | accidental += 'x'; 36 | alter += -2; 37 | } 38 | while (alter > 0) { 39 | accidental += '#'; 40 | alter += -1; 41 | } 42 | return accidental; 43 | } 44 | function mtof(midi) { 45 | return Math.pow(2, ((midi - 69) / 12)) * 440; 46 | } 47 | 48 | // this makes a validation function for a string type defined by 'name' 49 | function makeValidation(name, exp, parser) { 50 | return function (input) { 51 | if (typeof input !== 'string') { 52 | throw new TypeError('Cannot validate ' + name + '. Input must be a string.'); 53 | } 54 | var validate = function () { 55 | return input.match(exp) ? true : false; 56 | }; 57 | return { 58 | valid: validate(), 59 | parse: function () { 60 | if (!validate()) { 61 | return false; 62 | } 63 | var captures = exp.exec(input); 64 | return parser(captures); 65 | } 66 | }; 67 | }; 68 | } 69 | function splitStringByPattern(str, pattern) { 70 | var output = []; 71 | while (pattern.test(str)) { 72 | var thisMatch = str.match(pattern); 73 | output.push(thisMatch[0]); 74 | str = str.slice(thisMatch[0].length); 75 | } 76 | return output; 77 | } 78 | 79 | function validateNoteName(noteName) { 80 | var noteRegex = /^([A-G])(b+|\#+|x+)?(\-?[0-9]+)?$/; 81 | return makeValidation('note', noteRegex, function (captures) { 82 | return { 83 | step: captures[1], 84 | accidental: captures[2] ? captures[2] : '', 85 | octave: captures[3] ? parseInt(captures[3], 10) : null 86 | }; 87 | })(noteName); 88 | } 89 | 90 | function validateIntervalName(intervalName) { 91 | var intervalRegex = /^(P|M|m|A+|d+)(\d+|U)$/; 92 | return makeValidation('interval', intervalRegex, function (captures) { 93 | return { 94 | quality: captures[1], 95 | size: parseInt(captures[2], 10) 96 | }; 97 | })(intervalName); 98 | } 99 | 100 | var Circle = /** @class */ (function () { 101 | function Circle(array) { 102 | this.array = array; 103 | this.size = array.length; 104 | } 105 | // define functions for simple circular lookup 106 | // most instances will override these functions 107 | // with custom accessors 108 | Circle.prototype.indexOf = function (item) { 109 | return this.array.indexOf(item); 110 | }; 111 | Circle.prototype.atIndex = function (index) { 112 | return this.array[modulo(index, this.size)]; 113 | }; 114 | return Circle; 115 | }()); 116 | function modulo(a, b) { 117 | if (a >= 0) { 118 | return a % b; 119 | } 120 | else { 121 | return ((a % b) + b) % b; 122 | } 123 | } 124 | function mod12(a) { 125 | return modulo(a, 12); 126 | } 127 | 128 | var fifths = new Circle(['F', 'C', 'G', 'D', 'A', 'E', 'B']); 129 | fifths.indexOf = function (noteName) { 130 | var step = noteName[0], accidental = noteName.slice(1), alter = accidentalToAlter(accidental); 131 | var index = this.array.indexOf(step); 132 | index = index + (this.size * alter); 133 | return index - 1; 134 | }; 135 | fifths.atIndex = function (index) { 136 | index = index + 1; 137 | var alter = Math.floor(index / this.array.length), accidental = alterToAccidental(alter); 138 | index = modulo(index, this.size); 139 | return this.array[index] + accidental; 140 | }; 141 | // these values represent the size of intervals arranged by fifths. 142 | // Given 4, each value is value[i] = mod7(value[i-1] + 4) with 143 | // the exception that zero is avoided by setting mod7(7) = 7 144 | var intervals = new Circle([4, 1, 5, 2, 6, 3, 7]); 145 | intervals.indexOf = function (intervalName) { 146 | var parsed = validateIntervalName(intervalName).parse(); 147 | if (!parsed) { 148 | throw new Error('Invalid interval name.'); 149 | } 150 | var quality = parsed.quality, size = parsed.size; 151 | // string to integer, make 'unison' into size 1 152 | // size = size === 'U' ? 1 : parseInt(size, 10); 153 | // normalize large intervals 154 | size = size <= 7 ? size : modulo(size, this.size); 155 | // adjust by -1 since array starts with P4 which is index -1 156 | var size_index = this.array.indexOf(size) - 1; 157 | // now calculate the correct index value based on the interval quality and size 158 | var index, len_A, len_d; 159 | if (quality === 'P' || quality === 'M') { 160 | index = size_index; 161 | } 162 | else if (quality === 'm') { 163 | index = size_index - this.size; 164 | } 165 | else if (quality.match(/A+/)) { 166 | len_A = quality.match(/A+/)[0].length; 167 | index = size_index + (this.size * len_A); 168 | } 169 | else if (quality.match(/d+/)) { 170 | len_d = quality.match(/d+/)[0].length; 171 | if (size === 1 || size === 4 || size === 5) { 172 | index = size_index - (this.size * len_d); 173 | } 174 | else { 175 | index = size_index - (this.size + (this.size * len_d)); 176 | } 177 | } 178 | return index; 179 | }; 180 | intervals.atIndex = function (index) { 181 | // adjustment needed since array starts with P4 which is index -1 182 | var idx = index + 1; 183 | // factor represents the number of trips around the circle needed 184 | // to get to index, and the sign represents the direction 185 | // negative: anticlockwise, positive: clockwise 186 | var factor = Math.floor(idx / this.size); 187 | // mod by the size to normalize the index now that we know the factor 188 | idx = modulo(idx, this.size); 189 | // the size of the resultant interval is now known 190 | var size = this.array[idx].toString(10); 191 | // time to calculate the quality 192 | var quality = ''; 193 | if (factor > 0) { 194 | for (var f = 0; f < factor; f += 1) { 195 | quality += 'A'; 196 | } 197 | } 198 | else if (factor === 0) { 199 | quality = idx < 3 ? 'P' : 'M'; 200 | } 201 | else if (factor === -1) { 202 | quality = idx < 3 ? 'd' : 'm'; 203 | } 204 | else if (factor < -1) { 205 | for (var nf = -1; nf > factor; nf -= 1) { 206 | quality += 'd'; 207 | } 208 | quality += idx < 3 ? 'd' : ''; 209 | } 210 | return quality + size; 211 | }; 212 | var pitchNames = new Circle(['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']); 213 | pitchNames.indexOf = function (member) { 214 | var parsed = validateNoteName(member).parse(); 215 | if (!parsed) { 216 | throw new Error('Invalid pitch name.'); 217 | } 218 | var alter = accidentalToAlter(parsed.accidental); 219 | var step_index = this.array.indexOf(parsed.step); 220 | // return pitch class if no octave given 221 | if (parsed.octave === null) { 222 | return mod12(step_index + alter); 223 | } 224 | return step_index + alter + (this.size * (parsed.octave + 1)); 225 | }; 226 | pitchNames.atIndex = function (index) { 227 | var octave = Math.floor(index / this.size) - 1; 228 | var note_index = mod12(index); 229 | return this.array[note_index] + octave.toString(10); 230 | }; 231 | 232 | var _circles = /*#__PURE__*/Object.freeze({ 233 | fifths: fifths, 234 | intervals: intervals, 235 | pitchNames: pitchNames 236 | }); 237 | 238 | function transpose(note_name, direction, interval) { 239 | if (direction !== 'up' && direction !== 'down') { 240 | throw new Error('Transpose direction must be either "up" or "down".'); 241 | } 242 | var parsed_n = validateNoteName(note_name).parse(); 243 | if (!parsed_n) { 244 | throw new Error('Invalid note name.'); 245 | } 246 | var parsed_i = validateIntervalName(interval).parse(); 247 | if (!parsed_i) { 248 | throw new Error('Invalid interval name.'); 249 | } 250 | var factor = direction === 'up' ? 1 : -1; 251 | var new_note_name = fifths.atIndex(fifths.indexOf(parsed_n.step + parsed_n.accidental) + 252 | (factor * intervals.indexOf(interval))); 253 | // check if octave adjustment is needed 254 | if (parsed_n.octave === null) { 255 | return new_note_name; 256 | } 257 | // octave adjustment 258 | var new_octave = parsed_n.octave + (factor * Math.floor(parsed_i.size / 8)); 259 | var normalized_steps = parsed_i.size > 7 ? (parsed_i.size % 7) - 1 : parsed_i.size - 1; 260 | if ((steps.indexOf(parsed_n.step) + normalized_steps) >= 7) { 261 | new_octave += factor; 262 | } 263 | return new_note_name + new_octave.toString(10); 264 | } 265 | function isString(input) { 266 | return typeof input === 'string'; 267 | } 268 | function isNumber(input) { 269 | return typeof input === 'number'; 270 | } 271 | // ensures that a function requiring a note (or similar type of) object as input 272 | // gets an object rather than a string representation of it. 273 | // 'obj' will be the function used to create the object. 274 | function toObject(input, obj) { 275 | if (isString(input)) { 276 | input = obj(input); 277 | } 278 | if (typeof input !== 'object') { 279 | throw new TypeError('Input must be an object or string.'); 280 | } 281 | return input; 282 | } 283 | 284 | var Note = /** @class */ (function () { 285 | function Note(noteInput) { 286 | var name; 287 | if (isString(noteInput)) { 288 | name = noteInput; 289 | } 290 | else if (isNumber(noteInput)) { 291 | name = pitchNames.atIndex(noteInput); 292 | } 293 | else { 294 | throw new TypeError('Note name must be a string or number.'); 295 | } 296 | var parsed = validateNoteName(name).parse(); 297 | if (!parsed) { 298 | throw new Error('Invalid note name.'); 299 | } 300 | this.name = name; 301 | this.type = 'note'; 302 | this.pitchClass = pitchNames.indexOf(parsed.step + parsed.accidental); 303 | this.parts = { 304 | step: parsed.step, 305 | accidental: parsed.accidental 306 | }; 307 | if (parsed.octave !== null) { 308 | this.setOctave(parsed.octave); 309 | } 310 | } 311 | Note.prototype.setOctave = function (octave) { 312 | if (!isNumber(octave)) { 313 | throw new TypeError('Octave must be a number.'); 314 | } 315 | this.name = this.parts.step + this.parts.accidental; 316 | this.type = 'pitch'; 317 | this.octave = octave; 318 | this.scientific = this.name + octave.toString(10); 319 | this.abc = scientificToAbc(this.scientific); 320 | this.midi = pitchNames.indexOf(this.scientific); 321 | this.frequency = mtof(this.midi); 322 | }; 323 | Note.prototype.isEquivalent = function (other) { 324 | other = toNote(other); 325 | if (this.name !== other.name) { 326 | return false; 327 | } 328 | if (this.type === 'pitch' && other.type === 'pitch' && this.octave !== other.octave) { 329 | return false; 330 | } 331 | return true; 332 | }; 333 | Note.prototype.isEnharmonic = function (other) { 334 | var otherNote = toNote(other); 335 | if (this.pitchClass !== otherNote.pitchClass) { 336 | return false; 337 | } 338 | if (this.type === 'pitch' && otherNote.type === 'pitch' && (Math.abs(this.midi - otherNote.midi) > 11)) { 339 | return false; 340 | } 341 | return true; 342 | }; 343 | Note.prototype.transpose = function (direction, interval) { 344 | return new Note(transpose(this.type === 'pitch' ? this.scientific : this.name, direction, interval)); 345 | }; 346 | Note.prototype.intervalTo = function (note) { 347 | var otherNote = toNote(note); 348 | return intervals.atIndex(fifths.indexOf(otherNote.name) - fifths.indexOf(this.name)); 349 | }; 350 | Note.prototype.intervalFrom = function (note) { 351 | var otherNote = toNote(note); 352 | return intervals.atIndex(fifths.indexOf(this.name) - fifths.indexOf(otherNote.name)); 353 | }; 354 | Note.prototype.up = function (interval) { 355 | return this.transpose('up', interval); 356 | }; 357 | Note.prototype.down = function (interval) { 358 | return this.transpose('down', interval); 359 | }; 360 | Note.prototype.toString = function () { 361 | var name; 362 | if (this.type === 'note') { 363 | name = this.name; 364 | } 365 | else if (this.type === 'pitch') { 366 | name = this.scientific; 367 | } 368 | return '[note ' + name + ']'; 369 | }; 370 | return Note; 371 | }()); 372 | function toNote(input) { 373 | if (isString(input)) { 374 | return new Note(input); 375 | } 376 | else { 377 | return input; 378 | } 379 | } 380 | 381 | function validateAbcNoteName(abcNoteName) { 382 | var abcRegex = /((?:\_|\=|\^)*)([a-g]|[A-G])((?:\,|\')*)/; 383 | return makeValidation('abc-note', abcRegex, function (captures) { 384 | return { 385 | accidental: captures[1] ? captures[1] : '', 386 | step: captures[2], 387 | adjustments: captures[3] ? captures[3] : '' 388 | }; 389 | })(abcNoteName); 390 | } 391 | 392 | function abc(abcInput) { 393 | var sci = abcToScientific(abcInput); 394 | return new Note(sci); 395 | } 396 | var accidentals = { 397 | "_": -1, 398 | "=": 0, 399 | "^": 1 400 | }; 401 | // octave adjustments 402 | var adjustments = { 403 | ",": -1, 404 | "'": 1 405 | }; 406 | function abcToScientific(abcInput) { 407 | var parsed = validateAbcNoteName(abcInput).parse(); 408 | if (!parsed) { 409 | throw new Error('Cannot convert ABC to scientific notation. Invalid ABC note name.'); 410 | } 411 | var step, alter = 0, accidental, octave; 412 | // if parsed step is a capital letter 413 | if (/[A-G]/.test(parsed.step)) { 414 | octave = 4; 415 | } 416 | else { // parsed step is lowercase 417 | octave = 5; 418 | } 419 | // get the total alter value of all accidentals present 420 | for (var c = 0; c < parsed.accidental.length; c++) { 421 | alter += accidentals[parsed.accidental[c]]; 422 | } 423 | // for each comma or apostrophe adjustment, adjust the octave value 424 | for (var d = 0; d < parsed.adjustments.length; d++) { 425 | octave += adjustments[parsed.adjustments[d]]; 426 | } 427 | step = parsed.step.toUpperCase(); 428 | accidental = alterToAccidental(alter); 429 | var output = step + accidental + octave.toString(10); 430 | if (!validateNoteName(output).valid) { 431 | throw new Error('Something went wrong converting ABC to scientific notation. Output invalid.'); 432 | } 433 | return output; 434 | } 435 | function scientificToAbc(scientific) { 436 | var parsed = validateNoteName(scientific).parse(); 437 | if (!parsed || parsed.octave === null) { 438 | throw new Error('Cannot convert scientific to ABC. Invalid scientific note name.'); 439 | } 440 | var abc_accidental = '', abc_step, abc_octave = ''; 441 | var alter = accidentalToAlter(parsed.accidental); 442 | // add abc accidental symbols until alter is consumed (alter === 0) 443 | while (alter < 0) { 444 | abc_accidental += '_'; 445 | alter += 1; 446 | } 447 | while (alter > 0) { 448 | abc_accidental += '^'; 449 | alter -= 1; 450 | } 451 | // step must be lowercase for octaves above 5 452 | // add apostrophes or commas to get abc_octave 453 | // to the correct value 454 | var o = parsed.octave; 455 | if (o >= 5) { 456 | abc_step = parsed.step.toLowerCase(); 457 | for (; o > 5; o--) { 458 | abc_octave += '\''; 459 | } 460 | } 461 | else { 462 | abc_step = parsed.step.toUpperCase(); 463 | for (; o < 4; o++) { 464 | abc_octave += ','; 465 | } 466 | } 467 | var output = abc_accidental + abc_step + abc_octave; 468 | if (!validateAbcNoteName(output).valid) { 469 | throw new Error('Something went wrong converting scientific to ABC. Output invalid.'); 470 | } 471 | return output; 472 | } 473 | 474 | function validateKeyName(keyName) { 475 | var keyRegex = /^([A-G])(b+|\#+|x+)* ?(m|major|minor)?$/i; 476 | return makeValidation('key', keyRegex, function (captures) { 477 | return { 478 | step: captures[1], 479 | accidental: captures[2] ? captures[2] : '', 480 | quality: captures[3] ? captures[3] : '' 481 | }; 482 | })(keyName); 483 | } 484 | 485 | var Key = /** @class */ (function () { 486 | function Key(keyInput) { 487 | // run input through validation 488 | var parsed = validateKeyName(keyInput).parse(); 489 | if (!parsed) { 490 | throw new Error('Invalid key name: ' + keyInput.toString()); 491 | } 492 | // assign mode based on the parsed input's quality 493 | if (/[a-g]/.test(parsed.step) || parsed.quality === 'minor' || parsed.quality === 'm') { 494 | this.mode = 'minor'; 495 | } 496 | else { 497 | this.mode = 'major'; 498 | } 499 | // now that we have the mode, enforce uppercase for root note 500 | parsed.step = parsed.step.toUpperCase(); 501 | // get fifths for major key 502 | this.fifths = fifths.indexOf(parsed.step + parsed.accidental); 503 | // minor is 3 fifths less than major 504 | if (this.mode === 'minor') { 505 | this.fifths -= 3; 506 | this.name = parsed.step.toLowerCase() + parsed.accidental + ' minor'; 507 | } 508 | else { 509 | this.name = parsed.step + parsed.accidental + ' major'; 510 | } 511 | } 512 | return Key; 513 | }()); 514 | 515 | var Interval = /** @class */ (function () { 516 | function Interval(intervalName) { 517 | var parsed = validateIntervalName(intervalName).parse(); 518 | if (!parsed) { 519 | throw new Error('Invalid interval name.'); 520 | } 521 | this.steps = parsed.size - 1; 522 | var normalizedSize = parsed.size > 7 ? (this.steps % 7) + 1 : parsed.size; 523 | this.name = intervalName; 524 | this.type = 'interval'; 525 | this.quality = parsed.quality; 526 | this.size = parsed.size; 527 | this.normalized = this.quality + normalizedSize.toString(10); 528 | this.species = getIntervalSpecies(normalizedSize); 529 | // this is kinda ugly but it works... 530 | // dividing by 7 evenly returns an extra octave if the value is a multiple of 7 531 | this.octaves = Math.floor(this.size / 7.001); 532 | this.semitones = getIntervalSemitones(this.quality, normalizedSize, this.octaves, this.species); 533 | } 534 | return Interval; 535 | }()); 536 | function getIntervalSemitones(quality, normalizedSize, octaves, species) { 537 | // semitones from root of each note of the major scale 538 | var major = [0, 2, 4, 5, 7, 9, 11]; 539 | // qualityInt represents the integer difference from a major or perfect quality interval 540 | // for example, m3 will yield -1 since a minor 3rd is one semitone less than a major 3rd 541 | var qualityInt = 0; 542 | var q1 = quality.slice(0, 1); 543 | switch (q1) { 544 | case 'P': 545 | case 'M': 546 | break; 547 | case 'm': 548 | qualityInt -= 1; 549 | break; 550 | case 'A': 551 | qualityInt += 1; 552 | break; 553 | case 'd': 554 | if (species === 'M') { 555 | qualityInt -= 2; 556 | } 557 | else { 558 | qualityInt -= 1; 559 | } 560 | break; 561 | } 562 | // handle additional augmentations or diminutions 563 | for (var q = 0; q < quality.slice(1).length; q++) { 564 | if (quality.slice(1)[q] === 'd') { 565 | qualityInt -= 1; 566 | } 567 | else if (quality.slice(1)[q] === 'A') { 568 | qualityInt += 1; 569 | } 570 | } 571 | return major[normalizedSize - 1] + qualityInt + (octaves * 12); 572 | } 573 | // 1,4,5 are treated differently than other interval sizes, 574 | // this helps to identify them immediately 575 | function getIntervalSpecies(size) { 576 | if (size === 1 || size === 4 || size === 5) { 577 | return 'P'; 578 | } 579 | else { 580 | return 'M'; 581 | } 582 | } 583 | 584 | var Pattern = /** @class */ (function () { 585 | function Pattern(intervals) { 586 | this.intervalNames = intervals; 587 | } 588 | Pattern.prototype.from = function (item) { 589 | var note = toObject(item, toNote$1); 590 | return new NoteCollection(this.intervalNames.map(function (d) { 591 | if (d === 'R') 592 | d = 'P1'; 593 | return note.up(d); 594 | })); 595 | }; 596 | return Pattern; 597 | }()); 598 | function toNote$1(item) { 599 | if (isString(item)) { 600 | return new Note(item); 601 | } 602 | else { 603 | return item; 604 | } 605 | } 606 | 607 | var NoteCollection = /** @class */ (function () { 608 | function NoteCollection(noteArray) { 609 | if (noteArray === void 0) { noteArray = []; } 610 | this.array = noteArray.map(function (d) { 611 | return toObject(d, toNote$2); 612 | }); 613 | } 614 | NoteCollection.prototype.contents = function () { 615 | return this.array; 616 | }; 617 | NoteCollection.prototype.each = function (fn) { 618 | this.array.forEach(fn); 619 | return this; 620 | }; 621 | NoteCollection.prototype.contains = function (item) { 622 | var note = toObject(item, toNote$2); 623 | var output = false; 624 | this.each(function (d) { 625 | if (d.isEquivalent(note)) 626 | output = true; 627 | }); 628 | return output; 629 | }; 630 | NoteCollection.prototype.add = function (item) { 631 | var note = toObject(item, toNote$2); 632 | this.array.push(note); 633 | return this; 634 | }; 635 | NoteCollection.prototype.remove = function (item) { 636 | var note = toObject(item, toNote$2); 637 | this.array = this.array.filter(function (d) { 638 | return !d.isEquivalent(note); 639 | }); 640 | return this; 641 | }; 642 | NoteCollection.prototype.map = function (fn) { 643 | return new NoteCollection(this.array.map(fn)); 644 | }; 645 | NoteCollection.prototype.names = function () { 646 | return this.array.map(function (d) { 647 | return d.name; 648 | }); 649 | }; 650 | NoteCollection.prototype.patternFrom = function (item) { 651 | var note = toObject(item, toNote$2); 652 | if (!this.contains(note)) 653 | return new Pattern([]); 654 | var intervals = []; 655 | this.each(function (d) { 656 | intervals.push(new Interval(d.intervalFrom(note))); 657 | }); 658 | intervals.sort(function (a, b) { 659 | return a.size - b.size; 660 | }); 661 | intervals = intervals.map(function (d) { 662 | var name = d.name !== 'P1' ? d.name : 'R'; 663 | return name; 664 | }); 665 | return new Pattern(intervals); 666 | }; 667 | return NoteCollection; 668 | }()); 669 | function toNote$2(string) { 670 | return new Note(string); 671 | } 672 | 673 | function validateChordName(chordName) { 674 | // lets split up this ugly regex 675 | var intro = /^/, root_note = /([A-G](?:b+|\#+|x+)?)/, species = /((?:maj|min|sus|aug|dim|mmaj|m|\-)?(?:\d+)?(?:\/\d+)?)?/, alterations = /((?:(?:add|sus)(?:\d+)|(?:sus|alt)|(?:\#|\+|b|\-)(?:\d+))*)/, bass_slash = /(\/)?/, bass_note = /([A-G](?:b+|\#+|x+)?)?/, outro = /$/; 676 | var chordRegex = new RegExp(intro.source + 677 | root_note.source + 678 | species.source + 679 | alterations.source + 680 | bass_slash.source + 681 | bass_note.source + 682 | outro.source); 683 | return makeValidation('chord', chordRegex, function (captures) { 684 | return { 685 | root: captures[1], 686 | species: captures[2] ? captures[2] : '', 687 | alterations: captures[3] ? captures[3] : '', 688 | slash: captures[4] ? captures[4] : '', 689 | bass: captures[5] ? captures[5] : '' 690 | }; 691 | })(chordName); 692 | } 693 | 694 | function piaCompare(a, b) { 695 | var qualities = ['d', 'm', 'P', 'M', 'A']; 696 | if (a.size < b.size) { 697 | return -1; 698 | } 699 | else if (a.size > b.size) { 700 | return 1; 701 | } 702 | else { 703 | if (qualities.indexOf(a.quality) < qualities.indexOf(b.quality)) { 704 | return -1; 705 | } 706 | else if (qualities.indexOf(a.quality) > qualities.indexOf(b.quality)) { 707 | return 1; 708 | } 709 | else { 710 | return 0; 711 | } 712 | } 713 | } 714 | function isFalse(thing) { 715 | return thing === false; 716 | } 717 | var ParsedIntervalArray = /** @class */ (function () { 718 | function ParsedIntervalArray(intervalArray) { 719 | this.array = []; 720 | for (var i = 0; i < intervalArray.length; i++) { 721 | if (intervalArray[i] === 'R') { 722 | this.array.push({ quality: 'P', size: 1 }); 723 | } 724 | else { 725 | var parsed = validateIntervalName(intervalArray[i]).parse(); 726 | if (!isFalse(parsed)) 727 | this.array.push(parsed); 728 | } 729 | } 730 | } 731 | ParsedIntervalArray.prototype.sort = function () { 732 | return this.array.sort(piaCompare); 733 | }; 734 | ParsedIntervalArray.prototype.add = function (interval) { 735 | var pInterval = validateIntervalName(interval).parse(); 736 | if (!isFalse(pInterval)) { 737 | for (var i = 0; i < this.array.length; i++) { 738 | if (this.array[i].size === pInterval.size && this.array[i].quality === pInterval.quality) { 739 | return; 740 | } 741 | } 742 | this.array.push(pInterval); 743 | this.sort(); 744 | } 745 | }; 746 | ParsedIntervalArray.prototype.remove = function (size) { 747 | // alias is the octave equivalent of size, for instance 748 | // the alias of 2 is 9, alias of 13 is 6 749 | var alias = size <= 7 ? size + 7 : size - 7; 750 | var updated = []; 751 | // add all intervals that are not of the given size or its alias 752 | for (var i = 0; i < this.array.length; i++) { 753 | if (this.array[i].size !== size && this.array[i].size !== alias) { 754 | updated.push(this.array[i]); 755 | } 756 | } 757 | this.array = updated; 758 | }; 759 | ParsedIntervalArray.prototype.update = function (interval) { 760 | var pInterval = validateIntervalName(interval).parse(); 761 | if (!isFalse(pInterval)) { 762 | // remove any intervals of the same size 763 | this.remove(pInterval.size); 764 | // add the new interval 765 | this.array.push(pInterval); 766 | this.sort(); 767 | } 768 | }; 769 | ParsedIntervalArray.prototype.unparse = function () { 770 | this.sort(); 771 | var output = []; 772 | for (var i = 0; i < this.array.length; i++) { 773 | var str = this.array[i].quality + this.array[i].size; 774 | if (str === 'P1') { 775 | output.push('R'); 776 | } 777 | else { 778 | output.push(str); 779 | } 780 | } 781 | return output; 782 | }; 783 | return ParsedIntervalArray; 784 | }()); 785 | var applyAlterations = (function () { 786 | var alteration_regex = /^(?:(?:add|sus|no)(?:\d+)|(?:sus|alt)|(?:n|b|\#|\+|\-)(?:\d+))/; 787 | // applies to alterations of the form (operation)(degree) such as 'b5' or '#9' 788 | var toInterval = function (alteration) { 789 | var valid = /(?:n|b|\#|\+|\-)(?:\d+)/; 790 | if (!valid.test(alteration)) { 791 | return false; 792 | } 793 | var operation = alteration.slice(0, 1); 794 | var degree = alteration.slice(1); 795 | if (operation === '+') { 796 | operation = '#'; 797 | } 798 | if (operation === '-') { 799 | operation = 'b'; 800 | } 801 | if (operation === '#') { 802 | return 'A' + degree; 803 | } 804 | if (operation === 'b') { 805 | if (degree === '5' || degree === '11' || degree === '4') { 806 | return 'd' + degree; 807 | } 808 | else { 809 | return 'm' + degree; 810 | } 811 | } 812 | if (operation === 'n') { 813 | if (degree === '5' || degree === '11' || degree === '4') { 814 | return 'P' + degree; 815 | } 816 | else { 817 | return 'M' + degree; 818 | } 819 | } 820 | }; 821 | /* might want this later 822 | var intervalType = function(parsed_interval) { 823 | if (parsed_interval.quality === 'P' || parsed_interval.quality === 'M') { 824 | return 'natural'; 825 | } else { 826 | return 'altered'; 827 | } 828 | }; 829 | */ 830 | var alterationType = function (alteration) { 831 | if (/sus/.test(alteration)) { 832 | return 'susX'; 833 | } 834 | if (/add/.test(alteration)) { 835 | return 'addX'; 836 | } 837 | if (/no/.test(alteration)) { 838 | return 'noX'; 839 | } 840 | if (/alt/.test(alteration)) { 841 | return 'alt'; 842 | } 843 | return 'binary'; 844 | }; 845 | function getNaturalInterval(size) { 846 | var normalized = size < 8 ? size : size % 7; 847 | if (normalized === 1 || normalized === 4 || normalized === 5) { 848 | return 'P' + size.toString(10); 849 | } 850 | else { 851 | return 'M' + size.toString(10); 852 | } 853 | } 854 | return function (intervalArray, alterations) { 855 | var pia = new ParsedIntervalArray(intervalArray); 856 | var alterationArray = splitStringByPattern(alterations, alteration_regex); 857 | // for each alteration... 858 | for (var a = 0; a < alterationArray.length; a++) { 859 | var thisAlteration = alterationArray[a]; 860 | switch (alterationType(thisAlteration)) { 861 | case 'binary': 862 | var asInterval = toInterval(thisAlteration); 863 | pia.update(asInterval); 864 | break; 865 | case 'susX': 866 | pia.remove(3); 867 | pia.add('P4'); 868 | break; 869 | case 'addX': 870 | var addition = parseInt(thisAlteration.slice(3), 10); 871 | pia.add(getNaturalInterval(addition)); 872 | break; 873 | case 'noX': 874 | var removal = parseInt(thisAlteration.slice(2), 10); 875 | pia.remove(removal); 876 | break; 877 | case 'alt': 878 | pia.update('d5'); 879 | pia.add('A5'); 880 | pia.update('m9'); 881 | pia.add('A9'); 882 | pia.update('m13'); 883 | break; 884 | } 885 | } 886 | return pia.unparse(); 887 | }; 888 | })(); 889 | 890 | var Chord = /** @class */ (function () { 891 | function Chord(chordName) { 892 | var parsed = validateChordName(chordName).parse(); 893 | if (!parsed) { 894 | throw new Error('Invalid chord name.'); 895 | } 896 | var speciesIntervals = getSpeciesIntervals(parsed.species); 897 | var memberIntervals = applyAlterations(speciesIntervals, parsed.alterations); 898 | this.name = chordName; 899 | this.type = 'chord'; 900 | this.root = new Note(parsed.root); 901 | this.formula = parsed.species + parsed.alterations; 902 | this.isSlash = parsed.slash === '/' ? true : false; 903 | this.bass = this.isSlash ? new Note(parsed.bass) : this.root; 904 | this.intervals = memberIntervals; 905 | this.notes = getChordNotes(this.intervals, this.root); 906 | } 907 | Chord.prototype.transpose = function (direction, interval) { 908 | var root = this.root.transpose(direction, interval); 909 | return new Chord(root.name + this.formula); 910 | }; 911 | Chord.prototype.toString = function () { 912 | return '[chord ' + this.name + ']'; 913 | }; 914 | return Chord; 915 | }()); 916 | function getChordNotes(intervals, root) { 917 | var output = []; 918 | output.push(root); 919 | for (var i = 1; i < intervals.length; i++) { 920 | output.push(root.up(intervals[i])); 921 | } 922 | return new NoteCollection(output); 923 | } 924 | var getSpeciesIntervals = (function () { 925 | var basic_types = { 926 | five: ['R', 'P5'], 927 | maj: ['R', 'M3', 'P5'], 928 | min: ['R', 'm3', 'P5'], 929 | aug: ['R', 'M3', 'A5'], 930 | dim: ['R', 'm3', 'd5'], 931 | sus2: ['R', 'M2', 'P5'], 932 | sus4: ['R', 'P4', 'P5'] 933 | }; 934 | var extensions = { 935 | nine: ['M9'], 936 | eleven: ['M9', 'P11'], 937 | thirteen: ['M9', 'P11', 'M13'] 938 | }; 939 | var species_regex = /^(maj|min|mmin|m|aug|dim|alt|sus|\-)?((?:\d+)|(?:6\/9))?$/; 940 | return function getSpeciesIntervals(species) { 941 | // easy stuff 942 | if (species in basic_types) { 943 | return basic_types[species]; 944 | } 945 | if (species === '') { 946 | return basic_types.maj; 947 | } 948 | if (species === '5') { 949 | return basic_types.five; 950 | } 951 | if (species === 'm' || species === '-') { 952 | return basic_types.min; 953 | } 954 | if (species === 'sus') { 955 | return basic_types.sus4; 956 | } 957 | var output = []; 958 | var captures = species_regex.exec(species); 959 | var prefix = captures[1] ? captures[1] : '', degree = captures[2] ? captures[2] : ''; 960 | switch (prefix) { 961 | case '': 962 | if (degree === '6/9') { 963 | output = output.concat(basic_types.maj, ['M6', 'M9']); 964 | } 965 | else { 966 | output = output.concat(basic_types.maj, degree === '6' ? 'M6' : 'm7'); 967 | } 968 | break; 969 | case 'maj': 970 | output = output.concat(basic_types.maj, degree === '6' ? 'M6' : 'M7'); 971 | break; 972 | case 'min': 973 | case 'm': 974 | case '-': 975 | output = output.concat(basic_types.min, degree === '6' ? 'M6' : 'm7'); 976 | break; 977 | case 'aug': 978 | output = output.concat(basic_types.aug, degree === '6' ? 'M6' : 'm7'); 979 | break; 980 | case 'dim': 981 | output = output.concat(basic_types.dim, 'd7'); 982 | break; 983 | case 'mmaj': 984 | output = output.concat(basic_types.min, 'M7'); 985 | break; 986 | default: 987 | break; 988 | } 989 | switch (degree) { 990 | case '9': 991 | output = output.concat(extensions.nine); 992 | break; 993 | case '11': 994 | output = output.concat(extensions.eleven); 995 | break; 996 | case '13': 997 | output = output.concat(extensions.thirteen); 998 | break; 999 | default: 1000 | break; 1001 | } 1002 | return output; 1003 | }; 1004 | })(); 1005 | 1006 | var motive; 1007 | (function (motive) { 1008 | motive.abc = abc; 1009 | motive.key = function (keyInput) { 1010 | return new Key(keyInput); 1011 | }; 1012 | motive.note = function (noteInput) { 1013 | return new Note(noteInput); 1014 | }; 1015 | motive.chord = function (chordInput) { 1016 | return new Chord(chordInput); 1017 | }; 1018 | motive.interval = function (intervalInput) { 1019 | return new Interval(intervalInput); 1020 | }; 1021 | motive.pattern = function (patternInput) { 1022 | return new Pattern(patternInput); 1023 | }; 1024 | motive.noteCollection = function (noteCollectionInput) { 1025 | return new NoteCollection(noteCollectionInput); 1026 | }; 1027 | motive.circles = _circles; 1028 | motive.constructors = { 1029 | Note: Note, 1030 | Interval: Interval, 1031 | Chord: Chord 1032 | }; 1033 | })(motive || (motive = {})); 1034 | var motive$1 = motive; 1035 | 1036 | module.exports = motive$1; 1037 | -------------------------------------------------------------------------------- /dist/motive.umd.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = global || self, global.motive = factory()); 5 | }(this, function () { 'use strict'; 6 | 7 | var operators = { 8 | 'b': -1, 9 | '#': 1, 10 | 'x': 2 11 | }; 12 | var steps = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; 13 | 14 | function accidentalToAlter(accidental) { 15 | if (!accidental) { 16 | return 0; 17 | } 18 | var totalSymbolValue = 0; 19 | // look up the value of each symbol in the parsed accidental 20 | for (var a = 0; a < accidental.length; a++) { 21 | totalSymbolValue += operators[accidental[a]]; 22 | } 23 | // add the total value of the accidental to alter 24 | return totalSymbolValue; 25 | } 26 | function alterToAccidental(alter) { 27 | if (typeof alter === 'undefined') { 28 | throw new Error('Cannot convert alter to accidental, none given.'); 29 | } 30 | if (alter === 0 || alter === null) { 31 | return ''; 32 | } 33 | var accidental = ''; 34 | while (alter < 0) { 35 | accidental += 'b'; 36 | alter += 1; 37 | } 38 | while (alter > 1) { 39 | accidental += 'x'; 40 | alter += -2; 41 | } 42 | while (alter > 0) { 43 | accidental += '#'; 44 | alter += -1; 45 | } 46 | return accidental; 47 | } 48 | function mtof(midi) { 49 | return Math.pow(2, ((midi - 69) / 12)) * 440; 50 | } 51 | 52 | // this makes a validation function for a string type defined by 'name' 53 | function makeValidation(name, exp, parser) { 54 | return function (input) { 55 | if (typeof input !== 'string') { 56 | throw new TypeError('Cannot validate ' + name + '. Input must be a string.'); 57 | } 58 | var validate = function () { 59 | return input.match(exp) ? true : false; 60 | }; 61 | return { 62 | valid: validate(), 63 | parse: function () { 64 | if (!validate()) { 65 | return false; 66 | } 67 | var captures = exp.exec(input); 68 | return parser(captures); 69 | } 70 | }; 71 | }; 72 | } 73 | function splitStringByPattern(str, pattern) { 74 | var output = []; 75 | while (pattern.test(str)) { 76 | var thisMatch = str.match(pattern); 77 | output.push(thisMatch[0]); 78 | str = str.slice(thisMatch[0].length); 79 | } 80 | return output; 81 | } 82 | 83 | function validateNoteName(noteName) { 84 | var noteRegex = /^([A-G])(b+|\#+|x+)?(\-?[0-9]+)?$/; 85 | return makeValidation('note', noteRegex, function (captures) { 86 | return { 87 | step: captures[1], 88 | accidental: captures[2] ? captures[2] : '', 89 | octave: captures[3] ? parseInt(captures[3], 10) : null 90 | }; 91 | })(noteName); 92 | } 93 | 94 | function validateIntervalName(intervalName) { 95 | var intervalRegex = /^(P|M|m|A+|d+)(\d+|U)$/; 96 | return makeValidation('interval', intervalRegex, function (captures) { 97 | return { 98 | quality: captures[1], 99 | size: parseInt(captures[2], 10) 100 | }; 101 | })(intervalName); 102 | } 103 | 104 | var Circle = /** @class */ (function () { 105 | function Circle(array) { 106 | this.array = array; 107 | this.size = array.length; 108 | } 109 | // define functions for simple circular lookup 110 | // most instances will override these functions 111 | // with custom accessors 112 | Circle.prototype.indexOf = function (item) { 113 | return this.array.indexOf(item); 114 | }; 115 | Circle.prototype.atIndex = function (index) { 116 | return this.array[modulo(index, this.size)]; 117 | }; 118 | return Circle; 119 | }()); 120 | function modulo(a, b) { 121 | if (a >= 0) { 122 | return a % b; 123 | } 124 | else { 125 | return ((a % b) + b) % b; 126 | } 127 | } 128 | function mod12(a) { 129 | return modulo(a, 12); 130 | } 131 | 132 | var fifths = new Circle(['F', 'C', 'G', 'D', 'A', 'E', 'B']); 133 | fifths.indexOf = function (noteName) { 134 | var step = noteName[0], accidental = noteName.slice(1), alter = accidentalToAlter(accidental); 135 | var index = this.array.indexOf(step); 136 | index = index + (this.size * alter); 137 | return index - 1; 138 | }; 139 | fifths.atIndex = function (index) { 140 | index = index + 1; 141 | var alter = Math.floor(index / this.array.length), accidental = alterToAccidental(alter); 142 | index = modulo(index, this.size); 143 | return this.array[index] + accidental; 144 | }; 145 | // these values represent the size of intervals arranged by fifths. 146 | // Given 4, each value is value[i] = mod7(value[i-1] + 4) with 147 | // the exception that zero is avoided by setting mod7(7) = 7 148 | var intervals = new Circle([4, 1, 5, 2, 6, 3, 7]); 149 | intervals.indexOf = function (intervalName) { 150 | var parsed = validateIntervalName(intervalName).parse(); 151 | if (!parsed) { 152 | throw new Error('Invalid interval name.'); 153 | } 154 | var quality = parsed.quality, size = parsed.size; 155 | // string to integer, make 'unison' into size 1 156 | // size = size === 'U' ? 1 : parseInt(size, 10); 157 | // normalize large intervals 158 | size = size <= 7 ? size : modulo(size, this.size); 159 | // adjust by -1 since array starts with P4 which is index -1 160 | var size_index = this.array.indexOf(size) - 1; 161 | // now calculate the correct index value based on the interval quality and size 162 | var index, len_A, len_d; 163 | if (quality === 'P' || quality === 'M') { 164 | index = size_index; 165 | } 166 | else if (quality === 'm') { 167 | index = size_index - this.size; 168 | } 169 | else if (quality.match(/A+/)) { 170 | len_A = quality.match(/A+/)[0].length; 171 | index = size_index + (this.size * len_A); 172 | } 173 | else if (quality.match(/d+/)) { 174 | len_d = quality.match(/d+/)[0].length; 175 | if (size === 1 || size === 4 || size === 5) { 176 | index = size_index - (this.size * len_d); 177 | } 178 | else { 179 | index = size_index - (this.size + (this.size * len_d)); 180 | } 181 | } 182 | return index; 183 | }; 184 | intervals.atIndex = function (index) { 185 | // adjustment needed since array starts with P4 which is index -1 186 | var idx = index + 1; 187 | // factor represents the number of trips around the circle needed 188 | // to get to index, and the sign represents the direction 189 | // negative: anticlockwise, positive: clockwise 190 | var factor = Math.floor(idx / this.size); 191 | // mod by the size to normalize the index now that we know the factor 192 | idx = modulo(idx, this.size); 193 | // the size of the resultant interval is now known 194 | var size = this.array[idx].toString(10); 195 | // time to calculate the quality 196 | var quality = ''; 197 | if (factor > 0) { 198 | for (var f = 0; f < factor; f += 1) { 199 | quality += 'A'; 200 | } 201 | } 202 | else if (factor === 0) { 203 | quality = idx < 3 ? 'P' : 'M'; 204 | } 205 | else if (factor === -1) { 206 | quality = idx < 3 ? 'd' : 'm'; 207 | } 208 | else if (factor < -1) { 209 | for (var nf = -1; nf > factor; nf -= 1) { 210 | quality += 'd'; 211 | } 212 | quality += idx < 3 ? 'd' : ''; 213 | } 214 | return quality + size; 215 | }; 216 | var pitchNames = new Circle(['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']); 217 | pitchNames.indexOf = function (member) { 218 | var parsed = validateNoteName(member).parse(); 219 | if (!parsed) { 220 | throw new Error('Invalid pitch name.'); 221 | } 222 | var alter = accidentalToAlter(parsed.accidental); 223 | var step_index = this.array.indexOf(parsed.step); 224 | // return pitch class if no octave given 225 | if (parsed.octave === null) { 226 | return mod12(step_index + alter); 227 | } 228 | return step_index + alter + (this.size * (parsed.octave + 1)); 229 | }; 230 | pitchNames.atIndex = function (index) { 231 | var octave = Math.floor(index / this.size) - 1; 232 | var note_index = mod12(index); 233 | return this.array[note_index] + octave.toString(10); 234 | }; 235 | 236 | var _circles = /*#__PURE__*/Object.freeze({ 237 | fifths: fifths, 238 | intervals: intervals, 239 | pitchNames: pitchNames 240 | }); 241 | 242 | function transpose(note_name, direction, interval) { 243 | if (direction !== 'up' && direction !== 'down') { 244 | throw new Error('Transpose direction must be either "up" or "down".'); 245 | } 246 | var parsed_n = validateNoteName(note_name).parse(); 247 | if (!parsed_n) { 248 | throw new Error('Invalid note name.'); 249 | } 250 | var parsed_i = validateIntervalName(interval).parse(); 251 | if (!parsed_i) { 252 | throw new Error('Invalid interval name.'); 253 | } 254 | var factor = direction === 'up' ? 1 : -1; 255 | var new_note_name = fifths.atIndex(fifths.indexOf(parsed_n.step + parsed_n.accidental) + 256 | (factor * intervals.indexOf(interval))); 257 | // check if octave adjustment is needed 258 | if (parsed_n.octave === null) { 259 | return new_note_name; 260 | } 261 | // octave adjustment 262 | var new_octave = parsed_n.octave + (factor * Math.floor(parsed_i.size / 8)); 263 | var normalized_steps = parsed_i.size > 7 ? (parsed_i.size % 7) - 1 : parsed_i.size - 1; 264 | if ((steps.indexOf(parsed_n.step) + normalized_steps) >= 7) { 265 | new_octave += factor; 266 | } 267 | return new_note_name + new_octave.toString(10); 268 | } 269 | function isString(input) { 270 | return typeof input === 'string'; 271 | } 272 | function isNumber(input) { 273 | return typeof input === 'number'; 274 | } 275 | // ensures that a function requiring a note (or similar type of) object as input 276 | // gets an object rather than a string representation of it. 277 | // 'obj' will be the function used to create the object. 278 | function toObject(input, obj) { 279 | if (isString(input)) { 280 | input = obj(input); 281 | } 282 | if (typeof input !== 'object') { 283 | throw new TypeError('Input must be an object or string.'); 284 | } 285 | return input; 286 | } 287 | 288 | var Note = /** @class */ (function () { 289 | function Note(noteInput) { 290 | var name; 291 | if (isString(noteInput)) { 292 | name = noteInput; 293 | } 294 | else if (isNumber(noteInput)) { 295 | name = pitchNames.atIndex(noteInput); 296 | } 297 | else { 298 | throw new TypeError('Note name must be a string or number.'); 299 | } 300 | var parsed = validateNoteName(name).parse(); 301 | if (!parsed) { 302 | throw new Error('Invalid note name.'); 303 | } 304 | this.name = name; 305 | this.type = 'note'; 306 | this.pitchClass = pitchNames.indexOf(parsed.step + parsed.accidental); 307 | this.parts = { 308 | step: parsed.step, 309 | accidental: parsed.accidental 310 | }; 311 | if (parsed.octave !== null) { 312 | this.setOctave(parsed.octave); 313 | } 314 | } 315 | Note.prototype.setOctave = function (octave) { 316 | if (!isNumber(octave)) { 317 | throw new TypeError('Octave must be a number.'); 318 | } 319 | this.name = this.parts.step + this.parts.accidental; 320 | this.type = 'pitch'; 321 | this.octave = octave; 322 | this.scientific = this.name + octave.toString(10); 323 | this.abc = scientificToAbc(this.scientific); 324 | this.midi = pitchNames.indexOf(this.scientific); 325 | this.frequency = mtof(this.midi); 326 | }; 327 | Note.prototype.isEquivalent = function (other) { 328 | other = toNote(other); 329 | if (this.name !== other.name) { 330 | return false; 331 | } 332 | if (this.type === 'pitch' && other.type === 'pitch' && this.octave !== other.octave) { 333 | return false; 334 | } 335 | return true; 336 | }; 337 | Note.prototype.isEnharmonic = function (other) { 338 | var otherNote = toNote(other); 339 | if (this.pitchClass !== otherNote.pitchClass) { 340 | return false; 341 | } 342 | if (this.type === 'pitch' && otherNote.type === 'pitch' && (Math.abs(this.midi - otherNote.midi) > 11)) { 343 | return false; 344 | } 345 | return true; 346 | }; 347 | Note.prototype.transpose = function (direction, interval) { 348 | return new Note(transpose(this.type === 'pitch' ? this.scientific : this.name, direction, interval)); 349 | }; 350 | Note.prototype.intervalTo = function (note) { 351 | var otherNote = toNote(note); 352 | return intervals.atIndex(fifths.indexOf(otherNote.name) - fifths.indexOf(this.name)); 353 | }; 354 | Note.prototype.intervalFrom = function (note) { 355 | var otherNote = toNote(note); 356 | return intervals.atIndex(fifths.indexOf(this.name) - fifths.indexOf(otherNote.name)); 357 | }; 358 | Note.prototype.up = function (interval) { 359 | return this.transpose('up', interval); 360 | }; 361 | Note.prototype.down = function (interval) { 362 | return this.transpose('down', interval); 363 | }; 364 | Note.prototype.toString = function () { 365 | var name; 366 | if (this.type === 'note') { 367 | name = this.name; 368 | } 369 | else if (this.type === 'pitch') { 370 | name = this.scientific; 371 | } 372 | return '[note ' + name + ']'; 373 | }; 374 | return Note; 375 | }()); 376 | function toNote(input) { 377 | if (isString(input)) { 378 | return new Note(input); 379 | } 380 | else { 381 | return input; 382 | } 383 | } 384 | 385 | function validateAbcNoteName(abcNoteName) { 386 | var abcRegex = /((?:\_|\=|\^)*)([a-g]|[A-G])((?:\,|\')*)/; 387 | return makeValidation('abc-note', abcRegex, function (captures) { 388 | return { 389 | accidental: captures[1] ? captures[1] : '', 390 | step: captures[2], 391 | adjustments: captures[3] ? captures[3] : '' 392 | }; 393 | })(abcNoteName); 394 | } 395 | 396 | function abc(abcInput) { 397 | var sci = abcToScientific(abcInput); 398 | return new Note(sci); 399 | } 400 | var accidentals = { 401 | "_": -1, 402 | "=": 0, 403 | "^": 1 404 | }; 405 | // octave adjustments 406 | var adjustments = { 407 | ",": -1, 408 | "'": 1 409 | }; 410 | function abcToScientific(abcInput) { 411 | var parsed = validateAbcNoteName(abcInput).parse(); 412 | if (!parsed) { 413 | throw new Error('Cannot convert ABC to scientific notation. Invalid ABC note name.'); 414 | } 415 | var step, alter = 0, accidental, octave; 416 | // if parsed step is a capital letter 417 | if (/[A-G]/.test(parsed.step)) { 418 | octave = 4; 419 | } 420 | else { // parsed step is lowercase 421 | octave = 5; 422 | } 423 | // get the total alter value of all accidentals present 424 | for (var c = 0; c < parsed.accidental.length; c++) { 425 | alter += accidentals[parsed.accidental[c]]; 426 | } 427 | // for each comma or apostrophe adjustment, adjust the octave value 428 | for (var d = 0; d < parsed.adjustments.length; d++) { 429 | octave += adjustments[parsed.adjustments[d]]; 430 | } 431 | step = parsed.step.toUpperCase(); 432 | accidental = alterToAccidental(alter); 433 | var output = step + accidental + octave.toString(10); 434 | if (!validateNoteName(output).valid) { 435 | throw new Error('Something went wrong converting ABC to scientific notation. Output invalid.'); 436 | } 437 | return output; 438 | } 439 | function scientificToAbc(scientific) { 440 | var parsed = validateNoteName(scientific).parse(); 441 | if (!parsed || parsed.octave === null) { 442 | throw new Error('Cannot convert scientific to ABC. Invalid scientific note name.'); 443 | } 444 | var abc_accidental = '', abc_step, abc_octave = ''; 445 | var alter = accidentalToAlter(parsed.accidental); 446 | // add abc accidental symbols until alter is consumed (alter === 0) 447 | while (alter < 0) { 448 | abc_accidental += '_'; 449 | alter += 1; 450 | } 451 | while (alter > 0) { 452 | abc_accidental += '^'; 453 | alter -= 1; 454 | } 455 | // step must be lowercase for octaves above 5 456 | // add apostrophes or commas to get abc_octave 457 | // to the correct value 458 | var o = parsed.octave; 459 | if (o >= 5) { 460 | abc_step = parsed.step.toLowerCase(); 461 | for (; o > 5; o--) { 462 | abc_octave += '\''; 463 | } 464 | } 465 | else { 466 | abc_step = parsed.step.toUpperCase(); 467 | for (; o < 4; o++) { 468 | abc_octave += ','; 469 | } 470 | } 471 | var output = abc_accidental + abc_step + abc_octave; 472 | if (!validateAbcNoteName(output).valid) { 473 | throw new Error('Something went wrong converting scientific to ABC. Output invalid.'); 474 | } 475 | return output; 476 | } 477 | 478 | function validateKeyName(keyName) { 479 | var keyRegex = /^([A-G])(b+|\#+|x+)* ?(m|major|minor)?$/i; 480 | return makeValidation('key', keyRegex, function (captures) { 481 | return { 482 | step: captures[1], 483 | accidental: captures[2] ? captures[2] : '', 484 | quality: captures[3] ? captures[3] : '' 485 | }; 486 | })(keyName); 487 | } 488 | 489 | var Key = /** @class */ (function () { 490 | function Key(keyInput) { 491 | // run input through validation 492 | var parsed = validateKeyName(keyInput).parse(); 493 | if (!parsed) { 494 | throw new Error('Invalid key name: ' + keyInput.toString()); 495 | } 496 | // assign mode based on the parsed input's quality 497 | if (/[a-g]/.test(parsed.step) || parsed.quality === 'minor' || parsed.quality === 'm') { 498 | this.mode = 'minor'; 499 | } 500 | else { 501 | this.mode = 'major'; 502 | } 503 | // now that we have the mode, enforce uppercase for root note 504 | parsed.step = parsed.step.toUpperCase(); 505 | // get fifths for major key 506 | this.fifths = fifths.indexOf(parsed.step + parsed.accidental); 507 | // minor is 3 fifths less than major 508 | if (this.mode === 'minor') { 509 | this.fifths -= 3; 510 | this.name = parsed.step.toLowerCase() + parsed.accidental + ' minor'; 511 | } 512 | else { 513 | this.name = parsed.step + parsed.accidental + ' major'; 514 | } 515 | } 516 | return Key; 517 | }()); 518 | 519 | var Interval = /** @class */ (function () { 520 | function Interval(intervalName) { 521 | var parsed = validateIntervalName(intervalName).parse(); 522 | if (!parsed) { 523 | throw new Error('Invalid interval name.'); 524 | } 525 | this.steps = parsed.size - 1; 526 | var normalizedSize = parsed.size > 7 ? (this.steps % 7) + 1 : parsed.size; 527 | this.name = intervalName; 528 | this.type = 'interval'; 529 | this.quality = parsed.quality; 530 | this.size = parsed.size; 531 | this.normalized = this.quality + normalizedSize.toString(10); 532 | this.species = getIntervalSpecies(normalizedSize); 533 | // this is kinda ugly but it works... 534 | // dividing by 7 evenly returns an extra octave if the value is a multiple of 7 535 | this.octaves = Math.floor(this.size / 7.001); 536 | this.semitones = getIntervalSemitones(this.quality, normalizedSize, this.octaves, this.species); 537 | } 538 | return Interval; 539 | }()); 540 | function getIntervalSemitones(quality, normalizedSize, octaves, species) { 541 | // semitones from root of each note of the major scale 542 | var major = [0, 2, 4, 5, 7, 9, 11]; 543 | // qualityInt represents the integer difference from a major or perfect quality interval 544 | // for example, m3 will yield -1 since a minor 3rd is one semitone less than a major 3rd 545 | var qualityInt = 0; 546 | var q1 = quality.slice(0, 1); 547 | switch (q1) { 548 | case 'P': 549 | case 'M': 550 | break; 551 | case 'm': 552 | qualityInt -= 1; 553 | break; 554 | case 'A': 555 | qualityInt += 1; 556 | break; 557 | case 'd': 558 | if (species === 'M') { 559 | qualityInt -= 2; 560 | } 561 | else { 562 | qualityInt -= 1; 563 | } 564 | break; 565 | } 566 | // handle additional augmentations or diminutions 567 | for (var q = 0; q < quality.slice(1).length; q++) { 568 | if (quality.slice(1)[q] === 'd') { 569 | qualityInt -= 1; 570 | } 571 | else if (quality.slice(1)[q] === 'A') { 572 | qualityInt += 1; 573 | } 574 | } 575 | return major[normalizedSize - 1] + qualityInt + (octaves * 12); 576 | } 577 | // 1,4,5 are treated differently than other interval sizes, 578 | // this helps to identify them immediately 579 | function getIntervalSpecies(size) { 580 | if (size === 1 || size === 4 || size === 5) { 581 | return 'P'; 582 | } 583 | else { 584 | return 'M'; 585 | } 586 | } 587 | 588 | var Pattern = /** @class */ (function () { 589 | function Pattern(intervals) { 590 | this.intervalNames = intervals; 591 | } 592 | Pattern.prototype.from = function (item) { 593 | var note = toObject(item, toNote$1); 594 | return new NoteCollection(this.intervalNames.map(function (d) { 595 | if (d === 'R') 596 | d = 'P1'; 597 | return note.up(d); 598 | })); 599 | }; 600 | return Pattern; 601 | }()); 602 | function toNote$1(item) { 603 | if (isString(item)) { 604 | return new Note(item); 605 | } 606 | else { 607 | return item; 608 | } 609 | } 610 | 611 | var NoteCollection = /** @class */ (function () { 612 | function NoteCollection(noteArray) { 613 | if (noteArray === void 0) { noteArray = []; } 614 | this.array = noteArray.map(function (d) { 615 | return toObject(d, toNote$2); 616 | }); 617 | } 618 | NoteCollection.prototype.contents = function () { 619 | return this.array; 620 | }; 621 | NoteCollection.prototype.each = function (fn) { 622 | this.array.forEach(fn); 623 | return this; 624 | }; 625 | NoteCollection.prototype.contains = function (item) { 626 | var note = toObject(item, toNote$2); 627 | var output = false; 628 | this.each(function (d) { 629 | if (d.isEquivalent(note)) 630 | output = true; 631 | }); 632 | return output; 633 | }; 634 | NoteCollection.prototype.add = function (item) { 635 | var note = toObject(item, toNote$2); 636 | this.array.push(note); 637 | return this; 638 | }; 639 | NoteCollection.prototype.remove = function (item) { 640 | var note = toObject(item, toNote$2); 641 | this.array = this.array.filter(function (d) { 642 | return !d.isEquivalent(note); 643 | }); 644 | return this; 645 | }; 646 | NoteCollection.prototype.map = function (fn) { 647 | return new NoteCollection(this.array.map(fn)); 648 | }; 649 | NoteCollection.prototype.names = function () { 650 | return this.array.map(function (d) { 651 | return d.name; 652 | }); 653 | }; 654 | NoteCollection.prototype.patternFrom = function (item) { 655 | var note = toObject(item, toNote$2); 656 | if (!this.contains(note)) 657 | return new Pattern([]); 658 | var intervals = []; 659 | this.each(function (d) { 660 | intervals.push(new Interval(d.intervalFrom(note))); 661 | }); 662 | intervals.sort(function (a, b) { 663 | return a.size - b.size; 664 | }); 665 | intervals = intervals.map(function (d) { 666 | var name = d.name !== 'P1' ? d.name : 'R'; 667 | return name; 668 | }); 669 | return new Pattern(intervals); 670 | }; 671 | return NoteCollection; 672 | }()); 673 | function toNote$2(string) { 674 | return new Note(string); 675 | } 676 | 677 | function validateChordName(chordName) { 678 | // lets split up this ugly regex 679 | var intro = /^/, root_note = /([A-G](?:b+|\#+|x+)?)/, species = /((?:maj|min|sus|aug|dim|mmaj|m|\-)?(?:\d+)?(?:\/\d+)?)?/, alterations = /((?:(?:add|sus)(?:\d+)|(?:sus|alt)|(?:\#|\+|b|\-)(?:\d+))*)/, bass_slash = /(\/)?/, bass_note = /([A-G](?:b+|\#+|x+)?)?/, outro = /$/; 680 | var chordRegex = new RegExp(intro.source + 681 | root_note.source + 682 | species.source + 683 | alterations.source + 684 | bass_slash.source + 685 | bass_note.source + 686 | outro.source); 687 | return makeValidation('chord', chordRegex, function (captures) { 688 | return { 689 | root: captures[1], 690 | species: captures[2] ? captures[2] : '', 691 | alterations: captures[3] ? captures[3] : '', 692 | slash: captures[4] ? captures[4] : '', 693 | bass: captures[5] ? captures[5] : '' 694 | }; 695 | })(chordName); 696 | } 697 | 698 | function piaCompare(a, b) { 699 | var qualities = ['d', 'm', 'P', 'M', 'A']; 700 | if (a.size < b.size) { 701 | return -1; 702 | } 703 | else if (a.size > b.size) { 704 | return 1; 705 | } 706 | else { 707 | if (qualities.indexOf(a.quality) < qualities.indexOf(b.quality)) { 708 | return -1; 709 | } 710 | else if (qualities.indexOf(a.quality) > qualities.indexOf(b.quality)) { 711 | return 1; 712 | } 713 | else { 714 | return 0; 715 | } 716 | } 717 | } 718 | function isFalse(thing) { 719 | return thing === false; 720 | } 721 | var ParsedIntervalArray = /** @class */ (function () { 722 | function ParsedIntervalArray(intervalArray) { 723 | this.array = []; 724 | for (var i = 0; i < intervalArray.length; i++) { 725 | if (intervalArray[i] === 'R') { 726 | this.array.push({ quality: 'P', size: 1 }); 727 | } 728 | else { 729 | var parsed = validateIntervalName(intervalArray[i]).parse(); 730 | if (!isFalse(parsed)) 731 | this.array.push(parsed); 732 | } 733 | } 734 | } 735 | ParsedIntervalArray.prototype.sort = function () { 736 | return this.array.sort(piaCompare); 737 | }; 738 | ParsedIntervalArray.prototype.add = function (interval) { 739 | var pInterval = validateIntervalName(interval).parse(); 740 | if (!isFalse(pInterval)) { 741 | for (var i = 0; i < this.array.length; i++) { 742 | if (this.array[i].size === pInterval.size && this.array[i].quality === pInterval.quality) { 743 | return; 744 | } 745 | } 746 | this.array.push(pInterval); 747 | this.sort(); 748 | } 749 | }; 750 | ParsedIntervalArray.prototype.remove = function (size) { 751 | // alias is the octave equivalent of size, for instance 752 | // the alias of 2 is 9, alias of 13 is 6 753 | var alias = size <= 7 ? size + 7 : size - 7; 754 | var updated = []; 755 | // add all intervals that are not of the given size or its alias 756 | for (var i = 0; i < this.array.length; i++) { 757 | if (this.array[i].size !== size && this.array[i].size !== alias) { 758 | updated.push(this.array[i]); 759 | } 760 | } 761 | this.array = updated; 762 | }; 763 | ParsedIntervalArray.prototype.update = function (interval) { 764 | var pInterval = validateIntervalName(interval).parse(); 765 | if (!isFalse(pInterval)) { 766 | // remove any intervals of the same size 767 | this.remove(pInterval.size); 768 | // add the new interval 769 | this.array.push(pInterval); 770 | this.sort(); 771 | } 772 | }; 773 | ParsedIntervalArray.prototype.unparse = function () { 774 | this.sort(); 775 | var output = []; 776 | for (var i = 0; i < this.array.length; i++) { 777 | var str = this.array[i].quality + this.array[i].size; 778 | if (str === 'P1') { 779 | output.push('R'); 780 | } 781 | else { 782 | output.push(str); 783 | } 784 | } 785 | return output; 786 | }; 787 | return ParsedIntervalArray; 788 | }()); 789 | var applyAlterations = (function () { 790 | var alteration_regex = /^(?:(?:add|sus|no)(?:\d+)|(?:sus|alt)|(?:n|b|\#|\+|\-)(?:\d+))/; 791 | // applies to alterations of the form (operation)(degree) such as 'b5' or '#9' 792 | var toInterval = function (alteration) { 793 | var valid = /(?:n|b|\#|\+|\-)(?:\d+)/; 794 | if (!valid.test(alteration)) { 795 | return false; 796 | } 797 | var operation = alteration.slice(0, 1); 798 | var degree = alteration.slice(1); 799 | if (operation === '+') { 800 | operation = '#'; 801 | } 802 | if (operation === '-') { 803 | operation = 'b'; 804 | } 805 | if (operation === '#') { 806 | return 'A' + degree; 807 | } 808 | if (operation === 'b') { 809 | if (degree === '5' || degree === '11' || degree === '4') { 810 | return 'd' + degree; 811 | } 812 | else { 813 | return 'm' + degree; 814 | } 815 | } 816 | if (operation === 'n') { 817 | if (degree === '5' || degree === '11' || degree === '4') { 818 | return 'P' + degree; 819 | } 820 | else { 821 | return 'M' + degree; 822 | } 823 | } 824 | }; 825 | /* might want this later 826 | var intervalType = function(parsed_interval) { 827 | if (parsed_interval.quality === 'P' || parsed_interval.quality === 'M') { 828 | return 'natural'; 829 | } else { 830 | return 'altered'; 831 | } 832 | }; 833 | */ 834 | var alterationType = function (alteration) { 835 | if (/sus/.test(alteration)) { 836 | return 'susX'; 837 | } 838 | if (/add/.test(alteration)) { 839 | return 'addX'; 840 | } 841 | if (/no/.test(alteration)) { 842 | return 'noX'; 843 | } 844 | if (/alt/.test(alteration)) { 845 | return 'alt'; 846 | } 847 | return 'binary'; 848 | }; 849 | function getNaturalInterval(size) { 850 | var normalized = size < 8 ? size : size % 7; 851 | if (normalized === 1 || normalized === 4 || normalized === 5) { 852 | return 'P' + size.toString(10); 853 | } 854 | else { 855 | return 'M' + size.toString(10); 856 | } 857 | } 858 | return function (intervalArray, alterations) { 859 | var pia = new ParsedIntervalArray(intervalArray); 860 | var alterationArray = splitStringByPattern(alterations, alteration_regex); 861 | // for each alteration... 862 | for (var a = 0; a < alterationArray.length; a++) { 863 | var thisAlteration = alterationArray[a]; 864 | switch (alterationType(thisAlteration)) { 865 | case 'binary': 866 | var asInterval = toInterval(thisAlteration); 867 | pia.update(asInterval); 868 | break; 869 | case 'susX': 870 | pia.remove(3); 871 | pia.add('P4'); 872 | break; 873 | case 'addX': 874 | var addition = parseInt(thisAlteration.slice(3), 10); 875 | pia.add(getNaturalInterval(addition)); 876 | break; 877 | case 'noX': 878 | var removal = parseInt(thisAlteration.slice(2), 10); 879 | pia.remove(removal); 880 | break; 881 | case 'alt': 882 | pia.update('d5'); 883 | pia.add('A5'); 884 | pia.update('m9'); 885 | pia.add('A9'); 886 | pia.update('m13'); 887 | break; 888 | } 889 | } 890 | return pia.unparse(); 891 | }; 892 | })(); 893 | 894 | var Chord = /** @class */ (function () { 895 | function Chord(chordName) { 896 | var parsed = validateChordName(chordName).parse(); 897 | if (!parsed) { 898 | throw new Error('Invalid chord name.'); 899 | } 900 | var speciesIntervals = getSpeciesIntervals(parsed.species); 901 | var memberIntervals = applyAlterations(speciesIntervals, parsed.alterations); 902 | this.name = chordName; 903 | this.type = 'chord'; 904 | this.root = new Note(parsed.root); 905 | this.formula = parsed.species + parsed.alterations; 906 | this.isSlash = parsed.slash === '/' ? true : false; 907 | this.bass = this.isSlash ? new Note(parsed.bass) : this.root; 908 | this.intervals = memberIntervals; 909 | this.notes = getChordNotes(this.intervals, this.root); 910 | } 911 | Chord.prototype.transpose = function (direction, interval) { 912 | var root = this.root.transpose(direction, interval); 913 | return new Chord(root.name + this.formula); 914 | }; 915 | Chord.prototype.toString = function () { 916 | return '[chord ' + this.name + ']'; 917 | }; 918 | return Chord; 919 | }()); 920 | function getChordNotes(intervals, root) { 921 | var output = []; 922 | output.push(root); 923 | for (var i = 1; i < intervals.length; i++) { 924 | output.push(root.up(intervals[i])); 925 | } 926 | return new NoteCollection(output); 927 | } 928 | var getSpeciesIntervals = (function () { 929 | var basic_types = { 930 | five: ['R', 'P5'], 931 | maj: ['R', 'M3', 'P5'], 932 | min: ['R', 'm3', 'P5'], 933 | aug: ['R', 'M3', 'A5'], 934 | dim: ['R', 'm3', 'd5'], 935 | sus2: ['R', 'M2', 'P5'], 936 | sus4: ['R', 'P4', 'P5'] 937 | }; 938 | var extensions = { 939 | nine: ['M9'], 940 | eleven: ['M9', 'P11'], 941 | thirteen: ['M9', 'P11', 'M13'] 942 | }; 943 | var species_regex = /^(maj|min|mmin|m|aug|dim|alt|sus|\-)?((?:\d+)|(?:6\/9))?$/; 944 | return function getSpeciesIntervals(species) { 945 | // easy stuff 946 | if (species in basic_types) { 947 | return basic_types[species]; 948 | } 949 | if (species === '') { 950 | return basic_types.maj; 951 | } 952 | if (species === '5') { 953 | return basic_types.five; 954 | } 955 | if (species === 'm' || species === '-') { 956 | return basic_types.min; 957 | } 958 | if (species === 'sus') { 959 | return basic_types.sus4; 960 | } 961 | var output = []; 962 | var captures = species_regex.exec(species); 963 | var prefix = captures[1] ? captures[1] : '', degree = captures[2] ? captures[2] : ''; 964 | switch (prefix) { 965 | case '': 966 | if (degree === '6/9') { 967 | output = output.concat(basic_types.maj, ['M6', 'M9']); 968 | } 969 | else { 970 | output = output.concat(basic_types.maj, degree === '6' ? 'M6' : 'm7'); 971 | } 972 | break; 973 | case 'maj': 974 | output = output.concat(basic_types.maj, degree === '6' ? 'M6' : 'M7'); 975 | break; 976 | case 'min': 977 | case 'm': 978 | case '-': 979 | output = output.concat(basic_types.min, degree === '6' ? 'M6' : 'm7'); 980 | break; 981 | case 'aug': 982 | output = output.concat(basic_types.aug, degree === '6' ? 'M6' : 'm7'); 983 | break; 984 | case 'dim': 985 | output = output.concat(basic_types.dim, 'd7'); 986 | break; 987 | case 'mmaj': 988 | output = output.concat(basic_types.min, 'M7'); 989 | break; 990 | default: 991 | break; 992 | } 993 | switch (degree) { 994 | case '9': 995 | output = output.concat(extensions.nine); 996 | break; 997 | case '11': 998 | output = output.concat(extensions.eleven); 999 | break; 1000 | case '13': 1001 | output = output.concat(extensions.thirteen); 1002 | break; 1003 | default: 1004 | break; 1005 | } 1006 | return output; 1007 | }; 1008 | })(); 1009 | 1010 | var motive; 1011 | (function (motive) { 1012 | motive.abc = abc; 1013 | motive.key = function (keyInput) { 1014 | return new Key(keyInput); 1015 | }; 1016 | motive.note = function (noteInput) { 1017 | return new Note(noteInput); 1018 | }; 1019 | motive.chord = function (chordInput) { 1020 | return new Chord(chordInput); 1021 | }; 1022 | motive.interval = function (intervalInput) { 1023 | return new Interval(intervalInput); 1024 | }; 1025 | motive.pattern = function (patternInput) { 1026 | return new Pattern(patternInput); 1027 | }; 1028 | motive.noteCollection = function (noteCollectionInput) { 1029 | return new NoteCollection(noteCollectionInput); 1030 | }; 1031 | motive.circles = _circles; 1032 | motive.constructors = { 1033 | Note: Note, 1034 | Interval: Interval, 1035 | Chord: Chord 1036 | }; 1037 | })(motive || (motive = {})); 1038 | var motive$1 = motive; 1039 | 1040 | return motive$1; 1041 | 1042 | })); 1043 | --------------------------------------------------------------------------------