Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697// meter.mjs — rhythm-structure helpers for /pop.//// fibSequence(count) -> [1, 2, 3, 5, 8, …]// fibPartition(n) -> Fibonacci addends that sum to n// fibMeter(totalBars, opts) -> section bar-lengths from the sequence// subdivideCell(cell, n) -> one .np cell split into n equal cells// subdivideNp(text, opts) -> subdivide notes across a whole .np score//// "Fibonacci division and addition for measures/bars" — divide a bar of// N beats into Fibonacci groups, or build a form whose section lengths// climb the sequence. "Note subdivision" — the Ableton move of cutting a// long note into 1/2 · 1/4 · 1/8 · 1/16 notes.
// ── Fibonacci ─────────────────────────────────────────────────────────// The sequence with no leading 1,1 duplicate — useful as distinct// rhythmic group sizes: 1, 2, 3, 5, 8, 13, 21 …export function fibSequence(count) { const seq = [1, 2]; while (seq.length < count) seq.push(seq[seq.length - 1] + seq[seq.length - 2]); return seq.slice(0, Math.max(0, count));}
// The "golden division" of a bar of `n` beats into two Fibonacci-leaning// parts: the larger is the biggest Fibonacci number strictly less than// n, the smaller is the remainder. 8 -> [5,3], 13 -> [8,5], 7 -> [5,2].// Recurse on the parts for nested groupings. n <= 2 bottoms out.export function fibPartition(n) { const v = Math.max(0, Math.round(n)); if (v === 0) return []; if (v <= 2) return v === 2 ? [1, 1] : [1]; const fibs = [1, 2]; while (fibs[fibs.length - 1] < v) fibs.push(fibs[fibs.length - 1] + fibs[fibs.length - 2]); let larger = 1; for (const f of fibs) if (f < v) larger = f; return [larger, v - larger];}
// Build a form: lay section lengths along the Fibonacci sequence until// they sum to `totalBars`. mode "additive" climbs (1,2,3,5,8…), mode// "descending" falls from the largest fitting value. The final section// is clamped so the lengths sum exactly to totalBars.export function fibMeter(totalBars, opts = {}) { const mode = opts.mode ?? "additive"; const total = Math.max(1, Math.round(totalBars)); let pool = fibSequence(16).filter((v) => v <= total); if (mode === "descending") pool = pool.slice().reverse();
const sections = []; let used = 0, idx = 0; while (used < total) { let len = pool[Math.min(idx, pool.length - 1)]; if (used + len > total) len = total - used; sections.push(len); used += len; idx++; } return sections;}
// ── .np note subdivision ──────────────────────────────────────────────// A .np cell is `NOTE:syllable*weight` — e.g. `E5:_*2`, or a chord// `A4+C5+E5:_*2`. splitting keeps the note(s) and syllable, dividing the// weight into `n` equal cells. `E5:_*2` ÷ 3 -> `E5:_*0.667 ×3`.export function subdivideCell(cell, n) { const parts = Math.max(1, Math.floor(n)); const m = cell.match(/^(.*?):([^*]*)\*([\d.]+)$/); if (!m) return [cell]; // not a weighted cell — leave it const [, notes, syll, weight] = m; const w = parseFloat(weight) / parts; const wStr = Number(w.toFixed(3)).toString(); return Array.from({ length: parts }, () => `${notes}:${syll}*${wStr}`);}
// Walk a whole .np score and subdivide cells. By default every cell is// split by `parts`; set `minBeats` to only cut notes at least that long// (the Ableton "divide the big note" behaviour). Comments and `# section`// headers pass through untouched.//// opts: { parts (default 2), minBeats (default 0) }export function subdivideNp(text, opts = {}) { const parts = Math.max(1, Math.floor(opts.parts ?? 2)); const minBeats = opts.minBeats ?? 0; return text .split("\n") .map((line) => { const trimmed = line.trim(); if (trimmed === "" || trimmed.startsWith("#")) return line; return line.replace(/\S+/g, (tok) => { const m = tok.match(/\*([\d.]+)$/); if (!m || parseFloat(m[1]) < minBeats) return tok; return subdivideCell(tok, parts).join(" "); }); }) .join("\n");}