Skip to content

Commit 77ad58d

Browse files
author
Ethan Cheung
committed
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.
1 parent 72d9102 commit 77ad58d

1 file changed

Lines changed: 151 additions & 19 deletions

File tree

Lines changed: 151 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
namespace Zinnia.Haptics
22
{
33
using UnityEngine;
4+
using UnityEngine.Events;
5+
using System;
46
using System.Collections;
7+
using Malimbe.MemberChangeMethod;
8+
using Malimbe.MemberClearanceMethod;
59
using Malimbe.PropertySerializationAttribute;
610
using Malimbe.XmlDocumentationAttribute;
711

@@ -13,22 +17,18 @@ public class AudioSourceHapticPulser : RoutineHapticPulser
1317
/// <summary>
1418
/// The waveform to represent the haptic pattern.
1519
/// </summary>
16-
[Serialized]
20+
[Serialized, Cleared]
1721
[field: DocumentedByXml]
1822
public AudioSource AudioSource { get; set; }
1923

2024
/// <summary>
21-
/// <see cref="AudioSettings.dspTime"/> of the last <see cref="OnAudioFilterRead"/>.
25+
/// Observer added to <see cref="AudioSource"/>.
2226
/// </summary>
23-
protected double filterReadDspTime;
27+
protected AudioSourceDataObserver observer;
2428
/// <summary>
25-
/// Audio data array of the last <see cref="OnAudioFilterRead"/>.
29+
/// The observed audio data.
2630
/// </summary>
27-
protected float[] filterReadData;
28-
/// <summary>
29-
/// Number of channels of the last <see cref="OnAudioFilterRead"/>.
30-
/// </summary>
31-
protected int filterReadChannels;
31+
protected readonly AudioSourceDataObserver.EventData audioData = new AudioSourceDataObserver.EventData();
3232

3333
/// <inheritdoc />
3434
public override bool IsActive()
@@ -42,36 +42,168 @@ public override bool IsActive()
4242
/// <returns>An Enumerator to manage the running of the Coroutine.</returns>
4343
protected override IEnumerator HapticProcessRoutine()
4444
{
45+
AddDataObserver();
4546
int outputSampleRate = AudioSettings.outputSampleRate;
46-
while (AudioSource.isPlaying)
47+
while (AudioSource != null && AudioSource.isPlaying)
4748
{
48-
int sampleIndex = (int)((AudioSettings.dspTime - filterReadDspTime) * outputSampleRate);
4949
float currentSample = 0;
50-
if (filterReadData != null && sampleIndex * filterReadChannels < filterReadData.Length)
50+
if (audioData.Data != null)
5151
{
52-
for (int i = 0; i < filterReadChannels; ++i)
52+
int sampleIndex = (int)((AudioSettings.dspTime - audioData.DspTime) * outputSampleRate) * audioData.Channels;
53+
sampleIndex = Mathf.Min(sampleIndex, audioData.Data.Length - audioData.Channels);
54+
for (int i = 0; i < audioData.Channels; ++i)
5355
{
54-
currentSample += filterReadData[sampleIndex + i];
56+
currentSample += Mathf.Abs(audioData.Data[sampleIndex + i]);
5557
}
56-
currentSample /= filterReadChannels;
58+
currentSample /= audioData.Channels;
5759
}
5860
HapticPulser.Intensity = currentSample * IntensityMultiplier;
5961
HapticPulser.Begin();
6062
yield return null;
6163
}
64+
RemoveDataObserver();
6265
ResetIntensity();
6366
}
6467

6568
/// <summary>
66-
/// Store currently playing audio data and additional data.
69+
/// Adds a <see cref="AudioSourceHapticPulserDataObserver"/> to the <see cref="AudioSource"/>.
70+
/// </summary>
71+
protected virtual void AddDataObserver()
72+
{
73+
if (AudioSource == null)
74+
{
75+
return;
76+
}
77+
78+
observer = AudioSource.gameObject.AddComponent<AudioSourceDataObserver>();
79+
observer.DataObserved.AddListener(Receive);
80+
}
81+
82+
/// <summary>
83+
/// Remove the <see cref="AudioSourceHapticPulserDataObserver"/> from the <see cref="AudioSource"/>.
84+
/// </summary>
85+
protected virtual void RemoveDataObserver()
86+
{
87+
if (observer == null)
88+
{
89+
return;
90+
}
91+
92+
observer.DataObserved.RemoveListener(Receive);
93+
Destroy(observer);
94+
observer = null;
95+
}
96+
97+
/// <summary>
98+
/// Receive audio data from <see cref="AudioSourceHapticPulserDataObserver"/>.
99+
/// </summary>
100+
protected virtual void Receive(AudioSourceDataObserver.EventData eventData)
101+
{
102+
audioData.Set(eventData);
103+
}
104+
105+
/// <summary>
106+
/// Called before <see cref="AudioSource"/> has been changed.
107+
/// </summary>
108+
[CalledBeforeChangeOf(nameof(AudioSource))]
109+
protected virtual void OnBeforeAudioSourceChange()
110+
{
111+
if (hapticRoutine == null)
112+
{
113+
return;
114+
}
115+
116+
RemoveDataObserver();
117+
}
118+
119+
/// <summary>
120+
/// Called after <see cref="AudioSource"/> has been changed.
121+
/// </summary>
122+
[CalledAfterChangeOf(nameof(AudioSource))]
123+
protected virtual void OnAfterAudioSourceChange()
124+
{
125+
if (hapticRoutine == null)
126+
{
127+
return;
128+
}
129+
130+
AddDataObserver();
131+
}
132+
}
133+
134+
/// <summary>
135+
/// Observes the <see cref="AudioSource"/> and emits the audio data.
136+
/// </summary>
137+
public class AudioSourceDataObserver : MonoBehaviour
138+
{
139+
/// <summary>
140+
/// Holds data about a <see cref="AudioSourceDataObserver"/> event.
141+
/// </summary>
142+
[Serializable]
143+
public class EventData
144+
{
145+
/// <summary>
146+
/// <see cref="AudioSettings.dspTime"/> of the last <see cref="OnAudioFilterRead"/>.
147+
/// </summary>
148+
[Serialized]
149+
[field: DocumentedByXml]
150+
public double DspTime { get; set; }
151+
/// <summary>
152+
/// Audio data array of the last <see cref="OnAudioFilterRead"/>.
153+
/// </summary>
154+
[Serialized]
155+
[field: DocumentedByXml]
156+
public float[] Data { get; set; }
157+
/// <summary>
158+
/// Number of channels of the last <see cref="OnAudioFilterRead"/>.
159+
/// </summary>
160+
[Serialized]
161+
[field: DocumentedByXml]
162+
public int Channels { get; set; }
163+
164+
public EventData Set(EventData source)
165+
{
166+
return Set(source.DspTime, source.Data, source.Channels);
167+
}
168+
169+
public EventData Set(double dspTime, float[] data, int channels)
170+
{
171+
DspTime = dspTime;
172+
Data = data;
173+
Channels = channels;
174+
return this;
175+
}
176+
177+
public void Clear()
178+
{
179+
Set(default, default, default);
180+
}
181+
}
182+
183+
/// <summary>
184+
/// Defines the event with the <see cref="EventData"/>.
185+
/// </summary>
186+
[Serializable]
187+
public class UnityEvent : UnityEvent<EventData> { }
188+
189+
/// <summary>
190+
/// Emitted whenever the audio data is observed.
191+
/// </summary>
192+
[DocumentedByXml]
193+
public UnityEvent DataObserved = new UnityEvent();
194+
/// <summary>
195+
/// The data to emit with an event.
196+
/// </summary>
197+
protected readonly EventData eventData = new EventData();
198+
199+
/// <summary>
200+
/// Emits audio data.
67201
/// </summary>
68202
/// <param name="data">An array of floats comprising the audio data.</param>
69203
/// <param name="channels">An int that stores the number of channels of audio data passed to this delegate.</param>
70204
protected virtual void OnAudioFilterRead(float[] data, int channels)
71205
{
72-
filterReadDspTime = AudioSettings.dspTime;
73-
filterReadData = data;
74-
filterReadChannels = channels;
206+
DataObserved?.Invoke(eventData.Set(AudioSettings.dspTime, data, channels));
75207
}
76208
}
77209
}

0 commit comments

Comments
 (0)