summaryrefslogtreecommitdiff
path: root/client/lib
diff options
context:
space:
mode:
Diffstat (limited to 'client/lib')
-rw-r--r--client/lib/color.js33
-rw-r--r--client/lib/hall.js81
-rw-r--r--client/lib/kalimba.js56
-rw-r--r--client/lib/keys.js41
-rw-r--r--client/lib/mouse.js67
-rw-r--r--client/lib/output.js8
-rw-r--r--client/lib/sampler.js61
-rw-r--r--client/lib/startAudioContext.js181
-rw-r--r--client/lib/util.js111
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,
+}
+