summaryrefslogtreecommitdiff
path: root/client/lib
diff options
context:
space:
mode:
authorJules Laplace <jules@okfoc.us>2017-04-26 21:13:55 -0400
committerJules Laplace <jules@okfoc.us>2017-04-26 21:13:55 -0400
commita356074dd17029ada978381ca005684a3533102e (patch)
tree43925d492e6dff19a853b4859ed13b093bc8b61c /client/lib
cells..
Diffstat (limited to 'client/lib')
-rw-r--r--client/lib/color.js31
-rw-r--r--client/lib/intonation.js162
-rw-r--r--client/lib/kalimba.js46
-rw-r--r--client/lib/keys.js39
-rw-r--r--client/lib/scales.js150
-rw-r--r--client/lib/util.js3
6 files changed, 431 insertions, 0 deletions
diff --git a/client/lib/color.js b/client/lib/color.js
new file mode 100644
index 0000000..bd5b7ce
--- /dev/null
+++ b/client/lib/color.js
@@ -0,0 +1,31 @@
+
+const palettes = [
+ [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 1.0, 1.0], [0.00, 0.33, 0.67]],
+ [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 1.0, 1.0], [0.00, 0.10, 0.20]],
+ [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 1.0, 1.0], [0.30, 0.20, 0.20]],
+ [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 1.0, 0.5], [0.80, 0.90, 0.30]],
+ [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [1.0, 0.7, 0.4], [0.00, 0.15, 0.20]],
+ [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [2.0, 1.0, 0.0], [0.50, 0.20, 0.25]],
+ [[0.8, 0.5, 0.4], [0.2, 0.4, 0.2], [2.0, 1.0, 1.0], [0.00, 0.25, 0.25]],
+]
+
+let palette = palettes[0]
+
+function channel (t, a, b, c, d, add, mul) {
+ return a + b * Math.cos(2 * Math.PI * (c * t + d)) * mul + add
+}
+
+function color (t, add, mul) {
+ let a, b, c, d
+ const rgb = []
+ for (var i = 0; i < 3; i++) {
+ a = palette[0][i]
+ b = palette[1][i]
+ c = palette[2][i]
+ d = palette[3][i]
+ rgb[i] = Math.round(channel(t, a, b, c, d, add, mul) * 255)
+ }
+ return 'rgb(' + rgb + ')'
+}
+
+export default color
diff --git a/client/lib/intonation.js b/client/lib/intonation.js
new file mode 100644
index 0000000..e5465af
--- /dev/null
+++ b/client/lib/intonation.js
@@ -0,0 +1,162 @@
+module.exports = (function(){
+ var Intonation = function(opt){
+ opt = this.opt = Object.assign({
+ name: "",
+ 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.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
+})()
diff --git a/client/lib/kalimba.js b/client/lib/kalimba.js
new file mode 100644
index 0000000..b64f204
--- /dev/null
+++ b/client/lib/kalimba.js
@@ -0,0 +1,46 @@
+import Tone from 'tone'
+import { choice } from './util'
+
+const player_count = 4
+
+const compressor = new Tone.Compressor(-30, 3).toMaster()
+
+const samples = [
+ { root: 226, fn: 'samples/380737__cabled-mess__sansula-01-a-raw.wav', },
+ { root: 267, fn: 'samples/380736__cabled-mess__sansula-02-c-raw.wav', },
+ { root: 340, fn: 'samples/380735__cabled-mess__sansula-03-e-raw.wav', },
+ { root: 452, fn: 'samples/380733__cabled-mess__sansula-06-a-02-raw.wav', },
+// { root: 507, fn: 'samples/380734__cabled-mess__sansula-07-b-h-raw.wav', },
+// { root: 535, fn: 'samples/380731__cabled-mess__sansula-08-c-raw.wav', },
+// { root: 671, fn: 'samples/380732__cabled-mess__sansula-09-e-raw.wav', },
+]
+
+samples.forEach((sample) => {
+ sample.players = []
+ sample.index = -1
+ for (let i = 0; i < player_count; i++) {
+ let fn = sample.fn
+ if (window.location.href.match(/asdf.us/)) {
+ fn = 'http://asdf.us/kalimba/' + fn
+ }
+ let player = new Tone.Player({
+ url: fn,
+ retrigger: true,
+ playbackRate: 1,
+ })
+ player.connect(compressor)
+ sample.players.push(player)
+ }
+})
+
+function play (freq) {
+ const best = { sample: choice(samples) }
+ best.sample.index = (best.sample.index + 1) % player_count
+
+ const player = best.sample.players[ best.sample.index ]
+ player.playbackRate = freq / best.sample.root
+ player.start()
+}
+
+export default { play }
+
diff --git a/client/lib/keys.js b/client/lib/keys.js
new file mode 100644
index 0000000..c9e51ac
--- /dev/null
+++ b/client/lib/keys.js
@@ -0,0 +1,39 @@
+const keys = {}
+const key_numbers = {}
+const letters = "zxcvbnmasdfghjklqwertyuiop"
+const numbers = "1234567890"
+
+let callback = function(){}
+
+letters.toUpperCase().split("").map(function(k,i){
+ keys[k.charCodeAt(0)] = i
+})
+
+numbers.split("").map(function(k,i){
+ keys[k.charCodeAt(0)] = i+letters.length
+ key_numbers[k.charCodeAt(0)] = true
+})
+
+window.addEventListener("keydown", keydown, true)
+function keydown (e) {
+ if (e.altKey || e.ctrlKey || e.metaKey) {
+ e.stopPropagation()
+ return
+ }
+ if (document.activeElement instanceof HTMLInputElement &&
+ (e.keyCode in key_numbers)) {
+ e.stopPropagation()
+ return
+ }
+ if (! (e.keyCode in keys)) return
+ var index = keys[e.keyCode]
+ if (e.shiftKey) index += letters.length
+ index -= 7
+ callback(index)
+}
+
+function listen (fn) {
+ callback = fn
+}
+
+export default { listen } \ No newline at end of file
diff --git a/client/lib/scales.js b/client/lib/scales.js
new file mode 100644
index 0000000..1462504
--- /dev/null
+++ b/client/lib/scales.js
@@ -0,0 +1,150 @@
+import Intonation from './intonation'
+
+const meantone = `! meanquar.scl
+!
+1/4-comma meantone scale. Pietro Aaron's temperament (1523)
+ 12
+!
+ 76.04900
+ 193.15686
+ 310.26471
+ 5/4
+ 503.42157
+ 579.47057
+ 696.57843
+ 25/16
+ 889.73529
+ 1006.84314
+ 1082.89214
+ 2/1
+`
+
+const mavila = `! mavila12.scl
+!
+A 12-note mavila scale (for warping meantone-based music), 5-limit TOP
+ 12
+!
+-30.99719
+ 163.50770
+ 358.01258
+ 327.01540
+ 521.52028
+ 490.52310
+ 685.02798
+ 654.03080
+ 848.53568
+ 1043.04057
+ 1012.04338
+ 1206.54826
+`
+
+const carlos_alpha = `! carlos_alpha.scl
+!
+Wendy Carlos' Alpha scale with perfect fifth divided in nine
+ 18
+!
+ 78.00000
+ 156.00000
+ 234.00000
+ 312.00000
+ 390.00000
+ 468.00000
+ 546.00000
+ 624.00000
+ 702.00000
+ 780.00000
+ 858.00000
+ 936.00000
+ 1014.00000
+ 1092.00000
+ 1170.00000
+ 1248.00000
+ 1326.00000
+ 1404.00000
+`
+
+const lamonte = `! young-lm_piano.scl
+!
+LaMonte Young's Well-Tempered Piano
+12
+!
+567/512
+9/8
+147/128
+21/16
+1323/1024
+189/128
+3/2
+49/32
+7/4
+441/256
+63/32
+2/1
+`
+
+const scales = [
+ {
+ intervals: '1/1 9/8 5/4 4/3 3/2 5/3 15/8 2/1',
+ name: "harmonic scale",
+ },
+ {
+ root: 450,
+ intervals: '1/1 9/8 5/4 4/3 3/2 5/3 15/8 2/1',
+ name: "harmonic scale @ 450",
+ },
+ {
+ tet: 5,
+ },
+ {
+ tet: 12,
+ },
+ {
+ tet: 17,
+ },
+ {
+ intervals: '1/1 81/80 33/32 21/20 16/15 12/11 11/10 10/9 9/8 8/7 7/6 32/27 6/5 11/9 5/4 14/11 9/7 21/16 4/3 27/20 11/8 7/5 10/7 16/11 40/27 3/2 32/21 14/9 11/7 8/5 18/11 5/3 27/16 12/7 7/4 16/9 9/5 20/11 11/6 15/8 40/21 64/33 160/81 2/1',
+ name: "harry partch scale",
+ },
+ {
+ scl: lamonte,
+ },
+ {
+ scl: meantone,
+ },
+ {
+ scl: mavila,
+ },
+ {
+ scl: carlos_alpha,
+ },
+].map( (opt) => new Intonation(opt) )
+
+let scale = scales[0]
+
+function build () {
+ scales.forEach( (scale, i) => {
+ scale.heading = document.createElement('div')
+ scale.heading.innerHTML = scale.name
+ scale.heading.classList.add('heading')
+ scale.heading.addEventListener('click', function(){
+ set_scale(i)
+ })
+ scale_list.appendChild(scale.heading)
+ })
+
+ set_scale(0)
+}
+
+function set_scale (i){
+ if (scale) {
+ scale.heading.classList.remove('selected')
+ }
+ scale = scales[i]
+ scale.heading.classList.add('selected')
+}
+
+function current () {
+ return scale
+}
+
+export default { scales, current, build }
diff --git a/client/lib/util.js b/client/lib/util.js
new file mode 100644
index 0000000..b2d95f5
--- /dev/null
+++ b/client/lib/util.js
@@ -0,0 +1,3 @@
+function choice (a){ return a[ Math.floor(Math.random() * a.length) ] }
+
+export { choice } \ No newline at end of file