Created
August 23, 2025 08:01
-
-
Save davidorex/ad2655f318875c387b4f06bbc284fbea to your computer and use it in GitHub Desktop.
Rhythms module for gesture midi generation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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