diff options
| author | Jake Zerrer <him@jakezerrer.com> | 2025-11-26 15:10:20 -0500 |
|---|---|---|
| committer | Jake Zerrer <him@jakezerrer.com> | 2025-12-02 10:54:13 -0500 |
| commit | d9c4799bd25de09e0cea9fa9c4b384ae977c8cec (patch) | |
| tree | e8b5e0eba926f43bdfd5a811f860b98776d3e1af | |
| parent | 890c1efe33bad0bb938f8795291c479ebc073033 (diff) | |
mini-notation parser: map notes to values
| -rw-r--r-- | src/unheard/strudel/mini_notation_compiler.clj | 85 | ||||
| -rw-r--r-- | test/unheard/strudel/mini_notation_compiler_test.clj | 96 |
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))))) |
