summaryrefslogtreecommitdiff
path: root/DEVLOG.md
diff options
context:
space:
mode:
Diffstat (limited to 'DEVLOG.md')
-rw-r--r--DEVLOG.md676
1 files changed, 676 insertions, 0 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?
+