diff --git a/brain.js b/brain.js index f927f61..2a5fc52 100644 --- a/brain.js +++ b/brain.js @@ -23,7 +23,7 @@ function reactionWeight(spec, row) { // inputs: 5 numbers (tempo, energy, key on the circle, minor) + WORDS hashed slots for the song's words: the DJ's tags // (full weight), the title (half) and the lyrics (a third). Signed hashing, so "wobble bass" and "harmonica" land in // slots of their own and the net can learn them; the words block is unit-normalised so a wordy song isn't louder. -const NUM = 5, WORDS = 48, NIN = NUM + WORDS, NH = 12; +const NUM = 6, WORDS = 48, NIN = NUM + WORDS, NH = 12; const tokens = (s) => String(s || "").toLowerCase().replace(/\[[^\]]*\]/g, " ").split(/[^a-z0-9#+']+/).filter((w) => w.length > 1 && !STOP.has(w)); const STOP = new Set(["the","and","a","an","of","to","in","on","with","for","is","it","my","me","i","you","we","your","our","this","that","at","by","or","as","be","are","was","so","no","not","do","oh","yeah","la","na","from","into","up","down","out","all","just","like","im","its","dont","can","will"]); function feats(spec) { @@ -31,6 +31,7 @@ function feats(spec) { const h = keyHue(spec.key) * Math.PI / 180, minor = /m(in|inor)?\b/i.test(spec.key || "") && !/maj/i.test(spec.key || ""); const x = new Array(NIN).fill(0); x[0] = (bpm - 60) / 60 - 1; x[1] = en * 2 - 1; x[2] = Math.cos(h); x[3] = Math.sin(h); x[4] = minor ? 1 : -1; + x[5] = (Math.min(300, Math.max(30, +spec.seconds || 120)) - 30) / 135 - 1; // length: 30 s → -1, 300 s → +1 const add = (words, w) => { for (const t of new Set(words)) { const hh = hash32("w:" + t); x[NUM + hh % WORDS] += ((hh >>> 16) & 1 ? 1 : -1) * w; } }; add(tokens(spec.tags), 1); add(tokens(spec.title), 0.5); add(tokens(spec.lyrics), 0.33); let n = 0; for (let i = NUM; i < NIN; i++) n += x[i] * x[i]; n = Math.sqrt(n) || 1; @@ -169,8 +170,8 @@ const cssv = (n) => getComputedStyle(document.documentElement).getPropertyValue( function drawBrain(canvas, net, spec, pulse = 0) { const ctx = canvas.getContext("2d"), W = canvas.width, H = canvas.height; ctx.clearRect(0, 0, W, H); ctx.fillStyle = cssv("--card"); ctx.fillRect(0, 0, W, H); - const cols = [W * 0.16, W * 0.5, W * 0.86], ins = ["tempo", "energy", "key·x", "key·y", "minor", "words"]; - const yI = (i) => H * (0.12 + 0.76 * i / 5), yH = (i) => H * (0.07 + 0.86 * i / (NH - 1)), yO = H / 2; + const cols = [W * 0.16, W * 0.5, W * 0.86], ins = ["tempo", "energy", "key·x", "key·y", "minor", "length", "words"]; + const yI = (i) => H * (0.1 + 0.8 * i / NUM), yH = (i) => H * (0.07 + 0.86 * i / (NH - 1)), yO = H / 2; if (!net) { ctx.fillStyle = cssv("--dim"); ctx.font = `${W * 0.035}px -apple-system, system-ui, sans-serif`; ctx.textAlign = "center"; ctx.fillText("react to three tracks and the brain wakes up", W / 2, H / 2); return; } const x = spec ? feats(spec) : new Array(NIN).fill(0), { h, y } = net.forward(x); // the words block is shown as one input: its activation is how much of the song's wording the net recognises