summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Zerrer <him@jakezerrer.com>2025-12-01 15:16:25 -0500
committerJake Zerrer <him@jakezerrer.com>2025-12-01 15:16:40 -0500
commitacf63cc2308da8708a0bc23877806fa19dac3ad2 (patch)
treec3f4b65e76dc90cedaeb1ff28b48a66bf5a5bb32
parentff57f6567978a91b6aeb71347ccb6d0ba7028a38 (diff)
Add parallel composition
-rw-r--r--src/unheard/cycles.clj26
-rw-r--r--test/unheard/cycles_test.clj28
2 files changed, 51 insertions, 3 deletions
diff --git a/src/unheard/cycles.clj b/src/unheard/cycles.clj
index 513b090..5511d64 100644
--- a/src/unheard/cycles.clj
+++ b/src/unheard/cycles.clj
@@ -29,6 +29,20 @@
[& 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 scalar? [x]
(not (and (map? x) (:type x))))
@@ -49,6 +63,10 @@
(= :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)))))
(defn unfold-node [node start end iteration]
@@ -72,7 +90,13 @@
(let [children (:v node)
n (count children)
child-idx (mod iteration n)]
- (unfold-node (nth children child-idx) start end (quot 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)))))
(defn unfold
"Unfolds a pattern tree into concrete time intervals.
diff --git a/test/unheard/cycles_test.clj b/test/unheard/cycles_test.clj
index b912895..ef9aca7 100644
--- a/test/unheard/cycles_test.clj
+++ b/test/unheard/cycles_test.clj
@@ -1,6 +1,6 @@
(ns unheard.cycles-test
(:require [clojure.test :refer [deftest is testing]]
- [unheard.cycles :refer [l f unfold]]))
+ [unheard.cycles :refer [l f p unfold]]))
(deftest unfold-tests
(testing "single scalar"
@@ -62,4 +62,28 @@
(testing "fork with nested list subdivides correctly"
(is (= [[0 1/3 :a] [1/3 2/3 :b] [2/3 1 :c]
[1 2 :x]]
- (unfold 1 (f (l :a :b :c) :x))))))
+ (unfold 1 (f (l :a :b :c) :x)))))
+
+ (testing "simple parallel - all children at same time"
+ (is (= [[0 1 :a] [0 1 :b] [0 1 :c]]
+ (unfold 1 (p :a :b :c)))))
+
+ (testing "parallel with list - children subdivide in parallel"
+ (is (= [[0 1/2 :a] [1/2 1 :b]
+ [0 1/2 :c] [1/2 1 :d]]
+ (unfold 1 (p (l :a :b) (l :c :d))))))
+
+ (testing "parallel with fork - forks extend together"
+ (is (= [[0 1 :a] [0 1 :c]
+ [1 2 :b] [1 2 :d]]
+ (unfold 1 (p (f :a :b) (f :c :d))))))
+
+ (testing "list containing parallel"
+ (is (= [[0 1/2 :x] [0 1/2 :y]
+ [1/2 1 :z]]
+ (unfold 1 (l (p :x :y) :z)))))
+
+ (testing "parallel with different cycle lengths"
+ (is (= [[0 1 :a] [0 1 :b]
+ [1 2 :a] [1 2 :c]]
+ (unfold 1 (p :a (f :b :c)))))))