From 0c582b3895f820590e55944d41a86d8df1085875 Mon Sep 17 00:00:00 2001 From: Ethan Cheung Date: Sat, 2 Nov 2019 02:46:04 +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/Audio.meta | 8 ++ Runtime/Audio/AudioSourceDataObserver.cs | 104 ++++++++++++++++++ Runtime/Audio/AudioSourceDataObserver.cs.meta | 11 ++ Runtime/Haptics/AudioSourceHapticPulser.cs | 94 ++++++++++++---- 4 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 Runtime/Audio.meta create mode 100644 Runtime/Audio/AudioSourceDataObserver.cs create mode 100644 Runtime/Audio/AudioSourceDataObserver.cs.meta diff --git a/Runtime/Audio.meta b/Runtime/Audio.meta new file mode 100644 index 00000000..7f0ce74e --- /dev/null +++ b/Runtime/Audio.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0ef98c6cf0eb71e4da673fdda0eb7d7d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioSourceDataObserver.cs b/Runtime/Audio/AudioSourceDataObserver.cs new file mode 100644 index 00000000..6fa40b6f --- /dev/null +++ b/Runtime/Audio/AudioSourceDataObserver.cs @@ -0,0 +1,104 @@ +namespace Zinnia.Audio +{ + using UnityEngine; + using UnityEngine.Events; + using System; + using Malimbe.PropertySerializationAttribute; + using Malimbe.XmlDocumentationAttribute; + + /// + /// Observes the and emits the audio data. + /// + [RequireComponent(typeof(AudioSource))] + 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(); + + /// + /// Returns whether the is player. + /// + public virtual bool IsAudioSourcePlaying() => audioSource != null && audioSource.isPlaying; + + /// + /// The to observe. + /// + protected AudioSource audioSource; + + /// + /// Caches the . + /// + protected virtual void Awake() + { + audioSource = GetComponent(); + } + + /// + /// 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) + { + DataObserved?.Invoke(eventData.Set(AudioSettings.dspTime, data, channels)); + } + } +} \ No newline at end of file diff --git a/Runtime/Audio/AudioSourceDataObserver.cs.meta b/Runtime/Audio/AudioSourceDataObserver.cs.meta new file mode 100644 index 00000000..c18fb1b5 --- /dev/null +++ b/Runtime/Audio/AudioSourceDataObserver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 001ca6370643f01489fab678a6f730de +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Haptics/AudioSourceHapticPulser.cs b/Runtime/Haptics/AudioSourceHapticPulser.cs index 8c6991ed..b80f4d13 100644 --- a/Runtime/Haptics/AudioSourceHapticPulser.cs +++ b/Runtime/Haptics/AudioSourceHapticPulser.cs @@ -2,8 +2,11 @@ { using UnityEngine; using System.Collections; + using Malimbe.MemberChangeMethod; + using Malimbe.MemberClearanceMethod; using Malimbe.PropertySerializationAttribute; using Malimbe.XmlDocumentationAttribute; + using Zinnia.Audio; /// /// Creates a haptic pattern based on the waveform of an and utilizes a to create the effect. @@ -11,29 +14,47 @@ public class AudioSourceHapticPulser : RoutineHapticPulser { /// - /// The waveform to represent the haptic pattern. + /// Observer that provides audio data from a . /// - [Serialized] + [Serialized, Cleared] [field: DocumentedByXml] - public AudioSource AudioSource { get; set; } + public AudioSourceDataObserver Observer { get; set; } /// - /// of the last . + /// A reused data instance. /// - protected double filterReadDspTime; + protected readonly AudioSourceDataObserver.EventData audioData = new AudioSourceDataObserver.EventData(); + /// - /// Audio data array of the last . + /// Subscribes as a listener to the . /// - protected float[] filterReadData; + protected virtual void SubscribeToObserver() + { + if (Observer == null) + { + return; + } + + Observer.DataObserved.AddListener(Receive); + } + /// - /// Number of channels of the last . + /// Unsubscribes from listening to the . /// - protected int filterReadChannels; + protected virtual void UnsubscribeFromObserver() + { + if (Observer == null) + { + return; + } + + Observer.DataObserved.RemoveListener(Receive); + } /// public override bool IsActive() { - return base.IsActive() && AudioSource != null; + return base.IsActive() && Observer != null; } /// @@ -42,36 +63,63 @@ public override bool IsActive() /// An Enumerator to manage the running of the Coroutine. protected override IEnumerator HapticProcessRoutine() { + SubscribeToObserver(); int outputSampleRate = AudioSettings.outputSampleRate; - while (AudioSource.isPlaying) + while (Observer != null && Observer.IsAudioSourcePlaying()) { - 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; } + UnsubscribeFromObserver(); ResetIntensity(); } /// - /// Store currently playing audio data and additional data. + /// Receive audio data from . + /// + protected virtual void Receive(AudioSourceDataObserver.EventData eventData) + { + audioData.Set(eventData); + } + + /// + /// Called before has been changed. + /// + [CalledBeforeChangeOf(nameof(Observer))] + protected virtual void OnBeforeObserverChange() + { + if (hapticRoutine == null) + { + return; + } + + UnsubscribeFromObserver(); + } + + /// + /// Called after has been changed. /// - /// 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) + [CalledAfterChangeOf(nameof(Observer))] + protected virtual void OnAfterObserverChange() { - filterReadDspTime = AudioSettings.dspTime; - filterReadData = data; - filterReadChannels = channels; + if (hapticRoutine == null) + { + return; + } + + SubscribeToObserver(); } } } \ No newline at end of file