Skip to content

Commit 92467a5

Browse files
committed
Add effect processors.
1 parent 3c66c2a commit 92467a5

10 files changed

Lines changed: 390 additions & 19 deletions

VoiceCraft.Network/Audio/Effects/DirectionalEffect.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Text.Json.Serialization;
33
using LiteNetLib.Utils;
44
using VoiceCraft.Core;
5+
using VoiceCraft.Core.Audio;
6+
using VoiceCraft.Core.World;
57
using VoiceCraft.Network.Interfaces;
68

79
namespace VoiceCraft.Network.Audio.Effects
@@ -22,6 +24,9 @@ public float WetDry
2224
get;
2325
set => field = Math.Clamp(value, 0.0f, 1.0f);
2426
} = 1.0f;
27+
28+
public IAudioEffectProcessor GetProcessor(VoiceCraftEntity entity) =>
29+
new DirectionalEffectProcessor(this, entity);
2530

2631
public void Update(IAudioEffect audioEffect)
2732
{
@@ -55,6 +60,63 @@ public void Dispose()
5560
}
5661
}
5762

63+
public class DirectionalEffectProcessor : IAudioEffectProcessor
64+
{
65+
private readonly DirectionalEffect _effect;
66+
private readonly SampleLerpVolume _lerpVolume1;
67+
private readonly SampleLerpVolume _lerpVolume2;
68+
public IAudioEffect Effect => _effect;
69+
public VoiceCraftEntity Entity { get; }
70+
public event Action<IAudioEffectProcessor>? OnDisposed;
71+
72+
public DirectionalEffectProcessor(DirectionalEffect effect, VoiceCraftEntity entity)
73+
{
74+
_effect = effect;
75+
Entity = entity;
76+
_lerpVolume1 = new SampleLerpVolume(Constants.SampleRate, TimeSpan.FromMilliseconds(20));
77+
_lerpVolume2 = new SampleLerpVolume(Constants.SampleRate, TimeSpan.FromMilliseconds(20));
78+
Effect.OnDisposed += _ => Dispose();
79+
}
80+
81+
public void Process(VoiceCraftEntity to, Span<float> buffer)
82+
{
83+
var bitmask = Entity.TalkBitmask & to.ListenBitmask & Entity.EffectBitmask & to.EffectBitmask;
84+
if ((bitmask & Effect.Bitmask) == 0) return;
85+
86+
var rot = (float)(Math.Atan2(to.Position.Z - Entity.Position.Z, to.Position.X - Entity.Position.X) -
87+
to.Rotation.Y * Math.PI / 180);
88+
var left = (float)Math.Max(0.5 - Math.Cos(rot) * 0.5, 0.2);
89+
var right = (float)Math.Max(0.5 + Math.Cos(rot) * 0.5, 0.2);
90+
91+
_lerpVolume1.TargetVolume = left;
92+
_lerpVolume2.TargetVolume = right;
93+
94+
for (var i = 0; i < buffer.Length; i += 2)
95+
{
96+
var leftOutput = _lerpVolume1.Transform(buffer[i]);
97+
var rightOutput = _lerpVolume2.Transform(buffer[i + 1]);
98+
99+
buffer[i] = leftOutput * _effect.WetDry + buffer[i] * (1.0f - _effect.WetDry);
100+
buffer[i + 1] = rightOutput * _effect.WetDry + buffer[i + 1] * (1.0f - _effect.WetDry);
101+
102+
_lerpVolume1.Step();
103+
_lerpVolume2.Step();
104+
}
105+
}
106+
107+
public void Dispose()
108+
{
109+
try
110+
{
111+
OnDisposed?.Invoke(this);
112+
}
113+
finally
114+
{
115+
GC.SuppressFinalize(this);
116+
}
117+
}
118+
}
119+
58120
[JsonSourceGenerationOptions(WriteIndented = true)]
59121
[JsonSerializable(typeof(DirectionalEffect), GenerationMode = JsonSourceGenerationMode.Metadata)]
60122
public partial class DirectionalEffectGenerationContext : JsonSerializerContext;

VoiceCraft.Network/Audio/Effects/EchoEffect.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Text.Json.Serialization;
33
using LiteNetLib.Utils;
44
using VoiceCraft.Core;
5+
using VoiceCraft.Core.Audio;
6+
using VoiceCraft.Core.World;
57
using VoiceCraft.Network.Interfaces;
68

79
namespace VoiceCraft.Network.Audio.Effects;
@@ -40,6 +42,9 @@ public EchoEffect()
4042
Delay = 0.5f;
4143
}
4244

45+
public IAudioEffectProcessor GetProcessor(VoiceCraftEntity entity) =>
46+
new EchoEffectProcessor(this, entity);
47+
4348
public void Update(IAudioEffect audioEffect)
4449
{
4550
if(audioEffect is not EchoEffect echoEffect)
@@ -78,6 +83,53 @@ public void Dispose()
7883
}
7984
}
8085

86+
public class EchoEffectProcessor : IAudioEffectProcessor
87+
{
88+
private readonly EchoEffect _effect;
89+
private readonly FractionalDelayLine _delayLine;
90+
91+
public IAudioEffect Effect => _effect;
92+
public VoiceCraftEntity Entity { get; }
93+
public event Action<IAudioEffectProcessor>? OnDisposed;
94+
95+
public EchoEffectProcessor(EchoEffect effect, VoiceCraftEntity entity)
96+
{
97+
_effect = effect;
98+
Entity = entity;
99+
_delayLine = new FractionalDelayLine(Constants.SampleRate, _effect.Delay, InterpolationMode.Nearest);
100+
Effect.OnDisposed += _ => Dispose();
101+
}
102+
103+
public void Process(VoiceCraftEntity to, Span<float> buffer)
104+
{
105+
var bitmask = Entity.TalkBitmask & to.ListenBitmask & Entity.EffectBitmask & to.EffectBitmask;
106+
if ((bitmask & Effect.Bitmask) == 0) return;
107+
108+
_delayLine.Ensure(Constants.SampleRate, _effect.Delay);
109+
var delay = Constants.SampleRate * _effect.Delay;
110+
111+
for (var i = 0; i < buffer.Length; i++)
112+
{
113+
var delayed = _delayLine.Read(delay);
114+
var output = buffer[i] + delayed * _effect.Feedback;
115+
_delayLine.Write(output);
116+
buffer[i] = output * _effect.WetDry + buffer[i] * (1.0f - _effect.WetDry);
117+
}
118+
}
119+
120+
public void Dispose()
121+
{
122+
try
123+
{
124+
OnDisposed?.Invoke(this);
125+
}
126+
finally
127+
{
128+
GC.SuppressFinalize(this);
129+
}
130+
}
131+
}
132+
81133
[JsonSourceGenerationOptions(WriteIndented = true)]
82134
[JsonSerializable(typeof(EchoEffect), GenerationMode = JsonSourceGenerationMode.Metadata)]
83135
public partial class EchoEffectGenerationContext : JsonSerializerContext;

VoiceCraft.Network/Audio/Effects/MuffleEffect.cs

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Text.Json.Serialization;
33
using LiteNetLib.Utils;
44
using VoiceCraft.Core;
5+
using VoiceCraft.Core.Audio;
6+
using VoiceCraft.Core.World;
57
using VoiceCraft.Network.Interfaces;
68

79
namespace VoiceCraft.Network.Audio.Effects
@@ -11,21 +13,23 @@ public class MuffleEffect : IAudioEffect
1113
public static int SampleRate => Constants.SampleRate;
1214

1315
public EffectType EffectType => EffectType.Muffle;
14-
15-
[JsonIgnore]
16-
public ushort Bitmask { get; set; }
17-
16+
17+
[JsonIgnore] public ushort Bitmask { get; set; }
18+
1819
public event Action<IAudioEffect>? OnDisposed;
19-
20+
2021
public float WetDry
2122
{
2223
get;
2324
set => field = Math.Clamp(value, 0.0f, 1.0f);
2425
} = 1.0f;
2526

27+
public IAudioEffectProcessor GetProcessor(VoiceCraftEntity entity) =>
28+
new MuffleEffectProcessor(this, entity);
29+
2630
public void Update(IAudioEffect audioEffect)
2731
{
28-
if(audioEffect is not MuffleEffect muffleEffect)
32+
if (audioEffect is not MuffleEffect muffleEffect)
2933
throw new ArgumentException("Unexpected Audio Effect Type!", nameof(audioEffect));
3034
Bitmask = muffleEffect.Bitmask;
3135
WetDry = muffleEffect.WetDry;
@@ -40,7 +44,7 @@ public void Deserialize(NetDataReader reader)
4044
{
4145
WetDry = reader.GetFloat();
4246
}
43-
47+
4448
public void Dispose()
4549
{
4650
try
@@ -54,7 +58,50 @@ public void Dispose()
5458
}
5559
}
5660
}
57-
61+
62+
public class MuffleEffectProcessor : IAudioEffectProcessor
63+
{
64+
private readonly MuffleEffect _effect;
65+
private readonly BiQuadFilter _biQuadFilter;
66+
67+
public IAudioEffect Effect => _effect;
68+
public VoiceCraftEntity Entity { get; }
69+
public event Action<IAudioEffectProcessor>? OnDisposed;
70+
71+
public MuffleEffectProcessor(MuffleEffect effect, VoiceCraftEntity entity)
72+
{
73+
_effect = effect;
74+
Entity = entity;
75+
_biQuadFilter = new BiQuadFilter();
76+
Effect.OnDisposed += _ => Dispose();
77+
_biQuadFilter.SetLowPassFilter(Constants.SampleRate, 200, 1);
78+
}
79+
80+
public void Process(VoiceCraftEntity to, Span<float> buffer)
81+
{
82+
var bitmask = Entity.TalkBitmask & to.ListenBitmask & Entity.EffectBitmask & to.EffectBitmask;
83+
if ((bitmask & Effect.Bitmask) == 0) return;
84+
85+
for (var i = 0; i < buffer.Length; i++)
86+
{
87+
var output = _biQuadFilter.Transform(buffer[i]);
88+
buffer[i] = output * _effect.WetDry + buffer[i] * (1.0f - _effect.WetDry);
89+
}
90+
}
91+
92+
public void Dispose()
93+
{
94+
try
95+
{
96+
OnDisposed?.Invoke(this);
97+
}
98+
finally
99+
{
100+
GC.SuppressFinalize(this);
101+
}
102+
}
103+
}
104+
58105
[JsonSourceGenerationOptions(WriteIndented = true)]
59106
[JsonSerializable(typeof(MuffleEffect), GenerationMode = JsonSourceGenerationMode.Metadata)]
60107
public partial class MuffleEffectGenerationContext : JsonSerializerContext;

VoiceCraft.Network/Audio/Effects/ProximityEchoEffect.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
2+
using System.Numerics;
23
using System.Text.Json.Serialization;
34
using LiteNetLib.Utils;
45
using VoiceCraft.Core;
6+
using VoiceCraft.Core.Audio;
7+
using VoiceCraft.Core.World;
58
using VoiceCraft.Network.Interfaces;
69

710
namespace VoiceCraft.Network.Audio.Effects
@@ -40,6 +43,9 @@ public ProximityEchoEffect()
4043
Delay = 0.5f;
4144
}
4245

46+
public IAudioEffectProcessor GetProcessor(VoiceCraftEntity entity) =>
47+
new ProximityEchoEffectProcessor(this, entity);
48+
4349
public void Update(IAudioEffect audioEffect)
4450
{
4551
if(audioEffect is not ProximityEchoEffect proximityEchoEffect)
@@ -78,6 +84,61 @@ public void Dispose()
7884
}
7985
}
8086

87+
public class ProximityEchoEffectProcessor : IAudioEffectProcessor
88+
{
89+
private readonly ProximityEchoEffect _effect;
90+
private readonly FractionalDelayLine _delayLine;
91+
92+
public IAudioEffect Effect => _effect;
93+
public VoiceCraftEntity Entity { get; }
94+
public event Action<IAudioEffectProcessor>? OnDisposed;
95+
96+
public ProximityEchoEffectProcessor(ProximityEchoEffect effect, VoiceCraftEntity entity)
97+
{
98+
_effect = effect;
99+
Entity = entity;
100+
_delayLine = new FractionalDelayLine(Constants.SampleRate, _effect.Delay, InterpolationMode.Nearest);
101+
Effect.OnDisposed += _ => Dispose();
102+
}
103+
104+
public void Process(VoiceCraftEntity to, Span<float> buffer)
105+
{
106+
var bitmask = Entity.TalkBitmask & to.ListenBitmask & Entity.EffectBitmask & to.EffectBitmask;
107+
if ((bitmask & Effect.Bitmask) == 0) return;
108+
109+
var factor = 0f;
110+
if (_effect.Range != 0)
111+
{
112+
//The range at which the echo will take effect. Never set to 1.0 as it may cause infinite echo.
113+
var range = Math.Clamp(Vector3.Distance(Entity.Position, to.Position) / _effect.Range, 0.0f, 0.9f);
114+
factor = Math.Max(Entity.CaveFactor, to.CaveFactor) * range;
115+
}
116+
117+
_delayLine.Ensure(Constants.SampleRate, _effect.Delay);
118+
var delay = Constants.SampleRate * _effect.Delay;
119+
120+
for (var i = 0; i < buffer.Length; i++)
121+
{
122+
var delayed = _delayLine.Read(delay) * factor;
123+
var output = buffer[i] + delayed;
124+
_delayLine.Write(output);
125+
buffer[i] = output * _effect.WetDry + buffer[i] * (1.0f - _effect.WetDry);
126+
}
127+
}
128+
129+
public void Dispose()
130+
{
131+
try
132+
{
133+
OnDisposed?.Invoke(this);
134+
}
135+
finally
136+
{
137+
GC.SuppressFinalize(this);
138+
}
139+
}
140+
}
141+
81142
[JsonSourceGenerationOptions(WriteIndented = true)]
82143
[JsonSerializable(typeof(ProximityEchoEffect), GenerationMode = JsonSourceGenerationMode.Metadata)]
83144
public partial class ProximityEchoEffectGenerationContext : JsonSerializerContext;

0 commit comments

Comments
 (0)