diff options
Diffstat (limited to 'DEVLOG.md')
| -rw-r--r-- | DEVLOG.md | 676 |
1 files changed, 676 insertions, 0 deletions
@@ -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? + |
