From f0d0a87f30493d8c258ac93d8fb5fa5dbcb3c830 Mon Sep 17 00:00:00 2001 From: Ethan Cheung Date: Wed, 30 Oct 2019 00:40:38 +0800 Subject: [PATCH] fix(Haptics): allow AudioSourceHapticPulser be placed on other object The pulser uses OnAudioFilterRead, it has to be placed right beneath the audio source. But the way it is presented in the inpsector deceived us to think it can be placed elsewhere. This fix allows it to be placed on other object. Also fixed not getting pulse due to frame lag behind audio thread too much. --- Runtime/Haptics/AudioSourceHapticPulser.cs | 177 ++++++++++++++++++--- 1 file changed, 158 insertions(+), 19 deletions(-) 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