summaryrefslogtreecommitdiff
path: root/client/lib/intonation.js
blob: 341baab839eb06587ae2a104ed69c6e86bd2892f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
module.exports = (function(){
  var Intonation = function(opt){
    opt = this.opt = Object.assign({
      name: "",
      root: 466.164,
      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.name = this.opt.name || "interval list"
    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-1; i++) {
      n *= ratio
      scale.push(n)
    }
    this.name = this.opt.name || tet + "-tone equal temperament"
    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.name = this.opt.name || scl.description
    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
})()