Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 158 additions & 19 deletions Runtime/Haptics/AudioSourceHapticPulser.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -13,65 +17,200 @@ public class AudioSourceHapticPulser : RoutineHapticPulser
/// <summary>
/// The waveform to represent the haptic pattern.
/// </summary>
[Serialized]
[Serialized, Cleared]
[field: DocumentedByXml]
public AudioSource AudioSource { get; set; }

/// <summary>
/// <see cref="AudioSettings.dspTime"/> of the last <see cref="OnAudioFilterRead"/>.
/// Observer added to <see cref="AudioSource"/>.
/// </summary>
protected double filterReadDspTime;
protected AudioSourceDataObserver observer;
/// <summary>
/// Audio data array of the last <see cref="OnAudioFilterRead"/>.
/// The observed audio data.
/// </summary>
protected float[] filterReadData;
/// <summary>
/// Number of channels of the last <see cref="OnAudioFilterRead"/>.
/// </summary>
protected int filterReadChannels;
protected readonly AudioSourceDataObserver.EventData audioData = new AudioSourceDataObserver.EventData();

/// <inheritdoc />
public override bool IsActive()
{
return base.IsActive() && AudioSource != null;
}

/// <inheritdoc />
protected override void DoCancel()
{
RemoveDataObserver();
base.DoCancel();
}

/// <summary>
/// Enumerates through <see cref="AudioSource"/> and pulses for each amplitude of the wave.
/// </summary>
/// <returns>An Enumerator to manage the running of the Coroutine.</returns>
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();
}

/// <summary>
/// Store currently playing audio data and additional data.
/// Adds a <see cref="AudioSourceHapticPulserDataObserver"/> to the <see cref="AudioSource"/>.
/// </summary>
protected virtual void AddDataObserver()
{
if (AudioSource == null)
{
return;
}

observer = AudioSource.gameObject.AddComponent<AudioSourceDataObserver>();
observer.DataObserved.AddListener(Receive);
}

/// <summary>
/// Remove the <see cref="AudioSourceHapticPulserDataObserver"/> from the <see cref="AudioSource"/>.
/// </summary>
protected virtual void RemoveDataObserver()
{
if (observer == null)
{
return;
}

observer.DataObserved.RemoveListener(Receive);
Destroy(observer);
observer = null;
}

/// <summary>
/// Receive audio data from <see cref="AudioSourceHapticPulserDataObserver"/>.
/// </summary>
protected virtual void Receive(AudioSourceDataObserver.EventData eventData)
{
audioData.Set(eventData);
}

/// <summary>
/// Called before <see cref="AudioSource"/> has been changed.
/// </summary>
[CalledBeforeChangeOf(nameof(AudioSource))]
protected virtual void OnBeforeAudioSourceChange()
{
if (hapticRoutine == null)
{
return;
}

RemoveDataObserver();
}

/// <summary>
/// Called after <see cref="AudioSource"/> has been changed.
/// </summary>
[CalledAfterChangeOf(nameof(AudioSource))]
protected virtual void OnAfterAudioSourceChange()
{
if (hapticRoutine == null)
{
return;
}

AddDataObserver();
}
}

/// <summary>
/// Observes the <see cref="AudioSource"/> and emits the audio data.
/// </summary>
public class AudioSourceDataObserver : MonoBehaviour
{
/// <summary>
/// Holds data about a <see cref="AudioSourceDataObserver"/> event.
/// </summary>
[Serializable]
public class EventData
{
/// <summary>
/// <see cref="AudioSettings.dspTime"/> of the last <see cref="OnAudioFilterRead"/>.
/// </summary>
[Serialized]
[field: DocumentedByXml]
public double DspTime { get; set; }
/// <summary>
/// Audio data array of the last <see cref="OnAudioFilterRead"/>.
/// </summary>
[Serialized]
[field: DocumentedByXml]
public float[] Data { get; set; }
/// <summary>
/// Number of channels of the last <see cref="OnAudioFilterRead"/>.
/// </summary>
[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);
}
}

/// <summary>
/// Defines the event with the <see cref="EventData"/>.
/// </summary>
[Serializable]
public class UnityEvent : UnityEvent<EventData> { }

/// <summary>
/// Emitted whenever the audio data is observed.
/// </summary>
[DocumentedByXml]
public UnityEvent DataObserved = new UnityEvent();
/// <summary>
/// The data to emit with an event.
/// </summary>
protected readonly EventData eventData = new EventData();

/// <summary>
/// Emits audio data.
/// </summary>
/// <param name="data">An array of floats comprising the audio data.</param>
/// <param name="channels">An int that stores the number of channels of audio data passed to this delegate.</param>
protected virtual void OnAudioFilterRead(float[] data, int channels)
{
filterReadDspTime = AudioSettings.dspTime;
filterReadData = data;
filterReadChannels = channels;
DataObserved?.Invoke(eventData.Set(AudioSettings.dspTime, data, channels));
}
}
}