diff options
| author | Jake Zerrer <him@jakezerrer.com> | 2025-11-26 15:10:20 -0500 |
|---|---|---|
| committer | Jake Zerrer <him@jakezerrer.com> | 2025-12-01 15:49:41 -0500 |
| commit | 322b66627fd619c2ce0f2a35eae043e9304ee8bc (patch) | |
| tree | 848d78c8ff464eeb1e055a60e87de39fd3a1f62b | |
| parent | f940b2f1452b0cccfbb501c14c7f348e5dc1d2e6 (diff) | |
Devlog updates
| -rw-r--r-- | DEVLOG.md | 505 |
1 files changed, 505 insertions, 0 deletions
@@ -426,3 +426,508 @@ merge. (Generate one, then create :up, then include on emit, and then :down). I like this idea. +## November 28th, 2025 + +Very happy with wednesday's progress. Time to nail down a few more things. + +1. How should I match phrases up to input sources? +2. How should I account for there eventually being multiple input media + types, like OSC, DMX, etc., all with their own semantics? +3. How should input sources flow through phrase composition? + +Currently, `note` and `phrase` are called differently in a way that doesn't +make much sense to me. I need to play with that syntax a bit, too. + +There's another interesting question that is really sticky. Imagine for +a moment that I eventually want to take musical-like inputs from multiple +sources, e.g. midi and OSC. I want to perform a similar mapping on the +output side. Imagine I want to map from MIDI in to OSC out. This suggests +that using MIDI as my internal representation for harmonic information +probably isn't the best idea, even if it is the easiest. Another example +is non-standard tunings, or music where one wants frequency-level control. +Okay, so what would the internal representation of harmonic information +be? I want something fairly flexible but not crazy. Probably just +[frequency amplitude] to begin with. + +Anyway. I need to start thinking about these things, which are all related. + +Let's do some experimenting. + +Here is a current example: + +```clojure +(defn triad + [>c >tonic] + (phrase + ;; This is a major cord, + ;; held 32 32nd notes. + ;; The tonic can vary. + (note >c 0 0 32 >tonic) + (note >c 0 0 32 (m/latest #(+ % 4) >tonic)) + (note >c 0 0 32 (m/latest #(+ % 7) >tonic)))) + +(defn drums + [>clock] + (phrase (note >clock 1 1 1 (m/ap kick)) + (note >clock 1 9 1 (m/ap kick)) + (note >clock 1 17 1 (m/ap kick)) + (note >clock 1 25 1 (m/ap kick)) + (note >clock 1 1 1 (m/ap hat)) + (note >clock 1 5 1 (m/ap hat)) + (note >clock 1 9 1 (m/ap hat)) + (note >clock 1 13 1 (m/ap hat)) + (note >clock 1 17 1 (m/ap hat)) + (note >clock 1 21 1 (m/ap hat)) + (note >clock 1 25 1 (m/ap hat)) + (note >clock 1 29 1 (m/ap hat)) + (note >clock 1 5 1 (m/ap snare)) + (note >clock 1 13 1 (m/ap snare)) + (note >clock 1 21 1 (m/ap snare)) + (note >clock 1 29 1 (m/ap snare)))) + +(defn song' + [>clock >tonic] + (phrase ((triad >clock >tonic) 0) + ((triad >clock (m/latest #(+ % 12) >tonic)) 0) + ((drums >clock) 0))) +``` + +Things that stand out: +1. It is interesting that `phrase` doesn't take a flow. +2. I don't like that `note` takes a midi :ch argument. Too + coupled with a particular output medium. (But I also don't want + to get _too far_ from the specific output medium and risk creating + surprising behavior.) +3. These examples aren't actually great because we don't actually do + much that is interesting with argument passing. +4. The invocation of triad and drums in song is gross. +5. It is unclear how happens-before relationships would be structured. +6. We pass clock in explicitly, but it isn't actually clear why. +7. Should we encourage phrases to use argument names that are coupled + to input types, or to give them names that are meaningful to the + music itself? +8. How do I want to specify outputs? + +Here is a first pass at rewriting: + +```clojure +(defn triad + [>tonic] + (phrase + ;; This is a major cord, + ;; held 32 32nd notes. + ;; The tonic can vary. + (note 0 0 32 >tonic) + (note 0 0 32 (m/latest #(+ % 4) >tonic)) + (note 0 0 32 (m/latest #(+ % 7) >tonic)))) + +(defn drums + [] + (phrase (note 1 1 1 (m/ap kick)) + (note 1 9 1 (m/ap kick)) + (note 1 17 1 (m/ap kick)) + (note 1 25 1 (m/ap kick)) + (note 1 1 1 (m/ap hat)) + (note 1 5 1 (m/ap hat)) + (note 1 9 1 (m/ap hat)) + (note 1 13 1 (m/ap hat)) + (note 1 17 1 (m/ap hat)) + (note 1 21 1 (m/ap hat)) + (note 1 25 1 (m/ap hat)) + (note 1 29 1 (m/ap hat)) + (note 1 5 1 (m/ap snare)) + (note 1 13 1 (m/ap snare)) + (note 1 21 1 (m/ap snare)) + (note 1 29 1 (m/ap snare)))) + +(defn song' + [>tonic] + (phrase ((triad >tonic) 0) + ((triad (m/latest #(+ % 12) >tonic)) 0) + ((drums) 0))) +``` + +Looks cleaner without clock. This doesn't mean that +clock can't be passed explicitly, just that it isn't necessary +by default. + +What about specifying outputs? Already there is an interesting +decision to make. On drums, would one want to treat each drum as +its own output, or treat them as simply different parts of the same +instrument? + +What is an output? Is it an instrument? I think the only honest answer +is that this question can't be answered in general. Sometimes it will +make sense to think of a cow bell as an instrument, and sometimes as a +component of a percussion kit. + +The point of an output, I think, is to somehow group output information +together. I think it is fair to say that everything on the same output +will share some semantics. From a programming perspective, the state +of an output should be uniform: a single data structure, and a single +merge operation. + +The data structure and merge operation shouldn't be re-written for each +instrument. There will only be a few of these. Keyed instruments like +pianos will be able to share a single data structure and merge operation, +even if a harpsichord won't make use of velocity information. + +When writing a phrase, one specify the data structure and merge +operation. This actually isn't a property of the phrase, though, it +is a property of the particular part of the phrase that is being +described. Remember that a phrase may have many instruments in it, +and therefore many data structures and merge ops. + +This makes me wonder: beyond phrase and time object, is there another +concept that I should introduce? Something like "instrument"? + +```clojure +(defn triad + [>tonic] + (phrase + (instrument keyboard + (note 0 32 >tonic) + (note 0 32 (m/latest #(+ % 4) >tonic)) + (note 0 32 (m/latest #(+ % 7) >tonic))))) + +(defn drums + [] + (phrase + (instrument percussion + (note 1 (m/ap kick)) + (note 9 1 (m/ap kick)) + (note 17 1 (m/ap kick)) + (note 25 1 (m/ap kick)) + (note 1 1 (m/ap hat)) + (note 5 1 (m/ap hat)) + (note 9 1 (m/ap hat)) + (note 13 1 (m/ap hat)) + (note 17 1 (m/ap hat)) + (note 21 1 (m/ap hat)) + (note 25 1 (m/ap hat)) + (note 29 1 (m/ap hat)) + (note 5 1 (m/ap snare)) + (note 13 1 (m/ap snare)) + (note 21 1 (m/ap snare)) + (note 29 1 (m/ap snare))))) +``` + +Oh, interesting. `triad` isn't just a keyboard concept! + +Instrument feels like a pretty specific name. I don't think I love that. + +But it is bringing up an interesting thought. Phrases compose together, +but it seems like things like `notes` _do_ have some kind of wrapper +context that separates them for a phrase. We can actually see this distinction +repeated in the programming context, with the presence of `lift`. + +Maybe `lift` and `instrument` are the same thing? + +How would the generic concept of a triad be expressed outside +of the instrument context? Here's what I mean. I want to support the +creation of the music theory concept of a triad and import it into any +instrument such that it can be used in any phrase. + +Oh, maybe the problem is that the triad helper function should actually just +be something that returns three flows, one for each note in the interval. +Or even a flow-returning function that takes a root and a degree? +e.g. + +```clojure +(defn piano + [>root] + (let [triad (theory/triad :major >root)]) + (phrase + (instrument keyboard :keyboard ;; keyboard is the name of a data structure / merge op; + (note 1 32 (triad 1)) ;; :keyboard is declaring an abstract output destination + (note 1 32 (triad 3)) + (note 1 32 (triad 5))))) + +(defn drums + [] + (phrase + (instrument percussion :drums + (note 1 (m/ap kick)) + (note 9 1 (m/ap kick)) + (note 17 1 (m/ap kick)) + (note 25 1 (m/ap kick)) + (note 1 1 (m/ap hat)) + (note 5 1 (m/ap hat)) + (note 9 1 (m/ap hat)) + (note 13 1 (m/ap hat)) + (note 17 1 (m/ap hat)) + (note 21 1 (m/ap hat)) + (note 25 1 (m/ap hat)) + (note 29 1 (m/ap hat)) + (note 5 1 (m/ap snare)) + (note 13 1 (m/ap snare)) + (note 21 1 (m/ap snare)) + (note 29 1 (m/ap snare))))) + +(defn song' + [>root] + (phrase ((piano >root) 0) + ((piano (m/latest #(+ % 12) >root)) 0) + ((drums) 0))) +``` + +Question: How would I handle the fact that triad might represent notes +as notes, while an output method might represent in frequencies? I think +I would want some kind of `note` protocol that can convert between different +representations. + +I think it does make sense that, unlike phrases, outputs / instruments +can't be nested. + +Can a phrase have multiple instruments / outputs? I think the +answer should probably be "yes". That would mean abstractions could +return instruments rather than phrases. This would be nice because +it would remove a layer of unnecessary naming in the naming tree. Example: + +```clojure +(defn piano + [>root] + (let [triad (theory/triad :major >root)]) + (instrument keyboard :keyboard ;; keyboard is the name of a data structure / merge op; + (note 1 32 (triad 1)) ;; :keyboard is declaring an abstract output destination + (note 1 32 (triad 3)) + (note 1 32 (triad 5)))) + +(defn drums + [] + (instrument percussion :drums + (note 1 (m/ap kick)) + (note 9 1 (m/ap kick)) + (note 17 1 (m/ap kick)) + (note 25 1 (m/ap kick)) + (note 1 1 (m/ap hat)) + (note 5 1 (m/ap hat)) + (note 9 1 (m/ap hat)) + (note 13 1 (m/ap hat)) + (note 17 1 (m/ap hat)) + (note 21 1 (m/ap hat)) + (note 25 1 (m/ap hat)) + (note 29 1 (m/ap hat)) + (note 5 1 (m/ap snare)) + (note 13 1 (m/ap snare)) + (note 21 1 (m/ap snare)) + (note 29 1 (m/ap snare)))) + +(defn song' + [>root] + (phrase :first-verse + ((piano >root) 0) + ((piano (m/latest #(+ % 12) >root)) 0) + ((drums) 0))) +``` + +Okay, important to note here: +1. I think the name associated with the call to `instrument` is actually + setting the name on the implicit `phrase` that the call to instrument is + returning. +2. A goal that I am trying to achieve here is implicit, unambiguous naming. Here, + I've accomplished that for drums ([:first-verse :drums]) but not for piano. + Let's try again? + +```clojure +(defn piano + [>root] + (let [triad (theory/triad :major >root)]) + (instrument keyboard + (note 1 32 (triad 1)) + (note 1 32 (triad 3)) + (note 1 32 (triad 5)))) + +(defn drums + [] + (instrument percussion + (note 1 (m/ap kick)) + (note 9 1 (m/ap kick)) + (note 17 1 (m/ap kick)) + (note 25 1 (m/ap kick)) + (note 1 1 (m/ap hat)) + (note 5 1 (m/ap hat)) + (note 9 1 (m/ap hat)) + (note 13 1 (m/ap hat)) + (note 17 1 (m/ap hat)) + (note 21 1 (m/ap hat)) + (note 25 1 (m/ap hat)) + (note 29 1 (m/ap hat)) + (note 5 1 (m/ap snare)) + (note 13 1 (m/ap snare)) + (note 21 1 (m/ap snare)) + (note 29 1 (m/ap snare)))) + +(defn song' + [>root] + (phrase + ((piano >root) :piano-1 0) + ((piano (m/latest #(+ % 12) >root)) :piano-2 0) + ((drums) :drums 0))) +``` + +Here, `phrase` is returning an invokable object that receives a name. If I were +to repeat song twice, we would get: + +```clojure + +(defn song + [>root] + (phrase + ((song' >root) :first-verse 0) + ((song' >root) :second-verse 32))) +``` + +Now we have the following unambiguous output addresses: + +{:first-verse [:piano-1 :piano-2 :drums] + :second-verse [:piano-1 :piano-2 :drums]} + + +Here's an aesthetic goal - make phrase invocation look more like: + +```clojure +(defn song' + [>root] + (phrase + (piano >root :piano-1 0) + (piano (m/latest #(+ % 12) >root) :piano-2 0) + (drums :drums 0))) + +(defn song + [>root] + (phrase + (song' >root :first-verse 0) + (song' >root :second-verse 32))) +``` + +I'm still not sure that I understand what a `note` is. What are other +objects like it? You could make something like a `pulse`, representing a +sine wave controlling an LFO. I guess from a programming perspective, +a note is an instance of some kind of object that becomes a signal +associated with output state and an ouput 'context' that explains how +similar objects get merged together. That output context receives a +name, and the name is guaranteed to be unique through multiple +nestings. + +Here's a question. Should phrases be responsible for renaming their +children? This could help control name explosions, where a long composition +could have many many names to map before playback. + +Yeah, that seems worth exploring. You could have some kind of rename map: + +(phrase + {[:first-verse :piano-1] :piano-1 + [:second-verse :piano-1] :piano-1 + [:first-verse :piano-2] :piano-2 + [:second-verse :piano-2] :piano-2 + [:first-verse :drums] :drums + [:second-verse :drums] :drums}) + +This would mean that you could do + +```clojure +(play {:piano-1 midi-out-1 :piano-2 midi-out-2 :piano-3 midi-out-3} song) +``` + +Big question: Why is there this difference between the way +I represent inputs (arguments) and outputs? + +Questions from today that remain open: + +1. What does this look like with a more complex example? +2. What do happens-before relationships look like? (Marco?) +3. Why do inputs look familiar (arguments) while outputs look strange (keywords)? +4. Do we need this instrument / output concept? What is it, exactly? What does it do? +5. How would a swung clock fit into all of this? Here again, time seems a little + different. Swung clocks need to interact with timeline selection. + Time objects should probably specify their clock. At playback time, + you define one or more clocks. +6. I don't curently have a good answer to crossfades. +7. Need to brainstorm more on loops / goto / portals and their relationship + with phrases. + +Insights: + +The point of an output, I think, is to somehow group output information +together. I think it is fair to say that everything on the same output +will share some semantics. From a programming perspective, the state +of an output should be uniform: a single data structure, and a single +merge operation. + +The data structure and merge operation shouldn't be re-written for each +instrument. There will only be a few of these. Keyed instruments like +pianos will be able to share a single data structure and merge operation, +even if a harpsichord won't make use of velocity information. + +From a programming perspective, +a note is an instance of some kind of object that becomes a signal +associated with output state and an ouput 'context' that explains how +similar objects get merged together. That output context receives a +name, and the name is guaranteed to be unique through multiple +nestings. + +Music theory concepts like triad can be expsosed as flow-returning functions. + +Outputs / instruments can't be nested, unlike phrases. + +A phrase can contain multiple instruments / outputs. + +Create a note protocol that generalizes over concrete note representation. + +Phrases can simplify the names of their children by providing a rename map. + +Concepts to test: + +1. Confirm that working on a phrase in isolation and working on a whole piece are + similarly simple + +Other random thoughts: + +It seems worth giving time objects explicit +Every leaf must be a time object +At this point, time objects should contain other metadata, like their path from +enclosing phrases +Time objects and phrases should have a similar... calling convention? with regards to +offset (almost certainly) and duration (maybe) +Time objects will eventually need a serializable identity for display purposes +Both time objects and phrases should be reified for display/interactivity +purposes + +## November 29th, 2025 + +As an exercise, I'd like to create a function that compiles strudel +mini notation down to my IR. + +https://strudel.cc/learn/mini-notation/ +https://strudel.cc/learn/mondo-notation/ +https://strudel.cc/learn/factories/ +https://strudel.cc/learn/time-modifiers/ + +And why this? + +https://strudel.cc/learn/stepwise/ + +Also do a dissection of strudel's alignment system: + +https://strudel.cc/technical-manual/alignment/ + +And this section on voicing: + +https://strudel.cc/understand/voicings/ + +--- + +What would it mean to reify temporal specifiers, so that they become objects +which can be shortened / lengthened through a series of operations? Keeping in +mind that these little windows encapsulate arbitrary values. They would compose +into a callable which can somehow be invoked with the time object itself, e.g. +the note(s) + + +## December 1st, 2025 + +TODO upcoming: + + - Inspired by strudel, define a language of musical modifiers + - Read notes of Nov. 28 |
