summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Zerrer <him@jakezerrer.com>2025-12-01 15:38:37 -0500
committerJake Zerrer <him@jakezerrer.com>2025-12-01 15:39:11 -0500
commit2d1418040bac7bec0ebac8b9c6d89ced3af7faeb (patch)
treee6da9a133b60659fabfc980667ab7115d16c3736
parentf48bd390a2d25bab6f7202941c5eb3a4149ab5a3 (diff)
Add replicate function
-rw-r--r--src/unheard/cycles.clj34
-rw-r--r--test/unheard/cycles_test.clj66
2 files changed, 97 insertions, 3 deletions
diff --git a/src/unheard/cycles.clj b/src/unheard/cycles.clj
index acf1900..eb53c57 100644
--- a/src/unheard/cycles.clj
+++ b/src/unheard/cycles.clj
@@ -74,6 +74,21 @@
[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))))
@@ -116,6 +131,11 @@
(= :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]
@@ -167,7 +187,19 @@
(= :elongate (:type node))
;; Elongate just wraps a child - unfold the child with the same time bounds
- (unfold-node (:v node) start end iteration))))
+ (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.
diff --git a/test/unheard/cycles_test.clj b/test/unheard/cycles_test.clj
index 242b6fc..e2e1889 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 p rate elongate unfold]]))
+ [unheard.cycles :refer [l f p rate elongate rep unfold]]))
(deftest unfold-tests
(testing "single scalar"
@@ -157,4 +157,66 @@
(testing "rate with elongate inside list"
(is (= [[0 1/4 :a] [1/4 3/8 :b] [3/8 1/2 :c]
[1/2 3/4 :a] [3/4 7/8 :b] [7/8 1 :c]]
- (unfold 1 (rate 2 (l (elongate 2 :a) :b :c)))))))
+ (unfold 1 (rate 2 (l (elongate 2 :a) :b :c))))))
+
+ (testing "rep 2 - element repeats twice"
+ (is (= [[0 1/2 :a] [1/2 1 :a]]
+ (unfold 1 (rep 2 :a)))))
+
+ (testing "rep 3 - element repeats three times"
+ (is (= [[0 1/3 :a] [1/3 2/3 :a] [2/3 1 :a]]
+ (unfold 1 (rep 3 :a)))))
+
+ (testing "rep in list - subdivides its time slot"
+ (is (= [[0 1/6 :a] [1/6 1/3 :a] [1/3 1/2 :a]
+ [1/2 1 :b]]
+ (unfold 1 (l (rep 3 :a) :b)))))
+
+ (testing "rep with three elements in list"
+ (is (= [[0 1/6 :x] [1/6 1/3 :x]
+ [1/3 2/3 :y]
+ [2/3 1 :z]]
+ (unfold 1 (l (rep 2 :x) :y :z)))))
+
+ (testing "multiple reps in list"
+ (is (= [[0 1/6 :a] [1/6 1/3 :a]
+ [1/3 1/2 :b] [1/2 2/3 :b]
+ [2/3 1 :c]]
+ (unfold 1 (l (rep 2 :a) (rep 2 :b) :c)))))
+
+ (testing "rep with fork"
+ (is (= [[0 1/2 :a] [1/2 1 :a]
+ [1 2 :b]]
+ (unfold 1 (f (rep 2 :a) :b)))))
+
+ (testing "rep inside parallel"
+ (is (= [[0 1/2 :a] [1/2 1 :a]
+ [0 1 :b]]
+ (unfold 1 (p (rep 2 :a) :b)))))
+
+ (testing "rep with rate"
+ (is (= [[0 1/4 :a] [1/4 1/2 :a]
+ [1/2 3/4 :a] [3/4 1 :a]]
+ (unfold 1 (rep 2 (rate 2 :a))))))
+
+ (testing "rate with rep inside"
+ (is (= [[0 1/4 :a] [1/4 1/2 :a]
+ [1/2 3/4 :a] [3/4 1 :a]]
+ (unfold 1 (rate 2 (rep 2 :a))))))
+
+ (testing "rep in list without elongate"
+ (is (= [[0 1/6 :a] [1/6 1/3 :a]
+ [1/3 2/3 :b]
+ [2/3 1 :c]]
+ (unfold 1 (l (rep 2 :a) :b :c)))))
+
+ (testing "elongate and rep together"
+ (is (= [[0 1/6 :a] [1/6 1/3 :a]
+ [1/3 2/3 :b]
+ [2/3 1 :c]]
+ (unfold 1 (l (rep 2 (elongate 2 :a)) :b :c)))))
+
+ (testing "rep of list"
+ (is (= [[0 1/4 :a] [1/4 1/2 :b]
+ [1/2 3/4 :a] [3/4 1 :b]]
+ (unfold 1 (rep 2 (l :a :b)))))))