(ns unheard.cycles) (defn l "List combinator: subdivides time evenly among children, advancing in lockstep. Each child receives an equal portion of the parent's time duration. All children advance synchronously through their patterns. Equivalent to TidalCycles/Strudel 'fastcat' operator. Example: (l :a :b :c) with cycle-length 1 => [[0 1/3 :a] [1/3 2/3 :b] [2/3 1 :c]]" [& args] {:v (vec args) :type :l}) (defn f "Fork combinator: cycles through children sequentially across iterations. Each iteration selects one child in round-robin fashion. The selected child gets the full time duration for that iteration. Forks extend the total pattern duration by (num-children × child-cycles). Equivalent to TidalCycles/Strudel 'slowcat' operator. Example: (f :a :b :c) with cycle-length 1 => [[0 1 :a] [1 2 :b] [2 3 :c]]" [& args] {:v (vec args) :type :f}) (defn p "Parallel combinator: all children occur simultaneously. Each child gets the full time duration of the parent. All children are active at the same time, producing overlapping intervals. Equivalent to TidalCycles/Strudel 'stack' operator. Example: (p :a :b :c) with cycle-length 1 => [[0 1 :a] [0 1 :b] [0 1 :c]]" [& 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 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 rep "Replication modifier: repeats an element N times, subdividing its time equally. The element is repeated the specified number of times within the same time duration it would normally occupy. Each repetition gets an equal time slice. Equivalent to TidalCycles/Strudel '!' operator. Examples: (l (rep 3 :a) :b) - :a repeats 3x in first half, :b in second half (l :x (rep 2 :y) :z) - :y repeats 2x in middle third" [times node] {:v node :times times :type :rep}) (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." [node] (if (and (map? node) (= :elongate (:type node))) (:weight node) 1)) (defn gcd [a b] (if (zero? b) a (recur b (mod a b)))) (defn lcm [a b] (/ (* a b) (gcd a b))) (defn compute-cycle [node] (cond (scalar? node) 1 (= :f (:type node)) (let [children (:v node) n (count children)] (* n (reduce lcm 1 (map compute-cycle children)))) (= :l (:type node)) (let [children (:v node)] (reduce lcm 1 (map compute-cycle children))) (= :p (:type node)) (let [children (:v node)] (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)) (= :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)) (= :rep (:type node)) ;; Replication doesn't change the cycle count - it just subdivides time ;; The parent sees the same cycle count as the child (compute-cycle (:v node)) :else (throw (ex-info "Unknown node type in compute-cycle" {:node node :type (:type node)})))) (defn unfold-node [node start end iteration] (let [duration (- end start)] (cond (scalar? node) [[start end node]] (= :l (:type node)) (let [children (:v node) 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 (count children)) children weights)) (= :f (:type node)) (let [children (:v node) n (count children) child-idx (mod iteration n)] (unfold-node (nth children child-idx) start end (quot iteration n))) (= :p (:type node)) (let [children (:v node)] (mapcat (fn [child] (unfold-node child start end iteration)) 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)] (mapcat (fn [i] (unfold-node child (+ start (* i child-cycle-duration)) (+ start (* (inc i) child-cycle-duration)) i)) (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) (= :rep (:type node)) ;; Rep subdivides the time span into N equal parts and repeats the child (let [times (:times node) child (:v node) slice-duration (/ duration times)] (mapcat (fn [i] (unfold-node child (+ start (* i slice-duration)) (+ start (* (inc i) slice-duration)) iteration)) (range times))) :else (throw (ex-info "Unknown node type in unfold-node" {:node node :type (:type node)}))))) (defn unfold "Unfolds a pattern tree into concrete time intervals. Takes a cycle-length and a pattern node (scalar, list, or fork), and returns a vector of [start end value] intervals representing when each scalar value is active. Args: cycle-length - Duration of each iteration (can be any number) node - Pattern tree built from scalars, (l ...), and (f ...) Returns: Vector of [start end value] tuples, where start and end are rational numbers representing time positions. The total duration of the result is (* cycle-length (compute-cycle node)). Examples: (unfold 1 :a) => [[0 1 :a]] (unfold 1 (l :a :b)) => [[0 1/2 :a] [1/2 1 :b]] (unfold 1 (f :a :b)) => [[0 1 :a] [1 2 :b]]" [cycle-length node] (let [cycle-count (compute-cycle node)] (vec (mapcat (fn [i] (unfold-node node (* i cycle-length) (* (inc i) cycle-length) i)) (range cycle-count)))))