Ableton Link Beat Alignment

Hello,

I'm using the new Ableton Link integration in Tidal 1.9.

I'm confused because while tempo synchronization is working, beat/phase aligment are not. For example, sending a simple 4/4 pattern from Tidal results in the following midi in Ableton:

The beats are in the right tempo, but they are not in the right beat/phase for the 4/4 grid in Ableton.

Is this expected behavior? Is there some configuration I'm missing?

I'm actually sending OSC directly from Tidal to MaxMSP, which is then converting the OSC to MIDI for Ableton. My BootTidal.hs is using the defaultConfig, but maybe there's something else I need to set up?

Hi @jjw, that's interesting, is the offset always the same or does is vary across restarts?

Good question.

I ran some tests and it seems like the offset does not vary across restarts.

However, the offset is different for different tempos. So the offset is consistent for a given tempo, but different for each tempo.

Could it be that the configured latency is not correct for your setup? Please follow Multi-User Tidal | Tidal Cycles and report back. Hope this does the trick!

Thank you. It seems this is my problem. Using nudge I can get tidalcycles in phase with ableton. However, copying the same value into the oLatency does not work.

My BootTidal.hs has this:

:{
customTarget = Target {oName = "Max", -- Give your target a name
                      oAddress = "127.0.0.1", -- the target network address
                      oPort = 2020, -- the target network port
                      oLatency = 0.2, -- the latency (to smooth network jitter
                      oSchedule = Live,
                      oWindow = Nothing,
                      oHandshake = False,
                      oBusPort = Nothing
                      }
:}

tidal <- startTidal (customTarget {oLatency = 0.2, oAddress = "127.0.0.1", oPort = 2020}) (defaultConfig {cFrameTimespan = 1/20})

However, it seems the oLatency value isn't being used, as # nudge 0.2 will align the beats. (oLatency = 0.4 also has no effect)

i think i have an idea of what might be your issue, but i'll need to know first: how exactly are you converting the OSC messages to MIDI in your max patcher?
i've got a max license so feel free to share your patcher if that's ok with you, otherwise a screenshot+explanation could also work.

Could you try the cProcessAhead parameter, i.e. in:

tidal <- startTidal (customTarget {oLatency = 0.2, oAddress = "127.0.0.1", oPort = 2020}) (defaultConfig {cFrameTimespan = 1/20, cProcessAhead = 3/10})

I think 3/10 (aka 0.3) is the default, so you'll need to adjust from there. My guess is it should be 0.

I still find the difference between cProcessAhead and oLatency a bit unclear, I think the meaning changed with the new link-based scheduling..

the thing is, if you're using oSchedule = Live tidal will do its best to send the OSC messages right on time minus your specified latency value.
so if you're not delaying the midi notes you're generating in your max patcher by that same amount the timing will always be off by a fixed amount of milliseconds, which means that the beat alignment will be off by different amounts at different tempos.

the most robust solution would be to implement some kind of scheduling system within max that can make use of the timestamps tidal normally attaches to its OSC messages, similar to how superdirt handles it. never managed to get it to work with max objects myself, though i guess you could spin something up using javascript or node within max nowadays.

this way you'd also eliminate any jitter issues that you normally have when doing this (except for the inevitable jitter you get when crossing the boundary between a gen~ patcher and normal max, or in between MIDI-generating max4live objects and ableton itself, but that's another topic entirely...)

if you're up to the challenge, reading through this thread here would be a good start point:
Is there an accurate way to acquire system time (not [date])? - MaxMSP Forum | Cycling '74)

EDIT: in fact i think this entire topic isn't really explained well enough in the max and tidal documentation. the only really thorough explanation i've found is this one in the supercollider documenation, but since its written within the context of supercollider it's not easy to understand at all for most people...

@Robin_Hase Exactly, the max code doesn't do any scheduling. I'm using a patch made by someone else: HackYourDaw/TidalSynth2.0.amxd at main · fracnesco/HackYourDaw · GitHub

The relevant max bit:


It just listens to a udp port, filters by dirt/play, unpacks the tidal arguments, and then converts them into a midi note. Events are not delayed that I can see.

Before this, I actually wasn't aware that the Live scheduling was an option. I thought all the scheduling was always done by Supercollider. Scheduling in Max would be ideal. It's probably possible, but not simple to accomplish.

@yaxu Your intuition that cProcessAhead = 0 would help was correct! That brings things much closer to alignment. From there I just needed to add a very small amount of latency.

The final startTidal looks like this:

tidal <- startTidal (customTarget {oLatency = 0.01, oAddress = "127.0.0.1", oPort = 2020}) (defaultConfig {cFrameTimespan = 1/20, cProcessAhead = 0.0})

There is some jitter but that's expected.

I looked at the code a bit (Tidal/Tempo.hs at 14064165cd5b6719af4605087d6e81332920ad60 · tidalcycles/Tidal · GitHub, Tidal/Stream.hs at 14064165cd5b6719af4605087d6e81332920ad60 · tidalcycles/Tidal · GitHub) but I don't really understand what's going on. I can understand that getting strict timings using Live mode is not really possible.

I strongly advice against setting cProcessAhead to 0. It will cause Tidal to process events too late. The more complex patterns, the more it will be noticeable.

As recommended in the thread already, Live scheduling is probably your best option and should hopefully work pretty well. The other approaches rely on the receiver to look at the timestamp. If used with a receiver that doesn't look at the timestamp, things will behave very strange.

@yaxu, you are correct that oLatency changed meaning when Link scheduling was introduced. I believe it used to have inconsistent meaning between timestamped messages and live messages. It now always specifies how far ahead the sound should be produced to hit the speaker according to the Link time. I should have made a bigger point about this change so it was fully understood by the community and so we could better consider backwards compatibility. The different configuration parameters are explained here: The Boot File | Tidal Cycles

Ok, thank you all for your help. Tidalcycles and Ableton are now aligned without adjusting cProcessAhead, by setting oLatency to a very low value: 0.005.

I read your comment @Zalastax but I got confused because after followed the link I ended up reading this section of the documentation: Multi-User Tidal | Tidal Cycles . This led me to add the nudge value to the oLatency, which didn't work.

This section (the one you actually linked) is very helpful: Multi-User Tidal | Tidal Cycles. That cleared up how nudge, oLatency, and cProcessAhead work.

I think I was also confused because my latency is so low. I was adjusting nudge and latency in 0.1 or 0.01 increments, and those amounts were too coarse.