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
|
(ns unheard.cycles)
(defn l
"List combinator: subdivides time evenly among children, advancing in lockstep.
Each child receives an equal portion of the parent's time duration.
All children advance synchronously through their patterns.
Equivalent to TidalCycles/Strudel 'fastcat' operator.
Example:
(l :a :b :c) with cycle-length 1
=> [[0 1/3 :a] [1/3 2/3 :b] [2/3 1 :c]]"
[& args]
{:v (vec args) :type :l})
(defn f
"Fork combinator: cycles through children sequentially across iterations.
Each iteration selects one child in round-robin fashion.
The selected child gets the full time duration for that iteration.
Forks extend the total pattern duration by (num-children × child-cycles).
Equivalent to TidalCycles/Strudel 'slowcat' operator.
Example:
(f :a :b :c) with cycle-length 1
=> [[0 1 :a] [1 2 :b] [2 3 :c]]"
[& args]
{:v (vec args) :type :f})
(defn p
"Parallel combinator: all children occur simultaneously.
Each child gets the full time duration of the parent.
All children are active at the same time, producing overlapping intervals.
Equivalent to TidalCycles/Strudel 'stack' operator.
Example:
(p :a :b :c) with cycle-length 1
=> [[0 1 :a] [0 1 :b] [0 1 :c]]"
[& args]
{:v (vec args) :type :p})
(defn rate
"Rate modifier: scales the speed of a pattern by a given ratio.
A ratio > 1 speeds up the pattern (fits more cycles in the same time).
A ratio < 1 slows down the pattern (stretches it over more time).
A ratio of 2 means the pattern runs twice as fast.
A ratio of 1/2 means the pattern runs at half speed.
Equivalent to TidalCycles/Strudel '*' and '/' operators.
Examples:
(rate 2 (l :a :b)) - runs the list twice as fast
(rate 1/2 (f :a :b :c)) - runs the fork at half speed"
[ratio node]
{:v node :rate ratio :type :rate})
(defn elongate
"Elongation modifier: gives an element temporal weight.
When used within a list, an elongated element takes up proportionally
more time than other elements based on its weight. Elements without
elongation have a default weight of 1.
Equivalent to TidalCycles/Strudel '@' operator.
Examples:
(l (elongate 2 :a) :b :c) - :a takes twice as long as :b or :c
(l :x (elongate 3 :y) :z) - :y takes 3x as long as :x or :z"
[weight node]
{:v node :weight weight :type :elongate})
(defn rep
"Replication modifier: repeats an element N times, subdividing its time equally.
The element is repeated the specified number of times within the same
time duration it would normally occupy. Each repetition gets an equal
time slice.
Equivalent to TidalCycles/Strudel '!' operator.
Examples:
(l (rep 3 :a) :b) - :a repeats 3x in first half, :b in second half
(l :x (rep 2 :y) :z) - :y repeats 2x in middle third"
[times node]
{:v node :times times :type :rep})
(defn scalar? [x]
(not (and (map? x) (:type x))))
(defn get-weight
"Returns the weight of a node. Elongated nodes have their specified weight,
all other nodes have a default weight of 1."
[node]
(if (and (map? node) (= :elongate (:type node)))
(:weight node)
1))
(defn gcd [a b]
(if (zero? b) a (recur b (mod a b))))
(defn lcm [a b]
(/ (* a b) (gcd a b)))
(defn compute-cycle [node]
(cond
(scalar? node) 1
(= :f (:type node))
(let [children (:v node)
n (count children)]
(* n (reduce lcm 1 (map compute-cycle children))))
(= :l (:type node))
(let [children (:v node)]
(reduce lcm 1 (map compute-cycle children)))
(= :p (:type node))
(let [children (:v node)]
(reduce lcm 1 (map compute-cycle children)))
(= :rate (:type node))
;; Rate doesn't change the cycle count - it just compresses/expands time
;; The parent sees the same cycle count as the child
(compute-cycle (:v node))
(= :elongate (:type node))
;; Elongation doesn't change the cycle count - it just affects time division
;; The parent sees the same cycle count as the child
(compute-cycle (:v node))
(= :rep (:type node))
;; Replication doesn't change the cycle count - it just subdivides time
;; The parent sees the same cycle count as the child
(compute-cycle (:v node))))
(defn unfold-node [node start end iteration]
(let [duration (- end start)]
(cond
(scalar? node)
[[start end node]]
(= :l (:type node))
(let [children (:v node)
weights (map get-weight children)
total-weight (reduce + weights)
weight-offsets (reductions + 0 weights)]
(mapcat (fn [i child weight]
(let [child-start (+ start (* duration (/ (nth weight-offsets i) total-weight)))
child-end (+ start (* duration (/ (nth weight-offsets (inc i)) total-weight)))]
(unfold-node child child-start child-end iteration)))
(range (count children))
children
weights))
(= :f (:type node))
(let [children (:v node)
n (count children)
child-idx (mod iteration n)]
(unfold-node (nth children child-idx) start end (quot iteration n)))
(= :p (:type node))
(let [children (:v node)]
(mapcat (fn [child]
(unfold-node child start end iteration))
children))
(= :rate (:type node))
(let [ratio (:rate node)
child (:v node)
child-base-cycle (compute-cycle child)
;; rate scales how many times the base pattern repeats
;; rate 2 means fit 2x cycles in this span
;; rate 1/2 means fit 0.5x cycles (half a cycle)
num-child-cycles (* ratio child-base-cycle)
child-cycle-duration (/ duration num-child-cycles)]
(vec (mapcat (fn [i]
(unfold-node child
(+ start (* i child-cycle-duration))
(+ start (* (inc i) child-cycle-duration))
i))
(range num-child-cycles))))
(= :elongate (:type node))
;; Elongate just wraps a child - unfold the child with the same time bounds
(unfold-node (:v node) start end iteration)
(= :rep (:type node))
;; Rep subdivides the time span into N equal parts and repeats the child
(let [times (:times node)
child (:v node)
slice-duration (/ duration times)]
(vec (mapcat (fn [i]
(unfold-node child
(+ start (* i slice-duration))
(+ start (* (inc i) slice-duration))
iteration))
(range times)))))))
(defn unfold
"Unfolds a pattern tree into concrete time intervals.
Takes a cycle-length and a pattern node (scalar, list, or fork),
and returns a vector of [start end value] intervals representing
when each scalar value is active.
Args:
cycle-length - Duration of each iteration (can be any number)
node - Pattern tree built from scalars, (l ...), and (f ...)
Returns:
Vector of [start end value] tuples, where start and end are rational
numbers representing time positions.
The total duration of the result is (* cycle-length (compute-cycle node)).
Examples:
(unfold 1 :a)
=> [[0 1 :a]]
(unfold 1 (l :a :b))
=> [[0 1/2 :a] [1/2 1 :b]]
(unfold 1 (f :a :b))
=> [[0 1 :a] [1 2 :b]]"
[cycle-length node]
(let [cycle-count (compute-cycle node)]
(vec (mapcat (fn [i]
(unfold-node node
(* i cycle-length)
(* (inc i) cycle-length)
i))
(range cycle-count)))))
|