diff options
| author | Jake Zerrer <him@jakezerrer.com> | 2025-11-14 16:45:58 -0500 |
|---|---|---|
| committer | Jake Zerrer <him@jakezerrer.com> | 2025-11-17 15:17:39 -0500 |
| commit | c28969a3fbeb10f3aae89c6e1829e4540114f31d (patch) | |
| tree | 244e3a054088c920767c3d5c9621a5a6e6961ba5 /src/unheard/time_object.clj | |
| parent | b60b17e425b20d0fd0740f6e40fe47062c566a02 (diff) | |
Damn, I think timeline may even be mostly working, sans one bug
Diffstat (limited to 'src/unheard/time_object.clj')
| -rw-r--r-- | src/unheard/time_object.clj | 161 |
1 files changed, 93 insertions, 68 deletions
diff --git a/src/unheard/time_object.clj b/src/unheard/time_object.clj index 3556321..50deabc 100644 --- a/src/unheard/time_object.clj +++ b/src/unheard/time_object.clj @@ -2,32 +2,36 @@ (:require [missionary.core :as m] [helins.interval.map :as imap])) -;; DESIGN -;; A "time object" is any object with a lifetime that is temporally bounded. The -;; goal of the time object abstraction is to allow for for efficient, -;; low-latency allocation and deallocation of an unlimited number of time -;; objects. +;; A "time object" is any object with a lifetime that is temporally bounded. +;; A musical note that is part of a greater composition is the canonical example: +;; it exists between a start time and an end time, but doesn't exist otherwise. ;; -;;It is important that a time tree can be efficiently queried by both a range -;;(for UI) and a point (for a song). +;; A complex musical composition might consist of tens of thousands of these +;; time objects. However, during playback, only a small subset of all time +;; objects are computationally relevant. That subset is any time objects +;; whose timer interval overlaps with "now". ;; -;; IMPLEMENTATION -;; Time objects are stored in an interval tree. - -;; Requirements -;; - time objects returned by a timeline range query will include metadata like -;; start time and end time -;; - Flows associated with time objects should only mount or dismount when -;; the result of the query changes. I think the best way to accomplish this -;; is to have the query return a flow of time objects, and then have some -;; separate function responsible for "playing" these time objects. -;; -;; Question: -;; Should time objects take an interval tree as an argument, -;; or should they return a flow of interval information (start, end, value) -;; that can be fed into some kind of reactive interval map bookkeeper? -;; I think the latter. +;; For example, imagine a musical composition with three notes: +;; +;; [note start-time end-time] +;; +;; - [:a 0 10] +;; - [:b 5 15] +;; - [:c 20 25] +;; +;; At time 2, :a is computationally relevant. +;; At time 7, :a and :b are computationally relevant. +;; At time 17, nothing is computationally relevant. +;; +;; This namespace wraps the helins.interval.map data structure in a small +;; collection of functions producting missionary flows, building what amounts +;; to a reactive interval tree. Library users: ;; +;; - Create time objects using the `time-object` function. +;; - Combine time objects with `time-object-collection`. +;; - Combine time object collections with `merge-time-object-collection`. +;; - Instantiate a reactive interval tree with `timeline`. +;; - Query the timeline with `point-query` and `range-query`. (def id-counter (atom 0)) @@ -36,36 +40,30 @@ Value is a flow that will be consumed when the corresponding time tree is consumed at a point in time within the time-object's interval." - [>start >end >metadata >flow] + ;; NOTE: Might want to replace >flow with something more general like value. + ;; While it's true that in my usecase, I will ultimately end up collecting + ;; and booting flows from all time objects returned by `point-query`, + ;; that is kind of a separate concern. + [>range >metadata >flow] (m/ap - (let [id (swap! id-counter inc)] + (let [id (swap! id-counter inc) + v {:id id + :range >range + :metadata >metadata + :flow >flow}] (m/amb= - [id - :add - {:start >start - :end >end - :metadata >metadata - :flow >flow}] + [:add v] (try (m/? m/never) - (catch missionary.Cancelled _ [id :remove])))))) + (catch missionary.Cancelled _ [:remove v])))))) (comment (def cancel - ((m/reduce prn nil (time-object 1 2 :a (m/ap))) + ((m/reduce prn nil (time-object (m/ap [1 2]) :a (m/ap))) prn prn)) (cancel)) -(defn with-final - [] - (m/ap - (m/amb= - :start - (try - (m/? m/never) - (catch missionary.Cancelled _ :end))))) - (defn merge-flows [& flows] (m/ap (m/?< @@ -86,9 +84,9 @@ (def cancel ((m/reduce prn nil (time-object-collection - (time-object 1 2 :a (m/ap)) - (time-object 3 4 :b (m/ap)) - (time-object 5 6 :c (m/ap)))) prn prn)) + (time-object (m/ap [1 2]) :a (m/ap)) + (time-object (m/ap [3 4]) :b (m/ap)) + (time-object (m/ap [5 6]) :c (m/ap)))) prn prn)) (cancel)) @@ -105,13 +103,14 @@ ((m/reduce prn nil (merge-tocs (time-object-collection - (time-object 1 2 :a (m/ap)) - (time-object 3 4 :b (m/ap)) - (time-object 5 6 :c (m/ap))) + (time-object (m/ap [1 2]) :a (m/ap)) + (time-object (m/ap [3 4]) :b (m/ap)) + (time-object (m/ap [5 6]) :c (m/ap))) (time-object-collection - (time-object 1 2 :d (m/ap)) - (time-object 3 4 :e (m/ap)) - (time-object 5 6 :f (m/ap))))) prn prn)) + (time-object (m/ap [1 2]) :d (m/ap)) + (time-object (m/ap [3 4]) :e (m/ap)) + (time-object (m/ap [5 6]) :f (m/ap))))) prn prn)) + ;; Whoa! Running cancel twice cancels twice... ;; https://clojurians.slack.com/archives/CL85MBPEF/p1763154775780589?thread_ts=1763149125.436899&cid=CL85MBPEF (cancel)) @@ -119,7 +118,48 @@ (defn timeline "Primary timeline bookkeeping mehanism." [time-object-collection] - (m/ap)) + (m/ap + (let [actions (m/?< time-object-collection)] + (loop [tree imap/empty + actions actions] + (let [[k {:keys [id range] :as v}] (first actions)] + (case k + :add + ;; TODO: Raise if-let up a level to remove duplication + (let [[s e] (m/?< range)] + (if-let [next (seq (rest actions))] + (recur (imap/mark tree s e [id v]) next) + tree)) + :remove + (let [[s e] (m/?< range)] + (if-let [next (seq (rest actions))] + (do + (println "REMOVING" s e) + (recur (imap/erase tree s e [id v]) next)) + tree)) + :else + (if-let [next (seq (rest actions))] + (recur tree next) + tree))))))) + +;; TODO: Don't forget to ensure that ranges are turned into signals +;; +(comment + (def cancel + ((m/reduce prn nil + (timeline + (merge-tocs + (time-object-collection + (time-object (m/ap [1 2]) :a (m/ap)) + (time-object (m/ap [3 4]) :b (m/ap)) + (time-object (m/ap [5 6]) :c (m/ap))) + (time-object-collection + (time-object (m/ap [1 2]) :d (m/ap)) + (time-object (m/ap [3 4]) :e (m/ap)) + (time-object (m/ap [5 6]) :f (m/ap)))))) prn prn)) + + ;; NOTE: Cancellation is currently broken due to the above bug in merge-tocs + (cancel)) (defn point-query "Query a timeline. Returns a flow of time objects." @@ -131,22 +171,7 @@ (defn range-query "Range query. Returns a flow of time objects." [timeline >range]) + (defn run "Runs the flows associated with a collection of time objects." - ;; TODO: "Running" a time object has different meanings for different objects. - ;; How should I think about that? [>query-result]) - -;; How should a time tree work? -;; Well, we eventually want to end up with a single time tree -;; Is it important to be able to merge time trees together? -;; Part of me thinks "yes" -;; Time objects cannot be composed together - that is, you can't -;; take two time objects and add them together to get a new time object -;; However, you _can_ add two time objects together to get a time tree -;; The alternative to having intermediate time trees would be to have -;; some kind of time-object-collection abstraction. -;; time-object-collection would have a merge function. -;; A time object collection would be responsible for all of the bookkeeping -;; related to the lifetimes of time objects. -;; My suspicion is that this will |
