summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Zerrer <him@jakezerrer.com>2025-12-01 15:27:59 -0500
committerJake Zerrer <him@jakezerrer.com>2025-12-01 15:28:17 -0500
commitb2347cb2766e2da2a725e3c2228ef2480dfaa207 (patch)
tree73133e659a48f94daee7f93cf1ec60ad52bd61ad
parentacf63cc2308da8708a0bc23877806fa19dac3ad2 (diff)
Add rate function
-rw-r--r--src/unheard/cycles.clj41
-rw-r--r--test/unheard/cycles_test.clj46
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)))))))