summaryrefslogtreecommitdiff
path: root/src/unheard/cycles.clj
diff options
context:
space:
mode:
Diffstat (limited to 'src/unheard/cycles.clj')
-rw-r--r--src/unheard/cycles.clj111
1 files changed, 111 insertions, 0 deletions
diff --git a/src/unheard/cycles.clj b/src/unheard/cycles.clj
new file mode 100644
index 0000000..513b090
--- /dev/null
+++ b/src/unheard/cycles.clj
@@ -0,0 +1,111 @@
+(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 scalar? [x]
+ (not (and (map? x) (:type x))))
+
+(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)))))
+
+(defn unfold-node [node start end iteration]
+ (let [duration (- end start)]
+ (cond
+ (scalar? node)
+ [[start end node]]
+
+ (= :l (:type node))
+ (let [children (:v node)
+ n (count children)
+ slice-size (/ duration n)]
+ (mapcat (fn [i child]
+ (let [child-start (+ start (* i slice-size))
+ child-end (+ start (* (inc i) slice-size))]
+ (unfold-node child child-start child-end iteration)))
+ (range n)
+ children))
+
+ (= :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))))))
+
+(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)))))
+