Closed sowbug closed 1 year ago
More thoughts on "sneak a frame count into update_time()": we're allowing Controllers to be owners of Instruments. For it to be as accurate, a Controller needs the same amount of event-loop granularity as an Instrument gets, subject to the observation that "the whole point of Controls operating on a different time granularity from Ticks was because we were OK with control messages being produced only at MusicalTime granularity." Maybe having LfoController also be an Instrument is right after all, though it does start to raise questions whether separating tick()
into two kinds was the right thing to do.
Gaah.
This is done enough for it to stick.
I pulled on a thread. I was working on #108 to build the piano roll. I was happy with the v0 results, but doing it right needed a real time system. So I set off to make
PerfectTimeUnit
live up to its name. Now I haveMusicalTime
, which has bars/beats/parts/subparts, and seems to be smart enough to convert from samples a.k.a. frames and back.But while making these changes, I realized that everyone implementing the
TicksWithMessages
trait is doing the same thing, which is taking the current frame number, converting it into musical time, and then often returning without doing further work because it has already handled that musical time point. This makes sense because the sample rate is typically more granular thanMusicalTime
(e.g., 120 BPM = 2 beats/second 16 parts/beat 100 subparts/part = only 3,200 slices/second vs 44,100 samples/second). It seems like it would be better to haveOrchestrator
keep a masterMusicalTime
, advance it, and call everyone only when it changes.If we do that, then
TicksWithMessages
changes from this:to this:
Notable changes:
Controls
to better match that Controllers do this.update_time
is now a separate method rather than an argument totick()
. This paves the way forwork()
to be idempotent (explanation below).is_finished()
is no longer implied in thetick()
return value; rather, it's a separate method, and it no longer tells the exact frame where the entity went from not finished to finished.The new event loop will batch each method; that is, it'll call
update_time()
for everyone, thenwork()
for everyone, etc. This new control flow gives us a clear start and finish:update_time()
means the loop has begun, andis_finished()
means it's ending. We can then allowwork()
to be called multiple times in a single loop.Previously, we didn't fully specify how we'd dispatch the
Message
s thattick()
produced. We'd callupdate()
as needed, and if anyone cared about timestamping the messages they received, that was up to them. But it wasn't ever clear whether a message was for the current time slice (because someone else produced it during theirtick()
, which came after ourtick()
), or for the next one (because theirtick()
preceded ours). And things could get tricky if we'd already indicated that we were finished, but then someone else sends a MIDI message to us during that cycle.Problems:
LfoController
contains anOscillator
that produces control messages. The Oscillator expects to do work every frame. So either we decide thatLfoController
is also an Instrument (so it implements theTicks
trait and gets called for every frame), or we haveLfoController
batch-tick itsOscillator
each timework()
is called. I prefer the second approach because it becomes an implementation detail rather than makingLfoController
into an odd hybrid, and because the individualtick()
calls were going to be a waste anyway, since the whole point ofControls
operating on a different time granularity fromTicks
was because we were OK with control messages being produced only atMusicalTime
granularity.Timer
wants to operate on units of seconds. If we want to blend into the new world, then we'd convert seconds intoMusicalTime
and go with that. But we will lose the ability to fire the timer at the exact frame (e.g., 44100 if seconds=1 and sr=44100, since granularity ofControls
is only about every 10-15 samples). Alternatives: (1) live with a less-accurate timer, (2) change it to takeMusicalTime
as its unit of measurement, (3) sneak a frame count intoupdate_time()
and indicate in the contract that we might callupdate_time()
twice with the sameMusicalTime
but a different frame count.