/** * Lambdoma Triangle * @module index.js; */ import gcd from "compute-gcd"; import keys from "./lib/keys"; import color from "./lib/color"; import kalimba from "./lib/kalimba"; import sampler from "./lib/sampler"; import organ from "./lib/organ"; import midi from "./lib/midi"; import { getOutput } from "./lib/output"; import { browser, requestAudioContext, choice, roundFreq, frequencyInRange, mod, } from "./lib/util"; import { PRIMES } from "./lib/primes"; let instrument = kalimba; const root = 440; const s = 50; let w, h, ws, hs; const add_on = 0; const mul_on = 1.0; const add_off = 0.1; const mul_off = 0.9; let dragging = false; let erasing = false; let lastFreq = 0; let notes = []; let base_x = 0; let base_y = 0; let scaleMode = 0; let scaleModeCount = 5; let is_split = false; let frequencies; requestAudioContext(() => { const output = getOutput(); kalimba.load(output); organ.load(output); sampler.load(output, function ready() { instrument = sampler; }); build(); bind(); }); function build() { w = window.innerWidth; h = window.innerHeight; ws = Math.ceil(w / s); hs = Math.ceil(h / s); for (var i = 0; i < ws; i++) { notes[i] = []; for (var j = 0; j < hs; j++) { notes[i][j] = add(i, j); } } log(); } function rebuild() { notes.forEach((row) => row.forEach((note) => note.destroy())); build(); } function log() { const seen = {}; 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; } } } frequencies = Object.values(seen).sort((a, b) => a - b); console.log(frequencies); console.log(frequencies.length, "unique frequencies in 8x8"); } function play(freq) { if (!organ.isPlaying(freq.frequency)) { let frequency = freq.frequency; // while (frequency < root) { // frequency *= 2; // } // while (frequency > root) { // frequency /= 2; // } const rounded = roundFreq(freq.frequency); organ.play(frequency); notes.forEach((row) => row.forEach( (note) => note.rounded === rounded && note.div.classList.add("playing"), ), ); } } function trigger(freq) { if (frequencyInRange(freq.frequency)) { instrument.play(freq.frequency); } } function trigger_index(index) { const frequency = frequencies[index]; if (frequency) { instrument.play(frequency); } } function pause(freq) { organ.pause(freq.frequency); const rounded = roundFreq(freq.frequency); notes.forEach((row) => row.forEach( (note) => note.rounded === rounded && note.div.classList.remove("playing"), ), ); } function toggle(freq) { if (organ.isPlaying(freq.rounded) || freq.div.classList.contains("playing")) { pause(freq); } else { play(freq); } } function add(i, j) { const ii = i + base_x; const jj = j + base_y; let a, b; switch (scaleMode % scaleModeCount) { case 1: a = ii + 1; b = ii + jj + 2; break; case 2: a = ii + jj + 2; b = jj + 1; break; case 3: a = PRIMES[ii % PRIMES.length]; b = PRIMES[jj % PRIMES.length]; break; case 4: if (ii < jj) { a = 3 ** (ii + 1); b = 2 ** (jj + Math.ceil(Math.max(0, (ii * 1.0) / 2))); } else { a = 2 ** (ii + Math.ceil(Math.max(0, (jj * 1.2) / 2))); b = 3 ** jj; } // b = 2 ** jj; break; case 0: default: a = ii + 1; b = jj + 1; break; } const div = document.createElement("div"); const frequency = (root * a) / b; // const frequency = 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), div, i, j, playing: false, destroy: () => { div.parentNode && div.parentNode.removeChild(div); }, recolor: (numerator, denominator) => { let aa = a / numerator; let bb = b / denominator; if (aa < bb) { add = -Math.log(bb / aa) / 3.5; } else { add = Math.log(aa / bb) / 6; } let a_inv = a * denominator; let b_inv = b * numerator; let ba_gcd = gcd(a_inv, b_inv); let a_disp = a_inv / ba_gcd; let b_disp = b_inv / ba_gcd; frac = Math.log2(aa / bb) % 1; let frac_orig = Math.log2(a / b) % 1; if (frac < 0) { frac += 1; } if (frac_orig < 0) { frac += 1; } if (frac_orig === 0) { div.style.fontWeight = "900"; } else { div.style.fontWeight = "500"; } div.innerHTML = `
${a_disp}
/
${b_disp}
`; if (freq.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)) { div.classList.add("playing"); } }, }; freq.recolor(1, 1); if (browser.isDesktop) { div.addEventListener("mousedown", function (event) { if (event.button === 2) { // rightclick event.preventDefault(); // notes.forEach((row) => row.forEach((note) => note.recolor(a, b))); is_split = [a, b]; toggle(freq); return; } div.style.backgroundColor = color(frac, add + add_on, mul_on); dragging = true; trigger(freq); }); div.addEventListener("mouseenter", function () { div.style.backgroundColor = color(frac, add + add_on, mul_on); if (dragging) { trigger(freq); } }); div.addEventListener("mouseleave", function () { div.style.backgroundColor = color(frac, add + add_off, mul_off); }); div.addEventListener("contextmenu", function (event) { if (!event.ctrlKey || !event.metaKey || !event.altKey) { event.preventDefault(); } }); } else { div.addEventListener("touchstart", function (e) { e.preventDefault(); toggle(freq); erasing = !freq.playing; lastFreq = freq; }); } document.body.appendChild(div); return freq; } function bind() { window.addEventListener("resize", build); if (browser.isDesktop) { document.addEventListener("mousedown", (event) => { if (event.button !== 2) { dragging = true; } }); document.addEventListener("mouseup", () => { dragging = false; }); } else { document.addEventListener("touchstart", (e) => { e.preventDefault(); dragging = true; }); document.addEventListener("touchmove", (e) => { e.preventDefault(); 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) { if (dragging) { if (erasing) { pause(freq); } else { toggle(freq); } } lastFreq = freq; } }); document.addEventListener("touchend", () => { dragging = false; }); } } function keydown(e) { if (e.altKey || e.ctrlKey || e.metaKey) return; let step = 1; if (e.shiftKey) { step += 4; } console.log(e.keyCode); switch (e.keyCode) { case 37: // left base_x = Math.max(0, base_x - step); rebuild(); break; case 38: // up base_y = Math.max(0, base_y - step); rebuild(); break; case 39: // right base_x += step; rebuild(); break; case 40: // down base_y += step; rebuild(); break; case 220: // \ midi.enable(trigger_index); break; case 189: // - scaleMode = mod(scaleMode - 1, scaleModeCount); rebuild(); break; case 187: // = scaleMode = mod(scaleMode + 1, scaleModeCount); rebuild(); break; } } window.addEventListener("keydown", keydown, true); keys.listen(trigger_index);