diff options
| author | Jake Zerrer <him@jakezerrer.com> | 2025-11-26 15:10:20 -0500 |
|---|---|---|
| committer | Jake Zerrer <him@jakezerrer.com> | 2025-12-01 16:02:53 -0500 |
| commit | bc500bb5dc9f32b7b50ce39c5a98df13322e4167 (patch) | |
| tree | c123662150db8dd6752156e0c22d2f6859fa4047 | |
| parent | 322b66627fd619c2ce0f2a35eae043e9304ee8bc (diff) | |
Create `paste` operator
| -rw-r--r-- | src/unheard/cycles.clj | 38 | ||||
| -rw-r--r-- | test/unheard/cycles_test.clj | 62 |
2 files changed, 98 insertions, 2 deletions
diff --git a/src/unheard/cycles.clj b/src/unheard/cycles.clj index 29cad91..3cbfb97 100644 --- a/src/unheard/cycles.clj +++ b/src/unheard/cycles.clj @@ -92,6 +92,44 @@ (defn scalar? [x] (not (and (map? x) (:type x)))) +(defn paste + "Paste operator: replaces scalar values in a template pattern with provided values. + + Takes a template pattern and a sequence of replacement values. Each scalar + in the template (in depth-first order) is replaced by the corresponding value + from the replacement sequence. If a replacement value is nil, the original + scalar is preserved. + + This allows separating rhythmic structure from content. Any values can be used + as placeholders in the template (commonly keywords like :_ or numbers). + + Examples: + (paste (f :_ (f :_ :_) :_ :_) :c :e :g :b :d) + => (f :c (f :e :g) :b :d) + + (paste (f :_ (f :_ :_) :_ :_) :c :e nil :b :d) + => (f :c (f :e :_) :b :d) + + (paste (l 1 2 1) :a :b :c) + => (l :a :b :c)" + [template & values] + (let [values-seq (atom (seq values))] + (letfn [(replace-scalars [node] + (if (scalar? node) + (let [replacement (first @values-seq)] + (swap! values-seq rest) + (if (nil? replacement) + node + replacement)) + ;; Non-scalar: check if :v is a vector (l, f, p) or single value (rate, elongate, rep) + (let [v (:v node)] + (if (vector? v) + ;; Combinators with multiple children + (assoc node :v (mapv replace-scalars v)) + ;; Modifiers with single child + (assoc node :v (replace-scalars v))))))] + (replace-scalars template)))) + (defn get-weight "Returns the weight of a node. Elongated nodes have their specified weight, all other nodes have a default weight of 1." diff --git a/test/unheard/cycles_test.clj b/test/unheard/cycles_test.clj index e2e1889..c395755 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 rep unfold]])) + [unheard.cycles :refer [l f p rate elongate rep paste unfold]])) (deftest unfold-tests (testing "single scalar" @@ -219,4 +219,62 @@ (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))))))) + (unfold 1 (rep 2 (l :a :b)))))) + + (testing "paste - simple replacement in list" + (is (= (l :a :b :c) + (paste (l :_ :_ :_) :a :b :c)))) + + (testing "paste - simple replacement in fork" + (is (= (f :c (f :e :g) :b :d) + (paste (f :_ (f :_ :_) :_ :_) :c :e :g :b :d)))) + + (testing "paste - with nil preserves original" + (is (= (f :c (f :e :_) :b :d) + (paste (f :_ (f :_ :_) :_ :_) :c :e nil :b :d)))) + + (testing "paste - multiple nils" + (is (= (f :_ (f :e :_) :b :_) + (paste (f :_ (f :_ :_) :_ :_) nil :e nil :b nil)))) + + (testing "paste - with numbers as template" + (is (= (l :a :b :c) + (paste (l 1 2 1) :a :b :c)))) + + (testing "paste - nested structures" + (is (= (l :x (l :y :z) :w) + (paste (l :_ (l :_ :_) :_) :x :y :z :w)))) + + (testing "paste - with parallel" + (is (= (p :a :b :c) + (paste (p :_ :_ :_) :a :b :c)))) + + (testing "paste - complex nested with parallel" + (is (= (l :a (p :b :c) :d) + (paste (l :_ (p :_ :_) :_) :a :b :c :d)))) + + (testing "paste - with modifiers preserved" + (is (= (l (rate 2 :a) :b) + (paste (l (rate 2 :_) :_) :a :b)))) + + (testing "paste - with elongate" + (is (= (l (elongate 2 :x) :y :z) + (paste (l (elongate 2 :_) :_ :_) :x :y :z)))) + + (testing "paste - with rep" + (is (= (l (rep 3 :a) :b) + (paste (l (rep 3 :_) :_) :a :b)))) + + (testing "paste - unfolds correctly" + (is (= [[0 1/3 :c] [1/3 2/3 :e] [2/3 1 :g]] + (unfold 1 (paste (l :_ :_ :_) :c :e :g))))) + + (testing "paste - complex rhythm unfolds correctly" + (is (= [[0 1 :c] [1 2 :e] [2 3 :b] [3 4 :d] + [4 5 :c] [5 6 :g] [6 7 :b] [7 8 :d]] + (unfold 1 (paste (f :_ (f :_ :_) :_ :_) :c :e :g :b :d))))) + + (testing "paste - with nil unfolds correctly" + (is (= [[0 1 :c] [1 2 :e] [2 3 :b] [3 4 :d] + [4 5 :c] [5 6 :_] [6 7 :b] [7 8 :d]] + (unfold 1 (paste (f :_ (f :_ :_) :_ :_) :c :e nil :b :d)))))) |
