diff options
| author | Jake Zerrer <him@jakezerrer.com> | 2025-12-01 15:27:59 -0500 |
|---|---|---|
| committer | Jake Zerrer <him@jakezerrer.com> | 2025-12-01 15:28:17 -0500 |
| commit | b2347cb2766e2da2a725e3c2228ef2480dfaa207 (patch) | |
| tree | 73133e659a48f94daee7f93cf1ec60ad52bd61ad | |
| parent | acf63cc2308da8708a0bc23877806fa19dac3ad2 (diff) | |
Add rate function
| -rw-r--r-- | src/unheard/cycles.clj | 41 | ||||
| -rw-r--r-- | test/unheard/cycles_test.clj | 46 |
2 files changed, 83 insertions, 4 deletions
diff --git a/src/unheard/cycles.clj b/src/unheard/cycles.clj index 5511d64..b134ad7 100644 --- a/src/unheard/cycles.clj +++ b/src/unheard/cycles.clj @@ -43,6 +43,22 @@ [& args] {:v (vec args) :type :p}) +(defn rate + "Rate modifier: scales the speed of a pattern by a given ratio. + + A ratio > 1 speeds up the pattern (fits more cycles in the same time). + A ratio < 1 slows down the pattern (stretches it over more time). + A ratio of 2 means the pattern runs twice as fast. + A ratio of 1/2 means the pattern runs at half speed. + + Equivalent to TidalCycles/Strudel '*' and '/' operators. + + Examples: + (rate 2 (l :a :b)) - runs the list twice as fast + (rate 1/2 (f :a :b :c)) - runs the fork at half speed" + [ratio node] + {:v node :rate ratio :type :rate}) + (defn scalar? [x] (not (and (map? x) (:type x)))) @@ -67,7 +83,12 @@ (= :p (:type node)) (let [children (:v node)] - (reduce lcm 1 (map compute-cycle children))))) + (reduce lcm 1 (map compute-cycle children))) + + (= :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)))) (defn unfold-node [node start end iteration] (let [duration (- end start)] @@ -96,7 +117,23 @@ (let [children (:v node)] (mapcat (fn [child] (unfold-node child start end iteration)) - children))))) + children)) + + (= :rate (:type node)) + (let [ratio (:rate node) + child (:v node) + child-base-cycle (compute-cycle child) + ;; rate scales how many times the base pattern repeats + ;; rate 2 means fit 2x cycles in this span + ;; rate 1/2 means fit 0.5x cycles (half a cycle) + num-child-cycles (* ratio child-base-cycle) + child-cycle-duration (/ duration num-child-cycles)] + (vec (mapcat (fn [i] + (unfold-node child + (+ start (* i child-cycle-duration)) + (+ start (* (inc i) child-cycle-duration)) + i)) + (range num-child-cycles))))))) (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 ef9aca7..0f55a2c 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 unfold]])) + [unheard.cycles :refer [l f p rate unfold]])) (deftest unfold-tests (testing "single scalar" @@ -86,4 +86,46 @@ (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))))))) + (unfold 1 (p :a (f :b :c)))))) + + (testing "rate 2 - doubles speed of scalar" + (is (= [[0 1/2 :a] [1/2 1 :a]] + (unfold 1 (rate 2 :a))))) + + (testing "rate 1/2 - halves speed of scalar" + (is (= [[0 2 :a]] + (unfold 1 (rate 1/2 :a))))) + + (testing "rate 2 - doubles speed of list" + (is (= [[0 1/6 :a] [1/6 1/3 :b] [1/3 1/2 :c] + [1/2 2/3 :a] [2/3 5/6 :b] [5/6 1 :c]] + (unfold 1 (rate 2 (l :a :b :c)))))) + + (testing "rate 1/2 - halves speed of list" + (is (= [[0 1 :a] [1 2 :b]] + (unfold 1 (rate 1/2 (l :a :b)))))) + + (testing "rate 2 - doubles speed of fork" + ;; Fork has 3 children, cycle count is 3 + ;; rate 2 makes it repeat 2x, so 6 total cycles + (is (= [[0 1/6 :a] [1/6 1/3 :b] [1/3 1/2 :c] + [1/2 2/3 :a] [2/3 5/6 :b] [5/6 1 :c] + [1 7/6 :a] [7/6 4/3 :b] [4/3 3/2 :c] + [3/2 5/3 :a] [5/3 11/6 :b] [11/6 2 :c] + [2 13/6 :a] [13/6 7/3 :b] [7/3 5/2 :c] + [5/2 8/3 :a] [8/3 17/6 :b] [17/6 3 :c]] + (unfold 1 (rate 2 (f :a :b :c)))))) + + (testing "rate with parallel" + ;; Parallel has cycle 1, rate 2 repeats it twice + (is (= [[0 1/2 :a] [0 1/2 :b] + [1/2 1 :a] [1/2 1 :b]] + (unfold 1 (rate 2 (p :a :b)))))) + + (testing "rate inside list" + (is (= [[0 1/2 :x] [1/2 3/4 :y] [3/4 1 :y]] + (unfold 1 (l :x (rate 2 :y)))))) + + (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))))))) |
