Skip to content

Instantly share code, notes, and snippets.

@kurtdekker
Created November 25, 2024 05:19
Show Gist options
  • Save kurtdekker/6efc15a0ca1dbd52da0f3f07139d58e7 to your computer and use it in GitHub Desktop.
Save kurtdekker/6efc15a0ca1dbd52da0f3f07139d58e7 to your computer and use it in GitHub Desktop.
Shepard Tones for Unity3D
namespace Zooming
{
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// @kurtdekker - Shepard Tones for Unity3D.
//
// https://en.wikipedia.org/wiki/Shepard_tone
//
public class ShepardTones : MonoBehaviour
{
public class Tone
{
public float theta;
public float frequency;
public float amplitude;
}
List<Tone> AllTones = new List<Tone>();
// one over sample frequency
float sampleK;
// base frequency
const float F0 = 100;
const int octaveCount = 3;
const int octaveSpan = 4;
AudioSource azz;
public static ShepardTones Create()
{
GameObject go = new GameObject("ShepherdTones.Create();");
var st = go.AddComponent<ShepardTones>();
var azz = go.AddComponent<AudioSource>();
azz.bypassListenerEffects = true;
azz.Play();
st.azz = azz;
return st;
}
const float VolumeSnappiness = 2.0f;
float currentVolume;
float desiredVolume;
float actualVolume;
public void SetVolume( float v)
{
desiredVolume = Mathf.Clamp01(v);
}
bool dead;
public void SetDead()
{
dead = true;
}
void Start()
{
sampleK = 1.0f / 22050;
// this detunes the pair frequency by a linear amount, making
// for an ever-evolving beat frequency
float beatFrequencyOffset = Random.Range(1.0f, 3.0f);
// make all the octaves
for (int octave = 0; octave < octaveCount; octave++)
{
float f = F0 * Mathf.Pow(2, octave);
// the core frequency
AllTones.Add(new Tone
{
frequency = f,
});
// the linear detuned frequency
AllTones.Add(new Tone
{
frequency = f + beatFrequencyOffset,
});
}
}
void Update()
{
currentVolume = Mathf.Lerp(currentVolume, desiredVolume, VolumeSnappiness * Time.deltaTime);
actualVolume = currentVolume;
if (Time.timeScale == 0) actualVolume = 0;
// all sounds fall to zero
if (dead)
{
foreach (var tone in AllTones)
{
float f = tone.frequency;
// linear - sounds intentionally awful
float fx = f / 2 + 250;
f -= fx * 0.5f * Time.deltaTime;
tone.frequency = f;
tone.amplitude -= Time.deltaTime * 0.05f;
if (f < 100)
{
tone.amplitude = 0;
}
}
return;
}
// frequency bands:
// fade in between these frequencies
float F1 = F0;
float F2 = F1 * 2;
// fade out between these frequencies
float F3 = F1 * Mathf.Pow( 2, octaveSpan - 1);
float F4 = F3 * 2;
foreach (var tone in AllTones)
{
float f = tone.frequency;
f += f * 0.1f * Time.deltaTime;
if (f >= F4)
{
f /= (F4 / F1);
}
tone.frequency = f;
float amplitude = 1.0f;
// fading in?
if (f < F2)
{
amplitude = (f - F1) / (F2 - F1);
}
// fading out?
if (f > F3)
{
amplitude = 1.0f - (f - F3) / (F4 - F3);
}
tone.amplitude = amplitude;
}
}
private void OnAudioFilterRead(float[] data, int channels)
{
float volume = actualVolume;
for (int i = 0; i < data.Length; i += channels)
{
for (int channel = 0; channel < channels; channel++)
{
int n = i + channel;
float d0 = data[n];
foreach (var tone in AllTones)
{
float theta = tone.theta;
theta += sampleK * tone.frequency;
if (theta >= Mathf.PI * 2) theta -= Mathf.PI * 2;
tone.theta = theta;
float sample = Mathf.Sin(theta);
sample *= tone.amplitude;
d0 += sample;
}
data[n] = d0 * volume;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment