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