Skip to content

Commit f0d0a87

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 f0d0a87

File tree

1 file changed

+158
-19
lines changed

1 file changed

+158
-19
lines changed
Lines changed: 158 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,65 +17,200 @@ 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()
3535
{
3636
return base.IsActive() && AudioSource != null;
3737
}
3838

39+
/// <inheritdoc />
40+
protected override void DoCancel()
41+
{
42+
RemoveDataObserver();
43+
base.DoCancel();
44+
}
45+
3946
/// <summary>
4047
/// Enumerates through <see cref="AudioSource"/> and pulses for each amplitude of the wave.
4148
/// </summary>
4249
/// <returns>An Enumerator to manage the running of the Coroutine.</returns>
4350
protected override IEnumerator HapticProcessRoutine()
4451
{
52+
AddDataObserver();
4553
int outputSampleRate = AudioSettings.outputSampleRate;
46-
while (AudioSource.isPlaying)
54+
while (AudioSource != null && AudioSource.isPlaying)
4755
{
48-
int sampleIndex = (int)((AudioSettings.dspTime - filterReadDspTime) * outputSampleRate);
4956
float currentSample = 0;
50-
if (filterReadData != null && sampleIndex * filterReadChannels < filterReadData.Length)
57+
if (audioData.Data != null)
5158
{
52-
for (int i = 0; i < filterReadChannels; ++i)
59+
int sampleIndex = (int)((AudioSettings.dspTime - audioData.DspTime) * outputSampleRate) * audioData.Channels;
60+
sampleIndex = Mathf.Min(sampleIndex, audioData.Data.Length - audioData.Channels);
61+
for (int i = 0; i < audioData.Channels; ++i)
5362
{
54-
currentSample += filterReadData[sampleIndex + i];
63+
currentSample += Mathf.Abs(audioData.Data[sampleIndex + i]);
5564
}
56-
currentSample /= filterReadChannels;
65+
currentSample /= audioData.Channels;
5766
}
5867
HapticPulser.Intensity = currentSample * IntensityMultiplier;
5968
HapticPulser.Begin();
6069
yield return null;
6170
}
71+
RemoveDataObserver();
6272
ResetIntensity();
6373
}
6474

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

0 commit comments

Comments
 (0)