These noteOn triggers are to be expected on the first few calls of the hostTimeFilter.
If you peek into the internals - the hostTimeFilter is a basic linear regression that attempts to align the sample position of the client’s playhead to the global host time that is being maintained by all Link peers. Therefore, linear regression requires a few hundred sample points at first before it begins to output precise values.
This is why the Link documentation suggests that you begin running the host time filter as soon as you start your app - which is likely when the audio callback runs for a few seconds before the user hits the play button. Similarly, to maintain smooth syncing, this is why you should run the host time filter continuously for each and every audio callback regardless of whether Link is enabled or whether the internal play head is playing or not. By “running” the host time filter i mean, calling its sampleTimeToHostTime() method.
Further, if you inspect the beat values you receive from link and compare them to raw beat conversions of your internal sample position, you will find that occasionally (i.e. noticeably) Link WILL feed you some buffer overlaps or buffer gaps that result in so-called noteOn retriggers, or even skipping some expected notes altogether. You will have to account for this as a simple reality of the protocol.
Personally the way I do it is I keep track of two beat ranges: the current and previous. The current beat range is what I will pass down the audio chain to my sequencer/playhead/audio transport. The previous range is simply what the current range was in the previous audio callback.
To calculate the current range, I assign the previous range’s end value to the current start - this obviously forces the beat transition between audio callbacks to be precisely continuous, i.e. no gaps or overlaps. Then the current range’s end is calculated to be the Link session’s beat (calculated via session.beatAtTime()) + the size of the current audio callback buffer (in beats).
The result is that whenever Link gives you these buffer overlaps or gaps, your current beat range that you feed to the rest of your chain either shrinks or expands relative to the actual range that Link feeds you so that you may not strictly speaking “start” in sync, but you will always manage to “end” in sync and in a smooth manner. In practice, these variable ranges cannot be perceived, and the sync manages to “sound” even tighter when you do extreme/crazy fast adjustments to the bpm from any connected peer.