# 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, expected_duration: Ratio, // 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`: ```rust use num_rational::Ratio; pub struct Cell { id: DerivedId, duration: Ratio, // 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 = 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 = old_cells.iter() .map(|c| c.duration) .sum(); // After subdivision let new_duration: Ratio = 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, // 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, 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 ```