Impossible to accurately calculate the exact end time (DSP) of an audio clip that changed pitch?

So I have code (in Master Audio) that can accurately calculate when the end of an Audio Clip will fall - exactly - by using:
AudioSettings.dspTime + ((clip end position - clip start position) / Math.Abs(AudioSource.pitch)). This works without fail as long as we play it immediately so we can have gapless sound effect chains. However, if the pitch of the Audio Source is modified after it started playing, I believe it becomes impossible to calculate the new end time exactly due to a lack of math precision…this is my current calculation for that, which works about 1 in 5 times. Other times there is a slight click or hiccup between sounds.

AudioSettings.dspTime + ((clip end position - AudioSource.time) / Math.Abs(AudioSource.pitch).

I believe it can’t quite calculate it correctly because AudioSource.time is a float, whereas AudioSettings.dspTimes are a double. So it’s just the tiniest bit away from being accurate. I’m pretty sure this is all correct and there’s no way to correctly calculate it. Anyone with an idea to try please let me know!

-Brian

1 Like

Use a double then. Convert the AudioSource.timeSamples to a time that is a double.

How do you calculate the “clip end position”?
Is that clip length?
Should also convert samples to a double time to get the length.

The real problem with pitch shifting is that if the pitch is shifting over time, you can’t really trust your calculation for scheduling other audio, because the length is always changing.
So you have to know all the details of the pitch shift, and then you can try to work out what is going to happen, before it happens. When you are scheduling audio you obviously have some kind of buffer to ensure that the audio is loaded and plays on time. If someone shifts the pitch again within that buffer time and you haven’t calculated for it, your schedule is no longer accurate.

I was using clip.length yeah unless the user specified a custom end point, at which time it would be

EndPercent(float) * 0.01f * clip.length instead.

Yes we were doing a “pitch glide” which gradually changes the pitch over a period of time. So on each frame where it changes the pitch, we then recalculate the end position to we can setScheduledEndTime to the almost correct ending spot. But it doesn’t work.

Are you saying there’s something that uses clip.samples instead to get an accurate reading?

1 Like

So, I looked at this and it’s more difficult than I thought it was.

Because
The DSPtime is not realtime, it only updates with the buffer.
The timeSamples are not accurate for reading, they also only update with the buffer. They are really only useful for seeking to a sample.

This is very limiting. It means I had to create my own timers to sync with DSPtime and emulate it, are you doing the same or am I missing something?

One thing to keep in mind. Getting the length of the clip is tricky if you are changing the pitch, because changing the pitch changes the length. If you query the clip length, it always gives you pitch 1 length, and if you try to / by a pitch it simply gives you the length of that clip if it was played entirely in that pitch which is obviously not what we want.

Anyway. I think I found two ways of doing this, and they both have drawbacks.
The drawback that is common to both of them is:
You can’t start a pitch shift after scheduling and still expect it to schedule accurately. That’s a given, though.

I am using doubles for everything that I can, I haven’t really checked to see if it makes a difference.

The first method:
Work out the length of the audioclip at pitch: 1

clipPitch1Length = (double)audSauce1.clip.samples / (double)audSauce1.clip.frequency;

Create a timer that fetches the DSPtime and then adds deltatime

currDSPTime = AudioSettings.dspTime;
....
currDSPTime += Time.deltaTime;

Create a clip timer that converts deltatime to pitch 1

clipPitch1PlayingTime += (Time.deltaTime * audSauce1.pitch);

Convert the difference into the current pitch to get the real time difference for schedule

if (((clipPitch1Length - clipPitch1PlayingTime) / audSauce1.pitch) <= bufferTime)
{
audSauce2.PlayScheduled (currDSPTime + ((clipPitch1Length - clipPitch1PlayingTime) / audSauce1.pitch));

Drawbacks of method 1:
Shifting needs to end by the time you schedule.

The second method:
Work out the length of each of the segments old pitch, pitch shift, new pitch.
You can actually still be shifting the pitch into the buffer zone with this, because we are working everything out before hand. You just can’t start a new pitch shift in the buffer zone.

//Work out the length of the segment before the pitch shift
                estimateClipLengthTime = pitchshiftDelay / fromPitch;
//Work out the length of the segment where the pitch is shifting
//Because the shift is linear, we can average the length of the old and new pitch
                estimateClipLengthTime += ((pitchShiftTime / fromPitch) + (pitchShiftTime / targetPitch)) / 2d;
//Work out the length of the segment after the pitch shift, using the new pitch
                estimateClipLengthTime += (clipPitch1Length - pitchshiftDelay - pitchShiftTime) / targetPitch;
clipPlayingTime += Time.deltaTime;
...
if (estimateClipLengthTime - clipPlayingTime <= bufferTime)
{
audSauce2.PlayScheduled (currDSPTime + (estimateClipLengthTime - clipPlayingTime));

Drawbacks of method 2:
You must know the details of the pitch shift(s).
Pitch shifts must be linear
Your pitch shifts can’t overshoot the clip length, which makes sense anyway.

If you don’t want to calculate all the segments, but you do know all the pitch shifting information and just want to calculate the last segment, I think you can probably just work out an average pitch for the buffer segment that will give you the right time.

For example.
You hit your schedule audio condition.
Your pitch shift is still busy, and it will play to the end of the clip (to make the example easy)
Your pitch will shift from 1.1 to 1.2 by the end.
(1.1 + 1.2) / 2 is the average pitch.
That is how long it should take.
Method 1

audSauce2.PlayScheduled (currDSPTime + ((clipPitch1Length - clipPitch1PlayingTime) / averagePitch));

This assumes a linear pitch shift.

Thanks for the lengthy post.

I don’t have time to test these until Monday or so. I tried a few things and nothing was working consistently. Do you own my plugin Master Audio? We can take this to a private conversation if you like. Would be great to actually get this working in MA. If you don’t own the plugin I’d give you a voucher for it if you’d help to get this working :slight_smile: Or for any other plugin we have.

Yeah I own Master Audio

I made 2 mistakes on the same line which I have edited.

clipPitch1Length = audSauce1.clip.samples / AudioSettings.outputSampleRate;

-This doesn’t work unless your clip length is exactly an integer, and mine was, to make it easier for me to verify calculations. samples and outputsamplerate are both integers, so the result of that division is returned as an integer which destroys everything after the decimal.
You must convert them to doubles

clipPitch1Length = (double)audSauce1.clip.samples / (double)AudioSettings.outputSampleRate;

-Your clip sample rate doesn’t actually HAVE to match the project setting outputSampleRate. This means that we should be looking at the clip.frequency for our calculations instead.

clipPitch1Length = (double)audSauce1.clip.samples / (double)audSauce1.clip.frequency;

Ok, so method #2 doesn’t work if we don’t know about an upcoming pitch shift at the moment we start the clip playing? Is that what you’re saying? Because we will not know until the user calls “GlideByPitch” due to some event. So I should look into method #1?

I’m sure you can simply re-calculate the estimated time with method 2 on the fly each time a pitch shift starts.
As long as you know the duration of the shift, and the target pitch when the shift starts.
You just need some code that can break the clip into segments that can be calculated.

Well method 1 seems easier somehow so I will try that first. Thanks!

I’m confused here. Trying to do method #1 but I don’t understand what “bufferTime” is since it wasn’t an actual variable in the code snippets you posted.

Also, which part would happen each frame vs which part would be stored only from the initial play time?

Rather than trying to measure the duration. Could you instead use a Coroutine to keep an eye on AutoSource.isPlaying. Im not sure how accurate that method is. But Im assuming it will change to false when the clip ends, and you can then trigger the playing of the next clip?

That’s what we already do, but not with coroutine, just checking in Late Updater. Unfortunately you will have a tiny gap so this is no good.