diff --git a/.jshintrc b/.jshintrc index eb1ce46..c7d14b6 100644 --- a/.jshintrc +++ b/.jshintrc @@ -6,25 +6,27 @@ "TeoriaScale", "teoria", - "kNotes", - "kNoteIndex", "kDurations", - "kIntervals", - "kIntervalIndex", - "kQualityLong", - "kQualityTemp", - "kValidQualities", - "kQualityInversion", "kAlterations", "kSymbols", "kChordShort", - "kAccidentalSign", - "kAccidentalValue", "kStepNumber", "kIntervalSolfege", + "kQualityLong", - "getDistance", - "pad" + "pad", + "sum", + "mul", + "add", + "sub", + "A4", + "accidentals", + "sharp", + "fifths", + "notes", + "intervals", + "intervalFromFifth", + "intervalsIndex" ], "node": true, @@ -34,7 +36,6 @@ "maxlen": 80, "strict": false, "devel": true, - "curly": true, "newcap": true, "noempty": true, "nonew": true, @@ -46,5 +47,6 @@ "latedef": true, "undef": true, "evil": false, - "unused": true + "unused": true, + "laxcomma": true } diff --git a/README.md b/README.md index 6bdb727..85a5e67 100755 --- a/README.md +++ b/README.md @@ -116,14 +116,15 @@ teoria.note('a') // Create a note, A3 Documentation ------------------------ - -## TeoriaNote(name[, duration]) - - This function construct a teoria.note object. +## teoria.note (name | coord[, duration]) *name* - The name argument is the note name as a string. The note can both be expressed in scientific and Helmholtz notation. Some examples of valid note names: `Eb4`, `C#,,`, `C4`, `d#''`, `Ab2` +*coord* - If the first argument isn't a string, but a coord array, +it will instantiate a `TeoriaNote` instance. + *duration* - The duration argument is an optional `object` argument. The object has two also optional parameters: @@ -132,12 +133,6 @@ The object has two also optional parameters: - `dots` - The number of dots attached to the note. Defaults to `0`. -### teoria.note (TeoriaNote) - -The teoria.note object is teoria's interpretation and representation of a -musical note. When calling teoria.note you're actually instantiating a -`TeoriaNote` object. - ### teoria.note.fromKey(key) A static method that returns an instance of TeoriaNote set to the note at the given 88 key piano position, where A0 is key number 1. @@ -156,23 +151,29 @@ A static method returns an object containing two elements: *note* - A number ranging from 0-127 representing a MIDI note value -#### TeoriaNote.name +### teoria.note.fromString(note) + - Returns an instance of TeoriaNote representing the note name + +*note* - The name argument is the note name as a string. The note can both +be expressed in scientific and Helmholtz notation. +Some examples of valid note names: `Eb4`, `C#,,`, `C4`, `d#''`, `Ab2` + +#### TeoriaNote.name() - The name of the note, in lowercase letter (*only* the name, not the accidental signs) -#### TeoriaNote.octave +#### TeoriaNote.octave() - The numeric value of the octave of the note #### TeoriaNote.duration - The duration object as described in the constructor for TeoriaNote -#### TeoriaNote.accidental - - An object containing two elements: +#### TeoriaNote.accidental() + - Returns the string symbolic of the accidental sign (`x`, `#`, `b` or `bb`) -*sign* - The string symbolic of the accidental sign `#`, `x`, `b` or `bb` - -*value* - The numeric value (mostly used internally) of the sign: -`# = 1, x = 2, b = -1, bb = -2` +#### TeoriaNote.accidentalValue() + - Returns the numeric value (mostly used internally) of the sign: +`x = 2, # = 1, b = -1, bb = -2` #### TeoriaNote#key([whitenotes]) - Returns the piano key number. Fx A4 would return 49 @@ -383,8 +384,8 @@ absolute intervals that defines the scale. The default supported scales are: ### teoria.scale(tonic, scale) - Sugar function for constructing a new `TeoriaScale` object -### TeoriaScale.notes - - An array of `TeoriaNote`s which is the scale's notes +### TeoriaScale.notes() + - Returns an array of `TeoriaNote`s which is the scale's notes ### TeoriaScale.name - The name of the scale (if available). Type `string` or `undefined` @@ -424,28 +425,21 @@ scale step. Example 'first', 'second', 'fourth', 'seventh'. - A sugar function for the `#from` and `#between` methods of the same namespace and for creating `TeoriaInterval` objects. -#### teoria.interval(`string`: from[, `string`: direction]) - - Returns a `TeoriaInterval` object, with the given interval - -*from* - An interval in "simple-format" such as 'M2' for major second, and 'P5' for perfect fifth. Look further down for more details on this format. +#### teoria.interval(`string`: from) + - A sugar method for the `teoria.interval.toCoord` function -*direction* - An optional direction string, either `'up'` or `'down'`. Defaults to `'up'` - -#### teoria.interval(`TeoriaNote`: from, `string`: to[, `string`: direction) +#### teoria.interval(`TeoriaNote`: from, `string`: to) - A sugar method for the `teoria.interval.from` function #### teoria.interval(`TeoriaNote`: from, `TeoriaNote`: to) - A sugar method for the `teoria.interval.between` function -#### teoria.interval.from(from, to[, direction]) +#### teoria.interval.from(from, to) - Returns a note which lies a given interval away from a root note. *from* - The `TeoriaNote` which is the root of the measuring -*to* - An interval in "simple-format" such as 'M2' for -major second, and 'P5' for perfect fifth. - -*direction* - An optional direction string, either `'up'` or `'down'`. Defaults to `'up'` +*to* - A `TeoriaInterval` #### teoria.interval.between(from, to) - Returns an interval object which represents the interval between two notes. @@ -458,6 +452,9 @@ interval object would represent a minor third. teoria.interval.between(teoria.note("a"), teoria.note("c'")) -> teoria.interval('m3') ``` +#### teoria.interval.toCoord(simpleInterval) + - Returns a `TeoriaInterval` representing the interval expressed in string form. + #### teoria.interval.invert(simpleInterval) - Returns the inversion of the interval provided @@ -476,21 +473,34 @@ The number may be prefixed with a `-` to signify that its direction is down. E.g `m-3` means a descending minor third, and `P-5` means a descending perfect fifth. -## TeoriaInterval(intervalNumber, quality[, direction]) +## TeoriaInterval(coord) - A representation of a music interval -#### TeoriaInterval.interval +#### TeoriaInterval.coord + - The interval representation of the interval + +#### TeoriaInterval.number() - The interval number (A ninth = 9, A seventh = 7, fifteenth = 15) -#### TeoriaInterval.simpleIntervalType - - The type of interval (mostly used internally) +#### TeoriaInterval.value() + - The value of the interval - That is a ninth = 9, but a downwards ninth is = -9 + +#### TeoriaInterval.base() + - Returns the name of the simple interval (not compound) -#### TeoriaInterval.quality - - The quality of the interval (`'diminished'`, `'minor'`, `'perfect'`, `'major'` - or `'augmented'`) +#### TeoriaInterval.type() + - Returns the type of array, either `'perfect'` (1, 4, 5, 8) or `'minor'` (2, 3, 6, 7) -#### TeoriaInterval.direction - - The direction of the interval (defaults to `'up'`) +#### TeoriaInterval.quality([long]) + - The quality of the interval (`'dd'`, `'d'` `'m'`, `'p'`, `'M'`, `'A'` or `'AA'`) + + *long* is set to a truish value, then long quality names are returned: + `'doubly diminished'`, `'diminished'`, `'minor'`, etc. + +#### TeoriaInterval.direction([newDirection]) + - The direction of the interval + +*newDirection* - If supplied, then the interval's direction is changed #### TeoriaInterval#semitones() - Returns the `number` of semitones the interval span. @@ -523,6 +533,9 @@ teoria.interval('m23').compound() === 'm23' teoria.interval('P5').compound() === 'P5' ``` +#### TeoriaInterval#octaves() + - Returns the number of compound intervals + #### TeoriaInterval#isCompound() - Returns a boolean value, showing if the interval is a compound interval diff --git a/package.json b/package.json index 8c2850c..8c62470 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "teoria", - "version": "0.2.3", + "version": "0.3.0", "description": "Music theory for JavaScript", "homepage": "http://saebekassebil.github.com/teoria", "keywords": ["music", "theory", "jazz", "classical", "chord"], diff --git a/src/chord.js b/src/chord.js index 972c6a2..2c96a11 100644 --- a/src/chord.js +++ b/src/chord.js @@ -1,10 +1,6 @@ function TeoriaChord(root, name) { - if (!(root instanceof TeoriaNote)) { - return null; - } - name = name || ''; - this.name = root.name.toUpperCase() + root.accidental.sign + name; + this.name = root.name().toUpperCase() + root.accidental() + name; this.symbol = name; this.root = root; this.intervals = []; @@ -220,26 +216,22 @@ function TeoriaChord(root, name) { } if (bass) { - var intervals = this.intervals, bassInterval, inserted = 0, note; + var intervals = this.intervals, bassInterval, note; // Make sure the bass is atop of the root note - note = teoria.note(bass + (root.octave + 1)); + note = teoria.note(bass + (root.octave() + 1)); bassInterval = teoria.interval.between(root, note); - bass = bassInterval.simpleInterval; + bass = bassInterval.simple(); - if (bassInterval.direction === 'up') { - bassInterval = bassInterval.invert(); - bassInterval.direction = 'down'; - } + bassInterval = bassInterval.invert(); + bassInterval.direction('down'); this._voicing = [bassInterval]; for (i = 0; i < length; i++) { - if (intervals[i].interval === bass) { + if (intervals[i].simple() === bass) continue; - } - inserted++; - this._voicing[inserted] = intervals[i]; + this._voicing.push(intervals[i]); } } } @@ -304,11 +296,11 @@ TeoriaChord.prototype = { var third, fifth, seventh, intervals = this.intervals; for (var i = 0, length = intervals.length; i < length; i++) { - if (intervals[i].interval === 3) { + if (intervals[i].number() === 3) { third = intervals[i]; - } else if (intervals[i].interval === 5) { + } else if (intervals[i].number() === 5) { fifth = intervals[i]; - } else if (intervals[i].interval === 7) { + } else if (intervals[i].number() === 7) { seventh = intervals[i]; } } @@ -317,7 +309,7 @@ TeoriaChord.prototype = { return; } - third = (third.direction === 'down') ? third.invert() : third; + third = (third.direction() === 'down') ? third.invert() : third; third = third.simple(); if (fifth) { @@ -359,10 +351,10 @@ TeoriaChord.prototype = { for (i = 0; i < length; i++) { interval = this.intervals[i]; invert = interval.invert(); - if (interval.simpleIntervalType.name in has) { - has[interval.simpleIntervalType.name] = true; - } else if (invert.simpleIntervalType.name in has) { - has[invert.simpleIntervalType.name] = true; + if (interval.base() in has) { + has[interval.base()] = true; + } else if (invert.base() in has) { + has[invert.base()] = true; } } @@ -372,10 +364,10 @@ TeoriaChord.prototype = { for (i = 0; i < length; i++) { interval = this.intervals[i]; invert = interval.invert(); - if (interval.simpleIntervalType.name in has) { - has[interval.simpleIntervalType.name] = true; - } else if (invert.simpleIntervalType.name in has) { - has[invert.simpleIntervalType.name] = true; + if (interval.base() in has) { + has[interval.base()] = true; + } else if (invert.base() in has) { + has[invert.base()] = true; } } @@ -393,7 +385,7 @@ TeoriaChord.prototype = { interval = kStepNumber[interval]; for (i = 0, length = intervals.length; i < length; i++) { - if (intervals[i].interval === interval) { + if (intervals[i].number() === interval) { return teoria.interval.from(this.root, intervals[i]); } } @@ -411,8 +403,8 @@ TeoriaChord.prototype = { transpose: function(interval, direction) { this.root.transpose(interval, direction); - this.name = this.root.name.toUpperCase() + - this.root.accidental.sign + this.symbol; + this.name = this.root.name().toUpperCase() + + this.root.accidental() + this.symbol; return this; }, diff --git a/src/core.js b/src/core.js index 248c0ff..aebc8dd 100644 --- a/src/core.js +++ b/src/core.js @@ -5,55 +5,67 @@ // Copyright Jakob Miland (saebekassebil) // Teoria may be freely distributed under the MIT License. -(function teoriaClosure() { +(function teoriaScope() { 'use strict'; var teoria = {}; - var kNotes = { - 'c': { - name: 'c', - distance: 0, - index: 0 - }, - 'd': { - name: 'd', - distance: 2, - index: 1 - }, - 'e': { - name: 'e', - distance: 4, - index: 2 - }, - 'f': { - name: 'f', - distance: 5, - index: 3 - }, - 'g': { - name: 'g', - distance: 7, - index: 4 - }, - 'a': { - name: 'a', - distance: 9, - index: 5 - }, - 'b': { - name: 'b', - distance: 11, - index: 6 - }, - 'h': { - name: 'h', - distance: 11, - index: 6 - } + function add(note, interval) { + return [note[0] + interval[0], note[1] + interval[1]]; + } + + function sub(note, interval) { + return [note[0] - interval[0], note[1] - interval[1]]; + } + + function mul(note, interval) { + if (typeof interval === 'number') + return [note[0] * interval, note[1] * interval]; + else + return [note[0] * interval[0], note[1] * interval[1]]; + } + + function sum(coord) { + return coord[0] + coord[1]; + } + + // Note coordinates [octave, fifth] relative to C + var notes = { + c: [0, 0], + d: [-1, 2], + e: [-2, 4], + f: [1, -1], + g: [0, 1], + a: [-1, 3], + b: [-2, 5], + h: [-2, 5] }; - var kNoteIndex = ['c', 'd', 'e', 'f', 'g', 'a', 'b']; + var intervals = { + unison: [0, 0], + second: [3, -5], + third: [2, -3], + fourth: [1, -1], + fifth: [0, 1], + sixth: [3, -4], + seventh: [2, -2], + octave: [1, 0] + }; + + var intervalFromFifth = ['second', 'sixth', 'third', 'seventh', 'fourth', + 'unison', 'fifth']; + + var intervalsIndex = ['unison', 'second', 'third', 'fourth', 'fifth', + 'sixth', 'seventh', 'octave', 'ninth', 'tenth', + 'eleventh', 'twelfth', 'thirteenth', 'fourteenth', + 'fifteenth']; + + // linaer index to fifth = (2 * index + 1) % 7 + var fifths = ['f', 'c', 'g', 'd', 'a', 'e', 'b']; + var accidentals = ['bb', 'b', '', '#', 'x']; + + var sharp = [-4, 7]; + var A4 = add(notes.a, [4, 0]); var kDurations = { '0.25': 'longa', @@ -68,108 +80,19 @@ '128': 'hundred-twenty-eighth' }; - var kIntervals = [{ - name: 'first', - quality: 'perfect', - size: 0 - }, { - name: 'second', - quality: 'minor', - size: 1 - }, { - name: 'third', - quality: 'minor', - size: 3 - }, { - name: 'fourth', - quality: 'perfect', - size: 5 - }, { - name: 'fifth', - quality: 'perfect', - size: 7 - }, { - name: 'sixth', - quality: 'minor', - size: 8 - }, { - name: 'seventh', - quality: 'minor', - size: 10 - }, { - name: 'octave', - quality: 'perfect', - size: 12 - }]; - - var kIntervalIndex = { - 'first': 0, 'second': 1, 'third': 2, 'fourth': 3, - 'fifth': 4, 'sixth': 5, 'seventh': 6, 'octave': 7, - 'ninth': 8, 'tenth': 9, 'eleventh': 10, 'twelfth': 11, - 'thirteenth': 12, 'fourteenth': 13, 'fifteenth': 14 - }; - var kQualityLong = { - 'P': 'perfect', - 'M': 'major', - 'm': 'minor', - '-': 'minor', - 'A': 'augmented', - '+': 'augmented', - 'AA': 'doubly augmented', - 'd': 'diminished', - 'dd': 'doubly diminished', - - 'min': 'minor', - 'aug': 'augmented', - 'dim': 'diminished' - }; - - var kQualityTemp = { - 'perfect': 'P', - 'major': 'M', - 'minor': 'm', - 'augmented': 'A', - 'doubly augmented': 'AA', - 'diminished': 'd', - 'doubly diminished': 'dd' - }; - - var kValidQualities = { - perfect: { - 'doubly diminished': -2, - diminished: -1, - perfect: 0, - augmented: 1, - 'doubly augmented': 2 - }, - - minor: { - 'doubly diminished': -2, - diminished: -1, - minor: 0, - major: 1, - augmented: 2, - 'doubly augmented': 3 - } - }; - - var kQualityInversion = { - 'perfect': 'perfect', - 'major': 'minor', - 'minor': 'major', - 'augmented': 'diminished', - 'doubly augmented': 'doubly diminished', - 'diminished': 'augmented', - 'doubly diminished': 'doubly augmented' + P: 'perfect', + M: 'major', + m: 'minor', + A: 'augmented', + AA: 'doubly augmented', + d: 'diminished', + dd: 'doubly diminished' }; var kAlterations = { - perfect: ['doubly diminished', 'diminished', 'perfect', - 'augmented', 'doubly augmented'], - - minor: ['doubly diminished', 'diminished', 'minor', - 'major', 'augmented', 'doubly augmented'] + perfect: ['dd', 'd', 'P', 'A', 'AA'], + minor: ['dd', 'd', 'm', 'M', 'A', 'AA'] }; var kSymbols = { @@ -203,30 +126,16 @@ 'dominant': '7' }; - var kAccidentalSign = { - '-2': 'bb', - '-1': 'b', - '0': '', - '1': '#', - '2': 'x' - }; - - var kAccidentalValue = { - 'bb': -2, - 'b': -1, - '#': 1, - 'x': 2 - }; - var kStepNumber = { + 'unison': 1, 'first': 1, - 'tonic': 1, 'second': 2, 'third': 3, 'fourth': 4, 'fifth': 5, 'sixth': 6, 'seventh': 7, + 'octave': 8, 'ninth': 9, 'eleventh': 11, 'thirteenth': 13 @@ -280,18 +189,6 @@ 'A8': 'di', 'AA8': 'dai' }; - /** - * getDistance, returns the distance in semitones between two notes - */ - function getDistance(from, to) { - from = kNotes[from]; - to = kNotes[to]; - if (from.distance > to.distance) { - return (to.distance + 12) - from.distance; - } else { - return to.distance - from.distance; - } - } function pad(str, ch, len) { for (; len > 0; len--) { @@ -304,21 +201,20 @@ // teoria.note namespace - All notes should be instantiated // through this function. teoria.note = function(name, duration) { - return new TeoriaNote(name, duration); + if (typeof name === 'string') + return teoria.note.fromString(name, duration); + else + return new TeoriaNote(name, duration); }; teoria.note.fromKey = function(key) { var octave = Math.floor((key - 4) / 12); var distance = key - (octave * 12) - 4; - var note = kNotes[kNoteIndex[Math.round(distance / 2)]]; - var name = note.name; - if (note.distance < distance) { - name += '#'; - } else if (note.distance > distance) { - name += 'b'; - } + var name = fifths[(2 * Math.round(distance / 2) + 1) % 7]; + var note = add(sub(notes[name], A4), [octave + 1, 0]); + var diff = (key - 49) - sum(mul(note, [12, 7])); - return teoria.note(name + (octave + 1)); + return teoria.note(diff ? add(note, mul(sharp, diff)) : note); }; teoria.note.fromFrequency = function(fq, concertPitch) { @@ -330,13 +226,54 @@ originalFq = concertPitch * Math.pow(2, (key - 49) / 12); cents = 1200 * (Math.log(fq / originalFq) / Math.log(2)); - return {note: teoria.note.fromKey(key), cents: cents}; + return { note: teoria.note.fromKey(key), cents: cents }; }; teoria.note.fromMIDI = function(note) { return teoria.note.fromKey(note - 20); }; + teoria.note.fromString = function(name, dur) { + var scientific = /^([a-h])(x|#|bb|b?)(-?\d*)/i + , helmholtz = /^([a-h])(x|#|bb|b?)([,\']*)$/i + , parser, noteName, octave, accidental, note, lower; + + // Try scientific notation first + parser = name.match(scientific); + if (parser && name === parser[0] && parser[3].length) { + noteName = parser[1]; + octave = +parser[3]; + } else { + name = name.replace(/\u2032/g, "'").replace(/\u0375/g, ','); + + parser = name.match(helmholtz); + if (!parser || name !== parser[0]) + throw new Error('Invalid note format'); + + noteName = parser[1]; + octave = parser[3]; + lower = noteName === noteName.toLowerCase(); + + if (!octave.length) + octave = lower ? 3 : 2; + else if (octave.match(/^'+$/) && lower) + octave = 3 + octave.length; + else if (octave.match(/^,+$/) && !lower) + octave = 2 - octave.length; + else + throw new Error('Format must respect the Helmholtz format'); + } + + accidental = parser[2].length ? parser[2].toLowerCase() : ''; + noteName = noteName.toLowerCase(); + + note = [notes[noteName][0], notes[noteName][1]]; + note = add(note, [octave, 0]); + note = add(note, mul(sharp, accidentals.indexOf(accidental) - 2)); + + return new TeoriaNote(sub(note, A4), dur); + }; + // teoria.chord namespace - All chords should be instantiated // through this function. teoria.chord = function(name, symbol) { @@ -348,9 +285,8 @@ return new TeoriaChord(teoria.note(root[0].toLowerCase() + octave), name.substr(root[0].length)); } - } else if (name instanceof TeoriaNote) { - return new TeoriaChord(name, symbol || ''); - } + } else if (name instanceof TeoriaNote) + return new TeoriaChord(name, symbol); throw new Error('Invalid Chord. Couldn\'t find note name'); }; @@ -361,105 +297,66 @@ * Sugar function for #from and #between methods, with the possibility to * declare a interval by its string name: P8, M3, m7 etc. */ - teoria.interval = function(from, to, direction) { + teoria.interval = function(from, to) { var quality, intervalNumber, interval, match; // Construct a TeoriaInterval object from string representation - if (typeof from === 'string') { - match = from.match(/^(AA|A|P|M|m|d|dd)(-?\d+)$/); - if (!match) { - throw new Error('Invalid string-interval format'); - } + if (typeof from === 'string') + return teoria.interval.toCoord(from); - quality = kQualityLong[match[1]]; - intervalNumber = parseInt(match[2], 10); + if (typeof to === 'string' && from instanceof TeoriaNote) + return teoria.interval.from(from, teoria.interval.toCoord(to)); - // Uses the second argument 'to', as direction - direction = to === 'down' || intervalNumber < 0 ? 'down' : 'up'; - - return new TeoriaInterval(Math.abs(intervalNumber), quality, direction); - } - - if (typeof to === 'string' && from instanceof TeoriaNote) { - interval = teoria.interval(to, direction); - - return teoria.interval.from(from, interval); - } else if (to instanceof TeoriaNote && from instanceof TeoriaNote) { + if (to instanceof TeoriaNote && from instanceof TeoriaNote) return teoria.interval.between(from, to); - } else { - throw new Error('Invalid parameters'); - } + + throw new Error('Invalid parameters'); }; - /** - * Returns the note from a given note (from), with a given interval (to) - */ - teoria.interval.from = function(from, to) { - var note, diff, octave, index, dist, intval, dir; - dir = (to.direction === 'down') ? -1 : 1; + teoria.interval.toCoord = function(simple) { + var pattern = /^(AA|A|P|M|m|d|dd)(-?\d+)$/ + , parser, number, coord, quality, lower, octaves, base, type, alt, down; - intval = to.simpleInterval - 1; - intval = dir * intval; + parser = simple.match(pattern); + if (!parser) + throw new Error('Invalid simple format interval'); - index = kNotes[from.name].index + intval; + quality = parser[1]; + number = +parser[2]; + down = number < 0; + number = down ? -number : number; - if (index > kNoteIndex.length - 1) { - index = index - kNoteIndex.length; - } else if (index < 0) { - index = index + kNoteIndex.length; - } + lower = number > 8 ? ((number % 7) ? number % 7 : 7) : number; + octaves = (number - lower) / 7; - note = kNoteIndex[index]; - dist = getDistance(from.name, note); + base = intervals[intervalsIndex[lower - 1]]; + coord = add(base, [octaves, 0]); - if (dir > 0) { - diff = to.simpleIntervalType.size + to.qualityValue() - dist; - } else { - diff = getDistance(note, from.name) - - (to.simpleIntervalType.size + to.qualityValue()); + type = base[0] <= 1 ? 'perfect' : 'minor'; + if ((type === 'perfect' && (quality === 'M' || quality === 'm')) || + (type === 'minor' && quality === 'P')) { + throw new Error('Invalid interval quality'); } - diff += from.accidental.value; - octave = Math.floor((from.key() - from.accidental.value + dist - 4) / 12); - octave += 1 + dir * to.compoundOctaves; + alt = kAlterations[type].indexOf(quality) - 2; + coord = add(coord, mul(sharp, alt)); + coord = down ? mul(coord, -1) : coord; - if (diff >= 10) { - diff -= 12; - } else if (diff <= -10) { - diff += 12; - } - - if (to.simpleInterval === 8) { - octave += dir; - } else if (dir < 0) { - octave--; - } + return new TeoriaInterval(coord); + }; - note += kAccidentalSign[diff]; - return teoria.note(note + octave.toString(10)); + /** + * Returns the note from a given note (from), with a given interval (to) + */ + teoria.interval.from = function(from, to) { + return new TeoriaNote(add(from.coord, to.coord)); }; /** * Returns the interval between two instances of teoria.note */ teoria.interval.between = function(from, to) { - var semitones, interval, intervalInt, quality, - alteration, direction = 'up', dir = 1; - - semitones = to.key() - from.key(); - intervalInt = to.key(true) - from.key(true); - - if (intervalInt < 0) { - intervalInt = -intervalInt; - direction = 'down'; - dir = -1; - } - - interval = kIntervals[intervalInt % 7]; - alteration = kAlterations[interval.quality]; - quality = alteration[(dir * semitones - interval.size + 2) % 12]; - - return new TeoriaInterval(intervalInt + 1, quality, direction); + return new TeoriaInterval(sub(to.coord, from.coord)); }; teoria.interval.invert = function(sInterval) { @@ -468,9 +365,8 @@ // teoria.scale namespace - Scales are constructed through this function. teoria.scale = function(tonic, scale) { - if (!(tonic instanceof TeoriaNote)) { + if (!(tonic instanceof TeoriaNote)) tonic = teoria.note(tonic); - } return new TeoriaScale(tonic, scale); }; @@ -489,14 +385,13 @@ teoria.TeoriaInterval = TeoriaInterval; if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { + if (typeof module !== 'undefined' && module.exports) exports = module.exports = teoria; - } + exports.teoria = teoria; - } else if (typeof this !== 'undefined') { + } else if (typeof this !== 'undefined') this.teoria = teoria; - } else if (typeof window !== 'undefined') { + else if (typeof window !== 'undefined') window.teoria = teoria; - } })(); diff --git a/src/interval.js b/src/interval.js index 7867183..d44ed19 100644 --- a/src/interval.js +++ b/src/interval.js @@ -1,74 +1,113 @@ -function TeoriaInterval(intervalNum, quality, direction) { - var simple = (intervalNum >= 8 && intervalNum % 7 === 1) ? - intervalNum % 7 * 8 : ((intervalNum - 1) % 7) + 1; - var compoundOctaves = Math.ceil((intervalNum - simple) / 8); - var simpleIntervalType = kIntervals[simple - 1]; - - - if (!(quality in kValidQualities[simpleIntervalType.quality])) { - throw new Error('Invalid interval quality'); - } - - this.interval = intervalNum; - this.quality = quality; - this.direction = direction === 'down' ? 'down' : 'up'; - this.simpleInterval = simple; - this.simpleIntervalType = simpleIntervalType; - this.compoundOctaves = compoundOctaves; +function TeoriaInterval(coord) { + this.coord = coord; } TeoriaInterval.prototype = { + name: function() { + return intervalsIndex[this.number() - 1]; + }, + semitones: function() { - return this.simpleIntervalType.size + this.qualityValue() + - this.compoundOctaves * 12; + return sum(mul(this.coord, [12, 7])); + }, + + number: function() { + return Math.abs(this.value()); + }, + + value: function() { + var without = sub(this.coord, + mul(sharp, Math.floor((this.coord[1] - 2) / 7) + 1)) + , i, val; + + i = intervalFromFifth[without[1] + 5]; + val = kStepNumber[i] + (without[0] - intervals[i][0]) * 7; + + return (val > 0) ? val : val - 2; + }, + + type: function() { + return intervals[this.base()][0] <= 1 ? 'perfect' : 'minor'; + }, + + base: function() { + var fifth = sub(this.coord, mul(sharp, this.qualityValue()))[1], name; + fifth = this.value() > 0 ? fifth + 5 : -(fifth - 5) % 7; + + name = intervalFromFifth[fifth]; + if (name === 'unison' && this.number() >= 8) + name = 'octave'; + + return name; + }, + + direction: function(dir) { + if (dir) + this.coord = mul(this.coord, -1); + else + return this.semitones() >= 0 ? 'up' : 'down'; }, simple: function(ignore) { - var intval = this.simpleInterval; - intval = (this.direction === 'down' && !ignore) ? -intval : intval; + var number = this.value(); + number = number > 8 || number < -8 ? + ((number % 7) ? number % 7 : 7) : number; - return kQualityTemp[this.quality] + intval.toString(); + return this.quality() + (ignore ? Math.abs(number) : number); }, compound: function(ignore) { - var intval = this.simpleInterval + this.compoundOctaves * 7; - intval = (this.direction === 'down' && !ignore) ? -intval : intval; + var number = ignore ? this.number() : this.value(); - return kQualityTemp[this.quality] + intval.toString(); + return this.quality() + number; }, isCompound: function() { - return this.compoundOctaves > 0; + return this.number() > 8; + }, + + octaves: function() { + var without = sub(this.coord, mul(sharp, this.qualityValue())); + var octaves = without[0] - intervals[this.base()][0]; + + return octaves; }, invert: function() { - var intervalNumber = this.simpleInterval; + var i = this.base(); + var qual = this.qualityValue(); + var acc = this.type() === 'minor' ? -(qual - 1) : -qual; + var coord = intervals[intervalsIndex[9 - kStepNumber[i] - 1]]; + coord = add(coord, mul(sharp, acc)); - intervalNumber = 9 - intervalNumber; + return new TeoriaInterval(coord); + }, + + quality: function(lng) { + var quality = kAlterations[this.type()][this.qualityValue() + 2]; - return new TeoriaInterval(intervalNumber, - kQualityInversion[this.quality], this.direction); + return lng ? kQualityLong[quality] : quality; }, qualityValue: function() { - var defQuality = this.simpleIntervalType.quality, quality = this.quality; - - return kValidQualities[defQuality][quality]; + if (this.direction() === 'down') + return Math.floor((-this.coord[1] - 2) / 7) + 1; + else + return Math.floor((this.coord[1] - 2) / 7) + 1; }, equal: function(interval) { - return this.interval === interval.interval && - this.quality === interval.quality; + return sum(sub(this.coord, interval.coord)) === 0; }, greater: function(interval) { - var thisSemitones = this.semitones(); - var thatSemitones = interval.semitones(); + var semi = this.semitones(); + var isemi = interval.semitones(); // If equal in absolute size, measure which interval is bigger // For example P4 is bigger than A3 - return (thisSemitones === thatSemitones) ? - (this.interval > interval.interval) : (thisSemitones > thatSemitones); + return (semi === isemi) ? + (this.number() > interval.number()) : (semi > isemi); }, smaller: function(interval) { diff --git a/src/note.js b/src/note.js index 074c177..00516d7 100644 --- a/src/note.js +++ b/src/note.js @@ -1,100 +1,36 @@ -/** - * TeoriaNote - teoria.note - the note object - * - * This object is the representation of a note. - * The constructor must be called with a name, - * and optionally a duration argument. - * The first parameter (name) can be specified in either - * scientific notation (name+accidentals+octave). Fx: - * A4 - Cb3 - D#8 - Hbb - etc. - * Or in the Helmholtz notation: - * C,, - f#'' - d - Eb - etc. - * The second argument must be an object literal, with a - * 'value' property and/or a 'dots' property. By default, - * the duration value is 4 (quarter note) and dots is 0. - */ -function TeoriaNote(name, duration) { - if (typeof name !== 'string') { - return null; - } - +function TeoriaNote(coord, duration) { duration = duration || {}; - this.name = name; - this.duration = {value: duration.value || 4, dots: duration.dots || 0}; - this.accidental = {value: 0, sign: ''}; - var scientific = /^([a-h])(x|#|bb|b?)(-?\d*)/i; - var helmholtz = /^([a-h])(x|#|bb|b?)([,\']*)$/i; - var accidentalSign, accidentalValue, noteName, octave; - - // Start trying to parse scientific notation - var parser = name.match(scientific); - if (parser && name === parser[0] && parser[3].length !== 0) { // Scientific - noteName = parser[1].toLowerCase(); - octave = parseInt(parser[3], 10); - - if (parser[2].length > 0) { - accidentalSign = parser[2].toLowerCase(); - accidentalValue = kAccidentalValue[parser[2]]; - } - } else { // Helmholtz Notation - name = name.replace(/\u2032/g, "'").replace(/\u0375/g, ','); - - parser = name.match(helmholtz); - if (!parser || name !== parser[0]) { - throw new Error('Invalid note format'); - } + this.duration = { value: duration.value || 4, dots: duration.dots || 0 }; + this.coord = coord; +} - noteName = parser[1]; - octave = parser[3]; - if (parser[2].length > 0) { - accidentalSign = parser[2].toLowerCase(); - accidentalValue = kAccidentalValue[parser[2]]; - } +TeoriaNote.prototype = { + octave: function() { + return this.coord[0] + A4[0] - notes[this.name()][0] + + this.accidentalValue() * 4; + }, - if (octave.length === 0) { // no octave symbols - octave = (noteName === noteName.toLowerCase()) ? 3 : 2; - } else { - if (octave.match(/^'+$/)) { - if (noteName === noteName.toUpperCase()) { // If upper-case - throw new Error('Format must respect the Helmholtz notation'); - } - - octave = 3 + octave.length; - } else if (octave.match(/^,+$/)) { - if (noteName === noteName.toLowerCase()) { // If lower-case - throw new Error('Format must respect the Helmholtz notation'); - } - - octave = 2 - octave.length; - } else { - throw new Error('Invalid characters after note name.'); - } - } - } + name: function() { + return fifths[this.coord[1] + A4[1] - this.accidentalValue() * 7 + 1]; + }, - this.name = noteName.toLowerCase(); - this.octave = octave; + accidentalValue: function() { + return Math.round((this.coord[1] + A4[1] - 2) / 7); + }, - if (accidentalSign) { - this.accidental.value = accidentalValue; - this.accidental.sign = accidentalSign; - } -} + accidental: function() { + return accidentals[this.accidentalValue() + 2]; + }, -TeoriaNote.prototype = { /** * Returns the key number of the note */ - key: function(whitenotes) { - var noteValue; - if (whitenotes) { - noteValue = Math.ceil(kNotes[this.name].distance / 2); - return (this.octave - 1) * 7 + 3 + noteValue; - } else { - noteValue = kNotes[this.name].distance + this.accidental.value; - return (this.octave - 1) * 12 + 4 + noteValue; - } + key: function(white) { + if (white) + return this.coord[0] * 7 + this.coord[1] * 4 + 29; + else + return this.coord[0] * 12 + this.coord[1] * 7 + 49; }, /** @@ -104,14 +40,16 @@ TeoriaNote.prototype = { fq: function(concertPitch) { concertPitch = concertPitch || 440; - return concertPitch * Math.pow(2, (this.key() - 49) / 12); + return concertPitch * + Math.pow(2, (this.coord[0] * 12 + this.coord[1] * 7) / 12); }, /** * Returns the pitch class index (chroma) of the note */ chroma: function() { - var value = (kNotes[this.name].distance + this.accidental.value) % 12; + var value = (sum(mul(this.coord, [12, 7])) - 3) % 12; + return (value < 0) ? value + 12 : value; }, @@ -134,9 +72,7 @@ TeoriaNote.prototype = { */ transpose: function(interval, direction) { var note = teoria.interval(this, interval, direction); - this.name = note.name; - this.octave = note.octave; - this.accidental = note.accidental; + this.coord = note.coord; return this; }, @@ -154,19 +90,20 @@ TeoriaNote.prototype = { * Returns the Helmholtz notation form of the note (fx C,, d' F# g#'') */ helmholtz: function() { - var name = (this.octave < 3) ? this.name.toUpperCase() : - this.name.toLowerCase(); - var paddingChar = (this.octave < 3) ? ',' : '\''; - var paddingCount = (this.octave < 2) ? 2 - this.octave : this.octave - 3; + var octave = this.octave(); + var name = this.name(); + name = octave < 3 ? name.toUpperCase() : name.toLowerCase(); + var padchar = octave < 3 ? ',' : '\''; + var padcount = octave < 2 ? 2 - octave : octave - 3; - return pad(name + this.accidental.sign, paddingChar, paddingCount); + return pad(name + this.accidental(), padchar, padcount); }, /** * Returns the scientific notation form of the note (fx E4, Bb3, C#7 etc.) */ scientific: function() { - return this.name.toUpperCase() + this.accidental.sign + this.octave; + return this.name().toUpperCase() + this.accidental() + this.octave(); }, /** @@ -175,17 +112,17 @@ TeoriaNote.prototype = { enharmonics: function() { var enharmonics = [], key = this.key(), upper = this.interval('m2', 'up'), lower = this.interval('m2', 'down'); - var upperKey = upper.key() - upper.accidental.value; - var lowerKey = lower.key() - lower.accidental.value; + var upperKey = upper.key() - upper.accidentalValue(); + var lowerKey = lower.key() - lower.accidentalValue(); var diff = key - upperKey; if (diff < 3 && diff > -3) { - upper.accidental = {value: diff, sign: kAccidentalSign[diff]}; + upper.coord = add(upper.coord, mul(sharp, diff)); enharmonics.push(upper); } diff = key - lowerKey; if (diff < 3 && diff > -3) { - lower.accidental = {value: diff, sign: kAccidentalSign[diff]}; + lower.coord = add(lower.coord, mul(sharp, diff)); enharmonics.push(lower); } @@ -198,9 +135,8 @@ TeoriaNote.prototype = { } var interval = scale.tonic.interval(this), solfege, stroke, count; - if (interval.direction === 'down') { + if (interval.direction() === 'down') interval = interval.invert(); - } if (showOctaves) { count = (this.key(true) - scale.tonic.key(true)) / 7; @@ -242,19 +178,19 @@ TeoriaNote.prototype = { * as 0 evaluates to false in boolean context **/ scaleDegree: function(scale) { - var interval = scale.tonic.interval(this); - interval = (interval.direction === 'down' || - interval.simpleInterval === 8) ? interval.invert() : interval; + var val = scale.tonic.interval(this); + var num = val.number(); + val = (val.direction() === 'down' || (num > 7 && (num - 1) % 7 === 0)) ? + val.invert() : val; - return scale.scale.indexOf(interval.simple(true)) + 1; + return scale.scale.indexOf(val.simple(true)) + 1; }, /** * Returns the name of the note, with an optional display of octave number */ - toString: function(dontShow) { - var octave = dontShow ? '' : this.octave; - return this.name.toLowerCase() + this.accidental.sign + octave; + toString: function(dont) { + return this.name() + this.accidental() + (dont ? '' : this.octave()); } }; diff --git a/src/scale.js b/src/scale.js index d5896a1..269f814 100644 --- a/src/scale.js +++ b/src/scale.js @@ -1,5 +1,5 @@ function TeoriaScale(tonic, scale) { - var scaleName, i, length; + var scaleName, i; if (!(tonic instanceof TeoriaNote)) { throw new Error('Invalid Tonic'); @@ -8,9 +8,8 @@ function TeoriaScale(tonic, scale) { if (typeof scale === 'string') { scaleName = scale; scale = teoria.scale.scales[scale]; - if (!scale) { + if (!scale) throw new Error('Invalid Scale'); - } } else { for (i in teoria.scale.scales) { if (teoria.scale.scales.hasOwnProperty(i)) { @@ -23,28 +22,27 @@ function TeoriaScale(tonic, scale) { } this.name = scaleName; - this.notes = []; this.tonic = tonic; this.scale = scale; - - for (i = 0, length = scale.length; i < length; i++) { - this.notes.push(teoria.interval(tonic, scale[i])); - } } TeoriaScale.prototype = { - simple: function() { - var sNotes = []; + notes: function() { + var notes = []; - for (var i = 0, length = this.notes.length; i < length; i++) { - sNotes.push(this.notes[i].toString(true)); + for (var i = 0, length = this.scale.length; i < length; i++) { + notes.push(teoria.interval(this.tonic, this.scale[i])); } - return sNotes; + return notes; + }, + + simple: function() { + return this.notes().map(function(n) { return n.toString(true); }); }, type: function() { - var length = this.notes.length - 2; + var length = this.scale.length - 2; if (length < 8) { return ['di', 'tri', 'tetra', 'penta', 'hexa', 'hepta', 'octa'][length] + 'tonic'; @@ -54,23 +52,16 @@ TeoriaScale.prototype = { get: function(i) { i = (typeof i === 'string' && i in kStepNumber) ? kStepNumber[i] : i; - return this.notes[i - 1]; + return this.tonic.interval(this.scale[i - 1]); }, solfege: function(index, showOctaves) { - var i, length, solfegeArray = []; - - // Return specific index in scale - if (index) { + if (index) return this.get(index).solfege(this, showOctaves); - } - - // Return an array of solfege syllables - for (i = 0, length = this.notes.length; i < length; i++) { - solfegeArray.push(this.notes[i].solfege(this, showOctaves)); - } - return solfegeArray; + return this.notes().map(function(n) { + return n.solfege(this, showOctaves); + }); }, interval: function(interval, direction) { @@ -81,7 +72,6 @@ TeoriaScale.prototype = { transpose: function(interval, direction) { var scale = new TeoriaScale(this.tonic.interval(interval, direction), this.scale); - this.notes = scale.notes; this.scale = scale.scale; this.tonic = scale.tonic; diff --git a/test/intervals.js b/test/intervals.js index fcf1e73..e8da094 100644 --- a/test/intervals.js +++ b/test/intervals.js @@ -5,7 +5,7 @@ var vows = require('vows'), vows.describe('Intervals').addBatch({ 'Relative Intervals': { topic: function() { - return new teoria.TeoriaNote('F#,'); + return teoria.note('F#,'); }, 'Doubly diminished second': function(note) { @@ -191,7 +191,7 @@ vows.describe('Intervals').addBatch({ 'Doubly diminished second down': function() { assert.deepEqual(teoria.note('f').interval(teoria.note('ex')), - teoria.interval('dd2', 'down')); + teoria.interval('dd-2')); } }, @@ -253,7 +253,7 @@ vows.describe('Intervals').addBatch({ }, 'A 22nd has two compound octaves': function() { - assert.equal(teoria.interval('P22').compoundOctaves, 2); + assert.equal(teoria.interval('P22').octaves(), 2); }, 'A major seventh is greater than a minor seventh': function() { diff --git a/test/notes.js b/test/notes.js index 800439e..62a307d 100644 --- a/test/notes.js +++ b/test/notes.js @@ -5,15 +5,15 @@ var vows = require('vows'), vows.describe('TeoriaNote class').addBatch({ 'A4 - a\'': { topic: function() { - return new teoria.TeoriaNote('A4'); + return teoria.note('A4'); }, 'Octave should be 4': function(note) { - assert.equal(note.octave, 4); + assert.equal(note.octave(), 4); }, 'Note name is lower case': function(note) { - assert.equal(note.name, 'a'); + assert.equal(note.name(), 'a'); }, 'A4 is the 49th piano key': function(note) { @@ -35,23 +35,23 @@ vows.describe('TeoriaNote class').addBatch({ 'C#5 - c#\'\'': { topic: function() { - return new teoria.TeoriaNote('c#\'\''); + return teoria.note('c#\'\''); }, 'Octave should be 5': function(note) { - assert.equal(note.octave, 5); + assert.equal(note.octave(), 5); }, 'The name attribute of c# is just c': function(note) { - assert.equal(note.name, 'c'); + assert.equal(note.name(), 'c'); }, 'The accidental.sign attribute is #': function(note) { - assert.equal(note.accidental.sign, '#'); + assert.equal(note.accidental(), '#'); }, 'The accidental.value attribute is 1': function(note) { - assert.equal(note.accidental.value, 1); + assert.equal(note.accidentalValue(), 1); }, 'C#5 is the 53rd piano key': function(note) { @@ -71,21 +71,19 @@ vows.describe('TeoriaNote class').addBatch({ }, 'The interval between C#5 and A4 is a major third': function(note) { - var a4 = new teoria.TeoriaNote('A4'); + var a4 = teoria.note('A4'); - assert.deepEqual(note.interval(a4), - new teoria.TeoriaInterval(3, 'major', 'down')); + assert.deepEqual(note.interval(a4), teoria.interval('M-3')); }, 'The interval between C#5 and Eb6 is diminished tenth': function(note) { - var eb6 = new teoria.TeoriaNote('Eb6'); + var eb6 = teoria.note('Eb6'); - assert.deepEqual(note.interval(eb6), - new teoria.TeoriaInterval(10, 'diminished')); + assert.deepEqual(note.interval(eb6), teoria.interval('d10')); }, 'An diminished fifth away from C#5 is G5': function(note) { - var g5 = new teoria.TeoriaNote('G5'); + var g5 = teoria.note('G5'); assert.deepEqual(note.interval('d5'), g5); }, @@ -94,8 +92,79 @@ vows.describe('TeoriaNote class').addBatch({ var cis4 = teoria.note('c#4'); var db4 = teoria.note('db4'); - assert.deepEqual(cis4.interval(db4), - new teoria.TeoriaInterval(2, 'diminished')); + assert.deepEqual(cis4.interval(db4), teoria.interval('d2')); + } + }, + + 'Instantiate with coords': { + '[0, 0] is A4': function() { + assert.equal(teoria.note([0, 0]).scientific(), 'A4'); + }, + + '[-4, 4] is C#3': function() { + assert.equal(teoria.note([-4, 4]).scientific(), 'C#3'); + }, + + '[3, -4] is F5': function() { + assert.equal(teoria.note([3, -4]).scientific(), 'F5'); + }, + + '[4, -7] is Ab4': function() { + assert.equal(teoria.note([4, -7]).scientific(), 'Ab4'); + } + }, + + 'Instantiate from key': { + '#49 is A4': function() { + assert.equal(teoria.note.fromKey(49).scientific(), 'A4'); + }, + + '#20 is E2': function() { + assert.equal(teoria.note.fromKey(20).scientific(), 'E2'); + }, + + '#57 is F5': function() { + assert.equal(teoria.note.fromKey(57).scientific(), 'F5'); + }, + + '#72 is G#6': function() { + assert.equal(teoria.note.fromKey(72).scientific(), 'G#6'); + } + }, + + 'Instantiate from frequency': { + '391.995Hz is G4': function() { + assert.equal(teoria.note.fromFrequency(391.995).note.scientific(), 'G4'); + }, + + '220.000Hz is A3': function() { + assert.equal(teoria.note.fromFrequency(220.000).note.scientific(), 'A3'); + }, + + '155.563Hz is Eb3': function() { + assert.equal(teoria.note.fromFrequency(155.563).note.scientific(), 'Eb3'); + }, + + '2959.96Hz is F#7': function() { + assert.equal(teoria.note.fromFrequency(2959.96).note.scientific(), 'F#7'); + } + }, + + 'Instantiate from MIDI': { + 'MIDI#36 is C2': function() { + assert.equal(teoria.note.fromMIDI(36).scientific(), 'C2'); + }, + + 'MIDI#77 is F5': function() { + assert.equal(teoria.note.fromMIDI(77).scientific(), 'F5'); + }, + + 'MIDI#61 is Db4': function() { + assert.equal(teoria.note.fromMIDI(61).scientific(), 'Db4'); + }, + + 'MIDI#80 is G#5': function() { + assert.equal(teoria.note.fromMIDI(80).scientific(), 'G#5'); } }, diff --git a/test/scales.js b/test/scales.js index 0b0880b..1efb0cf 100644 --- a/test/scales.js +++ b/test/scales.js @@ -5,7 +5,7 @@ var vows = require('vows'), vows.describe('Scales').addBatch({ 'Ab2': { topic: function() { - return new teoria.TeoriaNote('Ab2'); + return teoria.note('Ab2'); }, 'Ionian/Major': function(note) { diff --git a/test/solfege.js b/test/solfege.js index a8c80dc..9b11fdb 100644 --- a/test/solfege.js +++ b/test/solfege.js @@ -4,7 +4,7 @@ var vows = require('vows'), vows.describe('Solfege').addBatch({ 'C in C minor': function() { - var note = new teoria.TeoriaNote('c'); + var note = teoria.note('c'); assert.equal(note.solfege(teoria.scale(note, 'minor')), 'do'); }, @@ -21,7 +21,7 @@ vows.describe('Solfege').addBatch({ }, 'C4 in C4 minor': function() { - var note = new teoria.TeoriaNote('c4'); + var note = teoria.note('c4'); var scale = teoria.scale(note, 'minor'); assert.equal(note.solfege(scale, true), 'do'); },