Previously, I discussed how I had solved the bike vibrations using audio clips and the downsides to that. In this post, I’ll be discussing why and how I would simulate it differently by generating a triangle wave in Unity at runtime. My first instinct is to use wave form formula that I’m used to seeing like this:
//Returns a value along a Triangle Wave form based on time
public float TriangleWave(float inTime, float frequency, float sampleRate, float amplitude)
{
return (amplitude - ((2 * amplitude) / Mathf.PI)) * Mathf.Acos(Mathf.Sin(((2 * Mathf.PI) * frequency * inTime / sampleRate)));
}
This way I could control it at runtime, there would be no need for transitioning to another audio clip, and theoretically no clipping… theoretically.
Running this as my data through OnAudioFilterRead(), if I modify the frequency at runtime, there is absolutely clipping, which in retrospect makes sense. Based on the fact that at the moment that I change the frequency with the same time input, two frequencies will not always line up, you would very much get a clip.
Exhibit A:

Clearly, this is something I was trying to avoid in the first place. But how do I solve this?
Luckily, I asked an audio programmer on what I was doing wrong. And they told me that I shouldn’t be thinking about time, but about phase.
This confused me for a bit. I had heard the term, but wasn’t certain of the definition so I looked it up and phase is just using rotations as your measurement for where the wave form is in “time” and 2π is the length of an entire wave form period.
2π? That’s in the equation I’m already using… am I already using phase? But I’m also using time…
What if I get rid of time?

//Returns a value along a Triangle Wave form
private float TriangleWavePhase(float amplitude, float phase)
{
return (amplitude - ((2 * amplitude) / Mathf.PI)) * Mathf.Acos(Mathf.Sin(phase));
}
Yep, much better.
Then I calculate the phase in OnAudioFilterRead() and make sure I’m only dealing with one period length at a time.
void OnAudioFilterRead(float[] data, int channels)
{
for (int i = 0; i < data.Length; i += channels)
{
phase += (2 * Mathf.PI) * frequency / sampleRate;
referenceValue = TriangleWavePhase(amplitude, phase);
data[i] = referenceValue;
if(phase >= 2* Mathf.PI)
{
phase -= 2 * Mathf.PI;
}
}
}
This removes clipping, is modifiable at runtime, saves disk space, less memory, performs much more efficiently, and can be controlled by the system that I’m already using for the audio events by simply adding new scriptable objects with overridden methods to control the frequency!
Seems like a much better time to me. 🕒
