summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--DEVLOG.md676
-rw-r--r--flake.nix1
-rw-r--r--portal.clj65
-rw-r--r--src/unheard/clock.clj104
-rw-r--r--src/unheard/cycles.clj224
-rw-r--r--src/unheard/dsl.clj52
-rw-r--r--src/unheard/instrument/minilab3.clj48
-rw-r--r--src/unheard/instrument/omx_27.clj11
-rw-r--r--src/unheard/interval.clj17
-rw-r--r--src/unheard/midi.clj12
-rw-r--r--src/unheard/midi_test.clj67
-rw-r--r--src/unheard/strudel/mini_notation_compiler.clj135
-rw-r--r--src/unheard/theory.clj33
-rw-r--r--src/unheard/time_object.clj14
-rw-r--r--src/unheard/time_object_test.clj33
-rw-r--r--test/unheard/cycles_test.clj238
-rw-r--r--test/unheard/strudel/mini_notation_compiler_test.clj135
17 files changed, 1114 insertions, 751 deletions
diff --git a/DEVLOG.md b/DEVLOG.md
index e56a920..c560552 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -931,3 +931,679 @@ TODO upcoming:
- Inspired by strudel, define a language of musical modifiers
- Read notes of Nov. 28
+
+## December 3rd, 2025
+
+Unheard made sound for the first time yesterday! Very exciting.
+
+Some ideas from yesterday that I want to carry forward:
+
+1. Create a function that takes channel, etc., and returns a note flow.
+2. Play with using dynamic variables? For... idk.
+
+WHOA! Strudel keeps blowing my mind.
+I just discovered that anything can take a pattern, e.g. scales: https://strudel.cc/workshop/first-notes/#scales
+WTF, so cool! This has broken my brain.
+
+Trying to unpack this.
+I guess this is how I'd think about this: each pattern describes
+values. (This is the hole in the mini-notation pattern.) Each pattern
+is associated with an _attribute type_: One might be pitch,
+another instrument. The attribute patterns are all merged together
+as if by parallel composition. In my DSL, I would think about the
+value of each attribute being a tuple of e.g. [:pitch :e4] or
+[:instrument :piano].
+
+What does this mean for me?
+
+One takeaway (I think) is that a composition has multiple instrument-like
+things, and that each instrument-like thing is a union of these
+various patterened attributes. At the top level, all of these unions play
+together via parallel composition.
+
+What would it mean for me to completely invert my playback model,
+where each instrument is a flow rather than each note? I don't think
+this is quite right, though.
+
+In particular, I'm not sure how to merge this idea into my functional composition
+model.
+
+But there is definitely something cool here: the attributes of
+an instrument are the timewise union of all patterns.
+
+Like, what if a rhythmic flow took as its input flows of other associated
+properties. (But what is a rhythmic flow? That idea doesn't exist yet.)
+
+Need to keep thinking about this
+
+I'm looking at this version of compiled-tetris:
+
+```clojure
+(defn n [>ch >val >vel]
+ (let [[ch val vel] (m/?< (m/latest vector >ch >val >vel))]
+ {ch {val :on}})))
+
+(def compiled-tetris
+ (p
+ (let [n (fn [val] (n (m/ap 0) (m/ap val) (m/ap 100)))]
+ (f (l (n e5) (l (n b4) (n c5)) (n d5) (l (n c5) (n b4)))
+ (l (n a4) (l (n a4) (n c5)) (n e5) (l (n d5) (n c5)))
+ (l (n b4) (l r (n c5)) (n d5) (n e5))
+ (l (n c5) (n a4) (n a4) r)
+ (l (l r (n d5)) (l r (n f5)) (n a5) (l (n g5) (n f5)))
+ (l (n e5) (l r (n c5)) (n e5) (l (n d5) (n c5)))
+ (l (n b4) (l (n b4) (n c5)) (n d5) (n e5))
+ (l (n c5) (n a4) (n a4) r)))
+
+ (let [n (fn [val] (n (m/ap 1) (m/ap val) (m/ap 100)))]
+ (f (rate 4 (l (n e2) (n e3)))
+ (rate 4 (l (n a2) (n a3)))
+ (l (rate 2 (l (n gs2) (n gs3))) (rate 2 (l (n e2) (n e3))))
+ (l (n a2) (n a3) (n a2) (n a3) (n a2) (n a3) (n b1) (n c2))
+ (rate 4 (l (n d2) (n d3)))
+ (rate 4 (l (n c2) (n c3)))
+ (l (rate 2 (l (n b1) (n b2))) (rate 2 (l (n e2) (n e3))))
+ (rate 4 (l (n a1) (n a2)))))))
+```
+
+Notice how it's possible to arbitrarily parameterize the various qualities of `note`.
+Cool. But how can I make it possible to _also_ parameterize an attribute of the notes
+using strudel syntax? For example, I also want octave to impact this instruments in
+this phrase:
+
+```clojure
+(defn n [>ch >val >vel]
+ (let [[ch val vel] (m/?< (m/latest vector >ch >val >vel))]
+ {ch {val :on}})))
+
+(def compiled-tetris
+ (p
+ ;; melody block
+ (let [n (fn [val] (n (m/ap 0) (m/ap val) (m/ap 100)))]
+ (f (l (n e5) (l (n b4) (n c5)) (n d5) (l (n c5) (n b4)))
+ (l (n a4) (l (n a4) (n c5)) (n e5) (l (n d5) (n c5)))
+ (l (n b4) (l r (n c5)) (n d5) (n e5))
+ (l (n c5) (n a4) (n a4) r)
+ (l (l r (n d5)) (l r (n f5)) (n a5) (l (n g5) (n f5)))
+ (l (n e5) (l r (n c5)) (n e5) (l (n d5) (n c5)))
+ (l (n b4) (l (n b4) (n c5)) (n d5) (n e5))
+ (l (n c5) (n a4) (n a4) r)))
+
+ ;; bass block
+ (let [n (fn [val] (n (m/ap 1) (m/ap val) (m/ap 100)))]
+ (f (rate 4 (l (n e2) (n e3)))
+ (rate 4 (l (n a2) (n a3)))
+ (l (rate 2 (l (n gs2) (n gs3))) (rate 2 (l (n e2) (n e3))))
+ (l (n a2) (n a3) (n a2) (n a3) (n a2) (n a3) (n b1) (n c2))
+ (rate 4 (l (n d2) (n d3)))
+ (rate 4 (l (n c2) (n c3)))
+ (l (rate 2 (l (n b1) (n b2))) (rate 2 (l (n e2) (n e3))))
+ (rate 4 (l (n a1) (n a2)))))
+
+ ;; octave block
+ (l (octave 0) (octave 1))))
+
+```
+
+Here, octave and note both are returning flows. How do we define
+the merge semantics of octave block? Should it merge with the bass
+block? The melody block? The melody block has many entities in it.
+
+This feels on the one hand like a lexical problem. _Maybe_ the
+solution should be limited to the speific semantics of the strudel
+mini-notation format. But I think probably not?
+
+Oh, this is very helpful:
+https://strudel.cc/learn/effects/#signal-chain
+
+Strudel has made this very concrete. Each pattern gets a sound,
+asdr, some filters, effects, and delay/reverb.
+
+What if I don't want that?
+
+Let's jump to a very different idea: name trees
+The idea is that nested phrases introduce nested names
+
+```clojure
+(def a (phrase ...))
+
+{`[a] ...
+ `[b a] ...}
+
+(def b
+ (phrase (a)))
+```
+
+Hm, what if `a` is used twice in `b`?
+Oh, I wrote about this on Nov. 28. The answer has to do with providing the name at invocation time.
+
+```clojure
+(def b (phrase ...))
+
+(def a
+ (phrase (b :b)))
+
+(a :a)
+
+{[:a :b] ...
+ [:a] ...}
+```
+
+I bring this up now because it might relate to this merging question.
+
+```clojure
+(defn n [>ch >val >vel]
+ (let [[ch val vel] (m/?< (m/latest vector >ch >val >vel))]
+ {ch {val :on}})))
+
+(def a
+ (phrase
+ (let [n (fn [val] (n (m/ap 0) (m/ap val) (m/ap 100)))]
+ (f (l (n e5) (l (n b4) (n c5)) (n d5) (l (n c5) (n b4)))
+ (l (n a4) (l (n a4) (n c5)) (n e5) (l (n d5) (n c5)))
+ (l (n b4) (l r (n c5)) (n d5) (n e5))
+ (l (n c5) (n a4) (n a4) r)
+ (l (l r (n d5)) (l r (n f5)) (n a5) (l (n g5) (n f5)))
+ (l (n e5) (l r (n c5)) (n e5) (l (n d5) (n c5)))
+ (l (n b4) (l (n b4) (n c5)) (n d5) (n e5))
+ (l (n c5) (n a4) (n a4) r)))))
+
+(def b
+ (phrase
+ (let [n (fn [val] (n (m/ap 1) (m/ap val) (m/ap 100)))]
+ (f (rate 4 (l (n e2) (n e3)))
+ (rate 4 (l (n a2) (n a3)))
+ (l (rate 2 (l (n gs2) (n gs3))) (rate 2 (l (n e2) (n e3))))
+ (l (n a2) (n a3) (n a2) (n a3) (n a2) (n a3) (n b1) (n c2))
+ (rate 4 (l (n d2) (n d3)))
+ (rate 4 (l (n c2) (n c3)))
+ (l (rate 2 (l (n b1) (n b2))) (rate 2 (l (n e2) (n e3))))
+ (rate 4 (l (n a1) (n a2)))))))
+
+(def octave
+ (phrase
+ (l (octave 0) (octave 1))))
+
+(def tetris
+ (phrase
+ (p
+ ;; melody block
+ (a :melody)
+ (octave :melody)
+
+ ;; bass block
+ (b :bass)
+ (octave :bass)
+ )))
+
+(tetris :t)
+
+;; end up with
+[[0 1 [[:t :melody] melody-note-flow-1]]
+ [0 1 [[:t :melody] melody-note-flow-2]]
+ [0 1 [[:t :melody] melody-octave-flow]]]
+
+;; melody-note-flow-1 might emit
+{:kind :note
+ :note 60}
+
+;; melody-octave-flow might emit
+{:kind :octave-transform
+ :dx 10}
+
+;; Maybe elements with the same name
+;; are paired with elements of a different kind?
+;; e.g.
+
+[{:kind :note :note 60} {:kind :octave-transform :dx 10} ;; note-1
+ {:kind :note :note 70} {:kind :octave-transform :dx 10} ;; note-2
+ ]
+
+;; Kind of interesting. But where does this merge order come from? Is it global?
+;; Maybe local to a phrase?
+;; And crucially: how do we know how to interpret any of this when it comes out the pipe at the end?
+
+;; One answer to the merge question is: merging must be order-independent. Though, I don't know how
+;; to prevent merging issues caused by duplicate invocations of the same kind.
+
+ ;; TODO:
+ ;; What about something with two notes, one octave transform, two cc params? Can these multiply?
+ ;; [note-1 octave cc-1]
+ ;; [note-1 octave cc-2]
+ ;; [note-2 octave cc-1]
+ ;; [note-2 octave cc-2]
+ ;; This doesn't really work, does it! Suddenly each note is appearing twice.
+
+;; This is a contrived example, though. Is there a better one?
+```
+
+---
+
+Oh, but here's another cool observation. The data structure that feeds
+into `timeline` - that is, a list of [start end value] tuples - is
+more or less ready to lock in. The only question in my mind is how
+`v` needs to be defined.
+
+This is a key observation to document:
+I can support arbitrary musical syntaxes, they just need to
+compile down to the [start end value] representation.
+
+## December 9th, 2025
+
+A few more observations:
+
+1. parallel composition is closely related to instruments. (Instruments are trees playing in parallel.)
+2. It might make sense to pass instrument specifiers down as arguments
+
+## December 10th, 2025
+
+What about tags?
+
+Consider:
+
+
+```clojure
+;; tetris
+(p
+ (tag :melody
+ (f (l _ (l _ _) _ (l _ _))
+ (l _ (l _ _) _ (l _ _))
+ (l _ (l r _) _ _)
+ (l _ _ _ r)
+ (l (l r _) (l r _) _ (l _ _))
+ (l _ (l r _) _ (l _ _))
+ (l _ (l _ _) _ _)
+ (l _ _ _ r)))
+ (tag :bass
+ (f (rate 4 (l _ _))
+ (rate 4 (l _ _))
+ (l (rate 2 (l _ _)) (rate 2 (l _ _)))
+ (l _ _ _ _ _ _ _ _)
+ (rate 4 (l _ _))
+ (rate 4 (l _ _))
+ (l (rate 2 (l _ _)) (rate 2 (l _ _)))
+ (rate 4 (l _ _)))))
+```
+
+When compiled, each interval of the above (except the outer p) would be tagged with either :melody or :bass.
+
+
+```clojure
+(tag :theme
+ (p
+ (tag :melody
+ (f (l _ (l _ _) _ (l _ _))
+ (l _ (l _ _) _ (l _ _))
+ (l _ (l r _) _ _)
+ (l _ _ _ r)
+ (l (l r _) (l r _) _ (l _ _))
+ (l _ (l r _) _ (l _ _))
+ (l _ (l _ _) _ _)
+ (l _ _ _ r)))
+ (tag :bass
+ (f (rate 4 (l _ _))
+ (rate 4 (l _ _))
+ (l (rate 2 (l _ _)) (rate 2 (l _ _)))
+ (l _ _ _ _ _ _ _ _)
+ (rate 4 (l _ _))
+ (rate 4 (l _ _))
+ (l (rate 2 (l _ _)) (rate 2 (l _ _)))
+ (rate 4 (l _ _))))))
+```
+
+Here, every interval would also be tagged with :theme.
+
+What might we do with tags?
+We could turn the output into tuples like this:
+
+
+```
+[0 1 #{:theme :melody} :c4]
+[0 1 #{:theme :bass} :d4]
+```
+
+That information could aid in interpretation at playback time?
+
+
+Note that every slot (that is, every `_`) is a place where a tag
+set can be placed.
+
+Without a tagging context, the tag is just the empty set.
+
+That is:
+
+```clojure
+(f (l _ (l _ _) _ (l _ _))
+ (l _ (l _ _) _ (l _ _))
+ (l _ (l r _) _ _)
+ (l _ _ _ r)
+ (l (l r _) (l r _) _ (l _ _))
+ (l _ (l r _) _ (l _ _))
+ (l _ (l _ _) _ _)
+ (l _ _ _ r))
+```
+
+Would result in each slot (that is, each interval) having a
+tag set of `#{}`.
+
+Let's see how tagging would accomplish a few goals.
+
+1. Play midi notes, alternating between two instrument types.
+2. Play two voices, swapping positions.
+3. Write a phrase, and then output it both in OSC and MIDI
+4. Write a phrase based on intervals, and then map those intervals
+ to a scale.
+5. Same as 4, but move the scale based on a knob. During t1, the
+ knob swaps between ionian and mixolydian. During t2, the knob
+ swaps between dorian and locrian.
+6. Two different instruments playing the same phrase, one shifted up
+ an octave
+7. A sine wave adds tremolo to one note in a chord. The frequency
+ of that tremolo is dictated by a knob.
+
+### No. 1:
+
+```clojure
+
+(import [midi-notes :as m])
+
+(def melody
+ (tag
+ :m/notes
+ (l :m/c1 :m/c2 :m/c3 :m/c2)))
+
+(def inst-changes
+ (tag
+ :inst
+ (l :synth-1 :synth-2)))
+
+(def song
+ (tag
+ :song
+ (p melody inst-changes)))
+
+[0 1 #{:song :m/notes} :m/c1]
+[0 2 #{:song :inst} :synth-1]
+[1 2 #{:song :m/notes} :m/c2]
+[2 3 #{:song :m/notes} :m/c3]
+[2 4 #{:song :inst} :synth-2]
+[3 4 #{:song :m/notes} :m/c2]
+```
+
+### No. 2:
+
+```clojure
+(import [midi-notes :as m])
+
+(def voice-1
+ (tag
+ :m/notes
+ (l :m/a1 :m/a2)))
+
+(def voice-2
+ (tag
+ :m/notes
+ (l :m/a2 :m/a1)))
+
+(def song
+ (p
+ (tag :v1 voice-1)
+ (tag :v2 voice-2)))
+
+[0 1 #{:v1 :m/notes} :m/a1]
+[0 1 #{:v2 :m/notes} :m/a2]
+[1 2 #{:v1 :m/notes} :m/a2]
+[1 2 #{:v2 :m/notes} :m/a1]
+
+```
+
+### No. 3
+
+```clojure
+(def melody
+ (tag
+ :an/notes
+ (l :an/a1 :an/a2 :an/a3)))
+
+[0 1 #{:an/notes} :an/a1]
+[1 2 #{:an/notes} :an/a2]
+[2 3 #{:an/notes} :an/a3]
+```
+
+One thing interesting about the above:
+:an/a1 could include its own tag. that is,
+it could be replaced with:
+
+```clojure
+(def a1
+ (tag
+ :an/notes
+ (l :an/a1)))
+```
+
+Then the whole thing could be rewritten:
+
+```clojure
+(def a1
+ (tag
+ :an/notes
+ (l :an/a1)))
+
+(def a2
+ (tag
+ :an/notes
+ (l :an/a2)))
+
+(def a3
+ (tag
+ :an/notes
+ (l :an/a3)))
+
+(def melody
+ (l a1 a2 a3))
+
+[0 1 #{:an/notes} :an/a1]
+[1 2 #{:an/notes} :an/a2]
+[2 3 #{:an/notes} :an/a3]
+```
+
+Note that the result is the same.
+
+### No. 4
+
+```clojure
+(def d1
+ (tag
+ :scale/degree
+ (l :sd/d1)))
+
+(def d2
+ (tag
+ :scale/degree
+ (l :sd/d2)))
+
+(def d3
+ (tag
+ :scale/degree
+ (l :sd/d3)))
+
+(def ionian
+ (tag
+ :mode
+ (l :ionian)))
+
+(def mixolydian
+ (tag
+ :mode
+ (l :mixolydian)))
+
+(def melody
+ (tag :tune
+ (p
+ (l d1 d2 d3)
+ (l ionian mixolydian))))
+
+[0 1 #{:tune :scale/degree} :sd/d1]
+[0 1.5 #{:tune :mode} :ionian]
+[1 2 #{:tune :scale/degree} :sd/d2]
+[1.5 3 #{:tune :mode} :mixolydian]
+[2 3 #{:tune :scale/degree} :sd/d3]
+```
+
+I think this one makes sense.
+
+### No. 5
+
+```clojure
+(def d1
+ (tag
+ :scale/degree
+ (l :sd/d1)))
+
+(def d2
+ (tag
+ :scale/degree
+ (l :sd/d2)))
+
+(def d3
+ (tag
+ :scale/degree
+ (l :sd/d3)))
+
+(def ionian
+ (tag
+ :mode
+ (l :ionian)))
+
+(def mixolydian
+ (tag
+ :mode
+ (l :mixolydian)))
+
+(def dorian ...)
+(def locrian ...)
+
+(def melody
+ (tag :tune
+ (p
+ (l d1 d2 d3)
+ (l
+ ;; x: flow combinator
+ (x
+ (m/ap
+ (if (< 0 (rescale 0 1 (m/?< (cv :k1-knob-1))) 0.5)
+ ionian
+ mixolydian)))
+ (x
+ (m/ap
+ (if (< 0 (rescale 0 1 (m/?< (cv :k1-knob-1))) 0.5)
+ dorian
+ locrian)))))))
+
+[0 1 #{:tune :scale/degree} :sd/d1]
+[0 1.5 #{:tune :mode} first-flow]
+[1 2 #{:tune :scale/degree} :sd/d2]
+[1.5 3 #{:tune :mode} second-flow]
+[2 3 #{:tune :scale/degree} :sd/d3]
+```
+
+Hm, we're back to flows in the value position.
+This makes me think that we should _always have a flow in value position_.
+This would obviate the x combinator introduced in
+the above example.
+
+Another insight that came from this is that there should be a global
+called cv that allows you to fetch control values by name.
+
+Is :k1-knob-1 compositional?
+And how do I ensure that the existence of :k1-knob-1 is known
+at compile time as an imput, so I can raise an exception if
+it isn't connected to anything?
+
+```clojure
+(def d1
+ (tag
+ :scale/degree
+ (l (m/ap :sd/d1))))
+
+(def ionian
+ (tag
+ :mode
+ (l (m/ap :ionian))))
+```
+
+Also, notice that I'm returning items that are themselves tagged entities from the flow.
+I think that this implies that tags are part of values.
+
+Put differently, a value has two components, :tag and :value.
+
+
+## No. 6
+
+This is a dumb way to accomplish no.6:
+
+```clojue
+(def c3
+ (tag
+ :an/notes
+ (l :an/c3)))
+
+(def d3
+ (tag
+ :an/notes
+ (l :an/d3)))
+
+(def e3
+ (tag
+ :an/notes
+ (l :an/e3)))
+
+(def f3
+ (tag
+ :an/notes
+ (l :an/f3)))
+
+(def riff
+ (l c3 d3 e3 f3))
+
+(def song
+ (p
+ (tag :inst-1
+ riff)
+ (tag :inst-3
+ (paste riff c4 d4 e4 f4))))
+```
+
+I don't love this one.
+
+### No. 7
+
+TODO I'm not even sure where to begin right now.
+
+---
+
+Summarizing a few insights:
+
+1. Flows will exist in value position. This implies that
+ values should always be wrapped in a flow.
+2. Flows will return tagged items. This implies that tags
+ must be part of value, not separate metadata.
+3. Tags returned by flows should somehow merge with their
+ container tags. This seems hard?
+4. It still isn't clear how we'll avoid naming collisions
+ with input labels.
+5. Interpretation of tagged values remains an open question.
+6. It isn't clear how to represent temporally-relative
+ structures, like make the next note one higher than the
+ previous.
+7. Similarly, counterpoint-like structures cannot currently
+ be expressed in a relative way.
+
+Next, need to think about interpretation.
+
+Should tags be a set? That loses hierarchical information.
+Is that bad?
+
+Oh, what about temporally relative structures, such as +1 tone?
+And counterpoint-like relations, like in no. 6?
+
diff --git a/flake.nix b/flake.nix
index 9f67512..d887f28 100644
--- a/flake.nix
+++ b/flake.nix
@@ -30,6 +30,7 @@
python3Packages.sphinx-rtd-theme
direnv
cloc
+ git-bug
];
};
}
diff --git a/portal.clj b/portal.clj
index 2e20bdf..4d4e59e 100644
--- a/portal.clj
+++ b/portal.clj
@@ -11,56 +11,38 @@
(defn rec
"Record flow f, tagging with id."
[id f]
- (m/ap
- (let [capturing? (atom nil)
- [tag value]
- (m/amb= [:focused-tags (m/?< >focused-tags)]
- [:event (m/?< f)])]
- (case tag
- :focused-tags
- (do
- (reset! capturing? (boolean (value id)))
- (m/amb))
+ (m/ap (let [capturing? (atom nil)
+ [tag value] (m/amb= [:focused-tags (m/?< >focused-tags)]
+ [:event (m/?< f)])]
+ (case tag
+ :focused-tags (do (reset! capturing? (boolean (value id))) (m/amb))
+ :event (do (swap! seen-tags conj id)
+ (when @capturing?
+ (m/? (m/via m/blk
+ ((requiring-resolve 'portal.api/submit)
+ [id value]))))
+ value)))))
- :event
- (do
- (swap! seen-tags conj id)
- (when @capturing?
- (m/? (m/via m/blk ((requiring-resolve 'portal.api/submit) [id value]))))
- value)))))
-
-(defn ptags
- "Print all available tags."
- [] @seen-tags)
+(defn ptags "Print all available tags." [] @seen-tags)
(def show-portal? (atom false))
(def >show-portal? (rec :show-portal? (m/signal (m/watch show-portal?))))
-(defn show-portal
- "Show portal window."
- []
- (reset! show-portal? true))
+(defn show-portal "Show portal window." [] (reset! show-portal? true))
-(defn hide-portal
- "Hide portal window."
- []
- (reset! show-portal? false))
+(defn hide-portal "Hide portal window." [] (reset! show-portal? false))
(def >portal-ui
- (m/ap
- (let [ui (m/? (m/via m/blk ((requiring-resolve 'portal.api/open))))]
- (m/amb= ui
- (try (m/? m/never)
- (finally
- ;; Were this blocking, I would put this on m/blk
- ((requiring-resolve 'portal.api/close) ui)
- (m/amb)))))))
+ (m/ap (let [ui (m/? (m/via m/blk ((requiring-resolve 'portal.api/open))))]
+ (m/amb= ui
+ (try (m/? m/never)
+ (finally
+ ;; Were this blocking, I would put this on m/blk
+ ((requiring-resolve 'portal.api/close) ui)
+ (m/amb)))))))
(def >portal-ui-toggle
- (m/ap
- (when (m/?< >show-portal?)
- (m/?< >portal-ui)
- (m/amb))))
+ (m/ap (when (m/?< >show-portal?) (m/?< >portal-ui) (m/amb))))
(defn cap
"Capture flow elements with specified ids to portal"
@@ -79,5 +61,4 @@
(def >portal-main
"Main entrypoint."
- (m/ap (m/amb= (do (m/?< >portal-ui-toggle)
- (m/amb)))))
+ (m/ap (m/amb= (do (m/?< >portal-ui-toggle) (m/amb)))))
diff --git a/src/unheard/clock.clj b/src/unheard/clock.clj
index d38b52a..b711ebe 100644
--- a/src/unheard/clock.clj
+++ b/src/unheard/clock.clj
@@ -2,30 +2,23 @@
(:require [missionary.core :as m]))
;; TODO: Put in missionary util
-(defn poll [init task]
- (m/ap (m/? (m/?> (m/seed (concat [(m/sp init)]
- (repeat task)))))))
+(defn poll
+ [init task]
+ (m/ap (m/? (m/?> (m/seed (concat [(m/sp init)] (repeat task)))))))
;; TODO: Put in missionary util
-(defn feedback [f init & args]
- (m/ap
- (m/amb
- init
- (let [rdv (m/rdv)
- x (m/?> (apply f (poll init rdv) args))]
- (m/amb
- (do
- (m/? (rdv x))
- (m/amb))
- x)))))
+(defn feedback
+ [f init & args]
+ (m/ap (m/amb init
+ (let [rdv (m/rdv)
+ x (m/?> (apply f (poll init rdv) args))]
+ (m/amb (do (m/? (rdv x)) (m/amb)) x)))))
;; Return value
;; TODO: Make part of public API
(defonce mono-clock-freq (atom 120)) ;; hz
-(defonce >mono-clock-freq
- (m/signal
- (m/watch mono-clock-freq)))
+(defonce >mono-clock-freq (m/signal (m/watch mono-clock-freq)))
(def >mono-clock
"This is the base monotonic clock used for driving playback.
@@ -38,57 +31,60 @@
frequency."
;; TODO: Change this behavior. I don't want to wait for
;; the current tick to complete during a frequency change..
- (m/signal
- (m/relieve
- (feedback (fn [v]
- (m/ap
- (let [[t freq] (m/?< (m/latest vector v >mono-clock-freq))]
- (m/? (m/compel (m/sleep (/ 1000 freq))))
- (System/nanoTime))))
- (System/nanoTime)))))
+ (m/signal (m/relieve
+ (feedback (fn [v]
+ (m/ap (let [[t freq]
+ (m/?<
+ (m/latest vector v >mono-clock-freq))]
+ (m/? (m/compel (m/sleep (/ 1000 freq))))
+ (System/nanoTime))))
+ (System/nanoTime)))))
(comment
- (def cancel
- ((m/reduce prn nil >mono-clock) prn prn))
-
+ (def cancel ((m/reduce prn nil >mono-clock) prn prn))
(reset! mono-clock-freq 50)
(reset! mono-clock-freq 10)
(reset! mono-clock-freq 1)
(cancel))
(defonce bpm (atom 120))
-(defonce >bpm
- (m/signal
- (m/watch bpm)))
+(defonce >bpm (m/signal (m/watch bpm)))
(def >beat-clock
"Counts beats at `bpm`. Guaranteed not to lose or gain time."
(m/signal
- (m/relieve
- (let [init-beat 0]
- (m/reductions {} init-beat
- (m/ap
- (let [state (object-array 2)
- last-beat-time-idx 0
- last-beat-number-idx 1
- _ (aset state last-beat-time-idx (System/nanoTime))
- _ (aset state last-beat-number-idx init-beat)
- [t bpm] (m/?< (m/latest vector >mono-clock >bpm))
- last-beat-time (aget state last-beat-time-idx)
- next-beat-time (+ last-beat-time (* 1000000000 (/ 60 bpm)))
- last-beat-number (aget state last-beat-number-idx)
- t-since-beat-start (- t last-beat-time)
- pct-through-current-beat (/ t-since-beat-start (- next-beat-time last-beat-time))]
- (when (<= next-beat-time t)
- ;; TODO confirm this won't lose or gain time
- (aset state last-beat-time-idx next-beat-time)
- (aset state last-beat-number-idx (inc last-beat-number)))
- (+ last-beat-number pct-through-current-beat))))))))
+ (m/relieve
+ (let [init-beat 0]
+ (m/reductions
+ {}
+ init-beat
+ (m/ap
+ (let [state (object-array 2)
+ last-beat-time-idx 0
+ last-beat-number-idx 1
+ _ (aset state last-beat-time-idx (System/nanoTime))
+ _ (aset state last-beat-number-idx init-beat)
+ ;; BUG 8a32cde: BPM changes can cause non-monotonic
+ ;; behavior!
+ [t bpm] (m/?< (m/latest vector >mono-clock >bpm))]
+ (if (= bpm 0)
+ (m/amb)
+ (let [last-beat-time (aget state last-beat-time-idx)
+ next-beat-time (+ last-beat-time
+ (* 1000000000 (/ 60 bpm)))
+ last-beat-number (aget state last-beat-number-idx)
+ t-since-beat-start (- t last-beat-time)
+ pct-through-current-beat (/ t-since-beat-start
+ (- next-beat-time
+ last-beat-time))]
+ (when (<= next-beat-time t)
+ ;; TODO confirm this won't lose or gain time
+ (aset state last-beat-time-idx next-beat-time)
+ (aset state last-beat-number-idx (inc last-beat-number)))
+ (+ last-beat-number pct-through-current-beat))))))))))
(comment
- (def cancel
- ((m/reduce prn nil >beat-clock) prn prn))
-
+ (def cancel ((m/reduce prn nil >beat-clock) prn prn))
(reset! bpm 60)
(reset! bpm 120)
(reset! mono-clock-freq 120)
diff --git a/src/unheard/cycles.clj b/src/unheard/cycles.clj
index a067142..b73b64a 100644
--- a/src/unheard/cycles.clj
+++ b/src/unheard/cycles.clj
@@ -40,7 +40,7 @@
(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})
+ {:v (vec args), :type :l})
(defn f
"Fork combinator: cycles through children sequentially across iterations.
@@ -55,7 +55,7 @@
(f :a :b :c) with cycle-length 1
=> [[0 1 :a] [1 2 :b] [2 3 :c]]"
[& args]
- {:v (vec args) :type :f})
+ {:v (vec args), :type :f})
(defn p
"Parallel combinator: all children occur simultaneously.
@@ -69,7 +69,7 @@
(p :a :b :c) with cycle-length 1
=> [[0 1 :a] [0 1 :b] [0 1 :c]]"
[& args]
- {:v (vec args) :type :p})
+ {:v (vec args), :type :p})
(defn rate
"Rate modifier: scales the speed of a pattern by a given ratio.
@@ -85,7 +85,7 @@
(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})
+ {:v node, :rate ratio, :type :rate})
(defn elongate
"Elongation modifier: gives an element temporal weight.
@@ -100,7 +100,7 @@
(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})
+ {:v node, :weight weight, :type :elongate})
(defn rep
"Replication modifier: repeats an element N times, subdividing its time equally.
@@ -115,10 +115,9 @@
(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})
+ {:v node, :times times, :type :rep})
-(defn scalar? [x]
- (not (and (map? x) (:type x))))
+(defn scalar? [x] (not (and (map? x) (:type x))))
(defn paste
"Paste operator: replaces scalar values in a template pattern with provided values.
@@ -146,10 +145,9 @@
(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)
+ (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
@@ -162,118 +160,106 @@
"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))
+ (if (and (map? node) (= :elongate (:type node))) (:weight node) 1))
-(defn gcd [a b]
- (if (zero? b) a (recur b (mod a b))))
+(defn gcd [a b] (if (zero? b) a (recur b (mod a b))))
-(defn lcm [a b]
- (/ (* a b) (gcd 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]
+(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)))
+ (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))
+ (= :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))
+ (= :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))
+ (= :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)})))))
+ :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.
@@ -303,10 +289,8 @@
=> [[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)))))
+ (vec (mapcat
+ (fn [i]
+ (unfold-node node (* i cycle-length) (* (inc i) cycle-length) i))
+ (range cycle-count)))))
diff --git a/src/unheard/dsl.clj b/src/unheard/dsl.clj
index e111725..d2bb286 100644
--- a/src/unheard/dsl.clj
+++ b/src/unheard/dsl.clj
@@ -3,59 +3,33 @@
[clojure.set :refer [union]]))
(comment
- ;; Parallel groups
- ;; Notes 1, 2, and 3 simultaneously
- ;; = should remind you of amb=
- ;; implicit duration of 1
+ ;; Parallel groups. Notes 1, 2, and 3 simultaneously = should remind you
+ ;; of amb= implicit duration of 1
[= 1 2 3]
- ;; Compiles to?
-
- ;; Same as above, but with duration 3
+ ;; Compiles to? Same as above, but with duration 3
([= 1 2 3] 3)
-
;; Notes 1, 2, and 3 all with different durations
- [=
- (1 2)
- (2 3)
- (3 4)]
-
- ;; Inner values override outer values
- ;; In this chord, 1 would have a duration of 3 while 2 and 3 would have a duration of 2
+ [= (1 2) (2 3) (3 4)]
+ ;; Inner values override outer values. In this chord, 1 would have a
+ ;; duration of 3 while 2 and 3 would have a duration of 2
([= (1 3) 2 3] 2)
-
;; Notes 1, 2, and 3 all with different durations and velocities
- [=
- (1 2 100)
- (2 3 110)
- (3 4 123)]
-
- ;; Sequential groups
- ;; Note 1, then note 2, then note 3
+ [= (1 2 100) (2 3 110) (3 4 123)]
+ ;; Sequential groups. Note 1, then note 2, then note 3
[1 2 3]
-
;; Note 1 duration 1, then note 2 duration 2, then note 3 duration 1
- [(1 1)
- (2 2)
- (3 1)]
-
+ [(1 1) (2 2) (3 1)]
;; Three chords played sequentially
- [[= 1 2 3]
- [= 1 2 3]
- [= 1 2 3]]
-
-;; Note 1, followed by a rest, followed by note 3
+ [[= 1 2 3] [= 1 2 3] [= 1 2 3]]
+ ;; Note 1, followed by a rest, followed by note 3
[1 (r) 3]
-
;; Unlike notes, rests are at most 2-tuples
;; (Think about it: Rests never have a note value)
-
- ;; Assign the note sequence 1 2 3 to the name loop1
- ;; The first argument is always the name; the last argument is always either
+ ;; Assign the note sequence 1 2 3 to the name loop1. The first argument
+ ;; is always the name; the last argument is always either
;; a sequential or parallel group
(=loop1 [1 2 3])
-
;; Use loop1
[1 (loop1) 2 3]
-
;; Middle arguments are variable names
(=loop2 dur ([1 2 3] dur)))
diff --git a/src/unheard/instrument/minilab3.clj b/src/unheard/instrument/minilab3.clj
index 65ca2ae..de78f22 100644
--- a/src/unheard/instrument/minilab3.clj
+++ b/src/unheard/instrument/minilab3.clj
@@ -1,31 +1,27 @@
(ns unheard.instrument.minilab3
(:require [unheard.instrument.util :refer [matching-control]]))
-(def device-name
- "CoreMIDI4J - Minilab3 MIDI")
+(def device-name "CoreMIDI4J - Minilab3 MIDI")
(def config
- {:knob
- {1 (matching-control 0 0 74)
- 2 (matching-control 0 0 71)
- 3 (matching-control 0 0 76)
- 4 (matching-control 0 0 77)
- 5 (matching-control 0 0 93)
- 6 (matching-control 0 0 18)
- 7 (matching-control 0 0 19)
- 8 (matching-control 0 0 16)}
- :fader
- {1 (matching-control 0 0 82)
- 2 (matching-control 0 0 83)
- 3 (matching-control 0 0 85)
- 4 (matching-control 0 0 17)}
- ;; TODO: git-bug fdf0f83
- :pad
- {1 (matching-control 0 9 36)
- 2 (matching-control 0 9 37)
- 3 (matching-control 0 9 38)
- 4 (matching-control 0 9 39)
- 5 (matching-control 0 9 40)
- 6 (matching-control 0 9 41)
- 7 (matching-control 0 9 42)
- 8 (matching-control 0 9 43)}})
+ {:knob {1 (matching-control 0 0 74),
+ 2 (matching-control 0 0 71),
+ 3 (matching-control 0 0 76),
+ 4 (matching-control 0 0 77),
+ 5 (matching-control 0 0 93),
+ 6 (matching-control 0 0 18),
+ 7 (matching-control 0 0 19),
+ 8 (matching-control 0 0 16)},
+ :fader {1 (matching-control 0 0 82),
+ 2 (matching-control 0 0 83),
+ 3 (matching-control 0 0 85),
+ 4 (matching-control 0 0 17)},
+ ;; TODO: git-bug fdf0f83
+ :pad {1 (matching-control 0 9 36),
+ 2 (matching-control 0 9 37),
+ 3 (matching-control 0 9 38),
+ 4 (matching-control 0 9 39),
+ 5 (matching-control 0 9 40),
+ 6 (matching-control 0 9 41),
+ 7 (matching-control 0 9 42),
+ 8 (matching-control 0 9 43)}})
diff --git a/src/unheard/instrument/omx_27.clj b/src/unheard/instrument/omx_27.clj
index ab3f28d..35f0ff8 100644
--- a/src/unheard/instrument/omx_27.clj
+++ b/src/unheard/instrument/omx_27.clj
@@ -4,9 +4,8 @@
(def device-name "CoreMIDI4J - omx-27")
(def config
- {:knob
- {1 (matching-control 0 0 21)
- 2 (matching-control 0 0 22)
- 3 (matching-control 0 0 23)
- 4 (matching-control 0 0 24)
- 5 (matching-control 0 0 61)}})
+ {:knob {1 (matching-control 0 0 21),
+ 2 (matching-control 0 0 22),
+ 3 (matching-control 0 0 23),
+ 4 (matching-control 0 0 24),
+ 5 (matching-control 0 0 61)}})
diff --git a/src/unheard/interval.clj b/src/unheard/interval.clj
index c10161c..bd14c0d 100644
--- a/src/unheard/interval.clj
+++ b/src/unheard/interval.clj
@@ -5,16 +5,13 @@
(deftype RatioValueInterval [start end value]
IInterval
- ;; HACK: coerce start to long to work around
- ;; class clojure.lang.BigInt cannot be cast to class java.lang.Comparable
- (getNormStart [_] (if (instance? clojure.lang.BigInt start)
- (long start)
- start))
- ;; HACK: coerce start to long to work around
- ;; class clojure.lang.BigInt cannot be cast to class java.lang.Comparable
- (getNormEnd [_] (if (instance? clojure.lang.BigInt end)
- (long end)
- end))
+ ;; HACK: coerce start to long to work around class clojure.lang.BigInt
+ ;; cannot be cast to class java.lang.Comparable
+ (getNormStart [_]
+ (if (instance? clojure.lang.BigInt start) (long start) start))
+ ;; HACK: coerce start to long to work around class clojure.lang.BigInt
+ ;; cannot be cast to class java.lang.Comparable
+ (getNormEnd [_] (if (instance? clojure.lang.BigInt end) (long end) end))
(getUniqueIdentifier [_] (str "[" start "," end "]"))
(compareTo [_ other]
(let [start-cmp (compare start (.getNormStart ^RatioValueInterval other))]
diff --git a/src/unheard/midi.clj b/src/unheard/midi.clj
index f0debad..c5cf925 100644
--- a/src/unheard/midi.clj
+++ b/src/unheard/midi.clj
@@ -31,6 +31,7 @@
(m/amb))
(m/?< >devices)
(try (m/? m/never)
+ (catch Exception e (println "EEE" e) (throw e))
(finally
(m/? (m/via
m/blk
@@ -69,6 +70,7 @@
:id :midi/device-opened,
:data {:device (str device)}})
(m/? (tfn device))
+ (catch Exception e (println "EEE" e) (throw e))
(finally (log/log! {:level :info, :id :midi/closing-device})
;; NOTE:
;; Be careful, (.close device) will wait for (.send
@@ -124,6 +126,7 @@
:data {:value (str v)}})
v)
(recur)))))))
+ (catch Exception e (println "EEE" e) (throw e))
(finally (log/log! {:level :info, :id :midi/closing-tx})
(m/? (m/via m/blk (.close transmitter)))
(log/log! {:level :info, :id :midi/tx-closed}))))))
@@ -154,6 +157,7 @@
(.send receiver v UNSCHEDULED-EVENT)))
(log/log! {:level :debug,
:id :midi/send-returned})))))
+ (catch Exception e (println "EEE" e) (throw e))
(finally (log/log! {:level :info, :id :midi/closing-rx})
(m/? (m/via m/blk (.close receiver)))
(log/log! {:level :info, :id :midi/rx-closed}))))))
@@ -281,10 +285,10 @@
short-messages (atom nil)
>short-messages (m/watch short-messages)]
(m/amb= (do (reset! short-messages nil)
- (m/? (<bus device-name
- (fn [v]
- (m/ap (try
- (let [msg (m/?< v)]
+ (m/?
+ (<bus device-name
+ (fn [v]
+ (m/ap (try (let [msg (m/?< v)]
(reset! short-messages msg))
(catch missionary.Cancelled c
;; When the upstream flow is
diff --git a/src/unheard/midi_test.clj b/src/unheard/midi_test.clj
index bcfde3c..790cf69 100644
--- a/src/unheard/midi_test.clj
+++ b/src/unheard/midi_test.clj
@@ -3,50 +3,27 @@
[hyperfiddle.rcf :refer [tests]])
(:import [javax.sound.midi ShortMessage]))
-(defn short-message [cmd ch d1 d2]
- (ShortMessage. cmd ch d1 d2))
+(defn short-message [cmd ch d1 d2] (ShortMessage. cmd ch d1 d2))
(tests "short-message->notes"
- (let [msgs
- [(short-message ShortMessage/NOTE_ON 0 1 100)
- ;; Irrelevant messages don't appear
- (short-message ShortMessage/CONTROL_CHANGE 0 1 100)
- (short-message ShortMessage/NOTE_ON 0 2 100)
- ;; Notes are removed
- (short-message ShortMessage/NOTE_OFF 0 2 100)
- (short-message ShortMessage/NOTE_OFF 0 1 100)
- (short-message ShortMessage/NOTE_ON 0 3 100)
- (short-message ShortMessage/NOTE_ON 0 4 100)
- ;; Specifying channel works
- (short-message ShortMessage/NOTE_ON 1 1 100)
- (short-message ShortMessage/NOTE_ON 1 2 100)
- ;; All notes off works
- (short-message ShortMessage/CONTROL_CHANGE 0 123 0)
- (short-message ShortMessage/CONTROL_CHANGE 1 123 0)]]
-
- (into []
- (transduce
- sut/short-message->notes
- conj
- []
- msgs))
- := [{0 {1 100}}
- {0 {1 100
- 2 100}}
- {0 {1 100}}
- {0 {}}
- {0 {3 100}}
- {0 {3 100
- 4 100}}
- {0 {3 100
- 4 100}
- 1 {1 100}}
- {0 {3 100
- 4 100}
- 1 {1 100
- 2 100}}
- {0 {}
- 1 {1 100
- 2 100}}
- {0 {}
- 1 {}}]))
+ (let [msgs [(short-message ShortMessage/NOTE_ON 0 1 100)
+ ;; Irrelevant messages don't appear
+ (short-message ShortMessage/CONTROL_CHANGE 0 1 100)
+ (short-message ShortMessage/NOTE_ON 0 2 100)
+ ;; Notes are removed
+ (short-message ShortMessage/NOTE_OFF 0 2 100)
+ (short-message ShortMessage/NOTE_OFF 0 1 100)
+ (short-message ShortMessage/NOTE_ON 0 3 100)
+ (short-message ShortMessage/NOTE_ON 0 4 100)
+ ;; Specifying channel works
+ (short-message ShortMessage/NOTE_ON 1 1 100)
+ (short-message ShortMessage/NOTE_ON 1 2 100)
+ ;; All notes off works
+ (short-message ShortMessage/CONTROL_CHANGE 0 123 0)
+ (short-message ShortMessage/CONTROL_CHANGE 1 123 0)]]
+ (into [] (transduce sut/short-message->notes conj [] msgs))
+ :=
+ [{0 {1 100}} {0 {1 100, 2 100}} {0 {1 100}} {0 {}} {0 {3 100}}
+ {0 {3 100, 4 100}} {0 {3 100, 4 100}, 1 {1 100}}
+ {0 {3 100, 4 100}, 1 {1 100, 2 100}} {0 {}, 1 {1 100, 2 100}}
+ {0 {}, 1 {}}]))
diff --git a/src/unheard/strudel/mini_notation_compiler.clj b/src/unheard/strudel/mini_notation_compiler.clj
index 6f469bb..36b2507 100644
--- a/src/unheard/strudel/mini_notation_compiler.clj
+++ b/src/unheard/strudel/mini_notation_compiler.clj
@@ -64,7 +64,8 @@
sym (symbol "unheard.midi.notes" normalized)]
sym))
-(defn- parse-number [s]
+(defn- parse-number
+ [s]
"Parse a number, returning either a long or ratio."
(if (str/includes? s "/")
(let [[num denom] (str/split s #"/")]
@@ -74,15 +75,16 @@
(declare parse-sequence)
(declare parse-element)
-(defn- parse-atom [s get-value]
+(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 (get-value s)))
+ (cond (= s "~") :r
+ (re-matches #"\d+(/\d+)?" s) (parse-number s)
+ :else (get-value s)))
-(defn- apply-modifiers [expr modifiers]
+(defn- apply-modifiers
+ [expr modifiers]
"Apply modifiers to an expression.
Modifiers is a map with keys: :rate, :elongate, :rep
Order: elongate/rep first (innermost), then rate (outermost)"
@@ -91,33 +93,34 @@
(:rep modifiers) (#(list 'rep (:rep modifiers) %))
(:rate modifiers) (#(list 'rate (:rate modifiers) %))))
-(defn- parse-token-with-modifiers [token]
+(defn- parse-token-with-modifiers
+ [token]
"Parse a token that may have modifiers like *2, /3, @2, !3"
- (let [;; Extract modifiers (note: /N is division, not a rational number in this context)
+ (let [;; Extract modifiers (note: /N is division, not a rational number
+ ;; in this context)
;; Check for *N/M first (multiplication with rational)
rate-match (re-find #"\*(\d+(?:/\d+)?)" token)
- ;; Only match /N if it's NOT part of *N/M (no preceding * and digits)
- div-match (when-not rate-match
- (re-find #"/(\d+)" token))
+ ;; Only match /N if it's NOT part of *N/M (no preceding * and
+ ;; digits)
+ div-match (when-not rate-match (re-find #"/(\d+)" token))
elong-match (re-find #"@(\d+(?:/\d+)?)" token)
rep-match (re-find #"!(\d+)" token)
-
;; Remove modifiers to get base token
base (-> token
(str/replace #"\*\d+(?:/\d+)?" "")
(str/replace #"/\d+" "")
(str/replace #"@\d+(?:/\d+)?" "")
(str/replace #"!\d+" ""))
-
- modifiers (cond-> {}
- rate-match (assoc :rate (parse-number (second rate-match)))
- div-match (assoc :rate (list '/ 1 (parse-long (second div-match))))
- elong-match (assoc :elongate (parse-number (second elong-match)))
- rep-match (assoc :rep (parse-long (second rep-match))))]
-
+ modifiers
+ (cond-> {}
+ rate-match (assoc :rate (parse-number (second rate-match)))
+ div-match (assoc :rate (list '/ 1 (parse-long (second div-match))))
+ elong-match (assoc :elongate (parse-number (second elong-match)))
+ rep-match (assoc :rep (parse-long (second rep-match))))]
[base modifiers]))
-(defn- split-on-comma [s]
+(defn- split-on-comma
+ [s]
"Split string on commas at depth 0 (not inside nested brackets).
Returns vector of substrings."
(loop [chars (seq s)
@@ -130,21 +133,18 @@
(cond
;; Track bracket depth
(or (= c \[) (= c \<) (= c \())
- (recur (rest chars) groups (conj current c) (inc depth))
-
+ (recur (rest chars) groups (conj current c) (inc depth))
(or (= c \]) (= c \>) (= c \)))
- (recur (rest chars) groups (conj current c) (dec depth))
-
+ (recur (rest chars) groups (conj current c) (dec depth))
;; Comma at depth 0 - split here
(and (= c \,) (zero? depth))
- (if (seq current)
- (recur (rest chars) (conj groups (str/join current)) [] depth)
- (recur (rest chars) groups [] depth))
-
- :else
- (recur (rest chars) groups (conj current c) depth))))))
+ (if (seq current)
+ (recur (rest chars) (conj groups (str/join current)) [] depth)
+ (recur (rest chars) groups [] depth))
+ :else (recur (rest chars) groups (conj current c) depth))))))
-(defn- parse-group [s open-char close-char combinator get-value]
+(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))
@@ -154,20 +154,19 @@
[_ modifiers] (parse-token-with-modifiers modifiers-part)
;; Extract content between brackets
inner-content (subs brackets-part 1 close-idx)
-
;; 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
+ ;; 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 % 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)]
+ (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)))
@@ -178,35 +177,31 @@
(cons combinator elements))))]
(apply-modifiers result modifiers)))
-(defn- tokenize [s]
+(defn- tokenize
+ [s]
"Tokenize a mini-notation string, respecting nested brackets."
(loop [chars (seq s)
tokens []
current []
depth 0]
(if (empty? chars)
- (if (seq current)
- (conj tokens (str/join current))
- tokens)
+ (if (seq current) (conj tokens (str/join current)) tokens)
(let [c (first chars)]
(cond
;; Track bracket depth
(or (= c \[) (= c \<) (= c \())
- (recur (rest chars) tokens (conj current c) (inc depth))
-
+ (recur (rest chars) tokens (conj current c) (inc depth))
(or (= c \]) (= c \>) (= c \)))
- (recur (rest chars) tokens (conj current c) (dec depth))
-
+ (recur (rest chars) tokens (conj current c) (dec depth))
;; Space separates tokens only at depth 0
(and (= c \space) (zero? depth))
- (if (seq current)
- (recur (rest chars) (conj tokens (str/join current)) [] depth)
- (recur (rest chars) tokens [] depth))
+ (if (seq current)
+ (recur (rest chars) (conj tokens (str/join current)) [] depth)
+ (recur (rest chars) tokens [] depth))
+ :else (recur (rest chars) tokens (conj current c) depth))))))
- :else
- (recur (rest chars) tokens (conj current c) depth))))))
-
-(defn- parse-parallel [token get-value]
+(defn- parse-parallel
+ [token get-value]
"Parse comma-separated elements into parallel structure."
(if (str/includes? token ",")
(let [parts (str/split token #",")
@@ -214,32 +209,26 @@
(cons 'p elements))
(parse-element token get-value)))
-(defn- parse-element [token get-value]
+(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 get-value)
-
+ (str/starts-with? token "[") (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 get-value)
-
+ (str/starts-with? token "<") (parse-group token \< \> 'f get-value)
;; Contains comma - parallel (p)
- (str/includes? token ",")
- (parse-parallel token get-value)
-
+ (str/includes? token ",") (parse-parallel token get-value)
;; Token with modifiers
- :else
- (let [[base modifiers] (parse-token-with-modifiers token)]
- (apply-modifiers (parse-atom base get-value) modifiers))))
+ :else (let [[base modifiers] (parse-token-with-modifiers token)]
+ (apply-modifiers (parse-atom base get-value) modifiers))))
-(defn- parse-sequence [s get-value]
+(defn- parse-sequence
+ [s get-value]
"Parse a space-separated sequence."
- (let [tokens (tokenize s)]
- (map #(parse-element % get-value) tokens)))
+ (let [tokens (tokenize s)] (map #(parse-element % get-value) tokens)))
(defn compile
"Compile a Strudel mini-notation string to unheard.cycles code.
@@ -284,6 +273,4 @@
([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)))))
+ (if (= 1 (count elements)) (first elements) (cons 'l elements)))))
diff --git a/src/unheard/theory.clj b/src/unheard/theory.clj
index 5314ea7..30977a9 100644
--- a/src/unheard/theory.clj
+++ b/src/unheard/theory.clj
@@ -6,27 +6,26 @@
(defn note
[>clock start duration >value]
- (lift (time-object start
- duration
- (m/stream
- (m/ap
- (let [[c v] (m/?> (m/relieve
- (m/latest vector >clock >value)))]
- v))))))
+ (lift (time-object
+ start
+ duration
+ (m/stream (m/ap (let [[c v] (m/?> (m/relieve
+ (m/latest vector >clock >value)))]
+ v))))))
;; BUG: 2d7f861
(defn read
[>clock timeline]
- (m/relieve
- (m/reductions {} nil
- (m/eduction (map vals)
- (m/reductions
- (fn [acc {:keys [id state value]}]
- (if (= :up state)
- (assoc acc id value)
- (dissoc acc id)))
- {}
- (reconcile-merge (point-query timeline >clock)))))))
+ (m/relieve (m/reductions
+ {}
+ nil
+ (m/eduction
+ (map vals)
+ (m/reductions
+ (fn [acc {:keys [id state value]}]
+ (if (= :up state) (assoc acc id value) (dissoc acc id)))
+ {}
+ (reconcile-merge (point-query timeline >clock)))))))
(comment
(def c (atom 0))
diff --git a/src/unheard/time_object.clj b/src/unheard/time_object.clj
index 8d07361..5b71e0a 100644
--- a/src/unheard/time_object.clj
+++ b/src/unheard/time_object.clj
@@ -37,10 +37,11 @@
(def c (phrase (b 0) (b 3)))
(c 0))
-(defn phrase->spans [phrase]
+(defn phrase->spans
+ [phrase]
(let [{:keys [time-objects]} (phrase 0)]
(map (fn [{:keys [start duration value]}] [start (+ start duration) value])
- time-objects)))
+ time-objects)))
(comment
(phrase->spans c))
@@ -49,9 +50,8 @@
"Primary timeline bookkeeping mehanism."
[spans]
(let [c (i/create-ratio-interval-collection)]
- (doall
- (for [[start end value] spans]
- (.add c (i/ratio-interval start end value))))
+ (doall (for [[start end value] spans]
+ (.add c (i/ratio-interval start end value))))
c))
(comment
@@ -65,8 +65,8 @@
(m/stream (m/ap (let [at (m/?< >at)]
(into #{}
(map #(.value %)
- (i/find-overlaps timeline
- (i/ratio-interval at at nil))))))))
+ (i/find-overlaps timeline
+ (i/ratio-interval at at nil))))))))
(comment
(def at (atom 0))
diff --git a/src/unheard/time_object_test.clj b/src/unheard/time_object_test.clj
index 07e49c3..13d9c91 100644
--- a/src/unheard/time_object_test.clj
+++ b/src/unheard/time_object_test.clj
@@ -11,59 +11,38 @@
>to1-end (m/watch to1-end)
to1-value (atom :to1-a)
>to1-value (m/watch to1-value)
-
to1 (sut/time-object >to1-start >to1-end >to1-value)
-
to2-start (atom 7)
>to2-start (m/watch to2-start)
to2-end (atom 15)
>to2-end (m/watch to2-end)
to2-value (atom :to1-b)
>to2-value (m/watch to2-value)
-
to2 (sut/time-object >to2-start >to2-end >to2-value)
-
toc1 (sut/time-object-collection to1 to2)
-
to3-start (atom 2)
>to3-start (m/watch to3-start)
to3-end (atom 3)
>to3-end (m/watch to3-end)
to3-value (atom :to3-a)
>to3-value (m/watch to3-value)
-
to3 (sut/time-object >to3-start >to3-end >to3-value)
-
to4-start (atom 8)
>to4-start (m/watch to4-start)
to4-end (atom 12)
>to4-end (m/watch to4-end)
to4-value (atom :to3-b)
>to4-value (m/watch to4-value)
-
to4 (sut/time-object >to4-start >to4-end >to4-value)
-
toc2 (sut/time-object-collection to3 to4)
-
combined (sut/time-object-collection toc1 toc2)
-
timeline (sut/timeline combined)
-
point-query-at (atom 0)
>point-query-at (m/watch point-query-at)
-
point-query-result (sut/point-query timeline >point-query-at)
-
- #_#_#_#_#_#_
- range-query-range (atom [0 10])
- >range-query-range (m/watch range-query-range)
-
- range-query-result (sut/range-query timeline >range-query-range)
- ]
-
- (def cancel
- ((m/reduce rcf/tap nil point-query-result) #() #(throw %)))
-
- (cancel)
-
- ))
+ #_#_#_#_#_#_range-query-range (atom [0 10]) >range-query-range
+ (m/watch range-query-range)
+ range-query-result
+ (sut/range-query timeline >range-query-range)]
+ (def cancel ((m/reduce rcf/tap nil point-query-result) #() #(throw %)))
+ (cancel)))
diff --git a/test/unheard/cycles_test.clj b/test/unheard/cycles_test.clj
index c395755..552b6e9 100644
--- a/test/unheard/cycles_test.clj
+++ b/test/unheard/cycles_test.clj
@@ -3,278 +3,160 @@
[unheard.cycles :refer [l f p rate elongate rep paste unfold]]))
(deftest unfold-tests
- (testing "single scalar"
- (is (= [[0 1 :a]]
- (unfold 1 :a))))
-
+ (testing "single scalar" (is (= [[0 1 :a]] (unfold 1 :a))))
(testing "simple list - time subdivision"
- (is (= [[0 1/3 :a] [1/3 2/3 :b] [2/3 1 :c]]
- (unfold 1 (l :a :b :c)))))
-
+ (is (= [[0 1/3 :a] [1/3 2/3 :b] [2/3 1 :c]] (unfold 1 (l :a :b :c)))))
(testing "simple fork - cycles through children"
- (is (= [[0 1 :a]
- [1 2 :b]
- [2 3 :c]]
- (unfold 1 (f :a :b :c)))))
-
+ (is (= [[0 1 :a] [1 2 :b] [2 3 :c]] (unfold 1 (f :a :b :c)))))
(testing "list with fork - fork gets time slice"
- (is (= [[0 1/2 :1] [1/2 1 :2]
- [1 3/2 :1] [3/2 2 :3]]
+ (is (= [[0 1/2 :1] [1/2 1 :2] [1 3/2 :1] [3/2 2 :3]]
(unfold 1 (l :1 (f :2 :3))))))
-
(testing "fork with list - each child subdivides its cycle"
- (is (= [[0 1/2 :a] [1/2 1 :b]
- [1 2 :c]]
- (unfold 1 (f (l :a :b) :c)))))
-
+ (is (= [[0 1/2 :a] [1/2 1 :b] [1 2 :c]] (unfold 1 (f (l :a :b) :c)))))
(testing "nested lists"
- (is (= [[0 1/2 :a] [1/2 3/4 :b] [3/4 1 :c]]
- (unfold 1 (l :a (l :b :c))))))
-
+ (is (= [[0 1/2 :a] [1/2 3/4 :b] [3/4 1 :c]] (unfold 1 (l :a (l :b :c))))))
(testing "nested forks"
- (is (= [[0 1 :a]
- [1 2 :b]
- [2 3 :c]
- [3 4 :d]
- [4 5 :a]
- [5 6 :e]
- [6 7 :c]
+ (is (= [[0 1 :a] [1 2 :b] [2 3 :c] [3 4 :d] [4 5 :a] [5 6 :e] [6 7 :c]
[7 8 :d]]
(unfold 1 (f :a (f :b :e) :c :d)))))
-
(testing "complex nested structure from example"
- (is (= [[0 1/2 :1] [1/2 3/4 :2] [3/4 1 :9] [1 3/2 :6] [3/2 2 :7]
- [2 5/2 :1] [5/2 3 :3] [3 7/2 :6] [7/2 4 :8]
- [4 9/2 :1] [9/2 5 :4] [5 11/2 :6] [11/2 6 :7]
- [6 13/2 :1] [13/2 27/4 :2] [27/4 7 :9] [7 15/2 :6] [15/2 8 :8]
- [8 17/2 :1] [17/2 9 :3] [9 19/2 :6] [19/2 10 :7]
+ (is (= [[0 1/2 :1] [1/2 3/4 :2] [3/4 1 :9] [1 3/2 :6] [3/2 2 :7] [2 5/2 :1]
+ [5/2 3 :3] [3 7/2 :6] [7/2 4 :8] [4 9/2 :1] [9/2 5 :4] [5 11/2 :6]
+ [11/2 6 :7] [6 13/2 :1] [13/2 27/4 :2] [27/4 7 :9] [7 15/2 :6]
+ [15/2 8 :8] [8 17/2 :1] [17/2 9 :3] [9 19/2 :6] [19/2 10 :7]
[10 21/2 :1] [21/2 11 :5] [11 23/2 :6] [23/2 12 :8]]
(unfold 2 (l :1 (f (l :2 :9) :3 (f :4 :5)) :6 (f :7 :8))))))
-
(testing "different cycle lengths"
- (is (= [[0 1/2 :a] [1/2 1 :b]]
- (unfold 1 (l :a :b))))
- (is (= [[0 1 :a] [1 2 :b]]
- (unfold 2 (l :a :b))))
- (is (= [[0 5/2 :a] [5/2 5 :b]]
- (unfold 5 (l :a :b)))))
-
+ (is (= [[0 1/2 :a] [1/2 1 :b]] (unfold 1 (l :a :b))))
+ (is (= [[0 1 :a] [1 2 :b]] (unfold 2 (l :a :b))))
+ (is (= [[0 5/2 :a] [5/2 5 :b]] (unfold 5 (l :a :b)))))
(testing "fork with nested list subdivides correctly"
- (is (= [[0 1/3 :a] [1/3 2/3 :b] [2/3 1 :c]
- [1 2 :x]]
+ (is (= [[0 1/3 :a] [1/3 2/3 :b] [2/3 1 :c] [1 2 :x]]
(unfold 1 (f (l :a :b :c) :x)))))
-
(testing "simple parallel - all children at same time"
- (is (= [[0 1 :a] [0 1 :b] [0 1 :c]]
- (unfold 1 (p :a :b :c)))))
-
+ (is (= [[0 1 :a] [0 1 :b] [0 1 :c]] (unfold 1 (p :a :b :c)))))
(testing "parallel with list - children subdivide in parallel"
- (is (= [[0 1/2 :a] [1/2 1 :b]
- [0 1/2 :c] [1/2 1 :d]]
+ (is (= [[0 1/2 :a] [1/2 1 :b] [0 1/2 :c] [1/2 1 :d]]
(unfold 1 (p (l :a :b) (l :c :d))))))
-
(testing "parallel with fork - forks extend together"
- (is (= [[0 1 :a] [0 1 :c]
- [1 2 :b] [1 2 :d]]
+ (is (= [[0 1 :a] [0 1 :c] [1 2 :b] [1 2 :d]]
(unfold 1 (p (f :a :b) (f :c :d))))))
-
(testing "list containing parallel"
- (is (= [[0 1/2 :x] [0 1/2 :y]
- [1/2 1 :z]]
- (unfold 1 (l (p :x :y) :z)))))
-
+ (is (= [[0 1/2 :x] [0 1/2 :y] [1/2 1 :z]] (unfold 1 (l (p :x :y) :z)))))
(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))))))
-
+ (is (= [[0 1 :a] [0 1 :b] [1 2 :a] [1 2 :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)))))
-
+ (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)))))
-
+ (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]]
+ (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))))))
-
+ (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]
+ ;; 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]]
+ (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))))))
-
+ (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))))))
-
(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)))))
-
+ (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)))))
-
+ (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))))))
-
+ (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]]
+ (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))))))
-
(testing "rep 2 - element repeats twice"
- (is (= [[0 1/2 :a] [1/2 1 :a]]
- (unfold 1 (rep 2 :a)))))
-
+ (is (= [[0 1/2 :a] [1/2 1 :a]] (unfold 1 (rep 2 :a)))))
(testing "rep 3 - element repeats three times"
- (is (= [[0 1/3 :a] [1/3 2/3 :a] [2/3 1 :a]]
- (unfold 1 (rep 3 :a)))))
-
+ (is (= [[0 1/3 :a] [1/3 2/3 :a] [2/3 1 :a]] (unfold 1 (rep 3 :a)))))
(testing "rep in list - subdivides its time slot"
- (is (= [[0 1/6 :a] [1/6 1/3 :a] [1/3 1/2 :a]
- [1/2 1 :b]]
+ (is (= [[0 1/6 :a] [1/6 1/3 :a] [1/3 1/2 :a] [1/2 1 :b]]
(unfold 1 (l (rep 3 :a) :b)))))
-
(testing "rep with three elements in list"
- (is (= [[0 1/6 :x] [1/6 1/3 :x]
- [1/3 2/3 :y]
- [2/3 1 :z]]
+ (is (= [[0 1/6 :x] [1/6 1/3 :x] [1/3 2/3 :y] [2/3 1 :z]]
(unfold 1 (l (rep 2 :x) :y :z)))))
-
(testing "multiple reps in list"
- (is (= [[0 1/6 :a] [1/6 1/3 :a]
- [1/3 1/2 :b] [1/2 2/3 :b]
- [2/3 1 :c]]
+ (is (= [[0 1/6 :a] [1/6 1/3 :a] [1/3 1/2 :b] [1/2 2/3 :b] [2/3 1 :c]]
(unfold 1 (l (rep 2 :a) (rep 2 :b) :c)))))
-
(testing "rep with fork"
- (is (= [[0 1/2 :a] [1/2 1 :a]
- [1 2 :b]]
- (unfold 1 (f (rep 2 :a) :b)))))
-
+ (is (= [[0 1/2 :a] [1/2 1 :a] [1 2 :b]] (unfold 1 (f (rep 2 :a) :b)))))
(testing "rep inside parallel"
- (is (= [[0 1/2 :a] [1/2 1 :a]
- [0 1 :b]]
- (unfold 1 (p (rep 2 :a) :b)))))
-
+ (is (= [[0 1/2 :a] [1/2 1 :a] [0 1 :b]] (unfold 1 (p (rep 2 :a) :b)))))
(testing "rep with rate"
- (is (= [[0 1/4 :a] [1/4 1/2 :a]
- [1/2 3/4 :a] [3/4 1 :a]]
+ (is (= [[0 1/4 :a] [1/4 1/2 :a] [1/2 3/4 :a] [3/4 1 :a]]
(unfold 1 (rep 2 (rate 2 :a))))))
-
(testing "rate with rep inside"
- (is (= [[0 1/4 :a] [1/4 1/2 :a]
- [1/2 3/4 :a] [3/4 1 :a]]
+ (is (= [[0 1/4 :a] [1/4 1/2 :a] [1/2 3/4 :a] [3/4 1 :a]]
(unfold 1 (rate 2 (rep 2 :a))))))
-
(testing "rep in list without elongate"
- (is (= [[0 1/6 :a] [1/6 1/3 :a]
- [1/3 2/3 :b]
- [2/3 1 :c]]
+ (is (= [[0 1/6 :a] [1/6 1/3 :a] [1/3 2/3 :b] [2/3 1 :c]]
(unfold 1 (l (rep 2 :a) :b :c)))))
-
(testing "elongate and rep together"
- (is (= [[0 1/6 :a] [1/6 1/3 :a]
- [1/3 2/3 :b]
- [2/3 1 :c]]
+ (is (= [[0 1/6 :a] [1/6 1/3 :a] [1/3 2/3 :b] [2/3 1 :c]]
(unfold 1 (l (rep 2 (elongate 2 :a)) :b :c)))))
-
(testing "rep of list"
- (is (= [[0 1/4 :a] [1/4 1/2 :b]
- [1/2 3/4 :a] [3/4 1 :b]]
+ (is (= [[0 1/4 :a] [1/4 1/2 :b] [1/2 3/4 :a] [3/4 1 :b]]
(unfold 1 (rep 2 (l :a :b))))))
-
(testing "paste - simple replacement in list"
- (is (= (l :a :b :c)
- (paste (l :_ :_ :_) :a :b :c))))
-
+ (is (= (l :a :b :c) (paste (l :_ :_ :_) :a :b :c))))
(testing "paste - simple replacement in fork"
(is (= (f :c (f :e :g) :b :d)
(paste (f :_ (f :_ :_) :_ :_) :c :e :g :b :d))))
-
(testing "paste - with nil preserves original"
(is (= (f :c (f :e :_) :b :d)
(paste (f :_ (f :_ :_) :_ :_) :c :e nil :b :d))))
-
(testing "paste - multiple nils"
(is (= (f :_ (f :e :_) :b :_)
(paste (f :_ (f :_ :_) :_ :_) nil :e nil :b nil))))
-
(testing "paste - with numbers as template"
- (is (= (l :a :b :c)
- (paste (l 1 2 1) :a :b :c))))
-
+ (is (= (l :a :b :c) (paste (l 1 2 1) :a :b :c))))
(testing "paste - nested structures"
- (is (= (l :x (l :y :z) :w)
- (paste (l :_ (l :_ :_) :_) :x :y :z :w))))
-
+ (is (= (l :x (l :y :z) :w) (paste (l :_ (l :_ :_) :_) :x :y :z :w))))
(testing "paste - with parallel"
- (is (= (p :a :b :c)
- (paste (p :_ :_ :_) :a :b :c))))
-
+ (is (= (p :a :b :c) (paste (p :_ :_ :_) :a :b :c))))
(testing "paste - complex nested with parallel"
- (is (= (l :a (p :b :c) :d)
- (paste (l :_ (p :_ :_) :_) :a :b :c :d))))
-
+ (is (= (l :a (p :b :c) :d) (paste (l :_ (p :_ :_) :_) :a :b :c :d))))
(testing "paste - with modifiers preserved"
- (is (= (l (rate 2 :a) :b)
- (paste (l (rate 2 :_) :_) :a :b))))
-
+ (is (= (l (rate 2 :a) :b) (paste (l (rate 2 :_) :_) :a :b))))
(testing "paste - with elongate"
(is (= (l (elongate 2 :x) :y :z)
(paste (l (elongate 2 :_) :_ :_) :x :y :z))))
-
(testing "paste - with rep"
- (is (= (l (rep 3 :a) :b)
- (paste (l (rep 3 :_) :_) :a :b))))
-
+ (is (= (l (rep 3 :a) :b) (paste (l (rep 3 :_) :_) :a :b))))
(testing "paste - unfolds correctly"
(is (= [[0 1/3 :c] [1/3 2/3 :e] [2/3 1 :g]]
(unfold 1 (paste (l :_ :_ :_) :c :e :g)))))
-
(testing "paste - complex rhythm unfolds correctly"
- (is (= [[0 1 :c] [1 2 :e] [2 3 :b] [3 4 :d]
- [4 5 :c] [5 6 :g] [6 7 :b] [7 8 :d]]
+ (is (= [[0 1 :c] [1 2 :e] [2 3 :b] [3 4 :d] [4 5 :c] [5 6 :g] [6 7 :b]
+ [7 8 :d]]
(unfold 1 (paste (f :_ (f :_ :_) :_ :_) :c :e :g :b :d)))))
-
(testing "paste - with nil unfolds correctly"
- (is (= [[0 1 :c] [1 2 :e] [2 3 :b] [3 4 :d]
- [4 5 :c] [5 6 :_] [6 7 :b] [7 8 :d]]
+ (is (= [[0 1 :c] [1 2 :e] [2 3 :b] [3 4 :d] [4 5 :c] [5 6 :_] [6 7 :b]
+ [7 8 :d]]
(unfold 1 (paste (f :_ (f :_ :_) :_ :_) :c :e nil :b :d))))))
diff --git a/test/unheard/strudel/mini_notation_compiler_test.clj b/test/unheard/strudel/mini_notation_compiler_test.clj
index 184a9ea..38ae8be 100644
--- a/test/unheard/strudel/mini_notation_compiler_test.clj
+++ b/test/unheard/strudel/mini_notation_compiler_test.clj
@@ -4,154 +4,85 @@
[unheard.strudel.mini-notation-compiler :refer [compile]]))
(deftest compile-tests
- (testing "single atom"
- (is (= :c
- (compile "c" keyword))))
-
- (testing "rest literal"
- (is (= :r
- (compile "~" keyword))))
-
+ (testing "single atom" (is (= :c (compile "c" keyword))))
+ (testing "rest literal" (is (= :r (compile "~" keyword))))
(testing "simple sequence - space separated"
- (is (= '(l :c :e :g)
- (compile "c e g" keyword))))
-
+ (is (= '(l :c :e :g) (compile "c e g" keyword))))
(testing "sequence with rest"
- (is (= '(l :c :r :g)
- (compile "c ~ g" keyword))))
-
+ (is (= '(l :c :r :g) (compile "c ~ g" keyword))))
(testing "subdivision with brackets"
- (is (= '(l :c (l :e :g) :b)
- (compile "c [e g] b" keyword))))
-
+ (is (= '(l :c (l :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]]" keyword))))
-
+ (is (= '(l :a (l :b (l :c :d))) (compile "a [b [c d]]" keyword))))
(testing "angle brackets - alternation"
- (is (= '(f :c :e :g :b)
- (compile "<c e g b>" keyword))))
-
+ (is (= '(f :c :e :g :b) (compile "<c e g b>" keyword))))
(testing "parallel with commas"
- (is (= '(p :c :e :g)
- (compile "c,e,g" keyword))))
-
+ (is (= '(p :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]" keyword))))
-
+ (is (= '(l (p :c :e :g) (p :d :f :a)) (compile "[c,e,g] [d,f,a]" keyword))))
(testing "rate modifier - multiplication"
- (is (= '(rate 2 :c)
- (compile "c*2" keyword))))
-
+ (is (= '(rate 2 :c) (compile "c*2" keyword))))
(testing "rate modifier - division"
- (is (= '(rate (/ 1 2) :c)
- (compile "c/2" keyword))))
-
+ (is (= '(rate (/ 1 2) :c) (compile "c/2" keyword))))
(testing "rate on group"
- (is (= '(rate 2 (l :e :g))
- (compile "[e g]*2" keyword))))
-
+ (is (= '(rate 2 (l :e :g)) (compile "[e g]*2" keyword))))
(testing "rate on alternation"
- (is (= '(rate 2 (f :c :e :g :b))
- (compile "<c e g b>*2" keyword))))
-
+ (is (= '(rate 2 (f :c :e :g :b)) (compile "<c e g b>*2" keyword))))
(testing "elongate modifier"
- (is (= '(elongate 3 :c)
- (compile "c@3" keyword))))
-
+ (is (= '(elongate 3 :c) (compile "c@3" keyword))))
(testing "elongate in sequence"
- (is (= '(l (elongate 2 :a) :b :c)
- (compile "a@2 b c" keyword))))
-
- (testing "replication modifier"
- (is (= '(rep 3 :c)
- (compile "c!3" keyword))))
-
+ (is (= '(l (elongate 2 :a) :b :c) (compile "a@2 b c" keyword))))
+ (testing "replication modifier" (is (= '(rep 3 :c) (compile "c!3" keyword))))
(testing "replication in sequence"
- (is (= '(l (rep 2 :x) :y)
- (compile "x!2 y" keyword))))
-
+ (is (= '(l (rep 2 :x) :y) (compile "x!2 y" keyword))))
(testing "multiple modifiers"
- (is (= '(rate 2 (elongate 3 :c))
- (compile "c@3*2" keyword))))
-
+ (is (= '(rate 2 (elongate 3 :c)) (compile "c@3*2" keyword))))
(testing "complex nested structure"
- (is (= '(l :c (f :e :g) :b :d)
- (compile "c <e g> b d" keyword))))
-
+ (is (= '(l :c (f :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" keyword))))
-
+ (is (= '(l :x (p :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]" keyword))))
-
(testing "chord sequence"
(is (= '(rate 2 (f (p :g3 :b3 :e4) (p :a3 :c3 :e4)))
(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" 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" keyword))))
-
(testing "rest in subdivision"
- (is (= '(l :b4 (l :r :c5) :d5 :e5)
- (compile "b4 [~ c5] d5 e5" keyword))))
-
- (testing "numbers as values"
- (is (= '(l 1 2 3)
- (compile "1 2 3" keyword))))
-
+ (is (= '(l :b4 (l :r :c5) :d5 :e5) (compile "b4 [~ c5] d5 e5" keyword))))
+ (testing "numbers as values" (is (= '(l 1 2 3) (compile "1 2 3" keyword))))
(testing "rational number rate"
- (is (= '(rate 3/2 :c)
- (compile "c*3/2" keyword))))
-
+ (is (= '(rate 3/2 :c) (compile "c*3/2" keyword))))
(testing "whitespace handling"
- (is (= '(l :a :b :c)
- (compile " a b c " keyword))))
-
+ (is (= '(l :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]" keyword))))
-
(testing "newlines are converted to spaces"
- (is (= '(f :a :b :c :d)
- (compile "<a b\nc d>" keyword))))
-
+ (is (= '(f :a :b :c :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>" keyword))))
-
+ (is (= '(f (l :a :b) (p :c :d)) (compile "<\n[a b]\n[c,d]\n>" keyword))))
(testing "polymeter - comma creates parallel of forks"
- (is (= '(p (f :a :b) (f :c :d))
- (compile "<a b, c d>" keyword))))
-
+ (is (= '(p (f :a :b) (f :c :d)) (compile "<a b, c d>" keyword))))
(testing "polymeter - three groups"
(is (= '(p (f :a :b) (f :c :d) (f :e :f))
(compile "<a b, c d, e f>" keyword))))
-
(testing "polymeter - unequal lengths"
- (is (= '(p (f :a :b :e) (f :c :d))
- (compile "<a b e, c d>" keyword))))
-
+ (is (= '(p (f :a :b :e) (f :c :d)) (compile "<a b e, c d>" keyword))))
(testing "polymeter - single element per group"
- (is (= '(p :a :c)
- (compile "<a, c>" keyword))))
-
+ (is (= '(p :a :c) (compile "<a, c>" keyword))))
(testing "polymeter - with nested structures"
(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 (p (f :a :b) (f :c :d)))
- (compile "<a b, c d>*2" keyword))))
-
+ (is (= '(rate 2 (p (f :a :b) (f :c :d))) (compile "<a b, c d>*2" keyword))))
(testing "polymeter - melody and bass pattern"
- (is (= '(p (f (l :e5 :b4) (l :a4 :c5)) (f (rate 4 (l :e2 :e3)) (rate 4 (l :a2 :a3))))
+ (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)))))