diff options
Diffstat (limited to 'client/lib')
| -rw-r--r-- | client/lib/color.js | 33 | ||||
| -rw-r--r-- | client/lib/hall.js | 81 | ||||
| -rw-r--r-- | client/lib/kalimba.js | 56 | ||||
| -rw-r--r-- | client/lib/keys.js | 41 | ||||
| -rw-r--r-- | client/lib/mouse.js | 67 | ||||
| -rw-r--r-- | client/lib/output.js | 8 | ||||
| -rw-r--r-- | client/lib/sampler.js | 61 | ||||
| -rw-r--r-- | client/lib/startAudioContext.js | 181 | ||||
| -rw-r--r-- | client/lib/util.js | 111 |
9 files changed, 639 insertions, 0 deletions
diff --git a/client/lib/color.js b/client/lib/color.js new file mode 100644 index 0000000..5f873b3 --- /dev/null +++ b/client/lib/color.js @@ -0,0 +1,33 @@ + +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, alpha) { + add = add || 0 + mul = mul || 1 + 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 'rgba(' + rgb + ',' + alpha + ')' +} + +export default color diff --git a/client/lib/hall.js b/client/lib/hall.js new file mode 100644 index 0000000..e07db80 --- /dev/null +++ b/client/lib/hall.js @@ -0,0 +1,81 @@ +import Tone from 'tone' +import output from './output' +import { clamp, choice } from './util' + +const SPEED_OF_SOUND = 0.0029154519 // ms/m + +const reverb = new Tone.Freeverb({ + roomSize: 0.8, + dampening: 12000, +}).connect(output) + +function db_per_meter(distance){ + return 1+20*(Math.log(1/distance)/Math.log(10)) +} +function db_to_percentage(db){ + return Math.pow(10, db/10) +} + +class Hall { + constructor(props) { + let speakers = this.speakers = [] + let gain, gainEl + let delay, delayEl, z + for (let i = 0; i < props.speakers; i++) { + speakers.push(new Speaker({ + speakers: props.speakers, + length: props.length, + i: i, + })) + } + } + getSpeaker(i){ + if (i < 1 && i !== 0) { + i *= this.speakers.length + } + return this.speakers[Math.floor(i) % this.speakers.length] + } + play(sound, i, freq, pan, time){ + const speaker = this.getSpeaker(i) + sound.play(freq, time || 0, pan < 0.5 ? speaker.panLeft : speaker.panRight) + return speaker + } +} + +class Speaker { + constructor(props){ + const z = props.length * (props.i + 0.5) / props.speakers + 3 + const reverb_z = props.length * (props.i + 0.5 + props.speakers - 4) / props.speakers + const z_db = db_per_meter(z) + const reverb_db = db_per_meter(reverb_z) + + this.z = z + + this.pan = Math.atan(3/this.z) + this.panLeft = new Tone.Panner(-this.pan) + this.panRight = new Tone.Panner(-this.pan) + + this.gain = db_to_percentage(z_db + 8) + this.reverbGain = db_to_percentage(reverb_db + 15) + + this.delay = z * SPEED_OF_SOUND + // console.log(z, this.gain.toFixed(4), this.reverbGain.toFixed(4), this.pan) + + this.input = new Tone.Delay(this.delay) + this.dryOutput = new Tone.Gain (this.gain) + this.wetOutput = new Tone.Gain (this.reverbGain) + + this.panLeft.connect(this.dryOutput) + this.panLeft.connect(this.wetOutput) + this.panRight.connect(this.dryOutput) + this.panRight.connect(this.wetOutput) + + this.input.connect(this.dryOutput) + this.input.connect(this.wetOutput) + + this.dryOutput.connect(output) + this.wetOutput.connect(reverb) + } +} + +export { Hall, Speaker }
\ No newline at end of file diff --git a/client/lib/kalimba.js b/client/lib/kalimba.js new file mode 100644 index 0000000..bc048ff --- /dev/null +++ b/client/lib/kalimba.js @@ -0,0 +1,56 @@ +import Tone from 'tone' +import { choice } from './util' + +const player_count = 1 + +const compressor = new Tone.Compressor(-30, 3).toMaster() + +const samples = [ + { root: 226, fn: 'samples/kalimba/380737__cabled-mess__sansula-01-a-raw.wav', }, + { root: 267, fn: 'samples/kalimba/380736__cabled-mess__sansula-02-c-raw.wav', }, + { root: 340, fn: 'samples/kalimba/380735__cabled-mess__sansula-03-e-raw.wav', }, + { root: 452, fn: 'samples/kalimba/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.replace('wav','mp3') + } + let player = new Tone.Player({ + url: fn, + retrigger: true, + playbackRate: 1, + }) + player.connect(compressor) + sample.players.push(player) + } +}) + +function play (freq, time) { +/* + while (freq < 440) { + freq *= 2 + } + while (freq > 880) { + freq /= 2 + } + freq /= 2 +*/ + time = time || Tone.now() + 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(time) +} + +export default { play } + diff --git a/client/lib/keys.js b/client/lib/keys.js new file mode 100644 index 0000000..5b98ace --- /dev/null +++ b/client/lib/keys.js @@ -0,0 +1,41 @@ +const keys = {} +const key_numbers = {} +const letters = "yxcvbnmasdfghjklqwertuiopz" +const numbers = "1234567890" + +let callback = function(){} + +letters.toUpperCase().split("").map(function(k,i){ + keys[k.charCodeAt(0)] = i +}) + +keys['Z'.charCodeAt(0)] = 0 + +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/mouse.js b/client/lib/mouse.js new file mode 100644 index 0000000..990b8ef --- /dev/null +++ b/client/lib/mouse.js @@ -0,0 +1,67 @@ +/* + +mouse.register({ + down: (x, y) => { + }, + move: (x, y, dx, dy) => { + }, + up: (x, y) => { + }, +}) + +*/ + + +let fns = { + down: [], + move: [], + up: [], +} + +document.body.scrollTo(0,0) +document.body.parentNode.scrollTo(0,0) +document.body.addEventListener("scroll", function(e){ e.preventDefault() }) +document.body.parentNode.addEventListener("scroll", function(e){ e.preventDefault() }) + +export default { + register: (callbacks) => { + callbacks.down && fns.down.push(callbacks.down) + callbacks.move && fns.move.push(callbacks.move) + callbacks.up && fns.up.push(callbacks.up) + } +} + +let x, y, dragging = false + +function down(e){ + x = e.pageX + y = e.pageY + dragging = true + fns.down.map(f => f(x, y)) +} +function move(e){ + if (!dragging) return + let dx = e.pageX - x + let dy = e.pageY - y + x = e.pageX + y = e.pageY + dragging = true + fns.move.map(f => f(x, y, dx, dy)) +} +function up(e){ + dragging = false + fns.up.map(f => f(x, y)) +} + +function touch(f){ + return (e) => { + e.preventDefault() + f(e.touches[0]) + } +} +document.body.addEventListener("mousedown", down) +document.body.addEventListener("mousemove", move) +document.body.addEventListener("mouseup", up) +document.body.addEventListener("touchstart", touch(down)) +document.body.addEventListener("touchmove", touch(move)) +document.body.addEventListener("touchup", touch(up)) diff --git a/client/lib/output.js b/client/lib/output.js new file mode 100644 index 0000000..53901b3 --- /dev/null +++ b/client/lib/output.js @@ -0,0 +1,8 @@ +import Tone from 'tone' + +// const compressor = new Tone.Compressor(-30, 3).toMaster() + +const compressor = new Tone.Compressor(-30, 3).toMaster() +const gain = new Tone.Gain(1).connect(compressor) + +export default gain diff --git a/client/lib/sampler.js b/client/lib/sampler.js new file mode 100644 index 0000000..481a940 --- /dev/null +++ b/client/lib/sampler.js @@ -0,0 +1,61 @@ +import Tone from 'tone' +import { lerp, choice } from './util' + +const player_count = 2 +const filter_count = 3 + +const crossfaders = [] + +export default class Sampler { + constructor(path, count){ + this.samples = (() => { + let s = '', a = [] + for (let i = 1; i < count; i++) { + const s = i < 10 ? '0' + i : i; + a.push({ root: 100, fn: path.replace(/{}/, s) }) + } + return a + })() + + this.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/glass/' + fn.replace('wav','mp3') + } + let player = new Tone.Player({ + url: fn, + retrigger: true, + playbackRate: 1, + }) + sample.players.push(player) + } + }) + } + choice(){ + return choice(this.samples) + } + play(freq, time, output) { + const best = this.choice() + best.index = (best.index + 1) % player_count + + const player = best.players[ best.index ] + + freq = freq || best.root + time = time || Tone.now() + + player.playbackRate = freq / best.root + if (player.loaded) { + player.stop() + player.disconnect() + player.connect(output) + player.start(time) + } else { + console.log('loading') + } + + return player + } +} 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..fd48768 --- /dev/null +++ b/client/lib/util.js @@ -0,0 +1,111 @@ +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, isAndroid, isMobile, isDesktop } + +function clamp(n,a,b){ return n<a?a:n<b?n:b } +function choice (a){ return a[ Math.floor(Math.random() * a.length) ] } +function mod(n,m){ return n-(m * Math.floor(n/m)) } +function randint(n){ return (Math.random()*n)|0 } +function randrange(a,b){ return a + Math.random() * (b-a) } +function randsign(){ return Math.random() >= 0.5 ? -1 : 1 } +function lerp(n,a,b){ return (b-a)*n+a } +function angle(x0,y0,x1,y1){ return Math.atan2(y1-y0,x1-x0) } +function dist(x0,y0,x1,y1){ return Math.sqrt(Math.pow(x1-x0,2)+Math.pow(y1-y0,2)) } +function xor(a,b){ a=!!a; b=!!b; return (a||b) && !(a&&b) } +function shuffle(a){ + for (var i = a.length; i > 0; i--){ + var r = randint(i) + var swap = a[i-1] + a[i-1] = a[r] + a[r] = swap + } + return a +} +// returns a gaussian random function with the given mean and stdev. +function gaussian(mean, stdev) { + let y2; + let use_last = false; + return () => { + let y1; + if (use_last) { + y1 = y2; + use_last = false; + } + else { + let x1, x2, w; + do { + x1 = 2.0 * Math.random() - 1.0; + x2 = 2.0 * Math.random() - 1.0; + w = x1 * x1 + x2 * x2; + } while( w >= 1.0); + w = Math.sqrt((-2.0 * Math.log(w))/w); + y1 = x1 * w; + y2 = x2 * w; + use_last = true; + } + + let retval = mean + stdev * y1; + if (retval > 0) + return retval; + return -retval; + } +} + +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, { + display: 'block', + position: 'absolute', + width: '100%', + height: '100%', + zIndex: '10000', + top: '0px', + left: '0px', + backgroundColor: 'rgba(0, 0, 0, 0.8)', + }) + Object.assign(button.style, { + display: 'block', + 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', + width: '150px', + }) + container.appendChild(button) + document.body.appendChild(container) + StartAudioContext.setContext(Tone.context) + StartAudioContext.on(button) + StartAudioContext.onStarted(_ => { + container.remove() + fn() + }) + } else { + fn() + } +} + +export { + clamp, choice, mod, lerp, angle, dist, xor, + randint, randrange, randsign, shuffle, gaussian, + browser, requestAudioContext, +} + |
