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
|
# Development log
# November 19th, 2025
## Rhythm
Developing a theory of rhythm for unheard.
There is a natural tension between relative and
absolute positioning of time objects in time.
Let me try to define relative positioning.
Absolute positioning is positioning a time object at an absolute
location on a timeline. For example, an absolutely positioned
time object might span [1 4]. Part of the definition might include
the constraint that the position of absolutely-positioned
time objects is known at compile time.
A relatively positioned time object is positioned relative to some
other entity (a container or another time object, for example).
A time object is relatively positioned if the related entity's
position is not known at compile time.
Maybe a more interesting differentiation is time objects that
are known to exist at compile time vs time objects that are not.
Relative positioning makes the following challenging:
-
Relative positioning makes seeking challenging, since you have
to play through the timeline to get to an event. It is also
not obviously clear how temporal indexing would work with
Absolute positioning is substantially less flexible. How do
loops work
I think this is probably one of the hardest pieces for me to get right. I'll be doing a lot of writing to help me better understand the problem space. Here are some of my high-level goals:
- A composer should be able to decompose their composition into phrases, just like a programmer can decompose a program into functions. (Hint: phrases are functions!) A composer should be able to work on a phrase in isolation, at times iterating upon it without regard for the entire composition. At other times, it should be easy for the composer to hear a phrase in concert with other phrases.
- It should be intuitive for a composer to write a piece that changes between time signatures.
- It should be intuitive for a composer to express polyrhythms.
- The playback engine should provide affordances for starting loops or phrases at the next downbeat. This interacts with the polyrhythm constraint in various ways. Regardless, I want to minimize surprise.
- Seeking (that is, jumping to a point in a composition) should be instantaneous. This has far-reaching consequences: in particular, it precludes certain kinds of iterative composition, where state t2 = f(state t1).
- Looping should be easy and flexible. Note, though, that the ban on iterative composition imposed by the previous bullet point seems to imply that looping can't exist at all! Fortunately, I have some theories for how to resolve this conflict. They need to be worked through, though.
- It should be possible to slow down the tempo of a song to (nearly) arbitrarily small values. Same for speeding up. This would allow for some kind of rhythmic fractals, where tempo slows down forever (while new subdivisions of tempo appear continuously). I've been looking into how "infinite zoom" fractal renderers represent zoom steps numerically, and hope to lift some of that into the playback engine.
- Related to the above, I want to provide some kind of mechanism for expressing recursive rhythms – that is, a rhythm that plays concurrently at tempo t, 2t, 1/2t, 4 t, 1/4t, etc. (A composer would specify the number of iterations they want to play above and below t.) This mechanism would account for tempo zooming automatically.
As the theory of rhythm develops, I'll find that some of the above goals are fundamentally incompatible, so I'll have to make choices. That will be part of the fun.
## Key questions
- The monotonic clock is ticking. That's cool. But what is the
relationship between monotonic clock time and a note's actual input?
That is, how does a clock (wall or pulse) query a note's presence?
Especially when they are composed?
## Things I'm pretty sure about
- Base unit should be a beat, and durations should be expressed
as fractional beats (e.g. 1/4, 2/1).
- Take a look at my "static" topology diagram, and consider that
flows can bind to positions. Given that, maybe it is possible
to have dynamic topologies. Put differently: As long as every
dynamic topology input flow is continuously defined, then going
"back in time" can mean "reversing the current state". This
_DOES NOT_ mean reversing time, or even guarenteeing that what
you play backward is the same as what you play forward.
What might happen when you hit play under this model?
You pass a flow into a player
The player sets time to init-time
The pulse clock starts pulsing
## Random notes
What is the "smallest time unit" that we want to represent?
We have to specify the number of "smallest time units" in a pulse.
I _think_ this is tightly tied to recursive zooming in. We can
specify durations as [128th notes, zoom level]
We increment zoom level when zooming in
We can set a "max frequency" for control information (say 100hz).
Then, our time-recursive function can short-circut any recursive objects
whose children have a minimum frequency that falls below the max frequency.
This will require that time objects emit min-frequency metadata,
which can be derived by finding the longest child.
This is a wild theory - test this tomorrow.
Actually, a better solution would be to just put the composer in control.
The composer should specify that they want n doublings or halvings of
a given phrase played concurrently.
## Solving for:
## Time signature changes
How does this interact with looping?
## Concurrent phrases with differing time signatures
It should be possible for concurrent time signatures to emit
their:
- Offset
- Numerator
They can be then merged together to create a data structure
containing lazy seqs of "beats" at each combination
Actually, just provide a function that takes offsets and numerators
and the "combination" that you're looking for, and returns
a lazy seq of beating indices
## Tick frequency
Monotonic clock frequency is tied to doubling or halving of tempo,
along with "discrete nyquist"
### Looping
What would it mean for each "phrase" to have its own derived timeline?
(Maybe not literally a phrase, maybe some container designed for this).
and what would it mean if these phrases could have timeline offsets
that repeat?
### Jump back
Similar to a repeat in music theory. A timeline GOTO.
### Static repeat
Not actually a loop. This is calling a phrase again and again,
adding the phrase's length to the offset at each repetition.
## Phrase isolation
It should be easy to play a phrase by itself. In fact, it should be possible to
loop a phrase so yo can just hear it while you're working on it
Should create repl.play and repl.repeat:
(play phrase)
(play phrase 130)
(repeat phrase)
(repeat phrase 130)
(stop)
### Start a phrase at the next downbeat
### Start a phrase at the next polyrhythmic co-downbeat
### Abstract phrases away as functions
### Make seeking both easy to reason about and performant
### Sensible reverse playback (rolling a tempo from pos to neg)
### Express fractional notes sensibly
# November 20th, 2025
## Summary of yesterday's "theory of rhythm" noodling:
Must support:
- Playing a phrase in isolation
- Looping
- Seeking
- Time signatures and time signature changes
- Coming in at the next downbeat
Don't box out:
- Polyrhythms
- Unlimited slowing and speeding
Looping means a few things:
- Jumping back, identical to a repeat in music notation. Timeline goto.
- Static repeat. That is, play phrase x n times. Note that this is is
repeating, not looping.
Insights:
- Don't support iterative composition, where f(t2) = f(f(t1)).
Iterative composition makes seek time linear with composition
length.
- The monotonic clock frequency is closely related to the maximum
number of events per second that we want to emit.
- Theoretically, infinite zoom could automatically derive the number
of doublings and halvings to play by looking at the monotonic
clock frequency and the longest event in a phrase. Doublings
that would result in no event changes due to all events falling
below the monotonic clock sampling rate could stop upward
recursion. This seems complicated to implement in practice,
but would technically work. (Think nyquist.)
- Concurrent time signatures could emit their start offset and
numerator. We could provide a function that takes offsets and
numerators and returns a lazy seq of polyrhythmic downbeat
positions.
Open questions:
Should base unit should be a beat, with durations based
as fractional units of a beat (e.g. 1/4, 4/1)? Or should base unit
account for time signature denominator? My hunch is that the internal
representation should be fractional beat, with helper functions that
convert current time signature to fractional beats.
## More brainstorming
Let me try to describe how the various decisions suggested above
could compose. I'll start at the end, and work backward.
You have a musical phrase representing your composition
or a part of it, and you want to play that phrase. The phrase
is a `timeline` object. The playback engine will query the
timeline object.
First,
you connect it to the playback engine:
```
(on-deck phrase input-map-f output-map-f)
```
`input-map-f` is a function that accepts the playback engine's available input
subsystems (initially just `{:midi midi-message-flow}`, but potentially
containing things like dmx and osc,) and returns a flow of a map wiring up the
input arguments of the composition.
Now, play the song:
```
(play 120)
```
`play` starts the clock, querying the timeline object as needed.
```
(pause)
```
`pause` pauses the piece.
```
(resume)
```
`resume` resumes the piece.
```
(play)
```
`play` with no arguments starts the piece from the beginning.
```
(loop)
```
`loop` with no args automatically resets the playback
position to the beginning at the end of the piece.
```
(loop start end)
```
This form of `loop` loops a segment of the composition.
(Aside: A pulse clock emits integers at the pulse rate. The
emitted integer represents the current _zoom level_.)
Open question: The playback engine queries the timeline at clock
positions. How does this relate to zoom levels? Is zoom level
part of the query?
### Random thoughts
Right now, phrases are eagerly composed together. This means that
if you redefine part of a piece, you have to redefine all of its
dependents, too. This might make interactive development annoying.
|