sowbug / groove

A digital audio workstation (DAW) engine.
Other
12 stars 0 forks source link

Revise TicksWithMessages #132

Closed sowbug closed 1 year ago

sowbug commented 1 year ago

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 have MusicalTime, 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 than MusicalTime (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 have Orchestrator keep a master MusicalTime, advance it, and call everyone only when it changes.

If we do that, then TicksWithMessages changes from this:

pub trait TicksWithMessages: Resets + Send + std::fmt::Debug {
    type Message;

    fn tick(&mut self, tick_count: usize) -> (Option<Vec<Self::Message>>, usize);
}

to this:

pub trait Controls: Resets + Send + std::fmt::Debug {
    type Message;

    fn update_time(&mut self, time: &Range<MusicalTime>);
    fn work(&mut self) -> (Option<Vec<Self::Message>>);
    fn is_finished(&self) -> bool;
}

Notable changes:

The new event loop will batch each method; that is, it'll call update_time() for everyone, then work() for everyone, etc. This new control flow gives us a clear start and finish: update_time() means the loop has begun, and is_finished() means it's ending. We can then allow work() to be called multiple times in a single loop.

Previously, we didn't fully specify how we'd dispatch the Messages that tick() produced. We'd call update() 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 their tick(), which came after our tick()), or for the next one (because their tick() 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:

sowbug commented 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.

sowbug commented 1 year ago

This is done enough for it to stick.