import gcd from "compute-gcd"; import keys from "./lib/keys"; import color from "./lib/color"; import kalimba from "./lib/kalimba"; import organ from "./lib/organ"; import { getOutput } from "./lib/output"; import { browser, requestAudioContext, choice } from "./lib/util"; import { PRIMES } from "./lib/primes"; // import life from "./lib/life"; 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 is_split = false; requestAudioContext(() => { const output = getOutput(); kalimba.load(output); organ.load(output); 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); } } } function rebuild() { notes.forEach((row) => row.forEach((note) => note.destroy())); build(); } function play(freq) { if (!freq.playing) { freq.playing = true; let frequency = freq.frequency; // while (frequency < root) { // frequency *= 2; // } // while (frequency > root) { // frequency /= 2; // } organ.play(frequency); freq.div.classList.add("playing"); } } function trigger(freq) { kalimba.play(freq.frequency); } function pause(freq) { if (freq.playing) { freq.playing = false; organ.pause(freq.frequency); freq.div.classList.remove("playing"); } } function toggle(freq) { if (freq.playing) { pause(freq); } else { play(freq); } } function add(i, j) { const a = i + 1 + base_x; const b = j + 1 + base_y; // const a = i + 1; // const b = i + j + 2; // const a = PRIMES[i]; // const b = PRIMES[i + j + 1]; 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, div, i, j, playing: false, destroy: () => { 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); } }, }; 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; if (instrument === organ) { toggle(freq); erasing = !freq.playing; } else { trigger(freq); } }); div.addEventListener("mouseenter", function () { div.style.backgroundColor = color(frac, add + add_on, mul_on); if (dragging) { if (instrument === organ) { if (erasing) { pause(freq); } else { toggle(freq); } } else { 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 swap_instrument() { instrument = instrument === kalimba ? organ : kalimba; } function keydown(e) { // console.log(e.keyCode) if (e.altKey || e.ctrlKey || e.metaKey) return; let step = 1; if (e.shiftKey) { step += 4; } 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; } } window.addEventListener("keydown", keydown, true); keys.listen(function (index) { index += 7; const x = index % 7; const y = Math.floor(index / 7); const a = x; const b = y + 1; const freq = notes[a][b]; console.log(a, b, freq.frequency); trigger(freq); // const freq = scales.current().index(index) // document.body.style.backgroundColor = color( index / scales.current().scale.length ) // instrument.toggle(freq) }); let hash = window.location.hash || window.location.search; if (hash.match("sin") || hash.match("organ")) { instrument = organ; }