GithubHelp home page GithubHelp logo

Comments (2)

helins avatar helins commented on July 19, 2024

Hi,

Thanks for your feedback! I would probably need to know more about what you are building, but I'll try to provide you with a general answer regarding side effects. First, a few words about the opposite of what you want in order to better understand the link between simulated time and real time (time in a context Vs. time in the real world).

That keyboard visualizer operates in two modes. In the "offline" mode, it reads a MIDI file and extracts all needed MIDI events. Once this is done, everything is pure. In the "online" mode, the context is stored in an atom and MIDI events are received live. The neat thing is that thanks to DSim, they both share 99% of the code.

So, in the online mode, MIDI events are side effects. Those side effects are translated into meaningful events in our system. Here, raw MIDI events are translated into events creating flows (typically finite ones), something like:

(fn on-midi-message [timestamp msg]
  (swap! *ctx
              (fn add-color-transition [ctx]
                 (dsim/e-conj ctx
                                      [timestamp]
                                      [:piano-key (:note msg) :color]
                                      (dsim/finite 2000
                                                        (fn compute-color [ctx] ...))))))

Note I schedule the creation of a the finite flow (ie. gradual transition of the color of a piano key), I don't compute anything else because I don't need to.

Other kind of side effects require you to know the exact state at a precise moment. Those side effects, happening at some point in time in the real world, dictate the current simulated point in time in the system (::ptime). When a frame is drawn, I have to know what to draw, hence run all events up to the timestamp of that frame. That would look like:

;; When provided with a context, returns a lazy sequence of all computed points in time.
;; We can navigate this sequence and stop when we need.

(def lazy-run
       (dsim/historic (dsim/ptime-engine)))


;; Besides executing everything up to when we need to draw, we sample (compute) all flows
;; so that we know their exact state at the moment of drawing.

(fn on-frame [timestamp]
  (let [ctx-2 (swap! *ctx
                             (fn update [ctx]
                               (last (take-while #(<= (dsim/next-ptime %)
                                                                  timestamp)
                                                         (lazy-run (dsim/e-conj ctx
                                                                                             [timestamp]
                                                                                             nil
                                                                                             dsim/sample))))))]

     (draw ctx-2)))

All that was about side effects impacting our context. I wanted to insist on the fact that the context will not move through its simulated time until we move it ourselves.

What about the opposite, when the context satisfies some condition, do a side effect? You have to think about that link between simulated time and real time. A side effect could be just another event in the context. Then it will execute only when we run the context and reach that ptime, the moment this side effectful event is scheduled in our simulated time.

What if after reaching a condition in our context, we want to schedule a side effect for later IN THE REAL WORLD? Such as "when this and that, send a MIDI Note-On message after 2000 real milliseconds". Then you would use in addition an external timer, something that is indeed capable of waiting 2000 milliseconds in the real world before executing a function. There are a few options, core.async is even one, even (Thread/sleep) is a poor's man solution.

However, for real time audio, typically, timing in the real world has to be precise. You'll have to do some research. For MIDI, I use a Sequencer object because I know it does lower-level stuff that will probably result in more accurate timing than what I would write on the go: Sequencer. I use both DSim for computing graphics (+ other stuff) and that Sequencer (from the javax.sound.midi package) for live audio stuff.

I hope it helps, feel free to provide an more concrete example.

from dsim.cljc.

telekid avatar telekid commented on July 19, 2024

Thanks for such a thorough answer! It really clarified a number of dsim concepts for me. on-midi-message and on-frame are particularly helpful illustrations, as they very clearly explain what I assume will prove to be fairly common online use-cases - namely, updating the simulation on events and on a schedule. (I imagine others might find it helpful if you included these and other similar examples in the readme.)

What about the opposite, when the context satisfies some condition, do a side effect?

and

What if after reaching a condition in our context, we want to schedule a side effect for later IN THE REAL WORLD?

These describe my use case, which does seem a bit trickier to conceptualize than updating the simulation itself on external inputs. Specifically, I'm looking to construct an environment where various forms of realtime musical expression are possible. For example, turning an encoder might modify the value of some musical parameter (now or in the future), e.g. a "pitch bend" MIDI CC message. (This seems like a potential usecase for dsim's sampling tools, actually.) Or perhaps a user might schedule a (finite or infinite) loop, of which some aspect may be similarly parameterized. Or perhaps hitting a note on a drum pad fires a MIDI Note On event, and schedules a Note Off event for 800ms in the future. You get the gist.

Then it will execute only when we run the context and reach that ptime, the moment this side effectful event is scheduled in our simulated time.

This is probably the clue that I need to meditate on. I'll probably end up making an event loop that looks something like this -

On an external input (e.g. a change in an encoder value),

  1. Update a stateful ctx atom using standard dsim mechanisms
  2. Cancel any timer cancel callbacks (from step 4) between now and some buffer, e.g. (+ now 100)
  3. Perform a range query on ::dsim/events, grabbing:
    a. the first event after now, and
    b. all events between now and buffer
  4. Schedule timers for the events in 3, and store their cancel callbacks in ctx to be used in successive iterations of step 2

Does that make sense? As a rough sketch, at least?

On a personal note, the timing of your publishing of this library is quite serendipitous - just a week ago, I was fumbling around in the dark with some of these ideas, and I sketched out something conceptually similar (at least at a high level). Here's a quick copy/paste of what I was working on:

(ns mm.events
 (:require [kdtree :as kdtree]))

(def schedule-default
  ;; Map of ticks to sets of event symbols
  {:ticks->event-id-sets {}
   ;; Map of events to the tick at which they're scheduled
   :event-ids->ticks {}
   ;; Map of event symbol to event
   :events {}
   ;; kd-tree containing all ticks with scheduled events
   :ticks (kdtree/build-tree [[-1]])})

(def schedule (atom schedule-default))

(defn reschedule-event
  "Set the time of an event on the schedule.

  The event needs an id and a tick.

  The event should already be scheduled. Reschedule-event only alters the
  event's time, not its other properties.

  To set all properties of an event, use add-event."
  [schedule event]
  (let [id (:event/id event)
        tick (:event/tick event)
        old-tick (get-in schedule [:event-ids->ticks id])
        schedule (if old-tick
                   (update-in schedule [:ticks->event-id-sets old-tick] disj id)
                   schedule)
        event-set-empty? (empty? (get-in schedule [:ticks->event-id-sets old-tick]))
        schedule (if event-set-empty?
                   (update schedule :ticks->event-id-sets dissoc old-tick)
                   schedule)
        schedule (if (and event-set-empty? old-tick)
                   (update schedule :ticks kdtree/delete [old-tick])
                   schedule)]
    (-> schedule
        (assoc-in [:events id :event/tick] tick)
        (assoc-in [:event-ids->ticks id] tick)
        (update-in [:ticks] kdtree/insert [tick])
        (update-in [:ticks->event-id-sets tick] #(if % (conj % id) #{id})))))

(defn add-event
  "Add a new event to the schedule.

  The event needs an id."
  [schedule event]
  (let [id (:event/id event)]
    (-> schedule
        (assoc-in [:events id] event)
        (reschedule-event event))))

(defn reset-schedule
  "Removes all events from the schedule."
  []
  (swap! schedule (constantly schedule-default)))

(defn remove-event
  "Removes an event from the schedule.

  The event needs an id."
  [m event]
  (let [id (:event/id event)
        old-event (get-in m [:events id])]
    (if old-event
      (let [old-tick (get-in m [:event-ids->ticks id])
            m (update m :events dissoc id)
            ;; TODO: Only delete if ticks are empty
            m (update m :ticks kdtree/delete [old-tick])
            m (update m :event-ids->ticks dissoc id)
            m (update-in m [:ticks->event-id-sets old-tick] disj id)
            event-set-empty? (empty? (get-in m [:ticks->event-id-sets old-tick]))]
        (if event-set-empty?
          (-> m
              (update :ticks->event-id-sets dissoc old-tick)
              (update :ticks kdtree/delete [old-tick]))
          m))
      m)))

;; ;; TODO: Efficient range query over :ticks->event-id-sets

(def e1 (gensym "e1"))
(def e2 (gensym "e2"))
(def e3 (gensym "e3"))

(reset-schedule)

;; NEXT: Sanity check removal
;; this isn't quite right yet
;; THEN: make events-in-range
(swap! schedule add-event {:event/id e1 :event/tick 10})
(swap! schedule add-event {:event/id e2 :event/tick 10})
(swap! schedule add-event {:event/id e3 :event/tick 10})
(swap! schedule reschedule-event {:event/id e1 :event/tick 20})
(swap! schedule reschedule-event {:event/id e2 :event/tick 30 :event/name "jake"})
(swap! schedule add-event {:event/id e2 :event/tick 3 :event/name "jake"})
(swap! schedule reschedule-event {:event/id e3 :event/tick 10})
(swap! schedule remove-event {:event/id e1})
(swap! schedule remove-event {:event/id e2})
(swap! schedule remove-event {:event/id e3})

(The :ticks key doesn't quite work yet, and I didn't think to use a sorted map for :ticks->events-id-sets, which would have allowed me to remove my dependency on kdtree... Ah well.)

I love that moment when you discover that the idea that you've been chipping away at is a well-formed area of study! Anyway - it's awesome to see such a fleshed out implementation of the concept. There's a lot for me to dig into here.

from dsim.cljc.

Related Issues (4)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.