AU Wrapper: Workaround for off-by-one error of timing information in Logic Pro X

My audio plug-in requires to know the time of the sample it’s currently processing.
To do this, I have something like the following code:

	// retrieve the current time of the playhead in samples, if possible
	std::optional<int64> playTimeOpt;
	if (auto *playHead = getPlayHead()) {
		AudioPlayHead::CurrentPositionInfo info;
		if (playHead->getCurrentPosition(info)) {
			if (info.isPlaying) {
				playTimeOpt = info.timeInSamples;
			}
		}
	}

	for (int i = 0; i < buffer.getNumSamples(); i++) {
		// calculate the absolute position of the current sample
		std::optional<int64> samplePlayTimeOpt;
		if (playTimeOpt) {
			samplePlayTimeOpt = *playTimeOpt + i;
		}

		// code using the sample time here
		// [...]
	}

This code works very well, however, there is an issue in Logic Pro X that causes an off-by-one error at the very start of the timeline with plugins that introduce latency.

In my case, it first reports a negative value for info.timeInSamples like -179 (I assume this is for latency compensation purposes), and in the next processBlock callback it reports 1868.
The problem is that the size of the first audio buffer I receive is 2048 samples, and (1868 - -179) = 2047.
Therefore, the code above determines the same sample play time twice in a row, which causes my application to reset some internal states, as it deems the input non-continuous.

This Logic bug only occurs at the very start of the timeline, these off-by-one errors don’t happen when I move the audio clip a few seconds into the timeline.

I’ve found a workaround: I modified juce_AU_Wrapper.mm to not use the sample time received by CallHostTransportState, and instead always use lastTimeStamp.mSampleTime, which has the correct value even at the start of the timeline.

I simply replaced this code

        if (CallHostTransportState (&playing,
                                    &playchanged,
                                    &outCurrentSampleInTimeLine,
                                    &looping,
                                    &outCycleStartBeat,
                                    &outCycleEndBeat) != noErr)
        {
            // If the host doesn't support this callback, then use the sample time from lastTimeStamp:
            outCurrentSampleInTimeLine = lastTimeStamp.mSampleTime;
        }

with

        CallHostTransportState (&playing,
                                    &playchanged,
                                    &outCurrentSampleInTimeLine,
                                    &looping,
                                    &outCycleStartBeat,
                                    &outCycleEndBeat);

        outCurrentSampleInTimeLine = lastTimeStamp.mSampleTime;

Logic is by far the biggest plugin host using the AU format, so I think it makes sense to incorporate this workaround into JUCE, especially since there are no downsides - the AU documentation specifically says that the timestamp provided to the AudioUnitRender function (which is the one that updates lastTimeStamp in the JUCE wrapper) must be monotonic, so it seems to be the right thing for the AudioPlayHead to return this value.

Tagging @tpoole, since he seems to do a lot of work on the AU wrapper :slight_smile:

I just realized that this workaround doesn’t actually work, since the AU only requires that timestamp provided to be continuous values, not that these values actually represent the position on the timeline. Therefore, please disregard this post.

I’ll go and file a bug report with Apple so they can fix the issue.

Hey guys - we also have an off-by-one sample error bug in Logic. I’ve had one of our projects ignore the bug by not resyncing if the sample position is out by only a single sample. However another project which @jacquesmignault is working on uses Tracktion Engine and this appears to also fall foul of the logic sample problem - or at least I think that’s what’s happening.

If you play a project in sync with Logic using the Tracktion playhead sync stuff and set the logic buffer size to 128 samples on Jacques’ computer you get constant glitching. This doesn’t happen in Protools or Reaper.

Does any one else have this problem still? Did you file a bug report with Logic? Maybe we can write a work around for Tracktion engine to ignore very small errors…

@dave96 - seen this problem before?

On which JUCE commit?

Looking at the relevant code I noticed something strange in a recent change from June.

1 Like

Yeah, I’d like to know if this can be solved in JUCE first.

In Tracktion Engine, synchroniseEditPosition we only post playhead position changes if the sync is broken by more than half a block size. I would have thought that would cover an off-by-one error?

@jimc in @jacquesmignault project, did you check if EditPlaybackContext::postPosition is being continuously called?