summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Zerrer <him@jakezerrer.com>2025-12-01 15:34:12 -0500
committerJake Zerrer <him@jakezerrer.com>2025-12-01 15:34:19 -0500
commitdaa831b0a44d344fb4256b004139c58f661f746d (patch)
treed4182d8caeb65c21b2c5dc0af29723672a56d0cd
parentb2347cb2766e2da2a725e3c2228ef2480dfaa207 (diff)
Add elongation
-rw-r--r--src/unheard/cycles.clj50
-rw-r--r--test/unheard/cycles_test.clj33
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)))))))