Skip to content

Instantly share code, notes, and snippets.

@davidorex
Created August 23, 2025 08:01
Show Gist options
  • Save davidorex/ad2655f318875c387b4f06bbc284fbea to your computer and use it in GitHub Desktop.
Save davidorex/ad2655f318875c387b4f06bbc284fbea to your computer and use it in GitHub Desktop.
Rhythms module for gesture midi generation
/**
* Rhythm Strategies Module for Gestural MIDI Generator
*
* Provides pluggable rhythm generation strategies for polyrhythmic pattern creation.
* Each strategy returns pattern lengths or rhythm patterns that determine when
* musical events trigger.
*/
// Helper function to generate Euclidean rhythm patterns
function generateEuclidean(k, n) {
// Bjorklund's algorithm for even distribution of k pulses in n steps
if (k >= n) return new Array(n).fill(1);
if (k === 0) return new Array(n).fill(0);
let groups = [];
for (let i = 0; i < k; i++) groups.push([1]);
for (let i = 0; i < n - k; i++) groups.push([0]);
while (groups.length > 1 && groups[groups.length - 1].length === 1) {
let newGroups = [];
let minLength = Math.min(groups.filter(g => g[0] === 1).length,
groups.filter(g => g[0] === 0).length);
let ones = groups.filter(g => g[0] === 1);
let zeros = groups.filter(g => g[0] === 0);
for (let i = 0; i < minLength; i++) {
newGroups.push(ones[i].concat(zeros[i]));
}
for (let i = minLength; i < ones.length; i++) {
newGroups.push(ones[i]);
}
for (let i = minLength; i < zeros.length; i++) {
newGroups.push(zeros[i]);
}
if (JSON.stringify(groups) === JSON.stringify(newGroups)) break;
groups = newGroups;
}
return groups.flat();
}
// Helper to calculate LCM for cycle length prediction
function calculateLCM(numbers) {
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
const lcm = (a, b) => (a * b) / gcd(a, b);
return numbers.reduce((acc, n) => lcm(acc, n), 1);
}
// Main rhythm strategies object
const RhythmStrategies = {
// Original prime number strategy
prime: {
name: "Prime Numbers",
description: "Maximum polyrhythmic independence - patterns rarely align",
icon: "🔢",
values: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47],
getNext: function(index) {
return this.values[index % this.values.length];
},
preview: function(count = 8) {
return Array.from({length: count}, (_, i) => this.getNext(i));
},
info: function(count = 4) {
const lengths = this.preview(count);
const lcm = calculateLCM(lengths);
return {
lengths: lengths,
lcm: lcm,
description: `Patterns align every ${lcm} beats`
};
}
},
// Fibonacci sequence
fibonacci: {
name: "Fibonacci Sequence",
description: "Natural growth patterns with golden ratio relationships",
icon: "🐚",
cache: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89],
getNext: function(index) {
// Use cached values, cycling through a reasonable range
const maxIndex = Math.min(index, 8); // Cap at 34 to keep musically useful
return this.cache[maxIndex];
},
preview: function(count = 8) {
return Array.from({length: count}, (_, i) => this.getNext(i));
},
info: function(count = 4) {
const lengths = this.preview(count);
const lcm = calculateLCM(lengths);
return {
lengths: lengths,
lcm: lcm,
description: `Golden ratio phasing, full cycle: ${lcm} beats`
};
}
},
// Euclidean rhythms with presets
euclidean: {
name: "Euclidean Rhythms",
description: "Mathematically even distributions found in world music",
icon: "🌍",
presets: [
{name: "Tresillo", k: 3, n: 8, culture: "Cuban"},
{name: "Cinquillo", k: 5, n: 8, culture: "Cuban"},
{name: "Bossa Nova", k: 5, n: 16, culture: "Brazilian"},
{name: "Rumba", k: 7, n: 16, culture: "African"},
{name: "Aksak", k: 7, n: 8, culture: "Turkish"},
{name: "Kakilambe", k: 9, n: 16, culture: "West African"}
],
getNext: function(index) {
const preset = this.presets[index % this.presets.length];
return {
length: preset.n,
pattern: generateEuclidean(preset.k, preset.n),
name: preset.name,
culture: preset.culture,
density: preset.k / preset.n
};
},
preview: function(count = 4) {
return Array.from({length: count}, (_, i) => {
const data = this.getNext(i);
return {
length: data.length,
name: data.name,
pattern: data.pattern.map(b => b ? '●' : '○').join('')
};
});
},
info: function(count = 4) {
const patterns = Array.from({length: count}, (_, i) => this.getNext(i));
const lengths = patterns.map(p => p.length);
const lcm = calculateLCM(lengths);
return {
patterns: patterns.map(p => ({
name: p.name,
visual: p.pattern.map(b => b ? '●' : '○').join(''),
density: Math.round(p.density * 100) + '%'
})),
lcm: lcm,
description: `World rhythm grooves, cycle: ${lcm} beats`
};
}
},
// Harmonic series (overtones)
harmonic: {
name: "Harmonic Series",
description: "Based on acoustic overtone series - naturally consonant",
icon: "🎵",
getNext: function(index) {
// 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16
const harmonics = [1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16];
return harmonics[index % harmonics.length];
},
preview: function(count = 8) {
return Array.from({length: count}, (_, i) => this.getNext(i));
},
info: function(count = 4) {
const lengths = this.preview(count);
const lcm = calculateLCM(lengths);
return {
lengths: lengths,
lcm: lcm,
description: `Harmonically related rhythms, cycle: ${lcm} beats`
};
}
},
// Powers of 2 (binary)
binary: {
name: "Powers of 2",
description: "Perfectly nested cycles - each twice as long as previous",
icon: "⚡",
getNext: function(index) {
const maxPower = 6; // Up to 64 beats
const power = (index % maxPower) + 1;
return Math.pow(2, power); // 2, 4, 8, 16, 32, 64
},
preview: function(count = 6) {
return Array.from({length: count}, (_, i) => this.getNext(i));
},
info: function(count = 4) {
const lengths = this.preview(count);
const lcm = Math.max(...lengths); // For powers of 2, LCM is the largest
return {
lengths: lengths,
lcm: lcm,
description: `Binary subdivisions, perfect nesting at ${lcm} beats`
};
}
},
// Common subdivisions (dance music)
subdivision: {
name: "Dance Subdivisions",
description: "Common factors for electronic/dance music grooves",
icon: "💃",
values: [4, 8, 12, 16, 24, 32],
getNext: function(index) {
return this.values[index % this.values.length];
},
preview: function(count = 6) {
return Array.from({length: count}, (_, i) => this.getNext(i));
},
info: function(count = 4) {
const lengths = this.preview(count);
const lcm = calculateLCM(lengths);
return {
lengths: lengths,
lcm: lcm,
description: `Dance-friendly subdivisions, cycle: ${lcm} beats`
};
}
},
// African polyrhythms
african: {
name: "African Polyrhythms",
description: "Traditional West African rhythm combinations",
icon: "🥁",
patterns: [
{beats: 3, name: "Simple triple"},
{beats: 4, name: "Simple quadruple"},
{beats: 6, name: "Compound duple"},
{beats: 8, name: "Extended quadruple"},
{beats: 12, name: "Full bell pattern"}
],
getNext: function(index) {
const pattern = this.patterns[index % this.patterns.length];
return pattern.beats;
},
preview: function(count = 5) {
return Array.from({length: count}, (_, i) => {
const pattern = this.patterns[i % this.patterns.length];
return `${pattern.beats} (${pattern.name})`;
});
},
info: function(count = 4) {
const lengths = Array.from({length: count}, (_, i) => this.getNext(i));
const lcm = calculateLCM(lengths);
return {
patterns: this.patterns.slice(0, count),
lcm: lcm,
description: `Traditional polyrhythms, cycle: ${lcm} beats`
};
}
},
// Indian tala cycles
indian: {
name: "Indian Tala Cycles",
description: "Classical Indian rhythmic cycles",
icon: "🕉️",
talas: [
{name: "Dadra", beats: 6, structure: "3+3"},
{name: "Rupak", beats: 7, structure: "3+2+2"},
{name: "Jhaptaal", beats: 10, structure: "2+3+2+3"},
{name: "Ektal", beats: 12, structure: "4+4+2+2"},
{name: "Teentaal", beats: 16, structure: "4+4+4+4"}
],
getNext: function(index) {
const tala = this.talas[index % this.talas.length];
return {
length: tala.beats,
name: tala.name,
structure: tala.structure
};
},
preview: function(count = 5) {
return Array.from({length: count}, (_, i) => {
const tala = this.getNext(i);
return `${tala.name}: ${tala.length} beats (${tala.structure})`;
});
},
info: function(count = 4) {
const talas = Array.from({length: count}, (_, i) => this.getNext(i));
const lengths = talas.map(t => t.length);
const lcm = calculateLCM(lengths);
return {
talas: talas,
lcm: lcm,
description: `Indian classical cycles, full rotation: ${lcm} beats`
};
}
},
// Irrational number approximations
irrational: {
name: "Irrational Numbers",
description: "Approximations of π, φ, e for never-quite-repeating patterns",
icon: "∞",
numbers: {
pi: [3, 14, 15, 9], // π ≈ 3.14159...
phi: [16, 18, 10, 8], // φ ≈ 1.618... (scaled by 10)
e: [27, 18, 28, 18], // e ≈ 2.71828...
sqrt2: [14, 14, 21, 13] // √2 ≈ 1.41421... (scaled by 10)
},
getNext: function(index) {
const sequences = Object.values(this.numbers);
const sequence = sequences[Math.floor(index / 4) % sequences.length];
return sequence[index % 4];
},
preview: function(count = 8) {
return Array.from({length: count}, (_, i) => this.getNext(i));
},
info: function(count = 4) {
const lengths = this.preview(count);
const lcm = calculateLCM(lengths);
return {
lengths: lengths,
lcm: lcm,
description: `Irrational approximations, quasi-periodic at ${lcm} beats`
};
}
},
// Custom user-defined patterns
custom: {
name: "Custom Pattern",
description: "User-defined rhythm lengths",
icon: "✏️",
values: [4, 4, 4, 4], // Default to 4/4
setValues: function(newValues) {
// Validate and set custom values
const validated = newValues
.filter(v => typeof v === 'number' && v > 0 && v <= 64)
.slice(0, 16); // Max 16 patterns
if (validated.length > 0) {
this.values = validated;
return true;
}
return false;
},
getNext: function(index) {
return this.values[index % this.values.length];
},
preview: function(count = null) {
const useCount = count || this.values.length;
return Array.from({length: useCount}, (_, i) => this.getNext(i));
},
info: function() {
const lcm = calculateLCM(this.values);
return {
lengths: this.values,
lcm: lcm,
description: `Custom pattern, cycle: ${lcm} beats`
};
}
}
};
// Utility functions for rhythm analysis
const RhythmUtils = {
// Check if a beat should trigger given a pattern
shouldTrigger: function(beatCount, pattern) {
if (typeof pattern === 'number') {
// Simple modulo check for numeric patterns
return beatCount % pattern === 0;
} else if (pattern.pattern && Array.isArray(pattern.pattern)) {
// Euclidean pattern check
const position = beatCount % pattern.length;
return pattern.pattern[position] === 1;
}
return false;
},
// Calculate when patterns will align
calculateAlignment: function(lengths) {
return calculateLCM(lengths);
},
// Generate a visual representation of rhythm interaction
visualizePolyrhythm: function(lengths, beats = 32) {
const tracks = lengths.map(length => {
const track = [];
for (let i = 0; i < beats; i++) {
track.push(i % length === 0 ? 1 : 0);
}
return track;
});
// Find alignment points
const alignments = [];
for (let i = 0; i < beats; i++) {
const sum = tracks.reduce((acc, track) => acc + track[i], 0);
if (sum === tracks.length) {
alignments.push(i);
}
}
return {
tracks: tracks,
alignments: alignments,
density: tracks.map(t => t.reduce((a, b) => a + b, 0))
};
},
// Suggest complementary rhythm based on existing patterns
suggestComplement: function(existingLengths, strategy = 'prime') {
const strat = RhythmStrategies[strategy];
if (!strat) return null;
// Find a rhythm that creates interesting relationships
const lcm = calculateLCM(existingLengths);
const candidates = [];
for (let i = 0; i < 10; i++) {
const candidate = strat.getNext(existingLengths.length + i);
const newLcm = calculateLCM([...existingLengths, candidate]);
// Look for candidates that don't dramatically increase the cycle
if (newLcm <= lcm * 2) {
candidates.push({
value: candidate,
lcm: newLcm,
increase: newLcm / lcm
});
}
}
// Return the most interesting (moderate increase in complexity)
candidates.sort((a, b) => Math.abs(a.increase - 1.5) - Math.abs(b.increase - 1.5));
return candidates[0] ? candidates[0].value : strat.getNext(existingLengths.length);
}
};
// Export for use in main application
if (typeof module !== 'undefined' && module.exports) {
module.exports = { RhythmStrategies, RhythmUtils };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment