summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Zerrer <him@jakezerrer.com>2025-11-26 15:10:20 -0500
committerJake Zerrer <him@jakezerrer.com>2025-12-02 10:54:13 -0500
commitd9c4799bd25de09e0cea9fa9c4b384ae977c8cec (patch)
treee8b5e0eba926f43bdfd5a811f860b98776d3e1af
parent890c1efe33bad0bb938f8795291c479ebc073033 (diff)
mini-notation parser: map notes to values
-rw-r--r--src/unheard/strudel/mini_notation_compiler.clj85
-rw-r--r--test/unheard/strudel/mini_notation_compiler_test.clj96
2 files changed, 95 insertions, 86 deletions
diff --git a/src/unheard/strudel/mini_notation_compiler.clj b/src/unheard/strudel/mini_notation_compiler.clj
index f3f597e..9e7b5cb 100644
--- a/src/unheard/strudel/mini_notation_compiler.clj
+++ b/src/unheard/strudel/mini_notation_compiler.clj
@@ -55,12 +55,13 @@
(declare parse-sequence)
(declare parse-element)
-(defn- parse-atom [s]
- "Parse a single atom (note, number, or rest)."
+(defn- parse-atom [s get-value]
+ "Parse a single atom (note, number, or rest).
+ Uses get-value function to convert note names to values."
(cond
(= s "~") :r
(re-matches #"\d+(/\d+)?" s) (parse-number s)
- :else (keyword s)))
+ :else (get-value s)))
(defn- apply-modifiers [expr modifiers]
"Apply modifiers to an expression.
@@ -124,7 +125,7 @@
:else
(recur (rest chars) groups (conj current c) depth))))))
-(defn- parse-group [s open-char close-char combinator]
+(defn- parse-group [s open-char close-char combinator get-value]
"Parse a bracketed group with a specific combinator."
;; Find the matching closing bracket, then extract modifiers after it
(let [close-idx (.lastIndexOf s (int close-char))
@@ -138,20 +139,21 @@
;; Special handling for angle brackets with commas (polymeter)
result (if (and (= combinator 'f) (str/includes? inner-content ","))
;; Split on commas and parse each group
+ ;; Create parallel composition of forked groups
(let [groups (split-on-comma inner-content)
- parsed-groups (map parse-sequence groups)
- ;; Zip groups together element-by-element and wrap in parallel
- max-length (apply max (map count parsed-groups))
- zipped (for [i (range max-length)]
- (let [elements (keep #(nth (vec %) i nil) parsed-groups)]
- (if (= 1 (count elements))
- (first elements)
- (cons 'p elements))))]
- (if (= 1 (count zipped))
- (first zipped)
- (cons 'f zipped)))
+ parsed-groups (map #(parse-sequence % get-value) groups)
+ ;; Wrap each group in fork (unless single element)
+ forked-groups (map (fn [group]
+ (let [elements (vec group)]
+ (if (= 1 (count elements))
+ (first elements)
+ (cons 'f elements))))
+ parsed-groups)]
+ (if (= 1 (count forked-groups))
+ (first forked-groups)
+ (cons 'p forked-groups)))
;; Normal handling without commas
- (let [elements (parse-sequence inner-content)]
+ (let [elements (parse-sequence inner-content get-value)]
(if (= 1 (count elements))
(first elements)
(cons combinator elements))))]
@@ -185,44 +187,52 @@
:else
(recur (rest chars) tokens (conj current c) depth))))))
-(defn- parse-parallel [token]
+(defn- parse-parallel [token get-value]
"Parse comma-separated elements into parallel structure."
(if (str/includes? token ",")
(let [parts (str/split token #",")
- elements (map parse-element parts)]
+ elements (map #(parse-element % get-value) parts)]
(cons 'p elements))
- (parse-element token)))
+ (parse-element token get-value)))
-(defn- parse-element [token]
+(defn- parse-element [token get-value]
"Parse a single element (may be atom, group, or have modifiers)."
(cond
;; Square brackets - subdivision (l)
;; Check if starts with [ (may have modifiers at end)
(str/starts-with? token "[")
- (parse-group token \[ \] 'l)
+ (parse-group token \[ \] 'l get-value)
;; Angle brackets - alternation (f)
;; Check if starts with < (may have modifiers at end)
(str/starts-with? token "<")
- (parse-group token \< \> 'f)
+ (parse-group token \< \> 'f get-value)
;; Contains comma - parallel (p)
(str/includes? token ",")
- (parse-parallel token)
+ (parse-parallel token get-value)
;; Token with modifiers
:else
(let [[base modifiers] (parse-token-with-modifiers token)]
- (apply-modifiers (parse-atom base) modifiers))))
+ (apply-modifiers (parse-atom base get-value) modifiers))))
-(defn- parse-sequence [s]
+(defn- parse-sequence [s get-value]
"Parse a space-separated sequence."
(let [tokens (tokenize s)]
- (map parse-element tokens)))
+ (map #(parse-element % get-value) tokens)))
(defn compile
"Compile a Strudel mini-notation string to unheard.cycles code.
+ Takes a string in Strudel mini-notation and a function to convert
+ note names to values.
+
+ Arguments:
+ s - The mini-notation string to compile
+ get-value - Function of one argument that converts note names to values
+ (defaults to keyword)
+
Returns a quoted expression that can be evaluated in the context
where unheard.cycles functions are available.
@@ -230,23 +240,24 @@
to allow for multi-line pattern notation.
Examples:
- (compile \"c e g\")
+ (compile \"c e g\" keyword)
=> (l :c :e :g)
- (compile \"c [e g] b\")
+ (compile \"c [e g] b\" keyword)
=> (l :c (l :e :g) :b)
- (compile \"<c e g b>*2\")
+ (compile \"<c e g b>*2\" keyword)
=> (rate 2 (f :c :e :g :b))
- (compile \"[c,e,g] [d,f,a]\")
+ (compile \"[c,e,g] [d,f,a]\" keyword)
=> (l (p :c :e :g) (p :d :f :a))
- (compile \"<a b\\nc d>\")
+ (compile \"<a b\\nc d>\" keyword)
=> (f :a :b :c :d)"
- [s]
- (let [normalized (str/replace s #"\n" " ")
- elements (parse-sequence (str/trim normalized))]
- (if (= 1 (count elements))
- (first elements)
- (cons 'l elements))))
+ ([s] (compile s keyword))
+ ([s get-value]
+ (let [normalized (str/replace s #"\n" " ")
+ elements (parse-sequence (str/trim normalized) get-value)]
+ (if (= 1 (count elements))
+ (first elements)
+ (cons 'l elements)))))
diff --git a/test/unheard/strudel/mini_notation_compiler_test.clj b/test/unheard/strudel/mini_notation_compiler_test.clj
index 04b6347..184a9ea 100644
--- a/test/unheard/strudel/mini_notation_compiler_test.clj
+++ b/test/unheard/strudel/mini_notation_compiler_test.clj
@@ -6,154 +6,152 @@
(deftest compile-tests
(testing "single atom"
(is (= :c
- (compile "c"))))
+ (compile "c" keyword))))
(testing "rest literal"
(is (= :r
- (compile "~"))))
+ (compile "~" keyword))))
(testing "simple sequence - space separated"
(is (= '(l :c :e :g)
- (compile "c e g"))))
+ (compile "c e g" keyword))))
(testing "sequence with rest"
(is (= '(l :c :r :g)
- (compile "c ~ g"))))
+ (compile "c ~ g" keyword))))
(testing "subdivision with brackets"
(is (= '(l :c (l :e :g) :b)
- (compile "c [e g] b"))))
+ (compile "c [e g] b" keyword))))
(testing "nested brackets"
(is (= '(l :a (l :b (l :c :d)))
- (compile "a [b [c d]]"))))
+ (compile "a [b [c d]]" keyword))))
(testing "angle brackets - alternation"
(is (= '(f :c :e :g :b)
- (compile "<c e g b>"))))
+ (compile "<c e g b>" keyword))))
(testing "parallel with commas"
(is (= '(p :c :e :g)
- (compile "c,e,g"))))
+ (compile "c,e,g" keyword))))
(testing "parallel in sequence"
(is (= '(l (p :c :e :g) (p :d :f :a))
- (compile "[c,e,g] [d,f,a]"))))
+ (compile "[c,e,g] [d,f,a]" keyword))))
(testing "rate modifier - multiplication"
(is (= '(rate 2 :c)
- (compile "c*2"))))
+ (compile "c*2" keyword))))
(testing "rate modifier - division"
(is (= '(rate (/ 1 2) :c)
- (compile "c/2"))))
+ (compile "c/2" keyword))))
(testing "rate on group"
(is (= '(rate 2 (l :e :g))
- (compile "[e g]*2"))))
+ (compile "[e g]*2" keyword))))
(testing "rate on alternation"
(is (= '(rate 2 (f :c :e :g :b))
- (compile "<c e g b>*2"))))
+ (compile "<c e g b>*2" keyword))))
(testing "elongate modifier"
(is (= '(elongate 3 :c)
- (compile "c@3"))))
+ (compile "c@3" keyword))))
(testing "elongate in sequence"
(is (= '(l (elongate 2 :a) :b :c)
- (compile "a@2 b c"))))
+ (compile "a@2 b c" keyword))))
(testing "replication modifier"
(is (= '(rep 3 :c)
- (compile "c!3"))))
+ (compile "c!3" keyword))))
(testing "replication in sequence"
(is (= '(l (rep 2 :x) :y)
- (compile "x!2 y"))))
+ (compile "x!2 y" keyword))))
(testing "multiple modifiers"
(is (= '(rate 2 (elongate 3 :c))
- (compile "c@3*2"))))
+ (compile "c@3*2" keyword))))
(testing "complex nested structure"
(is (= '(l :c (f :e :g) :b :d)
- (compile "c <e g> b d"))))
+ (compile "c <e g> b d" keyword))))
(testing "subdivision with parallel"
(is (= '(l :x (p :y :z) :w)
- (compile "x [y,z] w"))))
+ (compile "x [y,z] w" keyword))))
(testing "strudel example from docs"
(is (= '(l :e5 (l :b4 :c5) :d5 (l :c5 :b4))
- (compile "e5 [b4 c5] d5 [c5 b4]"))))
+ (compile "e5 [b4 c5] d5 [c5 b4]" keyword))))
(testing "chord sequence"
(is (= '(rate 2 (f (p :g3 :b3 :e4) (p :a3 :c3 :e4)))
- (compile "<[g3,b3,e4] [a3,c3,e4]>*2"))))
+ (compile "<[g3,b3,e4] [a3,c3,e4]>*2" keyword))))
(testing "elongated chord"
(is (= '(rate 2 (f (elongate 2 (p :g3 :b3 :e4)) (p :a3 :c3 :e4)))
- (compile "<[g3,b3,e4]@2 [a3,c3,e4]>*2"))))
+ (compile "<[g3,b3,e4]@2 [a3,c3,e4]>*2" keyword))))
(testing "replicated chord"
(is (= '(rate 2 (f (rep 2 (p :g3 :b3 :e4)) (p :a3 :c3 :e4)))
- (compile "<[g3,b3,e4]!2 [a3,c3,e4]>*2"))))
+ (compile "<[g3,b3,e4]!2 [a3,c3,e4]>*2" keyword))))
(testing "rest in subdivision"
(is (= '(l :b4 (l :r :c5) :d5 :e5)
- (compile "b4 [~ c5] d5 e5"))))
+ (compile "b4 [~ c5] d5 e5" keyword))))
(testing "numbers as values"
(is (= '(l 1 2 3)
- (compile "1 2 3"))))
+ (compile "1 2 3" keyword))))
(testing "rational number rate"
(is (= '(rate 3/2 :c)
- (compile "c*3/2"))))
+ (compile "c*3/2" keyword))))
(testing "whitespace handling"
(is (= '(l :a :b :c)
- (compile " a b c "))))
+ (compile " a b c " keyword))))
(testing "complex real-world pattern"
(is (= '(l (rate 2 (f :bd :sd)) (p :hat :hat :hat))
- (compile "<bd sd>*2 [hat,hat,hat]"))))
+ (compile "<bd sd>*2 [hat,hat,hat]" keyword))))
(testing "newlines are converted to spaces"
(is (= '(f :a :b :c :d)
- (compile "<a b\nc d>"))))
+ (compile "<a b\nc d>" keyword))))
(testing "multi-line pattern with parallel"
(is (= '(f (l :a :b) (p :c :d))
- (compile "<\n[a b]\n[c,d]\n>"))))
+ (compile "<\n[a b]\n[c,d]\n>" keyword))))
- (testing "polymeter - comma in angle brackets zips elements"
- (is (= '(f (p :a :c) (p :b :d))
- (compile "<a b, c d>"))))
+ (testing "polymeter - comma creates parallel of forks"
+ (is (= '(p (f :a :b) (f :c :d))
+ (compile "<a b, c d>" keyword))))
(testing "polymeter - three groups"
- (is (= '(f (p :a :c :e) (p :b :d :f))
- (compile "<a b, c d, e f>"))))
+ (is (= '(p (f :a :b) (f :c :d) (f :e :f))
+ (compile "<a b, c d, e f>" keyword))))
- (testing "polymeter - unequal lengths pads with nils"
- (is (= '(f (p :a :c) (p :b :d) :e)
- (compile "<a b e, c d>"))))
+ (testing "polymeter - unequal lengths"
+ (is (= '(p (f :a :b :e) (f :c :d))
+ (compile "<a b e, c d>" keyword))))
- (testing "polymeter - single element groups"
+ (testing "polymeter - single element per group"
(is (= '(p :a :c)
- (compile "<a, c>"))))
+ (compile "<a, c>" keyword))))
(testing "polymeter - with nested structures"
- (is (= '(f (p (l :a :b) (l :e :f))
- (p (l :c :d) (l :g :h)))
- (compile "<[a b] [c d], [e f] [g h]>"))))
+ (is (= '(p (f (l :a :b) (l :c :d)) (f (l :e :f) (l :g :h)))
+ (compile "<[a b] [c d], [e f] [g h]>" keyword))))
(testing "polymeter - with modifiers"
- (is (= '(rate 2 (f (p :a :c) (p :b :d)))
- (compile "<a b, c d>*2"))))
+ (is (= '(rate 2 (p (f :a :b) (f :c :d)))
+ (compile "<a b, c d>*2" keyword))))
(testing "polymeter - melody and bass pattern"
- (is (= '(f (p (l :e5 :b4) (rate 4 (l :e2 :e3)))
- (p (l :a4 :c5) (rate 4 (l :a2 :a3))))
- (compile "<[e5 b4] [a4 c5], [[e2 e3]*4] [[a2 a3]*4]>")))))
+ (is (= '(p (f (l :e5 :b4) (l :a4 :c5)) (f (rate 4 (l :e2 :e3)) (rate 4 (l :a2 :a3))))
+ (compile "<[e5 b4] [a4 c5], [[e2 e3]*4] [[a2 a3]*4]>" keyword)))))