Skip to content

Commit b717453

Browse files
committed
Re-built audio processing and fix some tests.
1 parent 92467a5 commit b717453

7 files changed

Lines changed: 144 additions & 53 deletions

File tree

VoiceCraft.Network.Tests/Performance/AllocationRegressionTests.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,18 +174,24 @@ private sealed class FakeNetPeer(Guid userGuid, Guid serverUserGuid, string loca
174174
private sealed class FakeVisibleEffect(bool result) : IAudioEffect, IVisible
175175
{
176176
public EffectType EffectType => EffectType.Visibility;
177+
public ushort Bitmask { get; set; }
178+
public event Action<IAudioEffect>? OnDisposed;
177179

178180
public bool Visibility(VoiceCraftEntity from, VoiceCraftEntity to, ushort effectBitmask)
179181
{
180182
return result;
181183
}
182-
183-
public void Process(VoiceCraftEntity from, VoiceCraftEntity to, ushort effectBitmask, Span<float> buffer)
184+
185+
public IAudioEffectProcessor GetProcessor(VoiceCraftEntity entity)
184186
{
187+
throw new NotSupportedException();
185188
}
186-
187-
public void Reset()
189+
190+
public void Update(IAudioEffect audioEffect)
188191
{
192+
if (audioEffect is not FakeVisibleEffect effect)
193+
throw new ArgumentException("Unexpected Audio Effect Type!", nameof(audioEffect));
194+
Bitmask = effect.Bitmask;
189195
}
190196

191197
public void Serialize(NetDataWriter writer)

VoiceCraft.Network.Tests/Systems/VisibilitySystemTests.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,24 @@ private sealed class FakeNetPeer(Guid userGuid, Guid serverUserGuid, string loca
8282
private sealed class FakeVisibleEffect(bool result) : IAudioEffect, IVisible
8383
{
8484
public EffectType EffectType => EffectType.Visibility;
85-
86-
public bool Visibility(VoiceCraftEntity from, VoiceCraftEntity to, ushort effectBitmask)
85+
public ushort Bitmask { get; set; }
86+
public event Action<IAudioEffect>? OnDisposed;
87+
88+
public void Update(IAudioEffect audioEffect)
8789
{
88-
return result;
90+
if (audioEffect is not FakeVisibleEffect effect)
91+
throw new ArgumentException("Unexpected Audio Effect Type!", nameof(audioEffect));
92+
Bitmask = effect.Bitmask;
8993
}
9094

91-
public void Process(VoiceCraftEntity from, VoiceCraftEntity to, ushort effectBitmask, Span<float> buffer)
95+
public IAudioEffectProcessor GetProcessor(VoiceCraftEntity entity)
9296
{
97+
throw new NotSupportedException();
9398
}
9499

95-
public void Reset()
100+
public bool Visibility(VoiceCraftEntity from, VoiceCraftEntity to, ushort effectBitmask)
96101
{
102+
return result;
97103
}
98104

99105
public void Serialize(NetDataWriter writer)

VoiceCraft.Network/Audio/Effects/DirectionalEffect.cs

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,25 @@ namespace VoiceCraft.Network.Audio.Effects
1111
public class DirectionalEffect : IAudioEffect
1212
{
1313
public static int SampleRate => Constants.SampleRate;
14-
14+
1515
public EffectType EffectType => EffectType.Directional;
16-
17-
[JsonIgnore]
18-
public ushort Bitmask { get; set; }
19-
16+
17+
[JsonIgnore] public ushort Bitmask { get; set; }
18+
2019
public event Action<IAudioEffect>? OnDisposed;
21-
20+
2221
public float WetDry
2322
{
2423
get;
2524
set => field = Math.Clamp(value, 0.0f, 1.0f);
2625
} = 1.0f;
27-
26+
2827
public IAudioEffectProcessor GetProcessor(VoiceCraftEntity entity) =>
2928
new DirectionalEffectProcessor(this, entity);
3029

3130
public void Update(IAudioEffect audioEffect)
3231
{
33-
if(audioEffect is not DirectionalEffect directionalEffect)
32+
if (audioEffect is not DirectionalEffect directionalEffect)
3433
throw new ArgumentException("Unexpected Audio Effect Type!", nameof(audioEffect));
3534
Bitmask = directionalEffect.Bitmask;
3635
WetDry = directionalEffect.WetDry;
@@ -59,12 +58,11 @@ public void Dispose()
5958
}
6059
}
6160
}
62-
61+
6362
public class DirectionalEffectProcessor : IAudioEffectProcessor
6463
{
6564
private readonly DirectionalEffect _effect;
66-
private readonly SampleLerpVolume _lerpVolume1;
67-
private readonly SampleLerpVolume _lerpVolume2;
65+
private readonly SampleLerpVolume[] _lerpVolume;
6866
public IAudioEffect Effect => _effect;
6967
public VoiceCraftEntity Entity { get; }
7068
public event Action<IAudioEffectProcessor>? OnDisposed;
@@ -73,8 +71,11 @@ public DirectionalEffectProcessor(DirectionalEffect effect, VoiceCraftEntity ent
7371
{
7472
_effect = effect;
7573
Entity = entity;
76-
_lerpVolume1 = new SampleLerpVolume(Constants.SampleRate, TimeSpan.FromMilliseconds(20));
77-
_lerpVolume2 = new SampleLerpVolume(Constants.SampleRate, TimeSpan.FromMilliseconds(20));
74+
_lerpVolume =
75+
[
76+
new SampleLerpVolume(Constants.SampleRate, TimeSpan.FromMilliseconds(20)),
77+
new SampleLerpVolume(Constants.SampleRate, TimeSpan.FromMilliseconds(20))
78+
];
7879
Effect.OnDisposed += _ => Dispose();
7980
}
8081

@@ -87,20 +88,18 @@ public void Process(VoiceCraftEntity to, Span<float> buffer)
8788
to.Rotation.Y * Math.PI / 180);
8889
var left = (float)Math.Max(0.5 - Math.Cos(rot) * 0.5, 0.2);
8990
var right = (float)Math.Max(0.5 + Math.Cos(rot) * 0.5, 0.2);
90-
91-
_lerpVolume1.TargetVolume = left;
92-
_lerpVolume2.TargetVolume = right;
9391

94-
for (var i = 0; i < buffer.Length; i += 2)
92+
_lerpVolume[0].TargetVolume = left;
93+
_lerpVolume[1].TargetVolume = right;
94+
95+
for (var i = 0; i < buffer.Length; i++)
9596
{
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();
97+
for (var c = 0; c < 2 && c + i < buffer.Length; c++)
98+
{
99+
var output = _lerpVolume[c].Transform(buffer[i]);
100+
buffer[i] = output * _effect.WetDry + buffer[i] * (1.0f - _effect.WetDry);
101+
_lerpVolume[c].Step();
102+
}
104103
}
105104
}
106105

@@ -116,7 +115,7 @@ public void Dispose()
116115
}
117116
}
118117
}
119-
118+
120119
[JsonSourceGenerationOptions(WriteIndented = true)]
121120
[JsonSerializable(typeof(DirectionalEffect), GenerationMode = JsonSourceGenerationMode.Metadata)]
122121
public partial class DirectionalEffectGenerationContext : JsonSerializerContext;

VoiceCraft.Network/Audio/Effects/ProximityEffect.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,11 @@ public void Process(VoiceCraftEntity to, Span<float> buffer)
107107

108108
for (var i = 0; i < buffer.Length; i += 2)
109109
{
110-
//Channel 1
111-
var output = _lerpVolume.Transform(buffer[i]);
112-
buffer[i] = output * _effect.WetDry + buffer[i] * (1.0f - _effect.WetDry);
113-
//Channel 2
114-
output = _lerpVolume.Transform(buffer[i + 1]);
115-
buffer[i + 1] = output * _effect.WetDry + buffer[i + 1] * (1.0f - _effect.WetDry);
110+
for (var c = 0; c < 2 && c + i < buffer.Length; c++)
111+
{
112+
var output = _lerpVolume.Transform(buffer[i]);
113+
buffer[i] = output * _effect.WetDry + buffer[i] * (1.0f - _effect.WetDry);
114+
}
116115
_lerpVolume.Step();
117116
}
118117
}

VoiceCraft.Network/Clients/VoiceCraftClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public abstract void SendPacket<T>(T packet,
129129

130130
public int Read(Span<float> buffer)
131131
{
132-
return AudioEffectSystem.Read(buffer);
132+
return AudioEffectSystem.Read(this, buffer);
133133
}
134134

135135
public void Write(Span<float> buffer)

VoiceCraft.Network/Systems/AudioEffectSystem.cs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
using System;
2+
using System.Buffers;
23
using System.Collections.Generic;
34
using System.Collections.Immutable;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Linq;
67
using System.Threading;
8+
using System.Threading.Tasks;
9+
using VoiceCraft.Core.Audio;
10+
using VoiceCraft.Core.World;
11+
using VoiceCraft.Network.Clients;
712
using VoiceCraft.Network.Interfaces;
13+
using VoiceCraft.Network.World;
814

915
namespace VoiceCraft.Network.Systems;
1016

@@ -114,9 +120,42 @@ public void ClearEffects()
114120
}
115121
}
116122

117-
public virtual int Read(Span<float> buffer)
123+
public int Read(VoiceCraftClient client, Span<float> buffer)
118124
{
119-
throw new NotSupportedException();
125+
var bufferLength = buffer.Length;
126+
var outputBuffer = ArrayPool<float>.Shared.Rent(bufferLength);
127+
outputBuffer.AsSpan(0, bufferLength).Clear();
128+
try
129+
{
130+
var read = 0;
131+
Parallel.ForEach(client.World.Entities.OfType<VoiceCraftClientEntity>(), x =>
132+
{
133+
var entityBuffer = ArrayPool<float>.Shared.Rent(bufferLength);
134+
var entitySpanBuffer = entityBuffer.AsSpan(0, bufferLength);
135+
entitySpanBuffer.Clear();
136+
try
137+
{
138+
var entityRead = ProcessEntityAudio(x, client, entitySpanBuffer);
139+
lock (_lock)
140+
{
141+
read = SampleMixer.Read(entitySpanBuffer[..entityRead], outputBuffer);
142+
// ReSharper disable once AccessToModifiedClosure
143+
read = Math.Max(read, entityRead);
144+
}
145+
}
146+
finally
147+
{
148+
ArrayPool<float>.Shared.Return(entityBuffer);
149+
}
150+
});
151+
152+
outputBuffer[..read].CopyTo(buffer);
153+
return read;
154+
}
155+
finally
156+
{
157+
ArrayPool<float>.Shared.Return(outputBuffer);
158+
}
120159
}
121160

122161
public void Dispose()
@@ -131,4 +170,28 @@ private void Dispose(bool disposing)
131170
ClearEffects();
132171
OnEffectSet = null;
133172
}
173+
174+
private int ProcessEntityAudio(VoiceCraftClientEntity from, VoiceCraftEntity to, Span<float> buffer)
175+
{
176+
var read = from.Read(buffer);
177+
if (read <= 0) read = buffer.Length; //Do a full read.
178+
ProcessEntityEffects(from, to, buffer[..read]); //Process Effects
179+
read = SampleVolume.Read(buffer[..read], from.Volume); //Adjust the volume of the entity.
180+
return read;
181+
}
182+
183+
private void ProcessEntityEffects(VoiceCraftClientEntity from, VoiceCraftEntity to, Span<float> buffer)
184+
{
185+
var snapshot = _audioEffectsSnapshot;
186+
foreach (var effect in snapshot)
187+
{
188+
if (!from.TryGetEffectProcessor(effect.Key, out var processor))
189+
{
190+
processor = effect.Value.GetProcessor(from);
191+
from.SetEffectProcessor(effect.Key, processor);
192+
}
193+
194+
processor.Process(to, buffer);
195+
}
196+
}
134197
}

VoiceCraft.Network/World/VoiceCraftClientEntity.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Linq;
45
using System.Threading;
56
using System.Threading.Tasks;
@@ -16,8 +17,11 @@ public class VoiceCraftClientEntity : VoiceCraftEntity
1617
{
1718
private readonly IAudioDecoder _decoder;
1819
private readonly JitterBuffer _jitterBuffer = new(TimeSpan.FromMilliseconds(100));
19-
private readonly SampleBufferProvider<float> _outputBuffer = new(Constants.OutputBufferSize)
20-
{ PrefillSize = Constants.PrefillBufferSize };
20+
21+
private readonly SampleBufferProvider<float> _outputBuffer =
22+
new(Constants.OutputBufferSize * Constants.PlaybackChannels)
23+
{ PrefillSize = Constants.PrefillBufferSize * Constants.PlaybackChannels };
24+
2125
private DateTime _lastPacket = DateTime.MinValue;
2226
private readonly Dictionary<ushort, IAudioEffectProcessor> _effectProcessors = new();
2327

@@ -29,7 +33,7 @@ public VoiceCraftClientEntity(int id, IAudioDecoder decoder) : base(id)
2933
_decoder = decoder;
3034
Task.Run(TaskLogicAsync);
3135
}
32-
36+
3337

3438
public bool IsVisible
3539
{
@@ -92,7 +96,8 @@ public void SetEffectProcessor(ushort bitmask, IAudioEffectProcessor? processor)
9296
switch (processor)
9397
{
9498
case null when _effectProcessors.Remove(bitmask, out var effectProcessor):
95-
effectProcessor.OnDisposed -= RemoveEffect; //Unsubscribe from effect dispose as it has been removed.
99+
effectProcessor.OnDisposed -=
100+
RemoveEffect; //Unsubscribe from effect dispose as it has been removed.
96101
effectProcessor.Dispose();
97102
return;
98103
case null:
@@ -102,12 +107,13 @@ public void SetEffectProcessor(ushort bitmask, IAudioEffectProcessor? processor)
102107
if (!_effectProcessors.TryGetValue(bitmask, out var oldProcessor))
103108
processor.OnDisposed += RemoveEffect; //Subscribe to effect dispose if it's a new effect.
104109
_effectProcessors[bitmask] = processor;
105-
110+
106111
//Dispose old processor if it exists.
107112
oldProcessor?.Dispose();
108113
}
114+
109115
return;
110-
116+
111117
void RemoveEffect(IAudioEffectProcessor effectProcessor)
112118
{
113119
lock (_audioLock)
@@ -118,6 +124,14 @@ void RemoveEffect(IAudioEffectProcessor effectProcessor)
118124
}
119125
}
120126

127+
public bool TryGetEffectProcessor(ushort bitmask, [NotNullWhen(true)] out IAudioEffectProcessor? effect)
128+
{
129+
lock (_audioLock)
130+
{
131+
return _effectProcessors.TryGetValue(bitmask, out effect);
132+
}
133+
}
134+
121135
public int Read(Span<float> buffer)
122136
{
123137
var read = 0;
@@ -129,7 +143,7 @@ public int Read(Span<float> buffer)
129143

130144
lock (_audioLock)
131145
read = _outputBuffer.Read(buffer);
132-
146+
133147
if (read <= 0)
134148
{
135149
Speaking = false;
@@ -153,7 +167,7 @@ public override void ReceiveAudio(byte[] buffer, ushort timestamp, float frameLo
153167

154168
public override void Destroy()
155169
{
156-
lock(_lock)
170+
lock (_lock)
157171
{
158172
_jitterBuffer.Reset();
159173
_decoder.Dispose();
@@ -215,6 +229,7 @@ private async Task TaskLogicAsync()
215229
{
216230
var startTick = Environment.TickCount;
217231
var readBuffer = new float[Constants.FrameSize];
232+
var stereoBuffer = new float[Constants.FrameSize * Constants.PlaybackChannels];
218233
while (!Destroyed)
219234
try
220235
{
@@ -227,10 +242,13 @@ private async Task TaskLogicAsync()
227242

228243
startTick += Constants.FrameSizeMs; //Step Forwards.
229244
Array.Clear(readBuffer); //Clear Read Buffer.
245+
Array.Clear(stereoBuffer); //Clear Stereo Buffer.
246+
230247
var read = GetNextPacket(readBuffer);
248+
read = SampleMonoToStereo.Read(readBuffer.AsSpan(0, read), stereoBuffer); //To Stereo
231249
if (read <= 0 || UserMuted) continue;
232250
lock (_audioLock)
233-
_outputBuffer.Write(readBuffer.AsSpan(0, read));
251+
_outputBuffer.Write(stereoBuffer.AsSpan(0, read));
234252
}
235253
catch
236254
{

0 commit comments

Comments
 (0)