You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CrowdControl/Assets/Feel/MMTools/Tools/MMAudio/AudioAnalyzer/MMAudioAnalyzer.cs

474 lines
14 KiB
C#

3 months ago
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MoreMountains.Tools;
using System;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
namespace MoreMountains.Tools
{
/// <summary>
/// A static class used to save / load peaks once they've been computed
/// </summary>
public static class PeaksSaver
{
public static float[] Peaks;
}
/// <summary>
/// An event you can listen to that will get automatically triggered for every remapped beat
/// </summary>
public struct MMBeatEvent
{
static private event Delegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
static public void Register(Delegate callback) { OnEvent += callback; }
static public void Unregister(Delegate callback) { OnEvent -= callback; }
public delegate void Delegate(string name, float value);
static public void Trigger(string name, float value)
{
OnEvent?.Invoke(name, value);
}
}
[Serializable]
public class Beat
{
public string Name = "Beat";
public enum Modes { Raw, Normalized, BufferedRaw, BufferedNormalized, Amplitude, NormalizedAmplitude, AmplitudeBuffered, NormalizedAmplitudeBuffered }
// remapped will send beat events when a threshold is passed, live just updates the value with whatever value is reading right now
public enum BeatValueModes { Remapped, Live }
public Modes Mode = Modes.BufferedNormalized;
public BeatValueModes BeatValueMode = BeatValueModes.Remapped;
[MMEnumCondition("Mode", (int)Modes.Raw, (int)Modes.Normalized, (int)Modes.BufferedRaw, (int)Modes.BufferedNormalized)]
public Color BeatColor = Color.cyan;
public int BandID = 0;
public float Threshold = 0.5f;
public float MinimumTimeBetweenBeats = 0.25f;
[MMEnumCondition("BeatValueMode", (int)BeatValueModes.Remapped)]
public float RemappedAttack = 0.05f;
[MMEnumCondition("BeatValueMode", (int)BeatValueModes.Remapped)]
public float RemappedDecay = 0.2f;
[MMReadOnly]
public bool BeatThisFrame;
[MMReadOnly]
public float CurrentValue;
[HideInInspector]
public float _previousValue;
[HideInInspector]
public float _lastBeatAt;
[HideInInspector]
public float _lastBeatValue;
[HideInInspector]
public bool _initialized = false;
public UnityEvent OnBeat;
public void InitializeIfNeeded(int id, int bandID)
{
if (!_initialized)
{
Mode = Modes.Normalized;
BeatValueMode = BeatValueModes.Remapped;
Name = "Beat " + id;
BeatColor = MMColors.RandomColor();
BandID = bandID;
Threshold = 0.3f + id * 0.02f;
if (Threshold > 0.6f) { Threshold -= 0.5f; }
Threshold = Threshold % 1f;
MinimumTimeBetweenBeats = 0.25f + id * 0.02f;
RemappedAttack = 0.05f + id * 0.01f;
RemappedDecay = 0.2f + id * 0.01f;
_initialized = true;
}
}
}
/// <summary>
/// This component lets you pick an audio source (either global : the whole scene's audio, a unique source, or the
/// microphone), and will cut it into chunks that you can then use to emit beat events, that other objects can consume and act upon.
/// The sample interval is the frequency at which sound will be analyzed, the amount of spectrum samples will determine the
/// accuracy of the sampling, the window defines the method used to reduce leakage, and the number of bands
/// will determine in how many bands you want to cut the sound. The more bands, the more levers you'll have to play with afterwards.
/// In general, for all of these settings, higher values mean better quality and lower performance. The buffer speed determines how
/// fast buffered band levels readjust.
/// </summary>
[AddComponentMenu("More Mountains/Tools/Audio/MMAudioAnalyzer")]
public class MMAudioAnalyzer : MonoBehaviour
{
public enum Modes { Global, AudioSource, Microphone }
[Header("Source")]
[MMInformation("This component lets you pick an audio source (either global : the whole scene's audio, a unique source, or the " +
"microphone), and will cut it into chunks that you can then use to emit beat events, that other objects can consume and act upon. " +
"The sample interval is the frequency at which sound will be analyzed, the amount of spectrum samples will determine the " +
"accuracy of the sampling, the window defines the method used to reduce leakage, and the number of bands " +
"will determine in how many bands you want to cut the sound. The more bands, the more levers you'll have to play with afterwards." +
"In general, for all of these settings, higher values mean better quality and lower performance. The buffer speed determines how " +
"fast buffered band levels readjust.", MoreMountains.Tools.MMInformationAttribute.InformationType.Info, false)]
[MMReadOnlyWhenPlaying]
public Modes Mode = Modes.Global;
[MMEnumCondition("Mode", (int)Modes.AudioSource)]
[MMReadOnlyWhenPlaying]
public AudioSource TargetAudioSource;
[MMEnumCondition("Mode", (int)Modes.Microphone)]
public int MicrophoneID = 0;
[Header("Sampling")]
[MMReadOnlyWhenPlaying]
public float SampleInterval = 0.02f;
[MMDropdown(2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192)]
[MMReadOnlyWhenPlaying]
public int SpectrumSamples = 1024;
[MMReadOnlyWhenPlaying]
public FFTWindow Window = FFTWindow.Rectangular;
[Range(1, 64)]
[MMReadOnlyWhenPlaying]
public int NumberOfBands = 8;
public float BufferSpeed = 2f;
[Header("Beat Events")]
public Beat[] Beats;
[HideInInspector]
public float[] RawSpectrum;
[HideInInspector]
public float[] BandLevels;
[HideInInspector]
public float[] BufferedBandLevels;
[HideInInspector]
public float[] BandPeaks;
[HideInInspector]
public float[] LastPeaksAt;
[HideInInspector]
public float[] NormalizedBandLevels;
[HideInInspector]
public float[] NormalizedBufferedBandLevels;
[HideInInspector]
public float Amplitude;
[HideInInspector]
public float NormalizedAmplitude;
[HideInInspector]
public float BufferedAmplitude;
[HideInInspector]
public float NormalizedBufferedAmplitude;
[HideInInspector]
public bool Active = false;
[HideInInspector]
public bool PeaksPasted = false;
protected const int _microphoneDuration = 5;
protected string _microphone;
protected float _microphoneStartedAt = 0f;
protected const float _microphoneDelay = 0.030f;
protected const float _microphoneFrequency = 24000f;
protected WaitForSeconds _sampleIntervalWaitForSeconds;
protected int _cachedNumberOfBands;
public virtual void FindPeaks()
{
float time = 0f;
while (time < TargetAudioSource.clip.length)
{
TargetAudioSource.time = time;
TargetAudioSource.GetSpectrumData(RawSpectrum, 0, Window);
time += SampleInterval;
ComputeBandLevels();
PeaksSaver.Peaks = BandPeaks;
}
}
public virtual void PastePeaks()
{
BandPeaks = PeaksSaver.Peaks;
PeaksSaver.Peaks = null;
PeaksPasted = true;
}
public virtual void ClearPeaks()
{
BandPeaks = null;
PeaksSaver.Peaks = null;
PeaksPasted = false;
}
protected virtual void Awake()
{
Initialization();
}
public virtual void Initialization()
{
_cachedNumberOfBands = NumberOfBands;
RawSpectrum = new float[SpectrumSamples];
BandLevels = new float[_cachedNumberOfBands];
BufferedBandLevels = new float[_cachedNumberOfBands];
// we make sure our peaks match our bands
if ((BandPeaks == null) || (BandPeaks.Length == 0))
{
BandPeaks = new float[_cachedNumberOfBands];
PeaksPasted = false;
}
if (BandPeaks.Length != BandLevels.Length)
{
BandPeaks = new float[_cachedNumberOfBands];
PeaksPasted = false;
}
LastPeaksAt = new float[_cachedNumberOfBands];
NormalizedBandLevels = new float[_cachedNumberOfBands];
NormalizedBufferedBandLevels = new float[_cachedNumberOfBands];
if ((Mode == Modes.AudioSource) && (TargetAudioSource == null))
{
Debug.LogError(this.name + " : this MMAudioAnalyzer needs a target audio source to operate.");
return;
}
if (Mode == Modes.Microphone)
{
#if !UNITY_WEBGL
GameObject audioSourceGo = new GameObject("Microphone");
SceneManager.MoveGameObjectToScene(audioSourceGo, this.gameObject.scene);
audioSourceGo.transform.SetParent(this.gameObject.transform);
TargetAudioSource = audioSourceGo.AddComponent<AudioSource>();
//UNCOMMENT_MICROPHONE string _microphone = Microphone.devices[MicrophoneID].ToString();
//UNCOMMENT_MICROPHONE TargetAudioSource.clip = Microphone.Start(_microphone, true, _microphoneDuration, (int)_microphoneFrequency);
//UNCOMMENT_MICROPHONE TargetAudioSource.Play();
_microphoneStartedAt = Time.time;
#endif
}
Active = true;
_sampleIntervalWaitForSeconds = new WaitForSeconds(SampleInterval);
StartCoroutine(Analyze());
}
protected virtual void Update()
{
HandleBuffer();
ComputeAmplitudes();
HandleBeats();
}
protected virtual IEnumerator Analyze()
{
while (true)
{
switch (Mode)
{
case Modes.AudioSource:
TargetAudioSource.GetSpectrumData(RawSpectrum, 0, Window);
break;
case Modes.Global:
AudioListener.GetSpectrumData(RawSpectrum, 0, Window);
break;
case Modes.Microphone:
#if !UNITY_WEBGL
int microphoneSamples = 0;
//UNCOMMENT_MICROPHONE microphoneSamples = Microphone.GetPosition(_microphone);
if (microphoneSamples / _microphoneFrequency > _microphoneDelay)
{
if (!TargetAudioSource.isPlaying)
{
TargetAudioSource.timeSamples = (int)(microphoneSamples - (_microphoneDelay * _microphoneFrequency));
TargetAudioSource.Play();
}
_microphoneStartedAt = Time.time;
}
AudioListener.GetSpectrumData(RawSpectrum, 0, Window);
#endif
break;
}
ComputeBandLevels();
yield return _sampleIntervalWaitForSeconds;
}
}
protected virtual void HandleBuffer()
{
for (int i = 0; i < BandLevels.Length; i++)
{
BufferedBandLevels[i] = Mathf.Max(BufferedBandLevels[i] * Mathf.Exp(-BufferSpeed * Time.deltaTime), BandLevels[i]);
NormalizedBandLevels[i] = BandLevels[i] / BandPeaks[i];
NormalizedBufferedBandLevels[i] = BufferedBandLevels[i] / BandPeaks[i];
}
}
protected virtual void ComputeBandLevels()
{
float coefficient = Mathf.Log(RawSpectrum.Length);
int offset = 0;
for (int i = 0; i < BandLevels.Length; i++)
{
float savedSum = 0f;
float next = Mathf.Exp(coefficient / BandLevels.Length * (i + 1));
float weight = 1f / (next - offset);
for (float sum = 0f; offset < next; offset++)
{
sum += RawSpectrum[offset];
savedSum = sum;
}
BandLevels[i] = Mathf.Sqrt(weight * savedSum);
if (BandLevels[i] > BandPeaks[i])
{
BandPeaks[i] = BandLevels[i];
LastPeaksAt[i] = Time.time;
}
}
}
protected virtual void ComputeAmplitudes()
{
Amplitude = 0f;
BufferedAmplitude = 0f;
NormalizedAmplitude = 0f;
NormalizedBufferedAmplitude = 0f;
for (int i = 0; i < _cachedNumberOfBands; i++)
{
Amplitude += BandLevels[i];
BufferedAmplitude += BufferedBandLevels[i];
NormalizedAmplitude += NormalizedBandLevels[i];
NormalizedBufferedAmplitude += NormalizedBufferedBandLevels[i];
}
Amplitude = Amplitude / _cachedNumberOfBands;
BufferedAmplitude = BufferedAmplitude / _cachedNumberOfBands;
NormalizedAmplitude = NormalizedAmplitude / _cachedNumberOfBands;
NormalizedBufferedAmplitude = NormalizedBufferedAmplitude / _cachedNumberOfBands;
}
protected virtual void HandleBeats()
{
if (Beats.Length <= 0)
{
return;
}
foreach (Beat beat in Beats)
{
float value = 0f;
beat.BeatThisFrame = false;
switch (beat.Mode)
{
case Beat.Modes.Amplitude:
value = Amplitude;
break;
case Beat.Modes.AmplitudeBuffered:
value = BufferedAmplitude;
break;
case Beat.Modes.BufferedNormalized:
value = NormalizedBufferedBandLevels[beat.BandID];
break;
case Beat.Modes.BufferedRaw:
value = BufferedBandLevels[beat.BandID];
break;
case Beat.Modes.Normalized:
value = NormalizedBandLevels[beat.BandID];
break;
case Beat.Modes.NormalizedAmplitude:
value = NormalizedAmplitude;
break;
case Beat.Modes.NormalizedAmplitudeBuffered:
value = NormalizedBufferedAmplitude;
break;
case Beat.Modes.Raw:
value = BandLevels[beat.BandID];
break;
}
if (beat.BeatValueMode == Beat.BeatValueModes.Live)
{
beat.CurrentValue = value;
}
else
{
// if audio value went below the bias during this frame
if ((beat._previousValue > beat.Threshold) && (value <= beat.Threshold))
{
// if minimum beat interval is reached
if (Time.time - beat._lastBeatAt > beat.MinimumTimeBetweenBeats)
{
OnBeat(beat, value);
}
}
// if audio value went above the bias during this frame
if ((beat._previousValue <= beat.Threshold) && (value > beat.Threshold))
{
// if minimum beat interval is reached
if (Time.time - beat._lastBeatAt > beat.MinimumTimeBetweenBeats)
{
OnBeat(beat, value);
}
}
beat._previousValue = value;
}
}
}
protected virtual void OnBeat(Beat beat, float rawValue)
{
beat._lastBeatAt = Time.time;
beat.BeatThisFrame = true;
if (beat.OnBeat != null)
{
beat.OnBeat.Invoke();
}
MMBeatEvent.Trigger(beat.Name, beat.CurrentValue);
StartCoroutine(RemapBeat(beat));
}
protected virtual IEnumerator RemapBeat(Beat beat)
{
float remapStartedAt = Time.time;
while (Time.time - remapStartedAt < beat.RemappedAttack + beat.RemappedDecay)
{
// attack
if (Time.time - remapStartedAt < beat.RemappedAttack)
{
beat.CurrentValue = Mathf.Lerp(0f, 1f, (Time.time - remapStartedAt) / beat.RemappedAttack);
}
if (Time.time - remapStartedAt > beat.RemappedAttack)
{
beat.CurrentValue = Mathf.Lerp(1f, 0f, (Time.time - remapStartedAt - beat.RemappedAttack) / beat.RemappedDecay);
}
yield return null;
}
beat.CurrentValue = 0f;
yield break;
}
protected virtual void OnValidate()
{
if ((Beats == null) || (Beats.Length == 0))
{
return;
}
int bandCounter = 0;
for (int i = 0; i < Beats.Length; i++)
{
if (bandCounter >= _cachedNumberOfBands)
{
bandCounter = 0;
}
Beats[i].InitializeIfNeeded(i, bandCounter);
bandCounter++;
}
}
}
}