diff options
| author | Josh Kingsley <josh@joshkingsley.me> | 2025-11-16 00:27:10 +0200 |
|---|---|---|
| committer | Josh Kingsley <josh@joshkingsley.me> | 2025-11-16 00:27:10 +0200 |
| commit | 302ae521a95bb840467eebee60885e228034ae2a (patch) | |
| tree | 64c35776759c3cd1d0d85fbcc95dbb23bf811eac /docs | |
| parent | 7d912937cc7a271cd4c85fa1108094055ffe730f (diff) | |
docs: add claude docs for musical model
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design/musical-model.md | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/docs/design/musical-model.md b/docs/design/musical-model.md new file mode 100644 index 0000000..f7b941a --- /dev/null +++ b/docs/design/musical-model.md @@ -0,0 +1,321 @@ +# Musical Model and Duration System + +**Last Updated:** 2025-11-15 + +## Overview + +This document describes how the CRDT represents musical time and rhythm, focusing on the relationship between cells, rows, and musical durations. + +## Core Concepts + +### Row Duration (Normalized) + +All rows have a **normalized duration of 1**. This is the internal representation used by the CRDT for calculations and validation. + +```rust +pub struct Row { + id: DerivedId, + cells: Vec<Cell>, + expected_duration: Ratio<u32>, // Always Ratio::new(1, 1) +} +``` + +### Base Cells and Musical Duration + +The **musical duration** of a row is determined by two factors: + +1. **base_cells_per_row** - How many base cells fit in the normalized row duration of 1 +2. **Base cell musical duration** - What musical duration each base cell represents (e.g., quarter note, eighth note) + +**Example 1: One measure of 4/4 time** +``` +base_cells_per_row = 16 +base cell duration = sixteenth note +row musical duration = 16 sixteenth notes = 1 measure of 4/4 +``` + +**Example 2: Two measures of 4/4 time** +``` +base_cells_per_row = 16 +base cell duration = eighth note +row musical duration = 16 eighth notes = 2 measures of 4/4 +``` + +**Example 3: One measure of 3/4 time** +``` +base_cells_per_row = 12 +base cell duration = sixteenth note +row musical duration = 12 sixteenth notes = 1 measure of 3/4 +``` + +## Cell Duration System + +### Rational Number Representation + +Cell durations are stored as exact ratios using Rust's `num_rational::Ratio<u32>`: + +```rust +use num_rational::Ratio; + +pub struct Cell { + id: DerivedId, + duration: Ratio<u32>, // Fraction of the normalized row duration +} +``` + +**Why ratios?** +- Musical durations are inherently rational (1/4, 1/8, 1/3, etc.) +- No floating-point rounding errors +- Exact validation that cell durations sum to row duration +- Natural representation for subdivision arithmetic + +### Base Cell Duration Calculation + +For a row with `base_cells_per_row` base cells: + +```rust +let base_cell_duration = Ratio::new(1, base_cells_per_row); + +// Examples: +// base_cells_per_row = 16 → each cell is 1/16 of the row +// base_cells_per_row = 12 → each cell is 1/12 of the row +``` + +### Subdivision Duration Calculation + +When subdividing N cells into M new cells, the total duration is preserved: + +```rust +// Original: N cells, each with duration d +let total_duration = n_cells * cell_duration; + +// After subdivision: M cells +let new_cell_duration = total_duration / m_cells; + +// Example: 4 cells of 1/16 → 3 triplets +// total = 4 * (1/16) = 4/16 = 1/4 +// new = (1/4) / 3 = 1/12 per cell +``` + +**Musical interpretation:** +``` +4 sixteenth notes → 3 eighth-note triplets +Duration: 1/4 of row → 1/4 of row (preserved) +Each new cell: 1/12 of row +``` + +## Duration Invariants + +### Row Duration Preservation + +**Invariant:** The sum of all non-deleted cell durations in a row must always equal the row's expected duration. + +```rust +fn validate_row_duration(row: &Row) -> Result<(), Error> { + let actual: Ratio<u32> = row.cells.iter() + .filter(|c| !c.deleted) + .map(|c| c.duration) + .sum(); + + if actual != row.expected_duration { + return Err(Error::DurationMismatch { + expected: row.expected_duration, + actual, + }); + } + + Ok(()) +} +``` + +### Subdivision Preservation + +**Invariant:** A `ChangeSubdivisions` operation must preserve the total duration of the cells it replaces. + +```rust +// Before subdivision +let old_duration: Ratio<u32> = old_cells.iter() + .map(|c| c.duration) + .sum(); + +// After subdivision +let new_duration: Ratio<u32> = new_cells.iter() + .map(|c| c.duration) + .sum(); + +assert_eq!(old_duration, new_duration); +``` + +## Musical Duration Mapping + +While the CRDT uses normalized durations (fractions of 1), the UI needs to display actual musical durations. + +### Common Mappings + +If base cell = quarter note: +``` +CRDT Duration → Musical Duration +1/1 → whole note +1/2 → half note +1/4 → quarter note +1/8 → eighth note +1/16 → sixteenth note +1/3 → quarter note triplet (3 per half note) +1/6 → eighth note triplet +1/12 → sixteenth note triplet +``` + +If base cell = eighth note: +``` +CRDT Duration → Musical Duration +1/2 → whole note +1/4 → half note +1/8 → quarter note +1/16 → eighth note +1/32 → sixteenth note +``` + +### Converting to Musical Duration + +```rust +pub struct MusicalDuration { + pub note_value: NoteValue, // Quarter, Eighth, etc. + pub dots: u8, // Dotted notes + pub tuplet: Option<Tuplet>, // Triplets, quintuplets, etc. +} + +pub enum NoteValue { + Whole, + Half, + Quarter, + Eighth, + Sixteenth, +} + +pub struct Tuplet { + pub notes: u32, // 3 for triplet, 5 for quintuplet + pub in_space_of: u32, // Usually notes - 1 +} + +fn to_musical_duration( + cell_duration: Ratio<u32>, + base_cell_musical_duration: NoteValue, +) -> MusicalDuration { + // Implementation depends on base cell duration and ratio + // Example: if base cell is sixteenth note and duration is 1/12, + // that's a sixteenth note triplet + todo!() +} +``` + +## Design Rationale + +### Why Normalized Row Duration? + +**Benefits:** +1. **Simplifies CRDT logic** - All rows have the same expected duration (1) +2. **Musical flexibility** - Same CRDT can represent different time signatures +3. **Clear separation** - CRDT handles ratios, UI handles musical interpretation +4. **Easier validation** - Always check if durations sum to 1 + +**Trade-off:** Requires mapping layer between CRDT and musical display + +### Why Not Store Musical Durations Directly? + +Storing "quarter note" or "eighth note" in the CRDT would: +- Couple the CRDT to Western musical notation +- Make subdivision calculations more complex +- Require context about time signature for validation +- Limit future extensions (non-Western music, variable time signatures) + +The normalized approach keeps the CRDT generic and the musical interpretation separate. + +## Future Considerations + +### Time Signature Changes + +Currently, the time signature is implicit in `base_cells_per_row`. Future enhancements might: +- Store explicit time signature metadata on rows +- Support time signature changes mid-document +- Validate subdivisions against time signature rules + +### Tempo and Absolute Time + +The CRDT currently deals with relative durations. Future work might add: +- Tempo markers (BPM) +- Conversion to absolute time (milliseconds) +- Synchronization with audio playback + +### Non-Standard Divisions + +Some musical contexts require unusual divisions: +- Nested tuplets (triplets of quintuplets) +- Irrational rhythms +- Microtiming adjustments + +The ratio system can represent any rational duration, supporting most musical use cases. + +## References + +- **CRDT Design:** `./crdt-design.md` +- **Conflict Resolution:** `./branch-based-merge.md` +- **Implementation:** `/crdt/src/lib.rs` + +## Examples + +### Example 1: Creating a Row + +```rust +// Create a row with 16 base cells (normalized duration 1) +// Musical interpretation: 16 sixteenth notes = 1 measure of 4/4 +let op = OpPayload::CreateGrid { + rows: 1, + base_cells_per_row: 16, +}; + +// Each cell has duration 1/16 +let cell_duration = Ratio::new(1, 16); +``` + +### Example 2: Subdividing into Triplets + +```rust +// Start with 4 cells, each 1/16 (= 1 quarter note) +let old_cells = vec![ + Cell { duration: Ratio::new(1, 16), .. }, + Cell { duration: Ratio::new(1, 16), .. }, + Cell { duration: Ratio::new(1, 16), .. }, + Cell { duration: Ratio::new(1, 16), .. }, +]; + +// Total duration = 4/16 = 1/4 +let total = Ratio::new(4, 16); + +// Subdivide into 3 triplets +// Each triplet = (1/4) / 3 = 1/12 +let new_cells = vec![ + Cell { duration: Ratio::new(1, 12), .. }, + Cell { duration: Ratio::new(1, 12), .. }, + Cell { duration: Ratio::new(1, 12), .. }, +]; + +// Validation: 3 * (1/12) = 3/12 = 1/4 ✓ +``` + +### Example 3: Concurrent Subdivisions + +```rust +// Initial row: 16 cells of 1/16 each +// Alice subdivides cells [0,1,2,3] into 3 triplets (each 1/12) +// Bob subdivides cells [2,3,4,5] into 5 quintuplets (each 1/20) + +// Conflict: cells 2 and 3 are targeted by both operations +// After both ops apply: +// - Cells 0,1 deleted by Alice +// - Cells 2,3,4,5 deleted by Bob +// - Cells 2,3 deleted by BOTH (conflict!) +// - Row has: original cells 6-15 + Alice's 3 triplets + Bob's 5 quintuplets +// - Duration: 10*(1/16) + 3*(1/12) + 5*(1/20) = incorrect! + +// This is why we need conflict detection +``` |
