diff options
Diffstat (limited to 'client')
| -rw-r--r-- | client/index.js | 194 | ||||
| -rw-r--r-- | client/lib/color.js | 2 | ||||
| -rw-r--r-- | client/lib/kalimba.js | 49 | ||||
| -rw-r--r-- | client/lib/organ.js | 43 | ||||
| -rw-r--r-- | client/lib/sampler.js | 23 | ||||
| -rw-r--r-- | client/lib/scales.js | 8 | ||||
| -rw-r--r-- | client/lib/util.js | 15 | ||||
| -rw-r--r-- | client/vendor/oktransition.js | 182 |
8 files changed, 397 insertions, 119 deletions
diff --git a/client/index.js b/client/index.js index 2c986dd..df127e4 100644 --- a/client/index.js +++ b/client/index.js @@ -11,20 +11,23 @@ import kalimba from "./lib/kalimba"; import sampler from "./lib/sampler"; import organ from "./lib/organ"; import midi from "./lib/midi"; +import oktransition from "./vendor/oktransition"; import { getOutput } from "./lib/output"; import { browser, requestAudioContext, + clamp, choice, - roundFreq, - frequencyInRange, + roundInterval, + intervalInRange, mod, } from "./lib/util"; import { scales } from "./lib/scales"; let instrument = kalimba; -const root = 440; +let grid = document.createElement("grid"); +let root = 440; const s = 50; let w, h, ws, hs; @@ -35,16 +38,17 @@ const mul_off = 0.9; let dragging = false; let erasing = false; -let lastFreq = 0; +let lastNote = 0; let notes = []; let base_x = 0; let base_y = 0; let scaleMode = 0; let is_split = false; -let frequencies; +let intervals; requestAudioContext(() => { const output = getOutput(); + document.body.appendChild(grid); kalimba.load(output); organ.load(output); sampler.load(output, function ready() { @@ -80,28 +84,28 @@ function log() { // console.log(notes); for (let i = 0; i < 8; i++) { for (let j = 0; j < 8; j++) { - const frequency = notes[i][j].frequency; - const rounded = roundFreq(frequency); - if (!seen[rounded] && frequencyInRange(frequency)) { - seen[rounded] = notes[i][j].frequency; + const interval = notes[i][j].interval; + const rounded = roundInterval(interval); + if (!seen[rounded] && intervalInRange(interval, root)) { + seen[rounded] = notes[i][j].interval; } } } - frequencies = Object.values(seen).sort((a, b) => a - b); - // console.log(frequencies); - console.log(frequencies.length, "unique frequencies in 8x8"); + intervals = Object.values(seen).sort((a, b) => a - b); + // console.log(intervals); + console.log(intervals.length, "unique intervals in 8x8"); } -function play(freq) { - if (!organ.isPlaying(freq.frequency)) { - let frequency = freq.frequency; - // while (frequency < root) { - // frequency *= 2; +function play(note) { + if (!organ.isPlaying(note.interval)) { + let interval = note.interval; + // while (interval < root) { + // interval *= 2; // } - // while (frequency > root) { - // frequency /= 2; + // while (interval > root) { + // interval /= 2; // } - const rounded = roundFreq(freq.frequency); - organ.play(frequency); + const rounded = roundInterval(note.interval); + organ.play(interval); notes.forEach((row) => row.forEach( (note) => note.rounded === rounded && note.div.classList.add("playing"), @@ -109,20 +113,20 @@ function play(freq) { ); } } -function trigger(freq) { - if (frequencyInRange(freq.frequency)) { - instrument.play(freq.frequency); +function trigger(note) { + if (intervalInRange(note.interval, root)) { + instrument.play(note.interval, root); } } function trigger_index(index) { - const frequency = frequencies[index]; - if (frequency) { - instrument.play(frequency); + const interval = intervals[index]; + if (interval) { + instrument.play(interval, root); } } -function pause(freq) { - organ.pause(freq.frequency); - const rounded = roundFreq(freq.frequency); +function pause(note) { + organ.pause(note.interval); + const rounded = roundInterval(note.interval); notes.forEach((row) => row.forEach( (note) => @@ -130,11 +134,11 @@ function pause(freq) { ), ); } -function toggle(freq) { - if (organ.isPlaying(freq.rounded) || freq.div.classList.contains("playing")) { - pause(freq); +function toggle(note) { + if (organ.isPlaying(note.rounded) || note.div.classList.contains("playing")) { + pause(note); } else { - play(freq); + play(note); } } @@ -146,16 +150,16 @@ function add(i, j) { const [a, b] = scale.get(ii, jj, i, j, base_x, base_y); const div = document.createElement("div"); - const frequency = (root * a) / b; - // const frequency = root * Math.pow(2, ((b / a) % 1) + 1); + const interval = a / b; + // const interval = root * Math.pow(2, ((b / a) % 1) + 1); let add = 0; let frac; div.style.left = i * s + "px"; div.style.top = j * s + "px"; - const freq = { - frequency, - rounded: roundFreq(frequency), + const note = { + interval, + rounded: roundInterval(interval, root), div, i, j, @@ -192,19 +196,19 @@ function add(i, j) { } div.innerHTML = `<div>${a_disp}</div><div>/</div><div>${b_disp}</div>`; - if (freq.playing) { + if (note.playing) { div.style.backgroundColor = color(frac, add + add_on, mul_on); } else { div.style.backgroundColor = color(frac, add + add_off, mul_off); } - if (organ.isPlaying(frequency)) { + if (organ.isPlaying(interval)) { div.classList.add("playing"); } }, }; - freq.recolor(1, 1); + note.recolor(1, 1); if (browser.isDesktop) { div.addEventListener("mousedown", function (event) { @@ -213,17 +217,17 @@ function add(i, j) { event.preventDefault(); // notes.forEach((row) => row.forEach((note) => note.recolor(a, b))); is_split = [a, b]; - toggle(freq); + toggle(note); return; } div.style.backgroundColor = color(frac, add + add_on, mul_on); dragging = true; - trigger(freq); + trigger(note); }); div.addEventListener("mouseenter", function () { div.style.backgroundColor = color(frac, add + add_on, mul_on); if (dragging) { - trigger(freq); + trigger(note); } }); div.addEventListener("mouseleave", function () { @@ -237,17 +241,16 @@ function add(i, j) { } else { div.addEventListener("touchstart", function (e) { e.preventDefault(); - toggle(freq); - erasing = !freq.playing; - lastFreq = freq; + toggle(note); + erasing = !note.playing; + lastNote = note; }); } - document.body.appendChild(div); - return freq; + grid.appendChild(div); + return note; } function bind() { - window.addEventListener("resize", build); if (browser.isDesktop) { document.addEventListener("mousedown", (event) => { if (event.button !== 2) { @@ -267,61 +270,124 @@ function bind() { const x = Math.floor(e.touches[0].pageX / s); const y = Math.floor(e.touches[0].pageY / s); if (!(x in notes) || !(y in notes[x])) return; - const freq = notes[x][y]; - if (freq !== lastFreq) { + const note = notes[x][y]; + if (note !== lastNote) { if (dragging) { if (erasing) { - pause(freq); + pause(note); } else { - toggle(freq); + toggle(note); } } - lastFreq = freq; + lastNote = note; } }); document.addEventListener("touchend", () => { dragging = false; }); } + window.addEventListener("resize", build); + window.addEventListener("keydown", keydown, true); + keys.listen(trigger_index); + document + .querySelector("#help .close") + .addEventListener("click", () => + document.querySelector("#help").classList.remove("visible"), + ); + document + .querySelector("#help-button") + .addEventListener("click", () => + document.querySelector("#help").classList.toggle("visible"), + ); } +let isReset = false; function keydown(e) { - if (e.altKey || e.ctrlKey || e.metaKey) return; let step = 1; if (e.shiftKey) { step += 4; } - // console.log(e.keyCode); + console.log(e.keyCode); switch (e.keyCode) { + case 27: // esc - PANIC + if (isReset) { + base_x = 0; + base_y = 0; + showMessage(`reset!`); + } + organ.stop(); + sampler.stop(); + rebuild(); + isReset = true; + setTimeout(() => (isReset = false), 500); + break; case 37: // left + if (e.altKey || e.ctrlKey || e.metaKey) return; base_x = Math.max(0, base_x - step); rebuild(); break; case 38: // up + if (e.altKey || e.ctrlKey || e.metaKey) return; base_y = Math.max(0, base_y - step); rebuild(); break; case 39: // right + if (e.altKey || e.ctrlKey || e.metaKey) return; base_x += step; rebuild(); break; case 40: // down + if (e.altKey || e.ctrlKey || e.metaKey) return; base_y += step; rebuild(); break; case 220: // \ midi.enable(trigger_index); break; + case 191: // ? + document.querySelector("#help").classList.toggle("visible"); + break; case 189: // - - scaleMode = mod(scaleMode - 1, scales.length); - rebuild(); + e.preventDefault(); + if (e.altKey || e.metaKey) { + root = clamp(root - (e.shiftKey ? 10 : 1), 1, 200000); + organ.setRoot(root); + showMessage(`Root: ${root} hz`); + } else { + scaleMode = mod(scaleMode - 1, scales.length); + rebuild(); + showMessage(scales[scaleMode].name); + } break; case 187: // = - scaleMode = mod(scaleMode + 1, scales.length); - rebuild(); + e.preventDefault(); + if (e.altKey || e.metaKey) { + root = clamp(root + (e.shiftKey ? 10 : 1), 1, 200000); + organ.setRoot(root); + showMessage(`Root: ${root} hz`); + } else { + scaleMode = mod(scaleMode + 1, scales.length); + rebuild(); + showMessage(scales[scaleMode].name); + } break; } } -window.addEventListener("keydown", keydown, true); -keys.listen(trigger_index); +let messageTransition; +function showMessage(message) { + const el = document.getElementById("message"); + el.innerHTML = message; + el.style.opacity = 1; + if (messageTransition) { + messageTransition.cancel(); + } + messageTransition = oktransition.add({ + obj: el.style, + from: { opacity: 1 }, + to: { opacity: 0 }, + delay: 1500, + duration: 2000, + easing: oktransition.easing.circ_out, + }); +} diff --git a/client/lib/color.js b/client/lib/color.js index bea0330..bbc137e 100644 --- a/client/lib/color.js +++ b/client/lib/color.js @@ -62,7 +62,7 @@ function color(t, add, mul) { 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); + rgb[i] = Math.round(channel(-t, a, b, c, d, add, mul) * 255); } return "rgb(" + rgb + ")"; } diff --git a/client/lib/kalimba.js b/client/lib/kalimba.js index 64ace00..53fcb99 100644 --- a/client/lib/kalimba.js +++ b/client/lib/kalimba.js @@ -18,7 +18,7 @@ const samples = [ // { root: 671, fn: 'samples/380732__cabled-mess__sansula-09-e-raw.wav', }, ]; -function load(output) { +function load({ output }) { samples.forEach((sample) => { sample.players = []; sample.index = -1; @@ -45,15 +45,12 @@ function load(output) { ); } -let last = 440; - -function play(freq) { - last = freq; +function play(interval, root) { const sample = choice(samples); sample.index = (sample.index + 1) % sample.players.length; const player = sample.players[sample.index]; - player.playbackRate = freq / sample.root; + player.playbackRate = (interval * root) / sample.root; player.start(); } @@ -63,24 +60,24 @@ function pause() { export default { load, play, pause }; -// for help tuning -function keydown(e) { - // console.log(e.keyCode) - if (e.metaKey && last) { - let step = e.shiftKey ? (e.ctrlKey ? 0.01 : 0.1) : 1; - switch (e.keyCode) { - case 38: // up - e.preventDefault(); - samples[0].root -= step; - play(last); - break; - case 40: // down - e.preventDefault(); - samples[0].root += step; - play(last); - break; - } - console.log(samples[0].root); - } -} +// for help tuning the kalimba samples +// function keydown(e) { +// // console.log(e.keyCode) +// if (e.metaKey && last) { +// let step = e.shiftKey ? (e.ctrlKey ? 0.01 : 0.1) : 1; +// switch (e.keyCode) { +// case 38: // up +// e.preventDefault(); +// samples[0].root -= step; +// play(last); +// break; +// case 40: // down +// e.preventDefault(); +// samples[0].root += step; +// play(last); +// break; +// } +// console.log(samples[0].root); +// } +// } // window.addEventListener("keydown", keydown, true); diff --git a/client/lib/organ.js b/client/lib/organ.js index 652351e..e66f89d 100644 --- a/client/lib/organ.js +++ b/client/lib/organ.js @@ -4,30 +4,31 @@ */ import Tone from "tone"; -import { roundFreq } from "./util"; +import { roundInterval } from "./util"; -const oscillators = {}; +let root = 440; +let oscillators = {}; let output; let lastPlayed; function load(out) { output = out; } - -function isPlaying(freq) { - const rounded = roundFreq(freq); +function isPlaying(interval) { + const rounded = roundInterval(interval); const osc = oscillators[rounded]; return osc && osc.playing; } -function play(freq) { +function play(interval) { if (!output) { return; } - const rounded = roundFreq(freq); + const rounded = roundInterval(interval); const osc = (oscillators[rounded] = oscillators[rounded] || {}); if (!osc.el) { - osc.el = new Tone.Oscillator(freq, "sine"); + osc.interval = interval; + osc.el = new Tone.Oscillator(interval * root, "sine"); osc.el.connect(output); } osc.el.start(); @@ -36,13 +37,31 @@ function play(freq) { return osc; } -function pause(freq) { - const rounded = roundFreq(freq); +function pause(interval) { + const rounded = roundInterval(interval); if (!oscillators[rounded]) return; const osc = (oscillators[rounded] = oscillators[rounded] || {}); - if (osc.el) osc.el.stop(); + if (osc.el) { + osc.el.stop(); + } osc.playing = false; return osc; } -export default { load, isPlaying, play, pause, oscillators }; +function setRoot(newRoot) { + root = newRoot; + for (const osc of Object.values(oscillators)) { + osc.el.frequency.value = osc.interval * newRoot; + } +} +function stop() { + for (const osc of Object.values(oscillators)) { + osc.el.stop(); + osc.el.disconnect(); + osc.playing = false; + delete osc.el; + } + oscillators = {}; +} + +export default { load, isPlaying, play, pause, stop, setRoot }; diff --git a/client/lib/sampler.js b/client/lib/sampler.js index 08f253d..69da86e 100644 --- a/client/lib/sampler.js +++ b/client/lib/sampler.js @@ -5,6 +5,8 @@ import Tone from "tone"; +let root = 440; + let output; let ready; let current = ""; @@ -72,14 +74,19 @@ export function loadSampleFromFile(file, url) { /** * Player */ -let last = 440; +let last = [1, 440]; -function play(freq) { - last = freq; +function stop() { + for (const sample of Object.values(samples)) { + sample.players.forEach((player) => player.stop()); + } +} +function play(interval, root) { + last = [interval, root]; const sample = samples[current]; sample.index = (sample.index + 1) % sample.players.length; const player = sample.players[sample.index]; - player.playbackRate = freq / sample.root; + player.playbackRate = (interval * root) / sample.root; player.start(); } @@ -87,7 +94,7 @@ function pause() { // no-op } -export default { load, play, pause }; +export default { load, play, pause, stop }; // for help tuning function keydown(e) { @@ -99,12 +106,14 @@ function keydown(e) { case 38: // up e.preventDefault(); sample.root -= step; - play(last); + stop(); + play(last[0], last[1]); break; case 40: // down e.preventDefault(); sample.root += step; - play(last); + stop(); + play(last[0], last[1]); break; } } diff --git a/client/lib/scales.js b/client/lib/scales.js index 87dcb0e..1e5afd6 100644 --- a/client/lib/scales.js +++ b/client/lib/scales.js @@ -19,11 +19,11 @@ import { let a, b; export const scales = [ - { name: "integer", get: (i, j) => [i + 1, j + 1] }, - { name: "subharmonic", get: (i, j) => [i + 1, i + j + 2] }, - { name: "harmonic", get: (i, j) => [i + j + 2, j + 1] }, + { name: "natural", get: (i, j) => [i + 1, j + 1] }, + { name: "undertone", get: (i, j) => [i + 1, i + j + 2] }, + { name: "overtone", get: (i, j) => [i + j + 2, j + 1] }, { - name: "prime", + name: "primes", reset: (x, y, w, h) => { a = Prime().skip(x).take(w).toJS(); b = Prime().skip(y).take(h).toJS(); diff --git a/client/lib/util.js b/client/lib/util.js index 5bf93dc..d0a3914 100644 --- a/client/lib/util.js +++ b/client/lib/util.js @@ -23,10 +23,13 @@ function choice(a) { function mod(n, m) { return n - m * Math.floor(n / m); } -function roundFreq(freq) { - return Math.round(freq * 100); +function roundInterval(interval) { + return Math.round(interval * 10000000); } -const frequencyInRange = (freq) => 20 < freq && freq < 15000; +const intervalInRange = (interval, root) => + 20 < interval * root && interval * root < 15000; +const lerp = (n, a, b) => (b - a) * n + a; +const clamp = (n, a = 0, b = 1) => (n < a ? a : n < b ? n : b); function requestAudioContext(fn) { if (window.location.protocol !== "https:") { @@ -75,7 +78,9 @@ export { choice, mod, browser, - roundFreq, - frequencyInRange, + lerp, + clamp, + roundInterval, + intervalInRange, requestAudioContext, }; diff --git a/client/vendor/oktransition.js b/client/vendor/oktransition.js new file mode 100644 index 0000000..816ffb0 --- /dev/null +++ b/client/vendor/oktransition.js @@ -0,0 +1,182 @@ +/* + oktransition.add({ + obj: el.style, + units: "px", + from: { left: 0 }, + to: { left: 100 }, + duration: 1000, + easing: oktransition.easing.circ_out, + update: function(obj){ + console.log(obj.left) + } + finished: function(){ + console.log("done") + } + }) +*/ + +const oktransition = {}; +let transitions = []; + +let last_t = 0; +let id = 0; + +const lerp = (n, a, b) => (b - a) * n + a; + +oktransition.speed = 1; +oktransition.add = (transition) => { + transition.id = id++; + transition.obj = transition.obj || {}; + if (transition.easing) { + if (typeof transition.easing === "string") { + transition.easing = oktransition.easing[transition.easing]; + } + } else { + transition.easing = oktransition.easing.linear; + } + if (!("from" in transition) && !("to" in transition)) { + transition.keys = []; + } else if (!("from" in transition)) { + transition.from = {}; + transition.keys = Object.keys(transition.to); + transition.keys.forEach(function (prop) { + transition.from[prop] = parseFloat(transition.obj[prop]); + }); + } else { + transition.keys = Object.keys(transition.from); + } + transition.delay = transition.delay || 0; + transition.start = last_t + transition.delay; + transition.done = false; + transition.after = transition.after || []; + transition.then = (fn) => { + transition.after.push(fn); + return transition; + }; + transition.tick = 0; + transition.skip = transition.skip || 1; + transition.dt = 0; + transition.cancel = () => + (transitions = transitions.filter((item) => item !== transition)); + transitions.push(transition); + return transition; +}; +oktransition.update = (t) => { + let done = false; + requestAnimationFrame(oktransition.update); + last_t = t * oktransition.speed; + if (transitions.length === 0) return; + transitions.forEach((transition, i) => { + const dt = Math.min(1.0, (t - transition.start) / transition.duration); + transition.tick++; + if ( + dt < 0 || + (dt < 1 && transition.tick % transition.skip != 0) || + transition.done + ) + return; + const ddt = transition.easing(dt); + transition.dt = ddt; + transition.keys.forEach((prop) => { + let val = lerp(ddt, transition.from[prop], transition.to[prop]); + if (transition.round) val = Math.round(val); + if (transition.units) val = Math.round(val) + transition.units; + transition.obj[prop] = val; + }); + if (transition.update) { + transition.update(transition.obj, dt); + } + if (dt === 1) { + if (transition.finished) { + transition.finished(transition); + } + if (transition.after.length) { + const twn = transition.after.shift(); + twn.obj = twn.obj || transition.obj; + twn.after = transition.after; + oktransition.add(twn); + } + if (transition.loop) { + transition.start = t + transition.delay; + } else { + done = true; + transition.done = true; + } + } + }); + if (done) { + transitions = transitions.filter((transition) => !transition.done); + } +}; + +requestAnimationFrame(oktransition.update); + +oktransition.easing = { + linear: (t) => { + return t; + }, + circ_out: (t) => { + return Math.sqrt(1 - (t = t - 1) * t); + }, + circ_in: (t) => { + return -(Math.sqrt(1 - t * t) - 1); + }, + circ_in_out: (t) => { + return (t *= 2) < 1 + ? -0.5 * (Math.sqrt(1 - t * t) - 1) + : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + quad_in: (n) => { + return Math.pow(n, 2); + }, + quad_out: (n) => { + return n * (n - 2) * -1; + }, + quad_in_out: (n) => { + n = n * 2; + if (n < 1) { + return Math.pow(n, 2) / 2; + } + return (-1 * (--n * (n - 2) - 1)) / 2; + }, + cubic_bezier: (mX1, mY1, mX2, mY2) => { + function A(aA1, aA2) { + return 1.0 - 3.0 * aA2 + 3.0 * aA1; + } + function B(aA1, aA2) { + return 3.0 * aA2 - 6.0 * aA1; + } + function C(aA1) { + return 3.0 * aA1; + } + + // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. + function CalcBezier(aT, aA1, aA2) { + return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; + } + + // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. + function GetSlope(aT, aA1, aA2) { + return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); + } + + function GetTForX(aX) { + // Newton raphson iteration + let aGuessT = aX; + for (let i = 0; i < 10; ++i) { + const currentSlope = GetSlope(aGuessT, mX1, mX2); + if (currentSlope == 0.0) return aGuessT; + const currentX = CalcBezier(aGuessT, mX1, mX2) - aX; + aGuessT -= currentX / currentSlope; + } + return aGuessT; + } + + return function (aX) { + if (mX1 == mY1 && mX2 == mY2) return aX; // linear + return CalcBezier(aX, mY1, mY2); + }; + }, +}; + +export default oktransition; |
