Sound for moving parts - movement rate problem.

I haven’t done a lot of work with audio in Unity, and I’m having trouble finding relevant information on what I’m trying to accomplish.

I’ve got some moving parts, and I want to play some audio when the part moves, and change the pitch of the sound depending on the motion speed.

The problem is that any inconsistencies in the motion mess with the playback.
So far, I’m getting things to work with ugly hacks - but I don’t like ugly hacks. I’m thinking the must be a better way to do this.

Here’s an example:
I’ve got a turret and a looping mechanical audio source. When the turret rotates, the sound should play and change pitch depending on the rotation rate of the turret.
This part is good in theory, but begins to fail in practice.

Here is a simple bit of rotation code (the most typical way to create it)

Quaternion _rot = Quaternion.LookRotation( target - rotator.transform.position, rotator.transform.up );
rotator.transform.rotation = Quaternion.RotateTowards( rotator.transform.rotation, _rot, _rotationRate * Time.deltaTime );

And here is some starting audio code:

void TurnSound ( AudioSource sound, float pitchMin, float rate, float rateMax ) {
    if ( rate > 0f ) {
        sound.pitch = pitchMin + (( rate / rateMax ) * (1f - pitchMin));
        if ( sound.isPlaying == false ) { // don't repeatedly call Play() if already playing
            sound.Play();
        }
    } else {
        sound.Stop();
    }
}

The audio will play at a pitch of 1 when at full rotation rate, and as the rate approaches 0, then pitch approaches pitchMin.
Rate is simply:

float rate = Quaternion.Angle( lastRotation, currentRotation ) / Time.deltaTime;

The problem, is that (depending on rotation technique) you get a frame or two where either rate momentarily isn’t 0 when not moving, or rate momentarily is 0 when moving.
In the above example, it’s the latter problem, and this happens every few frames causing the audio to rapidly start and stop causing lots of audio jitter and garbling. (Mainly when the turret isn’t rotating at full speed because it’s tracking a target.)

The workaround seems to be to something like this:

void TurnSound ( AudioSource sound, float pitchMin, float rate, float rateMax ) {
    if ( rate > .1f ) { // avoid near 0 values
        sound.pitch = pitchMin + (( rate / rateMax ) * (1f - pitchMin));
        if ( sound.isPlaying == false ) { // don't repeatedly call Play() if already playing
            sound.Play();
        }
        audioStopTime = Time.time + .1f; // continuously refresh stop time
    } else {
        if ( Time.time >= audioStopTime ) { // make sure rotation has been stopped for at least .1 seconds
            sound.Stop();
        }
    }
}

This way adds a rate threshold before the sound will begin, and adds a slight delay upon stopping that will gloss over any momentary 0 rate occurrences.
Unfortunately, I’m going to need an audioStopTime for every audio source. The clearest way seems to be to wrap a time variable and the audioSource together in a class or struct.

I feel like the code is becoming a mess for something that should be straightforward.

Maybe I’m just not yet well versed enough with audio.
Any advice?

Let’s try something really simple first: Average it over time.
Instead of wiring your calculated pitch to the actual pitch of the AudioSource directly, use Mathf.Lerp() to smooth out sudden changes.
This will increase the latency (the delay between making and hearing a change in the rotation rate) based on how slow/fast you perform the linear interpolation. Your job is to find the sweet-spot.

I don’t think that will help.
The periodic zeros have the effect of triggering Stop(), only to immediately Play() on the next frame. Averaging the pitch won’t prevent that.
During debugging, I’ve tried eliminating the pitch change to get a better understanding of the problem, and the issue persists. Also, debugging has confirmed that Stop() and Play() are continuously being called during non-max-rate motion.

As to why the zeros are being generated in the first place, I suspect it’s a byproduct of the built-in ‘don’t overshoot’ feature of Quaternion.RotateTowards()

I could average the reading of the rotation of the part itself over a few frames, but that doesn’t seem like much of an improvement over what I’m doing now.

Okay.
I do something similar with how doors squeak in my game. I’m really tired right now and it’s getting late, so I won’t be looking through the code right now. I’ll see if I find something of use tomorrow.

One thing you could check is how those zeros happen. Is it possible that you’re running into floating point precision errors when the movement during one frame is too slow?

The rotation technique is the quite standard way of doing things, so there’s probably not much I can do to prevent it. At least not without re-writing RotateTowards(), and that’s likely to produce more problems than it fixes.

Since I’m getting actual zeros, (and not just really small values) it might be something with the way Unity updates the rotation that occasionally gets out of sync with Update() (this isn’t an Update/FixedUpdate mismatch, either)

So I looked at my code. It probably won’t help you in your case, but the way I do it is by using the rigidbody’s angular velocity along the y axis as input. This works because in my case the objects in question are physics objects.

I just looked at your code again and this is something that stuck me as odd:

float rate = Quaternion.Angle( lastRotation, currentRotation ) / Time.deltaTime;

So what you’re doing here is determining how much the turret rotated by looking at how the rotation changed over time. This seems incredibly wasteful because at some point prior to that you must have already calculated that value in order to actually perform the rotation of the GameObject. Why not use that value instead?

Nope.
I have a rotation rate variable, but this is really a max rate, because once the turret is tracking an object, it will usually be rotating slower.
Quaternion.LookRotation() is the total rotation needed to turn towards the goal.
Quaternion.RotateTowards() uses LookRotation to find the actual stepping, and it doesn’t return how much the part moved, just where it ends up. So if I want how much it actually moved, I have to save the previous rotation.