diff --git a/Runtime/Haptics/AudioSourceHapticPulser.cs b/Runtime/Haptics/AudioSourceHapticPulser.cs index 8c6991ed..cbf5627d 100644 --- a/Runtime/Haptics/AudioSourceHapticPulser.cs +++ b/Runtime/Haptics/AudioSourceHapticPulser.cs @@ -1,7 +1,11 @@ namespace Zinnia.Haptics { using UnityEngine; + using UnityEngine.Events; + using System; using System.Collections; + using Malimbe.MemberChangeMethod; + using Malimbe.MemberClearanceMethod; using Malimbe.PropertySerializationAttribute; using Malimbe.XmlDocumentationAttribute; @@ -13,22 +17,18 @@ public class AudioSourceHapticPulser : RoutineHapticPulser /// /// The waveform to represent the haptic pattern. /// - [Serialized] + [Serialized, Cleared] [field: DocumentedByXml] public AudioSource AudioSource { get; set; } /// - /// of the last . + /// Observer added to . /// - protected double filterReadDspTime; + protected AudioSourceDataObserver observer; /// - /// Audio data array of the last . + /// The observed audio data. /// - protected float[] filterReadData; - /// - /// Number of channels of the last . - /// - protected int filterReadChannels; + protected readonly AudioSourceDataObserver.EventData audioData = new AudioSourceDataObserver.EventData(); /// public override bool IsActive() @@ -36,42 +36,181 @@ public override bool IsActive() return base.IsActive() && AudioSource != null; } + /// + protected override void DoCancel() + { + RemoveDataObserver(); + base.DoCancel(); + } + /// /// Enumerates through and pulses for each amplitude of the wave. /// /// An Enumerator to manage the running of the Coroutine. protected override IEnumerator HapticProcessRoutine() { + AddDataObserver(); int outputSampleRate = AudioSettings.outputSampleRate; - while (AudioSource.isPlaying) + while (AudioSource != null && AudioSource.isPlaying) { - int sampleIndex = (int)((AudioSettings.dspTime - filterReadDspTime) * outputSampleRate); float currentSample = 0; - if (filterReadData != null && sampleIndex * filterReadChannels < filterReadData.Length) + if (audioData.Data != null) { - for (int i = 0; i < filterReadChannels; ++i) + int sampleIndex = (int)((AudioSettings.dspTime - audioData.DspTime) * outputSampleRate) * audioData.Channels; + sampleIndex = Mathf.Min(sampleIndex, audioData.Data.Length - audioData.Channels); + for (int i = 0; i < audioData.Channels; ++i) { - currentSample += filterReadData[sampleIndex + i]; + currentSample += Mathf.Abs(audioData.Data[sampleIndex + i]); } - currentSample /= filterReadChannels; + currentSample /= audioData.Channels; } HapticPulser.Intensity = currentSample * IntensityMultiplier; HapticPulser.Begin(); yield return null; } + RemoveDataObserver(); ResetIntensity(); } /// - /// Store currently playing audio data and additional data. + /// Adds a to the . + /// + protected virtual void AddDataObserver() + { + if (AudioSource == null) + { + return; + } + + observer = AudioSource.gameObject.AddComponent(); + observer.DataObserved.AddListener(Receive); + } + + /// + /// Remove the from the . + /// + protected virtual void RemoveDataObserver() + { + if (observer == null) + { + return; + } + + observer.DataObserved.RemoveListener(Receive); + Destroy(observer); + observer = null; + } + + /// + /// Receive audio data from . + /// + protected virtual void Receive(AudioSourceDataObserver.EventData eventData) + { + audioData.Set(eventData); + } + + /// + /// Called before has been changed. + /// + [CalledBeforeChangeOf(nameof(AudioSource))] + protected virtual void OnBeforeAudioSourceChange() + { + if (hapticRoutine == null) + { + return; + } + + RemoveDataObserver(); + } + + /// + /// Called after has been changed. + /// + [CalledAfterChangeOf(nameof(AudioSource))] + protected virtual void OnAfterAudioSourceChange() + { + if (hapticRoutine == null) + { + return; + } + + AddDataObserver(); + } + } + + /// + /// Observes the and emits the audio data. + /// + public class AudioSourceDataObserver : MonoBehaviour + { + /// + /// Holds data about a event. + /// + [Serializable] + public class EventData + { + /// + /// of the last . + /// + [Serialized] + [field: DocumentedByXml] + public double DspTime { get; set; } + /// + /// Audio data array of the last . + /// + [Serialized] + [field: DocumentedByXml] + public float[] Data { get; set; } + /// + /// Number of channels of the last . + /// + [Serialized] + [field: DocumentedByXml] + public int Channels { get; set; } + + public EventData Set(EventData source) + { + return Set(source.DspTime, source.Data, source.Channels); + } + + public EventData Set(double dspTime, float[] data, int channels) + { + DspTime = dspTime; + Data = data; + Channels = channels; + return this; + } + + public void Clear() + { + Set(default, default, default); + } + } + + /// + /// Defines the event with the . + /// + [Serializable] + public class UnityEvent : UnityEvent { } + + /// + /// Emitted whenever the audio data is observed. + /// + [DocumentedByXml] + public UnityEvent DataObserved = new UnityEvent(); + /// + /// The data to emit with an event. + /// + protected readonly EventData eventData = new EventData(); + + /// + /// Emits audio data. /// /// An array of floats comprising the audio data. /// An int that stores the number of channels of audio data passed to this delegate. protected virtual void OnAudioFilterRead(float[] data, int channels) { - filterReadDspTime = AudioSettings.dspTime; - filterReadData = data; - filterReadChannels = channels; + DataObserved?.Invoke(eventData.Set(AudioSettings.dspTime, data, channels)); } } } \ No newline at end of file