summaryrefslogtreecommitdiff
path: root/docs/design/musical-model.md
diff options
context:
space:
mode:
authorJosh Kingsley <josh@joshkingsley.me>2025-11-16 00:27:10 +0200
committerJosh Kingsley <josh@joshkingsley.me>2025-11-16 00:27:10 +0200
commit302ae521a95bb840467eebee60885e228034ae2a (patch)
tree64c35776759c3cd1d0d85fbcc95dbb23bf811eac /docs/design/musical-model.md
parent7d912937cc7a271cd4c85fa1108094055ffe730f (diff)
docs: add claude docs for musical model
Diffstat (limited to 'docs/design/musical-model.md')
-rw-r--r--docs/design/musical-model.md321
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
+```