This guide maps the journey from abstract category theory to practical music generation using gay-rs. Each section references peer-reviewed theory, points to implementations in gay-rs, and provides runnable examples.
Imagine you have an infinite stream of colors:
- Hue advances by exactly 137.508° each step (golden angle)
- This never repeats, spreads evenly across the spectrum
- You map: hue → pitch, saturation → voices, lightness → amplitude
- Result: Deterministic, beautiful music from pure mathematics
Color 0: {hue: 0°, sat: 0.5, light: 0.5} → Note C4, 2 voices, soft
Color 1: {hue: 137.5°, sat: 0.4, light: 0.6} → Note E4, 2 voices, normal
Color 2: {hue: 275°, sat: 0.6, light: 0.4} → Note G#4, 3 voices, soft
...continues forever, never repeats
- Deterministic: Same seed always produces same colors/music
- Parallel: Each color can be computed independently
- Beautiful: Mathematics of golden ratio naturally pleases ears
- General: Works on any scale, genre, context
Try This Now (5 minutes):
cargo add gay-rsuse gay_rs::color::ColorGenerator;
fn main() {
let mut gen = ColorGenerator::new(42);
for i in 0..5 {
let color = gen.next_color();
println!("Color {}: hue={:.1}°", i, color.hue);
}
}Run it:
cargo run
# Color 0: hue=0.0°
# Color 1: hue=137.5°
# Color 2: hue=275.1°
# Color 3: hue=52.6°
# Color 4: hue=190.1°Observation: Hues spread evenly, never repeat exactly.
1. Sets and Functions (prerequisite for everything)
Reference: Conceptual Mathematics (Lawvere & Schanuel), Chapters 1-3
Key Ideas:
- A set is a collection of objects
- A function f: A → B maps each element of A to one element of B
- Composition: if f: A → B and g: B → C, then g∘f: A → C
Gay.rs Application:
// A function from seed → colors
fn color_at(seed: u64, index: usize) -> OkhslColor { ... }
// Composition: seed → colors → notes
let color = color_at(42, 0);
let note = color_to_note(&color, &MusicalScale::Major);2. Relations and Equivalence
Key Ideas:
- Equivalence relation: reflexive, symmetric, transitive
- Two colors are "equivalent" if they map to the same pitch class
3. Algebraic Structures
Reference: Group Theory Applications to Music, Chapter 1
Key Ideas:
- Cyclic group Z/12Z: pitch classes (C, C#, D, ..., B)
- Permutation group S₁₂: voice leading transformations
- Ring structure: chords as subsets of Z/12Z
Gay.rs Application:
pub enum MusicalScale {
Major, // [0, 2, 4, 5, 7, 9, 11] ⊆ Z/12Z
Minor, // [0, 2, 3, 5, 7, 8, 10]
Dorian, // [0, 2, 3, 5, 7, 9, 10]
// ... etc
}
// Hue → pitch class (homomorphism φ: ℝ/360° → Z/12Z)
let hue: f32 = 180.0;
let pitch_class = (hue / 30.0) as u8 % 12; // = 6 = F#Exercise 1.1: Verify cyclic group properties
// Check closure: (a + b) mod 12 ∈ Z12
for a in 0..12 {
for b in 0..12 {
let c = (a + b) % 12;
assert!(c < 12); // closure satisfied
}
}Exercise 1.2: Implement pitch class equivalence
// Two notes are equivalent if pitch_class(note1) == pitch_class(note2)
fn pitch_classes_equal(hue1: f32, hue2: f32) -> bool {
let pc1 = ((hue1 / 30.0) as u8) % 12;
let pc2 = ((hue2 / 30.0) as u8) % 12;
pc1 == pc2
}Exercise 1.3: Understand scale as subset
// A major scale is a subset of Z/12Z
let major = MusicalScale::Major;
let intervals = major.intervals();
println!("{:?}", intervals); // [0, 2, 4, 5, 7, 9, 11]
// Verify that color_to_note only produces notes in this scale
for i in 0..100 {
let color = color_at(42, i);
let note = color_to_note(&color, &MusicalScale::Major);
assert!(intervals.contains(¬e.pitch_class)); // should pass
}1. Categories and Functors
Reference: Conceptual Mathematics (Lawvere & Schanuel), Chapters 4-6
Definition: A category C consists of:
- Objects: X, Y, Z, ...
- Morphisms (arrows): f: X → Y
- Composition: f∘g makes sense
- Identity: 1ₓ: X → X always exists
- Associativity: (f∘g)∘h = f∘(g∘h)
Example: MusicalCategory
- Objects: Pitch classes {0, 1, ..., 11}
- Morphisms: Intervals (0 = unison, 1 = minor second, etc.)
- Composition: Interval addition mod 12
Gay.rs Application:
// Pitch space as a category
pub struct PitchSpace;
impl PitchSpace {
// Objects: Z/12Z
fn objects() -> Vec<u8> { (0..12).collect() }
// Morphism: distance from pitch a to pitch b
fn distance(a: u8, b: u8) -> u8 {
(b.wrapping_sub(a)) % 12
}
// Composition: intervals add
fn compose(interval1: u8, interval2: u8) -> u8 {
(interval1 + interval2) % 12
}
}2. Functors: Structure-Preserving Maps
Definition: A functor F: C → D preserves:
- Objects: F(X) is an object in D
- Morphisms: F(f: X → Y) = F(f): F(X) → F(Y)
- Composition: F(g∘f) = F(g)∘F(f)
- Identity: F(1ₓ) = 1_{F(X)}
Example: Color → Music Functor
F: ColorSpace → MusicSpace
Object: color {hue, sat, light} ↦ note {pitch, octave, duration}
Morphism: color differences ↦ pitch intervals
Gay.rs Implementation:
// Functor from colors to notes
pub fn color_to_note(color: &OkhslColor, scale: &MusicalScale) -> Note {
let pitch_class = ((color.hue / 30.0) as u8) % 12;
let octave = 3 + (color.lightness * 3.0) as i8;
Note { pitch_class, octave }
}
// Verify functor property: composition is preserved
let color1 = color_at(42, 0);
let color2 = color_at(42, 1);
let note1 = color_to_note(&color1, &scale);
let note2 = color_to_note(&color2, &scale);
// Hue difference preserved as pitch relationship
let hue_diff = (color2.hue - color1.hue) % 360.0;
let pitch_interval = ((note2.pitch_class - note1.pitch_class) % 12) as f32;
assert!((hue_diff / 30.0 - pitch_interval).abs() < 1.0); // approximately preserved3. Natural Transformations
Definition: A natural transformation η: F ⇒ G (between functors F, G: C → D) gives a morphism ηₓ: F(X) → G(X) for each object X, respecting all structure.
Example: Different musical scales are natural transformations
Music(scale1) ⇒ Music(scale2)
Gay.rs Application:
// Switch between scales seamlessly
pub fn transpose_scale(note: Note, from: &MusicalScale, to: &MusicalScale) -> Note {
let from_intervals = from.intervals();
let to_intervals = to.intervals();
// Find where note falls in source scale
let index = from_intervals.iter()
.position(|&i| i == note.pitch_class)
.unwrap_or(0);
// Map to target scale
Note {
pitch_class: to_intervals[index % to_intervals.len()],
octave: note.octave,
}
}Exercise 2.1: Verify category axioms for pitch space
// Associativity: (a + b + c) mod 12 = ((a + b) mod 12 + c) mod 12
for a in 0..12 {
for b in 0..12 {
for c in 0..12 {
let left = ((a + b) % 12 + c) % 12;
let right = (a + (b + c) % 12) % 12;
assert_eq!(left, right);
}
}
}Exercise 2.2: Implement and verify a functor
// Functor: Colors → Notes
// Verify: f(color1 + color2) ≈ f(color1) + f(color2) (in pitch space)Exercise 2.3: Write a natural transformation
// Transform from Major scale to Minor scale while preserving structure1. Monads: Abstracting Computation
Reference: Seven Sketches in Compositionality (Spivak et al.), Chapter on monads
A monad M on a category C gives:
- A way to abstract a computational pattern
- μ (multiplication): compose steps
- η (unit): return/lift
- Associativity: (μ ∘ M μ) = (μ ∘ μ M)
2. Free Monad: Decisions
Intuition: A free monad encodes a sequence of decisions as a tree structure
Decision Tree:
PlayNote(C4)
├─ then PlayNote(E4)
│ └─ then Rest(1.0)
└─ or PlayChord(C, E, G)
Gay.rs Architecture:
// Free monad for musical patterns
pub enum FreeMonad<Next> {
Pure(Vec<ScoreEvent>),
Suspend {
instruction: Instruction,
next: Box<FreeMonad<Next>>,
},
}
// Instructions represent choices
pub enum Instruction {
PlayNote { pitch: u8, duration: f32, amplitude: f32 },
PlayChord { pitches: Vec<u8>, duration: f32, amplitude: f32 },
Rest { duration: f32 },
Branch { condition: bool, then_branch: Box<Self>, else_branch: Box<Self> },
}3. Cofree Comonad: Environment
Intuition: A cofree comonad represents an infinite context or environment
Musical Matter:
current = { tempo: 120, timbre: "piano", volume: 1.0 }
next = { tempo: 118, timbre: "piano", volume: 0.95 }
next = { tempo: 116, timbre: "string", volume: 0.90 }
... (infinite stream)
Gay.rs Architecture:
pub struct MusicalMatter {
pub tempo: f32,
pub timbre: String,
pub volume: f32,
next: Box<MusicalMatter>,
}
impl MusicalMatter {
pub fn advance(&mut self, duration: f32) {
// Evolve to next state
self.tempo -= 0.1; // Gradual tempo slowdown
self.volume *= 0.99; // Fade
}
}4. Module Action: runs_on
The key insight: Pattern runs on Matter
Pattern (Free Monad) ⊗ Matter (Cofree Comonad)
↓ (runs_on)
Score Events
Gay.rs Implementation:
pub fn runs_on(
pattern: &FreeMonad<Note>,
matter: &mut MusicalMatter,
) -> Vec<ScoreEvent> {
match pattern {
FreeMonad::Pure(events) => events.clone(),
FreeMonad::Suspend { instruction, next } => {
let event = execute_instruction(instruction, matter);
matter.advance(event.duration);
let mut rest = runs_on(next, matter);
let mut result = vec![event];
result.append(&mut rest);
result
}
}
}Exercise 3.1: Build a simple free monad pattern
let pattern = FreeMonad::Suspend {
instruction: Instruction::PlayNote {
pitch: 0,
duration: 1.0,
amplitude: 0.8,
},
next: Box::new(FreeMonad::Suspend {
instruction: Instruction::Rest { duration: 0.5 },
next: Box::new(FreeMonad::Pure(vec![])),
}),
};Exercise 3.2: Create a musical matter context
let mut matter = MusicalMatter {
tempo: 120.0,
timbre: "piano".to_string(),
volume: 1.0,
next: Box::new(/* ... */),
};Exercise 3.3: Run pattern on matter
let events = runs_on(&pattern, &mut matter);
println!("{:?}", events); // Should produce ScoreEventsReference: Phipps's "Gallery of Phyllotaxis", Vigna & Blackman: "SplitMix64"
The Golden Ratio
φ = (1 + √5) / 2 ≈ 1.618033988749...
φ² ≈ 2.618033988749...
Golden angle = 360° / φ² ≈ 137.50776405026109°
Why This Angle?
- Never repeats (irrational rotation)
- Even distribution (minimizes gaps)
- Found in nature (sunflower seeds, pinecones)
- Aesthetically pleasing to humans
SplitMix64 RNG
state ← state + GOLDEN
z ← state
y ← (z ^ (z >> 30)) × MIX_1
return (y ^ (y >> 27)) × MIX_2
Properties:
- Fast (few operations)
- Good statistical properties
- Splittable (for parallelism)
- Deterministic (same seed = same sequence)
pub const GOLDEN_ANGLE: f64 = 137.50776405026109;
pub const GOLDEN: u64 = 0x9e3779b97f4a7c15;
pub fn color_at(seed: u64, index: usize) -> OkhslColor {
// Hue: advance by golden angle
let hue = (index as f32 * GOLDEN_ANGLE as f32) % 360.0;
// Saturation/Lightness: from SplitMix64
let rng_val = sm64(seed, index as u64);
let mut rng = SplitMix64::new(rng_val);
let sat = rng.next_f32() * 0.7 + 0.3;
let light = rng.next_f32() * 0.5 + 0.3;
OkhslColor::new(hue, sat, light)
}Exercise 4.1: Compute golden angle
let phi = (1.0 + 5.0_f64.sqrt()) / 2.0;
let golden_angle = 360.0 / (phi * phi);
println!("{}", golden_angle); // ≈ 137.508°Exercise 4.2: Verify no repeats in 100M colors
let mut seen = std::collections::HashSet::new();
for i in 0..100_000_000 {
let color = color_at(42, i);
let key = (color.hue.round() as i32,
(color.saturation * 100.0) as i32,
(color.lightness * 100.0) as i32);
assert!(!seen.contains(&key));
seen.insert(key);
}Exercise 4.3: Profile parallelism
use rayon::prelude::*;
let colors: Vec<_> = (0..10_000_000)
.into_par_iter()
.map(|i| color_at(42, i))
.collect();The Complete Flow
Seed (u64)
↓
SplitMix64 RNG
↓
Golden Angle Hue
↓
OkhslColor (hue, sat, light)
↓
ColorMusicMapper
├─ hue → pitch_class
├─ sat → density
└─ light → amplitude
↓
Musical Note
↓
FreeMonad Pattern
↓
MusicalMatter Context
↓
runs_on(Pattern, Matter)
↓
Score Events
↓
Audio Synthesis
Example 1: Infinite Ambient Music
let mut gen = ColorGenerator::new(42);
let mut matter = MusicalMatter::ambient();
for i in 0..1000 {
let color = gen.next_color();
let note = color_to_note(&color, &MusicalScale::Major);
let event = ScoreEvent {
pitch: note.pitch_class,
time: i as f32 * 0.5,
duration: 0.5,
amplitude: note.lightness_to_velocity(),
};
synthesize(event, matter);
}Example 2: Parallel Seed Mining
use gay_rs::parallel::find_best_seed;
let best = find_best_seed(42, 1000, 64);
println!("Best seed: {}", best.seed);
println!("Consonance: {}", best.consonance);
println!("Variety: {}", best.variety);Example 3: WASM Integration
import * as gay from 'gay.wasm';
const colors = gay.colors_batch(42, 0, 16);
colors.forEach(c => {
const note = gay.color_to_note(c, 'major');
synth.play(note);
});Exercise 5.1: Generate a complete piece
// Create a 2-minute piece with seed 42Exercise 5.2: Find aesthetically pleasing seeds
// Mine seeds and listen to outputExercise 5.3: Export to MIDI
// Convert score events to MIDI file| Book | Author | Purpose | Read When |
|---|---|---|---|
| Conceptual Mathematics | Lawvere & Schanuel | Category basics | Level 1 |
| Categories for the Working Mathematician | Mac Lane | Standard reference | Level 2 |
| Seven Sketches in Compositionality | Spivak et al. | Applied CT | Level 2-3 |
| The Topos of Music I: Theory | Mazzola | Mathematical music | Level 2-4 |
| The Topos of Music II: Performance | Mazzola | Realization | Level 4-5 |
| The Topos of Music III: Gestures | Mazzola | Interaction | Level 5 |
| Group Theory Applications to Music | Peck | Music group theory | Level 1 |
| SplitMix64 Paper | Vigna & Blackman | RNG algorithm | Level 4 |
| On the Sensations of Tone | Helmholtz | Physics of music | Level 4 |
| Generative Theory of Music | Lerdahl & Jackendoff | Music cognition | Level 3-4 |
- docs.rs: Gay.rs API documentation
- GitHub: Gay.rs repository
- TOPLAP.org: Live coding community
- Lines.io: Experimental music forum
After each level, ask yourself:
Level 0: Can I understand the basic idea of colors → music? Level 1: Can I explain sets, functions, and algebraic structures? Level 2: Can I explain categories, functors, and natural transformations? Level 3: Can I explain free/cofree monads and module actions? Level 4: Can I explain golden angle and why it's mathematically beautiful? Level 5: Can I build, deploy, and extend gay-rs for my own music?
- Start with Level 0 - Run the examples
- Progress through Levels 1-4 - Read theory, do exercises
- Reach Level 5 - Build your own projects
- Contribute back - Share your discoveries with the community
The journey from abstract mathematics to creative music is one of the most rewarding paths in digital art. Gay.rs provides the bridge—the bridge from theory to practice, from mathematics to sound, from logic to beauty.
Remember: "The next color determines the next sound."
Every line of code is an opportunity to let mathematics become music.