diff options
| author | Jake Zerrer <him@jakezerrer.com> | 2025-12-01 15:34:12 -0500 |
|---|---|---|
| committer | Jake Zerrer <him@jakezerrer.com> | 2025-12-01 15:34:19 -0500 |
| commit | daa831b0a44d344fb4256b004139c58f661f746d (patch) | |
| tree | d4182d8caeb65c21b2c5dc0af29723672a56d0cd | |
| parent | b2347cb2766e2da2a725e3c2228ef2480dfaa207 (diff) | |
Add elongation
| -rw-r--r-- | src/unheard/cycles.clj | 50 | ||||
| -rw-r--r-- | test/unheard/cycles_test.clj | 33 |
2 files changed, 73 insertions, 10 deletions
diff --git a/src/unheard/cycles.clj b/src/unheard/cycles.clj index b134ad7..acf1900 100644 --- a/src/unheard/cycles.clj +++ b/src/unheard/cycles.clj @@ -59,9 +59,32 @@ [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 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)))) @@ -88,6 +111,11 @@ (= :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)))) (defn unfold-node [node start end iteration] @@ -98,14 +126,16 @@ (= :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))] + 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 n) - children)) + (range (count children)) + children + weights)) (= :f (:type node)) (let [children (:v node) @@ -133,7 +163,11 @@ (+ start (* i child-cycle-duration)) (+ start (* (inc i) child-cycle-duration)) i)) - (range num-child-cycles))))))) + (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)))) (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 0f55a2c..242b6fc 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 unfold]])) + [unheard.cycles :refer [l f p rate elongate unfold]])) (deftest unfold-tests (testing "single scalar" @@ -128,4 +128,33 @@ (testing "nested rates" (is (= [[0 1/4 :a] [1/4 1/2 :a] [1/2 3/4 :a] [3/4 1 :a]] - (unfold 1 (rate 2 (rate 2 :a))))))) + (unfold 1 (rate 2 (rate 2 :a)))))) + + (testing "elongate 2 - element takes twice as long" + (is (= [[0 1/2 :a] [1/2 3/4 :b] [3/4 1 :c]] + (unfold 1 (l (elongate 2 :a) :b :c))))) + + (testing "elongate 3 - element takes three times as long" + (is (= [[0 3/5 :a] [3/5 4/5 :b] [4/5 1 :c]] + (unfold 1 (l (elongate 3 :a) :b :c))))) + + (testing "multiple elongations" + (is (= [[0 2/5 :a] [2/5 4/5 :b] [4/5 1 :c]] + (unfold 1 (l (elongate 2 :a) (elongate 2 :b) :c))))) + + (testing "elongate with fork" + (is (= [[0 1 :a] [1 2 :b]] + (unfold 1 (f (elongate 2 :a) :b))))) + + (testing "elongate inside parallel" + (is (= [[0 1 :a] [0 1 :b]] + (unfold 1 (p (elongate 2 :a) :b))))) + + (testing "elongate with rate" + (is (= [[0 1/2 :a] [1/2 1 :a]] + (unfold 1 (elongate 2 (rate 2 :a)))))) + + (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))))))) |
