var Intonation = (function(){ var Intonation = function(opt){ opt = this.opt = Object.assign({ root: 440, octave: 0, interval: 2, tet: 0, intervals: null, }, opt || {}) this.generate() } Intonation.prototype.generate = function(opt){ opt = Object.assign(this.opt, opt || {}) if (opt.scl) { this.generate_scl() } else if (opt.tet) { this.generate_tet() } else if (opt.intervals) { this.generate_intervals() } } Intonation.prototype.generate_intervals = function(){ var root = this.opt.root var interval_list = this.opt.intervals if (typeof interval_list == "string") { interval_list = interval_list.split(" ") } this.intervals = interval_list this.interval = this.opt.interval = parseInterval.call(this, interval_list.pop() ) this.scale = interval_list.map( parseIntervalString.bind(this) ).filter(function(v){ return !! v }) } Intonation.prototype.generate_tet = function(){ var scale = this.scale = [] var root = this.opt.root var tet = this.opt.tet var interval = this.interval = this.opt.interval var ratio = Math.pow( interval, 1/tet ) var n = root scale.push(n) for (var i = 0; i < tet; i++) { n *= ratio scale.push(n) } this.intervals = null } Intonation.prototype.generate_scl = function(){ var root = this.opt.root var scl = this.parse_scl( this.opt.scl ) this.intervals = scl.notes this.interval = scl.notes.pop() this.scale = scl.notes.map(function(v){ return v * root }) } Intonation.prototype.parse_scl = function(s){ var scl = {} scl.comments = [] scl.notes = [] s.trim().split("\n").forEach(function(line){ // Lines beginning with an exclamation mark are regarded as comments // and are to be ignored. if ( line.indexOf("!") !== -1 ) { scl.comments.push(line) } // The first (non comment) line contains a short description of the scale. // If there is no description, there should be an empty line. (nb: which is falsey) else if ( ! ('description' in scl) ) { scl.description = line } // The second line contains the number of notes. // The first note of 1/1 or 0.0 cents is implicit and not in the files. else if ( ! scl.notes.length) { scl.notes.push(1) } else { // If the value contains a period, it is a cents value, otherwise a ratio. var note = line.replace(/^[^-\.0-9]+/,"").replace(/[^-\/\.0-9]+$/,"") if ( note.indexOf(".") !== -1 ) { note = Math.pow( 2, (parseFloat(note) / 1200) ) } else { note = parseInterval(note) } if (note) { scl.notes.push(note) } } }) return scl } Intonation.prototype.index = function(i, octave){ octave = octave || this.opt.octave var f = this.scale[ mod(i, this.scale.length)|0 ] var pow = Math.floor(norm(i, 0, this.scale.length)) + octave f *= Math.pow(this.interval, pow) return f } Intonation.prototype.range = function(min, max){ var a = [] for (var i = min; i < max; i++) { a.push( this.index(i) ) } return a } Intonation.prototype.set_root = function(f){ this.opt.root = f this.generate() } Intonation.prototype.quantize_frequency = function(f){ if (f == 0) return 0 var scale_f = f var pow = 0 var interval = this.interval var scale = this.scale while (scale_f < root) { scale_f *= interval pow -= 1 } while (scale_f > root * interval) { scale_f /= interval pow += 1 } for (var i = 0; i < scale.length; i++) { if (scale_f > scale[i]) continue scale_f = scale[i] break } scale_f *= Math.pow(2, pow) return scale_f } Intonation.prototype.quantize_index = function(i){ return mod(index-1, this.scale.length)|0 } var parseInterval = Intonation.prototype.parse_interval = function (s) { if (typeof s == "number") return s if (! s.indexOf("/") == -1) return parseInt(s) var pp = s.split("/") var num = parseInt(pp[0]) var den = parseInt(pp[1]) if (isNaN(num)) return 1 if (isNaN(den) || den == 0) return num if (num == den) return 1 return num / den } var parseIntervalString = Intonation.prototype.parse_interval_string = function(s){ if (s.indexOf("/") !== -1) return parseInterval(s) * this.opt.root // intervals if (s.indexOf("f") !== -1) return parseFloat(s) // pure frequencies return parseFloat(s) } function norm(n,a,b){ return (n-a) / (b-a) } function mod(n,m){ return n-(m * Math.floor(n/m)) } return Intonation })()