diff options
Diffstat (limited to 'client/lib')
| -rw-r--r-- | client/lib/color.js | 31 | ||||
| -rw-r--r-- | client/lib/colundi.js | 170 | ||||
| -rw-r--r-- | client/lib/intonation.js | 162 | ||||
| -rw-r--r-- | client/lib/kalimba.js | 46 | ||||
| -rw-r--r-- | client/lib/keys.js | 39 | ||||
| -rw-r--r-- | client/lib/scales.js | 161 | ||||
| -rw-r--r-- | client/lib/startAudioContext.js | 181 | ||||
| -rw-r--r-- | client/lib/util.js | 58 |
8 files changed, 848 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/colundi.js b/client/lib/colundi.js new file mode 100644 index 0000000..e514ae7 --- /dev/null +++ b/client/lib/colundi.js @@ -0,0 +1,170 @@ +import { mod } from './util' + +const list = [ + { + "freq": 10.8, + "color": "rgb(0, 125, 197)" + }, + { + "freq": 33, + "color": "rgb(0, 144, 76)" + }, + { + "freq": 33.8, + "color": "rgb(0, 172, 204)" + }, + { + "freq": 55, + "color": "rgb(0, 145, 132)" + }, + { + "freq": 62.64, + "color": "rgb(165, 85, 117)" + }, + { + "freq": 63, + "color": "rgb(205, 157, 125)" + }, + { + "freq": 70, + "color": "rgb(242, 135, 183)" + }, + { + "freq": 73.6, + "color": "rgb(0, 102, 154)" + }, + { + "freq": 83, + "color": "rgb(126, 136, 63)" + }, + { + "freq": 98.4, + "color": "rgb(164, 84, 138)" + }, + { + "freq": 105, + "color": "rgb(208, 156, 80)" + }, + { + "freq": 110, + "color": "rgb(205, 232, 212)" + }, + { + "freq": 111, + "color": "rgb(109, 166, 159)" + }, + { + "freq": 147, + "color": "rgb(199, 234, 251)" + }, + { + "freq": 147.85, + "color": "rgb(13, 177, 75)" + }, + { + "freq": 172.06, + "color": "rgb(0, 158, 218)" + }, + { + "freq": 210.42, + "color": "rgb(0, 157, 188)" + }, + { + "freq": 221.23, + "color": "rgb(248, 169, 143)" + }, + { + "freq": 264, + "color": "rgb(150, 213, 210)" + }, + { + "freq": 293, + "color": "rgb(127, 82, 139)" + }, + { + "freq": 342, + "color": "rgb(3, 78, 162)" + }, + { + "freq": 396, + "color": "rgb(87, 196, 206)" + }, + { + "freq": 404, + "color": "rgb(85, 80, 139)" + }, + { + "freq": 408, + "color": "rgb(20, 78, 140)" + }, + { + "freq": 410, + "color": "rgb(52, 131, 170)" + }, + { + "freq": 413, + "color": "rgb(92, 45, 145)" + }, + { + "freq": 416, + "color": "rgb(199, 26, 121)" + }, + { + "freq": 417, + "color": "rgb(204, 123, 104)" + }, + { + "freq": 420.82, + "color": "rgb(0, 185, 240)" + }, + { + "freq": 440, + "color": "rgb(245, 130, 32)" + }, + { + "freq": 448, + "color": "rgb(249, 213, 229)" + }, + { + "freq": 528, + "color": "rgb(249, 166, 27)" + }, + { + "freq": 630, + "color": "rgb(96, 52, 56)" + }, + { + "freq": 639, + "color": "rgb(202, 125, 154)" + }, + { + "freq": 685, + "color": "rgb(201, 88, 115)" + }, + { + "freq": 852, + "color": "rgb(117, 112, 178)" + }, + { + "freq": 880, + "color": "rgb(103, 193, 140)" + }, + { + "freq": 1052, + "color": "rgb(156, 149, 201)" + }, + { + "freq": 12000, + "color": "rgb(165, 40, 104)" + } +] +function color (n) { + n = mod(n, list.length) + return list[n].color +} +function index (n) { + n = mod(n, list.length) + return list[n].freq +} + +export default { index, color, list } 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..8480f9e --- /dev/null +++ b/client/lib/scales.js @@ -0,0 +1,161 @@ +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] +let handleChange = function(){} + +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(){ + pick(i) + }) + scale_list.appendChild(scale.heading) + }) + + pick(0) +} + +function pick (i) { + if (scale) { + scale.heading && scale.heading.classList.remove('selected') + } + scale = scales[i] + scale.heading && scale.heading.classList.add('selected') + handleChange(scale) +} + +function current () { + return scale +} + +function onChange (fn) { + handleChange = fn +} + +function names () { + return scales.map( scale => scale.name ) +} + + +export default { scales, current, build, pick, names, onChange } diff --git a/client/lib/startAudioContext.js b/client/lib/startAudioContext.js new file mode 100644 index 0000000..f3a9793 --- /dev/null +++ b/client/lib/startAudioContext.js @@ -0,0 +1,181 @@ +/** + * StartAudioContext.js + * @author Yotam Mann + * @license http://opensource.org/licenses/MIT MIT License + * @copyright 2016 Yotam Mann + */ +(function (root, factory) { + if (typeof define === "function" && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.StartAudioContext = factory(); + } +}(this, function () { + + /** + * The StartAudioContext object + */ + var StartAudioContext = { + /** + * The audio context passed in by the user + * @type {AudioContext} + */ + context : null, + /** + * The TapListeners bound to the elements + * @type {Array} + * @private + */ + _tapListeners : [], + /** + * Callbacks to invoke when the audio context is started + * @type {Array} + * @private + */ + _onStarted : [], + }; + + + /** + * Set the context + * @param {AudioContext} ctx + * @returns {StartAudioContext} + */ + StartAudioContext.setContext = function(ctx){ + StartAudioContext.context = ctx; + return StartAudioContext; + }; + + /** + * Add a tap listener to the audio context + * @param {Array|Element|String|jQuery} element + * @returns {StartAudioContext} + */ + StartAudioContext.on = function(element){ + if (Array.isArray(element) || (NodeList && element instanceof NodeList)){ + for (var i = 0; i < element.length; i++){ + StartAudioContext.on(element[i]); + } + } else if (typeof element === "string"){ + StartAudioContext.on(document.querySelectorAll(element)); + } else if (element.jquery && typeof element.toArray === "function"){ + StartAudioContext.on(element.toArray()); + } else if (Element && element instanceof Element){ + //if it's an element, create a TapListener + var tap = new TapListener(element, onTap); + StartAudioContext._tapListeners.push(tap); + } + return StartAudioContext; + }; + + /** + * Bind a callback to when the audio context is started. + * @param {Function} cb + * @return {StartAudioContext} + */ + StartAudioContext.onStarted = function(cb){ + //if it's already started, invoke the callback + if (StartAudioContext.isStarted()){ + cb(); + } else { + StartAudioContext._onStarted.push(cb); + } + return StartAudioContext; + }; + + /** + * returns true if the context is started + * @return {Boolean} + */ + StartAudioContext.isStarted = function(){ + return (StartAudioContext.context !== null && StartAudioContext.context.state === "running"); + }; + + /** + * @class Listens for non-dragging tap ends on the given element + * @param {Element} element + * @internal + */ + var TapListener = function(element){ + + this._dragged = false; + + this._element = element; + + this._bindedMove = this._moved.bind(this); + this._bindedEnd = this._ended.bind(this); + + element.addEventListener("touchmove", this._bindedMove); + element.addEventListener("touchend", this._bindedEnd); + element.addEventListener("mouseup", this._bindedEnd); + }; + + /** + * drag move event + */ + TapListener.prototype._moved = function(e){ + this._dragged = true; + }; + + /** + * tap ended listener + */ + TapListener.prototype._ended = function(e){ + if (!this._dragged){ + onTap(); + } + this._dragged = false; + }; + + /** + * remove all the bound events + */ + TapListener.prototype.dispose = function(){ + this._element.removeEventListener("touchmove", this._bindedMove); + this._element.removeEventListener("touchend", this._bindedEnd); + this._element.removeEventListener("mouseup", this._bindedEnd); + this._bindedMove = null; + this._bindedEnd = null; + this._element = null; + }; + + /** + * Invoked the first time of the elements is tapped. + * Creates a silent oscillator when a non-dragging touchend + * event has been triggered. + */ + function onTap(){ + //start the audio context with a silent oscillator + if (StartAudioContext.context && !StartAudioContext.isStarted()){ + var osc = StartAudioContext.context.createOscillator(); + var silent = StartAudioContext.context.createGain(); + silent.gain.value = 0; + osc.connect(silent); + silent.connect(StartAudioContext.context.destination); + var now = StartAudioContext.context.currentTime; + osc.start(now); + osc.stop(now+0.5); + } + + //dispose all the tap listeners + if (StartAudioContext._tapListeners){ + for (var i = 0; i < StartAudioContext._tapListeners.length; i++){ + StartAudioContext._tapListeners[i].dispose(); + } + StartAudioContext._tapListeners = null; + } + //the onstarted callbacks + if (StartAudioContext._onStarted){ + for (var j = 0; j < StartAudioContext._onStarted.length; j++){ + StartAudioContext._onStarted[j](); + } + StartAudioContext._onStarted = null; + } + } + + return StartAudioContext; +})); + + diff --git a/client/lib/util.js b/client/lib/util.js new file mode 100644 index 0000000..72be9b7 --- /dev/null +++ b/client/lib/util.js @@ -0,0 +1,58 @@ +import Tone from 'tone' +import StartAudioContext from './startAudioContext' + +const isIphone = (navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i)) +const isIpad = (navigator.userAgent.match(/iPad/i)) +const isAndroid = (navigator.userAgent.match(/Android/i)) +const isMobile = isIphone || isIpad || isAndroid +const isDesktop = ! isMobile + +document.body.classList.add(isMobile ? 'mobile' : 'desktop') + +const browser = { isIphone, isIpad, isMobile, isDesktop } + +function choice (a){ return a[ Math.floor(Math.random() * a.length) ] } +function mod(n,m){ return n-(m * Math.floor(n/m)) } + +function requestAudioContext (fn) { + if (isMobile) { + const container = document.createElement('div') + const button = document.createElement('div') + button.innerHTML = 'Tap to start - please unmute your phone' + Object.assign(container.style, { + position: 'absolute', + width: '100%', + height: '100%', + zIndex: '10000', + top: '0px', + left: '0px', + backgroundColor: 'rgba(0, 0, 0, 0.8)', + }) + Object.assign(button.style, { + position: 'absolute', + left: '50%', + top: '50%', + padding: '20px', + backgroundColor: '#7F33ED', + color: 'white', + fontFamily: 'monospace', + borderRadius: '3px', + transform: 'translate3D(-50%,-50%,0)', + textAlign: 'center', + lineHeight: '1.5', + }) + container.appendChild(button) + document.body.appendChild(container) + StartAudioContext.setContext(Tone.context) + StartAudioContext.on(button) + StartAudioContext.onStarted(_ => { + container.remove() + fn() + }) + } else { + fn() + } +} + +export { choice, mod, browser, requestAudioContext } + |
