diff --git a/app.js b/app.js index 7397c20..d324c43 100644 --- a/app.js +++ b/app.js @@ -8,15 +8,17 @@ const maxInput = document.querySelector('#generate-tone input:nth-of-type(2)') const synth = new Tone.Synth().toDestination() let newTone -let lowest = 27.5 -let highest = 4186 +let lowest = 'a0' +let highest = 'c8' minInput.setAttribute('placeholder', lowest) maxInput.setAttribute('placeholder', highest) generateButton.addEventListener('click', () => { Tone.start() - newTone = (Math.random() * (highest - lowest)) + lowest + const minFreq = teoria.note(lowest).fq() + const maxFreq = teoria.note(highest).fq() + newTone = (Math.random() * (maxFreq - minFreq)) + minFreq toneDisplay.innerHTML = newTone }) @@ -29,9 +31,9 @@ stopButton.addEventListener('click', ()=>{ }) minInput.addEventListener('change', ()=>{ - lowest = parseFloat(minInput.value) + lowest = minInput.value }) maxInput.addEventListener('change', ()=>{ - highest = parseFloat(maxInput.value) + highest = maxInput.value }) diff --git a/index.html b/index.html index 8ea3705..fc13041 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ + @@ -15,8 +16,8 @@
Tone:
- - + + diff --git a/teoria.js b/teoria.js new file mode 100644 index 0000000..439d7ed --- /dev/null +++ b/teoria.js @@ -0,0 +1,1404 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.teoria = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) ? val : val - 2; + }, + + type: function() { + return knowledge.intervals[this.base()][0] <= 1 ? 'perfect' : 'minor'; + }, + + base: function() { + var product = vector.mul(knowledge.sharp, this.qualityValue()); + var fifth = vector.sub(this.coord, product)[1]; + fifth = this.value() > 0 ? fifth + 5 : -(fifth - 5) % 7; + fifth = fifth < 0 ? knowledge.intervalFromFifth.length + fifth : fifth; + + var name = knowledge.intervalFromFifth[fifth]; + if (name === 'unison' && this.number() >= 8) + name = 'octave'; + + return name; + }, + + direction: function(dir) { + if (dir) { + var is = this.value() >= 1 ? 'up' : 'down'; + if (is !== dir) + this.coord = vector.mul(this.coord, -1); + + return this; + } + else + return this.value() >= 1 ? 'up' : 'down'; + }, + + simple: function(ignore) { + // Get the (upwards) base interval (with quality) + var simple = knowledge.intervals[this.base()]; + var toAdd = vector.mul(knowledge.sharp, this.qualityValue()); + simple = vector.add(simple, toAdd); + + // Turn it around if necessary + if (!ignore) + simple = this.direction() === 'down' ? vector.mul(simple, -1) : simple; + + return new Interval(simple); + }, + + isCompound: function() { + return this.number() > 8; + }, + + octaves: function() { + var toSubtract, without, octaves; + + if (this.direction() === 'up') { + toSubtract = vector.mul(knowledge.sharp, this.qualityValue()); + without = vector.sub(this.coord, toSubtract); + octaves = without[0] - knowledge.intervals[this.base()][0]; + } else { + toSubtract = vector.mul(knowledge.sharp, -this.qualityValue()); + without = vector.sub(this.coord, toSubtract); + octaves = -(without[0] + knowledge.intervals[this.base()][0]); + } + + return octaves; + }, + + invert: function() { + var i = this.base(); + var qual = this.qualityValue(); + var acc = this.type() === 'minor' ? -(qual - 1) : -qual; + var idx = 9 - knowledge.stepNumber[i] - 1; + var coord = knowledge.intervals[knowledge.intervalsIndex[idx]]; + coord = vector.add(coord, vector.mul(knowledge.sharp, acc)); + + return new Interval(coord); + }, + + quality: function(lng) { + var quality = knowledge.alterations[this.type()][this.qualityValue() + 2]; + + return lng ? knowledge.qualityLong[quality] : quality; + }, + + qualityValue: function() { + 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.coord[0] === interval.coord[0] && + this.coord[1] === interval.coord[1]; + }, + + greater: function(interval) { + 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 (semi === isemi) ? + (this.number() > interval.number()) : (semi > isemi); + }, + + smaller: function(interval) { + return !this.equal(interval) && !this.greater(interval); + }, + + add: function(interval) { + return new Interval(vector.add(this.coord, interval.coord)); + }, + + toString: function(ignore) { + // If given true, return the positive value + var number = ignore ? this.number() : this.value(); + + return this.quality() + number; + } +}; + +Interval.toCoord = function(simple) { + var coord = toCoord(simple); + if (!coord) + throw new Error('Invalid simple format interval'); + + return new Interval(coord); +}; + +Interval.from = function(from, to) { + return from.interval(to); +}; + +Interval.between = function(from, to) { + return new Interval(vector.sub(to.coord, from.coord)); +}; + +Interval.invert = function(sInterval) { + return Interval.toCoord(sInterval).invert().toString(); +}; + +module.exports = Interval; + +},{"./knowledge":4,"./vector":8,"interval-coords":12}],4:[function(require,module,exports){ +// Note coordinates [octave, fifth] relative to C +module.exports = { + 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] + }, + + 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] + }, + + intervalFromFifth: ['second', 'sixth', 'third', 'seventh', 'fourth', + 'unison', 'fifth'], + + intervalsIndex: ['unison', 'second', 'third', 'fourth', 'fifth', + 'sixth', 'seventh', 'octave', 'ninth', 'tenth', + 'eleventh', 'twelfth', 'thirteenth', 'fourteenth', + 'fifteenth'], + +// linear index to fifth = (2 * index + 1) % 7 + fifths: ['f', 'c', 'g', 'd', 'a', 'e', 'b'], + accidentals: ['bb', 'b', '', '#', 'x'], + + sharp: [-4, 7], + A4: [3, 3], + + durations: { + '0.25': 'longa', + '0.5': 'breve', + '1': 'whole', + '2': 'half', + '4': 'quarter', + '8': 'eighth', + '16': 'sixteenth', + '32': 'thirty-second', + '64': 'sixty-fourth', + '128': 'hundred-twenty-eighth' + }, + + qualityLong: { + P: 'perfect', + M: 'major', + m: 'minor', + A: 'augmented', + AA: 'doubly augmented', + d: 'diminished', + dd: 'doubly diminished' + }, + + alterations: { + perfect: ['dd', 'd', 'P', 'A', 'AA'], + minor: ['dd', 'd', 'm', 'M', 'A', 'AA'] + }, + + symbols: { + 'min': ['m3', 'P5'], + 'm': ['m3', 'P5'], + '-': ['m3', 'P5'], + + 'M': ['M3', 'P5'], + '': ['M3', 'P5'], + + '+': ['M3', 'A5'], + 'aug': ['M3', 'A5'], + + 'dim': ['m3', 'd5'], + 'o': ['m3', 'd5'], + + 'maj': ['M3', 'P5', 'M7'], + 'dom': ['M3', 'P5', 'm7'], + 'ø': ['m3', 'd5', 'm7'], + + '5': ['P5'] + }, + + chordShort: { + 'major': 'M', + 'minor': 'm', + 'augmented': 'aug', + 'diminished': 'dim', + 'half-diminished': '7b5', + 'power': '5', + 'dominant': '7' + }, + + stepNumber: { + 'unison': 1, + 'first': 1, + 'second': 2, + 'third': 3, + 'fourth': 4, + 'fifth': 5, + 'sixth': 6, + 'seventh': 7, + 'octave': 8, + 'ninth': 9, + 'eleventh': 11, + 'thirteenth': 13 + }, + + // Adjusted Shearer syllables - Chromatic solfege system + // Some intervals are not provided for. These include: + // dd2 - Doubly diminished second + // dd3 - Doubly diminished third + // AA3 - Doubly augmented third + // dd6 - Doubly diminished sixth + // dd7 - Doubly diminished seventh + // AA7 - Doubly augmented seventh + intervalSolfege: { + 'dd1': 'daw', + 'd1': 'de', + 'P1': 'do', + 'A1': 'di', + 'AA1': 'dai', + 'd2': 'raw', + 'm2': 'ra', + 'M2': 're', + 'A2': 'ri', + 'AA2': 'rai', + 'd3': 'maw', + 'm3': 'me', + 'M3': 'mi', + 'A3': 'mai', + 'dd4': 'faw', + 'd4': 'fe', + 'P4': 'fa', + 'A4': 'fi', + 'AA4': 'fai', + 'dd5': 'saw', + 'd5': 'se', + 'P5': 'so', + 'A5': 'si', + 'AA5': 'sai', + 'd6': 'law', + 'm6': 'le', + 'M6': 'la', + 'A6': 'li', + 'AA6': 'lai', + 'd7': 'taw', + 'm7': 'te', + 'M7': 'ti', + 'A7': 'tai', + 'dd8': 'daw', + 'd8': 'de', + 'P8': 'do', + 'A8': 'di', + 'AA8': 'dai' + } +}; + +},{}],5:[function(require,module,exports){ +var scientific = require('scientific-notation'); +var helmholtz = require('helmholtz'); +var pitchFq = require('pitch-fq'); +var knowledge = require('./knowledge'); +var vector = require('./vector'); +var Interval = require('./interval'); + +function pad(str, ch, len) { + for (; len > 0; len--) { + str += ch; + } + + return str; +} + + +function Note(coord, duration) { + if (!(this instanceof Note)) return new Note(coord, duration); + duration = duration || {}; + + this.duration = { value: duration.value || 4, dots: duration.dots || 0 }; + this.coord = coord; +} + +Note.prototype = { + octave: function() { + return this.coord[0] + knowledge.A4[0] - knowledge.notes[this.name()][0] + + this.accidentalValue() * 4; + }, + + name: function() { + var value = this.accidentalValue(); + var idx = this.coord[1] + knowledge.A4[1] - value * 7 + 1; + return knowledge.fifths[idx]; + }, + + accidentalValue: function() { + return Math.round((this.coord[1] + knowledge.A4[1] - 2) / 7); + }, + + accidental: function() { + return knowledge.accidentals[this.accidentalValue() + 2]; + }, + + /** + * Returns the key number of the note + */ + 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; + }, + + /** + * Returns a number ranging from 0-127 representing a MIDI note value + */ + midi: function() { + return this.key() + 20; + }, + + /** + * Calculates and returns the frequency of the note. + * Optional concert pitch (def. 440) + */ + fq: function(concertPitch) { + return pitchFq(this.coord, concertPitch); + }, + + /** + * Returns the pitch class index (chroma) of the note + */ + chroma: function() { + var value = (vector.sum(vector.mul(this.coord, [12, 7])) - 3) % 12; + + return (value < 0) ? value + 12 : value; + }, + + interval: function(interval) { + if (typeof interval === 'string') interval = Interval.toCoord(interval); + + if (interval instanceof Interval) + return new Note(vector.add(this.coord, interval.coord), this.duration); + else if (interval instanceof Note) + return new Interval(vector.sub(interval.coord, this.coord)); + }, + + transpose: function(interval) { + this.coord = vector.add(this.coord, interval.coord); + return this; + }, + + /** + * Returns the Helmholtz notation form of the note (fx C,, d' F# g#'') + */ + helmholtz: function() { + 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(), padchar, padcount); + }, + + /** + * Returns the scientific notation form of the note (fx E4, Bb3, C#7 etc.) + */ + scientific: function() { + return this.name().toUpperCase() + this.accidental() + this.octave(); + }, + + /** + * Returns notes that are enharmonic with this note. + */ + enharmonics: function(oneaccidental) { + var key = this.key(), limit = oneaccidental ? 2 : 3; + + return ['m3', 'm2', 'm-2', 'm-3'] + .map(this.interval.bind(this)) + .filter(function(note) { + var acc = note.accidentalValue(); + var diff = key - (note.key() - acc); + + if (diff < limit && diff > -limit) { + var product = vector.mul(knowledge.sharp, diff - acc); + note.coord = vector.add(note.coord, product); + return true; + } + }); + }, + + solfege: function(scale, showOctaves) { + var interval = scale.tonic.interval(this), solfege, stroke, count; + if (interval.direction() === 'down') + interval = interval.invert(); + + if (showOctaves) { + count = (this.key(true) - scale.tonic.key(true)) / 7; + count = (count >= 0) ? Math.floor(count) : -(Math.ceil(-count)); + stroke = (count >= 0) ? '\'' : ','; + } + + solfege = knowledge.intervalSolfege[interval.simple(true).toString()]; + return (showOctaves) ? pad(solfege, stroke, Math.abs(count)) : solfege; + }, + + scaleDegree: function(scale) { + var inter = scale.tonic.interval(this); + + // If the direction is down, or we're dealing with an octave - invert it + if (inter.direction() === 'down' || + (inter.coord[1] === 0 && inter.coord[0] !== 0)) { + inter = inter.invert(); + } + + inter = inter.simple(true).coord; + + return scale.scale.reduce(function(index, current, i) { + var coord = Interval.toCoord(current).coord; + return coord[0] === inter[0] && coord[1] === inter[1] ? i + 1 : index; + }, 0); + }, + + /** + * Returns the name of the duration value, + * such as 'whole', 'quarter', 'sixteenth' etc. + */ + durationName: function() { + return knowledge.durations[this.duration.value]; + }, + + /** + * Returns the duration of the note (including dots) + * in seconds. The first argument is the tempo in beats + * per minute, the second is the beat unit (i.e. the + * lower numeral in a time signature). + */ + durationInSeconds: function(bpm, beatUnit) { + var secs = (60 / bpm) / (this.duration.value / 4) / (beatUnit / 4); + return secs * 2 - secs / Math.pow(2, this.duration.dots); + }, + + /** + * Returns the name of the note, with an optional display of octave number + */ + toString: function(dont) { + return this.name() + this.accidental() + (dont ? '' : this.octave()); + } +}; + +Note.fromString = function(name, dur) { + var coord = scientific(name); + if (!coord) coord = helmholtz(name); + return new Note(coord, dur); +}; + +Note.fromKey = function(key) { + var octave = Math.floor((key - 4) / 12); + var distance = key - (octave * 12) - 4; + var name = knowledge.fifths[(2 * Math.round(distance / 2) + 1) % 7]; + var subDiff = vector.sub(knowledge.notes[name], knowledge.A4); + var note = vector.add(subDiff, [octave + 1, 0]); + var diff = (key - 49) - vector.sum(vector.mul(note, [12, 7])); + + var arg = diff ? vector.add(note, vector.mul(knowledge.sharp, diff)) : note; + return new Note(arg); +}; + +Note.fromFrequency = function(fq, concertPitch) { + var key, cents, originalFq; + concertPitch = concertPitch || 440; + + key = 49 + 12 * ((Math.log(fq) - Math.log(concertPitch)) / Math.log(2)); + key = Math.round(key); + originalFq = concertPitch * Math.pow(2, (key - 49) / 12); + cents = 1200 * (Math.log(fq / originalFq) / Math.log(2)); + + return { note: Note.fromKey(key), cents: cents }; +}; + +Note.fromMIDI = function(note) { + return Note.fromKey(note - 20); +}; + +module.exports = Note; + +},{"./interval":3,"./knowledge":4,"./vector":8,"helmholtz":11,"pitch-fq":14,"scientific-notation":15}],6:[function(require,module,exports){ +var knowledge = require('./knowledge'); +var Interval = require('./interval'); + +var scales = { + aeolian: ['P1', 'M2', 'm3', 'P4', 'P5', 'm6', 'm7'], + blues: ['P1', 'm3', 'P4', 'd5', 'P5', 'm7'], + chromatic: ['P1', 'm2', 'M2', 'm3', 'M3', 'P4', + 'A4', 'P5', 'm6', 'M6', 'm7', 'M7'], + dorian: ['P1', 'M2', 'm3', 'P4', 'P5', 'M6', 'm7'], + doubleharmonic: ['P1', 'm2', 'M3', 'P4', 'P5', 'm6', 'M7'], + harmonicminor: ['P1', 'M2', 'm3', 'P4', 'P5', 'm6', 'M7'], + ionian: ['P1', 'M2', 'M3', 'P4', 'P5', 'M6', 'M7'], + locrian: ['P1', 'm2', 'm3', 'P4', 'd5', 'm6', 'm7'], + lydian: ['P1', 'M2', 'M3', 'A4', 'P5', 'M6', 'M7'], + majorpentatonic: ['P1', 'M2', 'M3', 'P5', 'M6'], + melodicminor: ['P1', 'M2', 'm3', 'P4', 'P5', 'M6', 'M7'], + minorpentatonic: ['P1', 'm3', 'P4', 'P5', 'm7'], + mixolydian: ['P1', 'M2', 'M3', 'P4', 'P5', 'M6', 'm7'], + phrygian: ['P1', 'm2', 'm3', 'P4', 'P5', 'm6', 'm7'], + wholetone: ['P1', 'M2', 'M3', 'A4', 'A5', 'A6'] +}; + +// synonyms +scales.harmonicchromatic = scales.chromatic; +scales.minor = scales.aeolian; +scales.major = scales.ionian; +scales.flamenco = scales.doubleharmonic; + +function Scale(tonic, scale) { + if (!(this instanceof Scale)) return new Scale(tonic, scale); + var scaleName, i; + if (!('coord' in tonic)) { + throw new Error('Invalid Tonic'); + } + + if (typeof scale === 'string') { + scaleName = scale; + scale = scales[scale]; + if (!scale) + throw new Error('Invalid Scale'); + } else { + for (i in scales) { + if (scales.hasOwnProperty(i)) { + if (scales[i].toString() === scale.toString()) { + scaleName = i; + break; + } + } + } + } + + this.name = scaleName; + this.tonic = tonic; + this.scale = scale; +} + +Scale.prototype = { + notes: function() { + var notes = []; + + for (var i = 0, length = this.scale.length; i < length; i++) { + notes.push(this.tonic.interval(this.scale[i])); + } + + return notes; + }, + + simple: function() { + return this.notes().map(function(n) { return n.toString(true); }); + }, + + type: function() { + var length = this.scale.length - 2; + if (length < 8) { + return ['di', 'tri', 'tetra', 'penta', 'hexa', 'hepta', 'octa'][length] + + 'tonic'; + } + }, + + get: function(i) { + var isStepStr = typeof i === 'string' && i in knowledge.stepNumber; + i = isStepStr ? knowledge.stepNumber[i] : i; + var len = this.scale.length; + var interval, octaves; + + if (i < 0) { + interval = this.scale[i % len + len - 1]; + octaves = Math.floor((i - 1) / len); + } else if (i % len === 0) { + interval = this.scale[len - 1]; + octaves = (i / len) - 1; + } else { + interval = this.scale[i % len - 1]; + octaves = Math.floor(i / len); + } + + return this.tonic.interval(interval).interval(new Interval([octaves, 0])); + }, + + solfege: function(index, showOctaves) { + if (index) + return this.get(index).solfege(this, showOctaves); + + return this.notes().map(function(n) { + return n.solfege(this, showOctaves); + }); + }, + + interval: function(interval) { + interval = (typeof interval === 'string') ? + Interval.toCoord(interval) : interval; + return new Scale(this.tonic.interval(interval), this.scale); + }, + + transpose: function(interval) { + var scale = this.interval(interval); + this.scale = scale.scale; + this.tonic = scale.tonic; + + return this; + } +}; +Scale.KNOWN_SCALES = Object.keys(scales); + +module.exports = Scale; + +},{"./interval":3,"./knowledge":4}],7:[function(require,module,exports){ +var knowledge = require('./knowledge'); + +module.exports = function(teoria) { + var Note = teoria.Note; + var Chord = teoria.Chord; + var Scale = teoria.Scale; + + Note.prototype.chord = function(chord) { + var isShortChord = chord in knowledge.chordShort; + chord = isShortChord ? knowledge.chordShort[chord] : chord; + + return new Chord(this, chord); + }; + + Note.prototype.scale = function(scale) { + return new Scale(this, scale); + }; +}; + +},{"./knowledge":4}],8:[function(require,module,exports){ +module.exports = { + add: function(note, interval) { + return [note[0] + interval[0], note[1] + interval[1]]; + }, + + sub: function(note, interval) { + return [note[0] - interval[0], note[1] - interval[1]]; + }, + + mul: function(note, interval) { + if (typeof interval === 'number') + return [note[0] * interval, note[1] * interval]; + else + return [note[0] * interval[0], note[1] * interval[1]]; + }, + + sum: function(coord) { + return coord[0] + coord[1]; + } +}; + +},{}],9:[function(require,module,exports){ +var accidentalValues = { + 'bb': -2, + 'b': -1, + '': 0, + '#': 1, + 'x': 2 +}; + +module.exports = function accidentalNumber(acc) { + return accidentalValues[acc]; +} + +module.exports.interval = function accidentalInterval(acc) { + var val = accidentalValues[acc]; + return [-4 * val, 7 * val]; +} + +},{}],10:[function(require,module,exports){ +var SYMBOLS = { + 'm': ['m3', 'P5'], + 'mi': ['m3', 'P5'], + 'min': ['m3', 'P5'], + '-': ['m3', 'P5'], + + 'M': ['M3', 'P5'], + 'ma': ['M3', 'P5'], + '': ['M3', 'P5'], + + '+': ['M3', 'A5'], + 'aug': ['M3', 'A5'], + + 'dim': ['m3', 'd5'], + 'o': ['m3', 'd5'], + + 'maj': ['M3', 'P5', 'M7'], + 'dom': ['M3', 'P5', 'm7'], + 'ø': ['m3', 'd5', 'm7'], + + '5': ['P5'], + + '6/9': ['M3', 'P5', 'M6', 'M9'] +}; + +module.exports = function(symbol) { + var c, parsing = 'quality', additionals = [], name, chordLength = 2 + var notes = ['P1', 'M3', 'P5', 'm7', 'M9', 'P11', 'M13']; + var explicitMajor = false; + + function setChord(name) { + var intervals = SYMBOLS[name]; + for (var i = 0, len = intervals.length; i < len; i++) { + notes[i + 1] = intervals[i]; + } + + chordLength = intervals.length; + } + + // Remove whitespace, commas and parentheses + symbol = symbol.replace(/[,\s\(\)]/g, ''); + for (var i = 0, len = symbol.length; i < len; i++) { + if (!(c = symbol[i])) + return; + + if (parsing === 'quality') { + var sub3 = (i + 2) < len ? symbol.substr(i, 3).toLowerCase() : null; + var sub2 = (i + 1) < len ? symbol.substr(i, 2).toLowerCase() : null; + if (sub3 in SYMBOLS) + name = sub3; + else if (sub2 in SYMBOLS) + name = sub2; + else if (c in SYMBOLS) + name = c; + else + name = ''; + + if (name) + setChord(name); + + if (name === 'M' || name === 'ma' || name === 'maj') + explicitMajor = true; + + + i += name.length - 1; + parsing = 'extension'; + } else if (parsing === 'extension') { + c = (c === '1' && symbol[i + 1]) ? +symbol.substr(i, 2) : +c; + + if (!isNaN(c) && c !== 6) { + chordLength = (c - 1) / 2; + + if (chordLength !== Math.round(chordLength)) + return new Error('Invalid interval extension: ' + c.toString(10)); + + if (name === 'o' || name === 'dim') + notes[3] = 'd7'; + else if (explicitMajor) + notes[3] = 'M7'; + + i += c >= 10 ? 1 : 0; + } else if (c === 6) { + notes[3] = 'M6'; + chordLength = Math.max(3, chordLength); + } else + i -= 1; + + parsing = 'alterations'; + } else if (parsing === 'alterations') { + var alterations = symbol.substr(i).split(/(#|b|add|maj|sus|M)/i), + next, flat = false, sharp = false; + + if (alterations.length === 1) + return new Error('Invalid alteration'); + else if (alterations[0].length !== 0) + return new Error('Invalid token: \'' + alterations[0] + '\''); + + var ignore = false; + alterations.forEach(function(alt, i, arr) { + if (ignore || !alt.length) + return ignore = false; + + var next = arr[i + 1], lower = alt.toLowerCase(); + if (alt === 'M' || lower === 'maj') { + if (next === '7') + ignore = true; + + chordLength = Math.max(3, chordLength); + notes[3] = 'M7'; + } else if (lower === 'sus') { + var type = 'P4'; + if (next === '2' || next === '4') { + ignore = true; + + if (next === '2') + type = 'M2'; + } + + notes[1] = type; // Replace third with M2 or P4 + } else if (lower === 'add') { + if (next === '9') + additionals.push('M9'); + else if (next === '11') + additionals.push('P11'); + else if (next === '13') + additionals.push('M13'); + + ignore = true + } else if (lower === 'b') { + flat = true; + } else if (lower === '#') { + sharp = true; + } else { + var token = +alt, quality, intPos; + if (isNaN(token) || String(token).length !== alt.length) + return new Error('Invalid token: \'' + alt + '\''); + + if (token === 6) { + if (sharp) + notes[3] = 'A6'; + else if (flat) + notes[3] = 'm6'; + else + notes[3] = 'M6'; + + chordLength = Math.max(3, chordLength); + return; + } + + // Calculate the position in the 'note' array + intPos = (token - 1) / 2; + if (chordLength < intPos) + chordLength = intPos; + + if (token < 5 || token === 7 || intPos !== Math.round(intPos)) + return new Error('Invalid interval alteration: ' + token); + + quality = notes[intPos][0]; + + // Alterate the quality of the interval according the accidentals + if (sharp) { + if (quality === 'd') + quality = 'm'; + else if (quality === 'm') + quality = 'M'; + else if (quality === 'M' || quality === 'P') + quality = 'A'; + } else if (flat) { + if (quality === 'A') + quality = 'M'; + else if (quality === 'M') + quality = 'm'; + else if (quality === 'm' || quality === 'P') + quality = 'd'; + } + + sharp = flat = false; + notes[intPos] = quality + token; + } + }); + parsing = 'ended'; + } else if (parsing === 'ended') { + break; + } + } + + return notes.slice(0, chordLength + 1).concat(additionals); +} + +},{}],11:[function(require,module,exports){ +var coords = require('notecoord'); +var accval = require('accidental-value'); + +module.exports = function helmholtz(name) { + var name = name.replace(/\u2032/g, "'").replace(/\u0375/g, ','); + var parts = name.match(/^(,*)([a-h])(x|#|bb|b?)([,\']*)$/i); + + if (!parts || name !== parts[0]) + throw new Error('Invalid formatting'); + + var note = parts[2]; + var octaveFirst = parts[1]; + var octaveLast = parts[4]; + var lower = note === note.toLowerCase(); + var octave; + + if (octaveFirst) { + if (lower) + throw new Error('Invalid formatting - found commas before lowercase note'); + + octave = 2 - octaveFirst.length; + } else if (octaveLast) { + if (octaveLast.match(/^'+$/) && lower) + octave = 3 + octaveLast.length; + else if (octaveLast.match(/^,+$/) && !lower) + octave = 2 - octaveLast.length; + else + throw new Error('Invalid formatting - mismatch between octave ' + + 'indicator and letter case') + } else + octave = lower ? 3 : 2; + + var accidentalValue = accval.interval(parts[3].toLowerCase()); + var coord = coords(note.toLowerCase()); + + coord[0] += octave; + coord[0] += accidentalValue[0] - coords.A4[0]; + coord[1] += accidentalValue[1] - coords.A4[1]; + + return coord; +}; + +},{"accidental-value":9,"notecoord":13}],12:[function(require,module,exports){ +var pattern = /^(AA|A|P|M|m|d|dd)(-?\d+)$/; + +// The interval it takes to raise a note a semitone +var sharp = [-4, 7]; + +var pAlts = ['dd', 'd', 'P', 'A', 'AA']; +var mAlts = ['dd', 'd', 'm', 'M', 'A', 'AA']; + +var baseIntervals = [ + [0, 0], + [3, -5], + [2, -3], + [1, -1], + [0, 1], + [3, -4], + [2, -2], + [1, 0] +]; + +module.exports = function(simple) { + var parser = simple.match(pattern); + if (!parser) return null; + + var quality = parser[1]; + var number = +parser[2]; + var sign = number < 0 ? -1 : 1; + + number = sign < 0 ? -number : number; + + var lower = number > 8 ? (number % 7 || 7) : number; + var octaves = (number - lower) / 7; + + var base = baseIntervals[lower - 1]; + var alts = base[0] <= 1 ? pAlts : mAlts; + var alt = alts.indexOf(quality) - 2; + + // this happens, if the alteration wasn't suitable for this type + // of interval, such as P2 or M5 (no "perfect second" or "major fifth") + if (alt === -3) return null; + + return [ + sign * (base[0] + octaves + sharp[0] * alt), + sign * (base[1] + sharp[1] * alt) + ]; +} + +// Copy to avoid overwriting internal base intervals +module.exports.coords = baseIntervals.slice(0); + +},{}],13:[function(require,module,exports){ +// First coord is octaves, second is fifths. Distances are 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] +}; + +module.exports = function(name) { + return name in notes ? [notes[name][0], notes[name][1]] : null; +}; + +module.exports.notes = notes; +module.exports.A4 = [3, 3]; // Relative to C0 (scientic notation, ~16.35Hz) +module.exports.sharp = [-4, 7]; + +},{}],14:[function(require,module,exports){ +module.exports = function(coord, stdPitch) { + if (typeof coord === 'number') { + stdPitch = coord; + return function(coord) { + return stdPitch * Math.pow(2, (coord[0] * 12 + coord[1] * 7) / 12); + } + } + + stdPitch = stdPitch || 440; + return stdPitch * Math.pow(2, (coord[0] * 12 + coord[1] * 7) / 12); +} + +},{}],15:[function(require,module,exports){ +var coords = require('notecoord'); +var accval = require('accidental-value'); + +module.exports = function scientific(name) { + var format = /^([a-h])(x|#|bb|b?)(-?\d*)/i; + + var parser = name.match(format); + if (!(parser && name === parser[0] && parser[3].length)) return; + + var noteName = parser[1]; + var octave = +parser[3]; + var accidental = parser[2].length ? parser[2].toLowerCase() : ''; + + var accidentalValue = accval.interval(accidental); + var coord = coords(noteName.toLowerCase()); + + coord[0] += octave; + coord[0] += accidentalValue[0] - coords.A4[0]; + coord[1] += accidentalValue[1] - coords.A4[1]; + + return coord; +}; + +},{"accidental-value":9,"notecoord":13}]},{},[1])(1) +}); \ No newline at end of file