1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
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
```
|