Ableton / link

Ableton Link
Other
1.1k stars 149 forks source link

Quantum changes seem to disrupt beats <--> seconds mapping #66

Closed jamshark70 closed 5 years ago

jamshark70 commented 5 years ago

I'm writing to ask for more detail on the correct way for SuperCollider to handle Link quantum changes.

Since issuing a few ill-chosen words on another thread (for which I apologize), I found one dumb mistake in SC's code (fixing that didn't solve the problem, though), and I tried adjusting beats relative to phase, which improved the behavior somewhat but didn't fully resolve it.

Current situation:

The SC clock has member variables mBeats and mQuantum, and I added a new one mPhaseOffset. When changing quantum, mPhaseOffset = fmod(mBeats, mQuantum) -- if we are at beat 16 and change quantum to 3, I believe Link will calculate phase = 1, but we need Link to see phase = 0 at that time. So then, wherever beats are passed into Link functions, we use mBeats - mPhaseOffset.

SetQuantum does this (it didn't before, but now it does):

    // Outside, linkTime = hrToLinkTime(secondsMatchingInBeats)
    // hrToLinkTime() converts high-resolution clock time to the time that Link wants
    void SetQuantum(double quantum, double inBeats, std::chrono::microseconds linkTime)
    {
        auto sessionState = mLink.captureAppSessionState();
        mQuantum = quantum;
        mPhaseOffset = fmod(inBeats, mQuantum);
        sessionState.requestBeatAtTime(inBeats - mPhaseOffset, linkTime, mQuantum);
        mLink.commitAppSessionState(sessionState);
        mCondition.notify_one();
    }

For simplicity, tempo = 60 bpm (1 bps) in these tests. Quantum changes are (as recommended) independently scheduled in each peer for the "next-bar" beat number on their own clock.

Test 1: Clocks started very close together, exact beat sync

A [beats, phase, secs] B [beats, phase, secs] Q change
[ 14.0, 2.0, 267.049451 ] [ 14.0, 2.0, 25.890796 ] (q = 4 initially)
[ 15.0, 3.0, 268.049451 ] [ 15.0, 3.0, 26.890796 ]
[ 16.0, 0.0, 271.049451 ] [ 16.0, 0.0, 29.890796 ] q --> 3 (bad)
[ 17.0, 1.0, 272.049451 ] [ 17.0, 1.0, 30.890796 ]
... deleted ...
[ 21.0, 2.0, 276.049451 ] [ 21.0, 2.0, 34.890796 ]
[ 22.0, 0.0, 277.049451 ] [ 22.0, 0.0, 35.890796 ] q --> 4 (OK)
[ 23.0, 1.0, 278.049451 ] [ 23.0, 1.0, 36.890796 ]

Test 2: Clocks started about a bar apart (machine B’s beats = A’s + 4)

A [beats, phase, secs] B [beats, phase, secs] Q change
[ 18.0, 2.0, 42.450004 ] [ 22.0, 2.0, 31.383952 ] (q = 4 initially)
[ 19.0, 3.0, 43.450004 ] [ 23.0, 3.0, 32.383952 ]
[ 20.0, 0.0, 44.450004 ] [ 24.0, 0.0, 33.383952 ] q --> 3 (OK)
[ 21.0, 1.0, 45.450004 ] [ 25.0, 1.0, 34.383952 ]
... deleted ...
[ 25.0, 2.0, 49.450004 ] [ 29.0, 2.0, 38.383952 ]
[ 26.0, 0.0, 52.450004 ] [ 30.0, 0.0, 41.383952 ] q --> 4 (bad)
[ 27.0, 1.0, 53.450004 ] [ 31.0, 1.0, 42.383952 ]

In the "bad" meter changes, both peers delay by the same amount -- so, according to the Link specification as described in the other thread, it's behaving as designed: beats occur at the same time, and the two peers remain in phase at all times.

But we suddenly have one beat occupying 3 seconds, where all of the other beats occupy 1 second, and there is no tempo change to account for this.

If SuperCollider is using quantum incorrectly, what should we be doing differently?

fgo-ableton commented 5 years ago

Thanks for the detailed explanation! Unfortunately Link can not provide continuous beat times when changing the quantum - and if I understand correctly, that is what you would expect. I'm afraid you have to do manual re-wrapping to support the desired scenario.

Here is what Link does: The quantum is used to align the downbeat to other peers that use the same quantum. As I said in the other thread, the quantum is only used when calculating the local state. It is never shared on the network and quantum changes are not orchestrated with other peers. It is used to determine a grid that tells us how to align the timelines of the individual peers, but there is no "master" timeline for all peers. And the point the individual timelines are aligned to, is opaque from the API. So in the examples above the new timeline for the new quantum is gap-less in case the change coincidentally happens at a beat time, that is a downbeat for both, the old and new quantum. But there is no way to assert that as a user of the API.

Here is how we deal with a related scenario in Live: We set the quantum when starting playback. In case the time signature in the arrangement timeline changes, we still query the beat time using the initial quantum and just iterate beat time from there - accepting that the phase of the new time signature might not match phase with peers using the same time signature.

Maybe a similar workaround would do for the scenario you have in mind to?

jamshark70 commented 5 years ago

OK, thanks for the advice. I think what SC will have to do is to implement a separate OSC protocol to transmit beatInBar (what Link calls "phase") alignment to other SC peers. I think we will have to require users to set the LinkClock's initial quantum to match other peers (to handle the case of e.g. Live playing in 4/4 and SC starting in phase alignment with that) and also send messages among SC peers to set the baseBarBeat such that all of them have a common grid.

More complicated than I expected, but possible. SC can set its baseBarBeat arbitrarily for its own concept of meter, while Link continues on its way with a fixed quantum.

It may be worth filing this as a future enhancement for Link.

Thanks again -- much appreciated!

fgo-ableton commented 5 years ago

Has this functionality actually been requested? Or is that a functionality somehow required by SCs beat clock? I'm just wondering if it might be worth sticking with what Link supports and see if that works out for SC users as well.

jamshark70 commented 5 years ago

It was a point of confusion for SC devs. We all expected Link's quantum to be analogous to SC's beatsPerBar, and that Link's phase sync would handle meter changes. That may be a documentation issue.

I'm thinking of a scenario like this:

  Meter: bpb=4       bpb=3
A beats: 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14
  Beats: |--|--|--|--|--|--|--|--|--|--|--|--|--|--|
   Bars: |           |        |        |        |
B beats:                         0  1  2  3  4  5  6

This is not necessarily Link's problem. I can handle this in SC by having B broadcast an OSC message asking other peers what is their current meter and phase -- finding that its beat 0 matches with the other's phase 1, it can set its own baseBarBeat = (myPhase - otherPhase) % beatsPerBar -- in this example, 2, so that a barline e.g. roundUp(4 - 2, 3) + 2 == 5 which does align with an A barline. Things get tricky if, later, C finds A and B and their phases disagree -- I'm still thinking about that. But I believe user expectation of LinkClock will be that beats and barlines stay in sync. Link doesn't currently do that by itself, so SC needs new logic to handle it.

Link might benefit from keeping quantum and baseBarBeat in the session state. Possibly it doesn't do this to avoid conflicts, but one could argue that simply declaring conflicts to be normal functioning sidesteps, rather than solves, the problem. (OTOH, by the "Keep It Simple, Stupid" principle, Link's current approach might well be the least objectionable one.)

fgo-ableton commented 5 years ago

Is see. We have thought about those scenarios too. Especially with the possibility of peers being able to connect during a jam, there are quite a few cases, where different behaviors make sense in different use cases. Thus the "Keep it Simple, Stupid" approach. I would still challenge the expectation, of bars always being in sync across all peers. I understand where it comes from. But from my experience it is not as badly required as one initially thinks, and not having it can lead to interesting results. But I understand, you don't think that would right approach for SC. Kudos for the ASCII art btw!

jamshark70 commented 5 years ago

My thought is to make it optional -- users could turn it off if they want arbitrary metric alignment.

Thanks again for the hint -- it's very useful.

I'm ok to close this now.