Skip to content

Latest commit

 

History

History
668 lines (529 loc) · 16.7 KB

File metadata and controls

668 lines (529 loc) · 16.7 KB

Gay.rs Learning Path: From Category Theory to Creative Coding

Overview

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.


Level 0: Intuition (30 minutes)

The Big Idea

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

Why This Matters

  1. Deterministic: Same seed always produces same colors/music
  2. Parallel: Each color can be computed independently
  3. Beautiful: Mathematics of golden ratio naturally pleases ears
  4. General: Works on any scale, genre, context

Prerequisite Knowledge: None

Try This Now (5 minutes):

cargo add gay-rs
use 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.


Level 1: Foundations (1 Week)

Core Concepts

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#

Exercises

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(&note.pitch_class));  // should pass
}

Level 2: Category Theory (2 Weeks)

Core Concepts

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 preserved

3. 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,
    }
}

Exercises

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 structure

Level 3: Monads in Music (2 Weeks)

Core Concepts

1. 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
        }
    }
}

Exercises

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 ScoreEvents

Level 4: Gay.jl & Golden Angle (1 Week)

Mathematical Beauty

Reference: 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)

Gay.rs Implementation

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)
}

Exercises

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();

Level 5: Integration & Production (1-2 Weeks)

Putting It All Together

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

Real-World Examples

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);
});

Exercises

Exercise 5.1: Generate a complete piece

// Create a 2-minute piece with seed 42

Exercise 5.2: Find aesthetically pleasing seeds

// Mine seeds and listen to output

Exercise 5.3: Export to MIDI

// Convert score events to MIDI file

Reference Library

Essential Books (Download via ananas.clj)

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

Online Resources


Self-Assessment

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?


Next Steps

  1. Start with Level 0 - Run the examples
  2. Progress through Levels 1-4 - Read theory, do exercises
  3. Reach Level 5 - Build your own projects
  4. 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.