v0.13.0.11

This commit is contained in:
Joonas Rikkonen
2021-04-22 17:33:08 +03:00
parent 0697d7fc64
commit 8bb31f2893
391 changed files with 17271 additions and 5949 deletions
@@ -30,6 +30,7 @@ namespace Barotrauma.Sounds
public override int FillStreamBuffer(int samplePos, short[] buffer)
{
if (!Stream) throw new Exception("Called FillStreamBuffer on a non-streamed sound!");
if (reader == null) throw new Exception("Called FillStreamBuffer when the reader is null!");
if (samplePos >= reader.TotalSamples * reader.Channels * 2) return 0;
@@ -41,7 +42,7 @@ namespace Barotrauma.Sounds
//MuffleBuffer(floatBuffer, reader.Channels);
CastBuffer(floatBuffer, buffer, readSamples);
return readSamples * 2;
return readSamples;
}
static void MuffleBuffer(float[] buffer, int sampleRate, int channelCount)
@@ -63,8 +64,20 @@ namespace Barotrauma.Sounds
ALFormat = reader.Channels == 1 ? Al.FormatMono16 : Al.FormatStereo16;
SampleRate = reader.SampleRate;
if (Buffers != null && SoundBuffers.BuffersGenerated < SoundBuffers.MaxBuffers)
{
Buffers.RequestAlBuffers(); FillBuffers();
}
}
public override void FillBuffers()
{
if (!Stream)
{
reader ??= new VorbisReader(Filename);
reader.DecodedPosition = 0;
int bufferSize = (int)reader.TotalSamples * reader.Channels;
float[] floatBuffer = new float[bufferSize];
@@ -86,26 +99,26 @@ namespace Barotrauma.Sounds
CastBuffer(floatBuffer, shortBuffer, readSamples);
Al.BufferData(ALBuffer, ALFormat, shortBuffer,
Al.BufferData(Buffers.AlBuffer, ALFormat, shortBuffer,
readSamples * sizeof(short), SampleRate);
int alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to set buffer data for non-streamed audio! " + Al.GetErrorString(alError));
throw new Exception("Failed to set regular buffer data for non-streamed audio! " + Al.GetErrorString(alError));
}
MuffleBuffer(floatBuffer, SampleRate, reader.Channels);
CastBuffer(floatBuffer, shortBuffer, readSamples);
Al.BufferData(ALMuffledBuffer, ALFormat, shortBuffer,
Al.BufferData(Buffers.AlMuffledBuffer, ALFormat, shortBuffer,
readSamples * sizeof(short), SampleRate);
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to set buffer data for non-streamed audio! " + Al.GetErrorString(alError));
throw new Exception("Failed to set muffled buffer data for non-streamed audio! " + Al.GetErrorString(alError));
}
reader.Dispose(); reader = null;
@@ -116,7 +129,7 @@ namespace Barotrauma.Sounds
{
if (Stream)
{
reader.Dispose();
reader?.Dispose();
}
base.Dispose();
@@ -14,35 +14,15 @@ namespace Barotrauma.Sounds
get { return disposed; }
}
public SoundManager Owner
{
get;
protected set;
}
public readonly SoundManager Owner;
public string Filename
{
get;
protected set;
}
public readonly string Filename;
public XElement XElement
{
get;
protected set;
}
public readonly XElement XElement;
public bool Stream
{
get;
protected set;
}
public readonly bool Stream;
public bool StreamsReliably
{
get;
protected set;
}
public readonly bool StreamsReliably;
public virtual SoundManager.SourcePoolIndex SourcePoolIndex
{
@@ -52,16 +32,10 @@ namespace Barotrauma.Sounds
}
}
private uint alBuffer;
public uint ALBuffer
private SoundBuffers buffers;
public SoundBuffers Buffers
{
get { return !Stream ? alBuffer : 0; }
}
private uint alMuffledBuffer;
public uint ALMuffledBuffer
{
get { return !Stream ? alMuffledBuffer : 0; }
get { return !Stream ? buffers : null; }
}
public int ALFormat
@@ -85,10 +59,10 @@ namespace Barotrauma.Sounds
public float BaseNear;
public float BaseFar;
public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement=null)
public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement=null, bool getFullPath=true)
{
Owner = owner;
Filename = Path.GetFullPath(filename.CleanUpPath()).CleanUpPath();
Filename = getFullPath ? Path.GetFullPath(filename.CleanUpPath()).CleanUpPath() : filename;
Stream = stream;
StreamsReliably = streamsReliably;
XElement = xElement;
@@ -169,69 +143,20 @@ namespace Barotrauma.Sounds
{
if (!Stream)
{
Al.GenBuffer(out alBuffer);
int alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to create OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError));
}
if (!Al.IsBuffer(alBuffer))
{
throw new Exception("Generated OpenAL buffer is invalid!");
}
Al.GenBuffer(out alMuffledBuffer);
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to create OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError));
}
if (!Al.IsBuffer(alMuffledBuffer))
{
throw new Exception("Generated OpenAL buffer is invalid!");
}
buffers = new SoundBuffers(this);
}
else
{
alBuffer = 0;
buffers = null;
}
}
public virtual void FillBuffers() { }
public virtual void DeleteALBuffers()
{
Owner.KillChannels(this);
if (alBuffer != 0)
{
if (!Al.IsBuffer(alBuffer))
{
throw new Exception("Buffer to delete is invalid!");
}
Al.DeleteBuffer(alBuffer); alBuffer = 0;
int alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to delete OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError));
}
}
if (alMuffledBuffer != 0)
{
if (!Al.IsBuffer(alMuffledBuffer))
{
throw new Exception("Buffer to delete is invalid!");
}
Al.DeleteBuffer(alMuffledBuffer); alMuffledBuffer = 0;
int alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to delete OpenAL buffer for non-streamed sound: " + Al.GetErrorString(alError));
}
}
buffers?.Dispose();
}
public virtual void Dispose()
@@ -0,0 +1,122 @@
using Barotrauma.Extensions;
using OpenAL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma.Sounds
{
public class SoundBuffers : IDisposable
{
private static HashSet<uint> bufferPool = new HashSet<uint>();
#if OSX
public const int MaxBuffers = 400; //TODO: check that this value works for macOS
#else
public const int MaxBuffers = 32000;
#endif
public static int BuffersGenerated { get; private set; } = 0;
private Sound sound;
public uint AlBuffer { get; private set; } = 0;
public uint AlMuffledBuffer { get; private set; } = 0;
public SoundBuffers(Sound sound) { this.sound = sound; }
public void Dispose()
{
if (AlBuffer != 0) { bufferPool.Add(AlBuffer); }
if (AlMuffledBuffer != 0) { bufferPool.Add(AlMuffledBuffer); }
AlBuffer = 0;
AlMuffledBuffer = 0;
}
public static void ClearPool()
{
bufferPool.ForEach(b => Al.DeleteBuffer(b));
bufferPool.Clear();
BuffersGenerated = 0;
}
public bool RequestAlBuffers()
{
if (AlBuffer != 0) { return false; }
int alError = 0;
while (bufferPool.Count < 2 && BuffersGenerated < MaxBuffers)
{
Al.GenBuffer(out uint newBuffer);
alError = Al.GetError();
if (alError != Al.NoError)
{
DebugConsole.AddWarning($"Error when generating sound buffer: {Al.GetErrorString(alError)}. {BuffersGenerated} buffer(s) were generated. No more sound buffers will be generated.");
BuffersGenerated = MaxBuffers;
}
else if (!Al.IsBuffer(newBuffer))
{
DebugConsole.AddWarning($"Error when generating sound buffer: result is not a valid buffer. {BuffersGenerated} buffer(s) were generated. No more sound buffers will be generated.");
BuffersGenerated = MaxBuffers;
}
else
{
bufferPool.Add(newBuffer);
BuffersGenerated++;
if (BuffersGenerated >= MaxBuffers)
{
DebugConsole.AddWarning($"{BuffersGenerated} buffer(s) were generated. No more sound buffers will be generated.");
}
}
}
if (bufferPool.Count >= 2)
{
AlBuffer = bufferPool.First();
bufferPool.Remove(AlBuffer);
AlMuffledBuffer = bufferPool.First();
bufferPool.Remove(AlMuffledBuffer);
return true;
}
//can't generate any more OpenAL buffers! we'll have to steal a buffer from someone...
foreach (var otherSound in sound.Owner.LoadedSounds)
{
if (otherSound == sound) { continue; }
if (otherSound.IsPlaying()) { continue; }
if (otherSound.Buffers == null) { continue; }
if (otherSound.Buffers.AlBuffer == 0) { continue; }
// Dispose all channels that are holding
// a reference to these buffers, otherwise
// an INVALID_OPERATION error will be thrown
// when attempting to set the buffer data later.
// Having the sources not play is not enough,
// as OpenAL assumes that you may want to call
// alSourcePlay without reassigning the buffer.
otherSound.Owner.KillChannels(otherSound);
AlBuffer = otherSound.Buffers.AlBuffer;
AlMuffledBuffer = otherSound.Buffers.AlMuffledBuffer;
otherSound.Buffers.AlBuffer = 0;
otherSound.Buffers.AlMuffledBuffer = 0;
// For performance reasons, sift the current sound to
// the end of the loadedSounds list, that way it'll
// be less likely to have its buffers stolen, which
// means less reuploads for frequently played sounds.
sound.Owner.MoveSoundToPosition(sound, sound.Owner.LoadedSoundCount-1);
if (!Al.IsBuffer(AlBuffer))
{
throw new Exception(sound.Filename + " has an invalid buffer!");
}
if (!Al.IsBuffer(AlMuffledBuffer))
{
throw new Exception(sound.Filename + " has an invalid muffled buffer!");
}
return true;
}
return false;
}
}
}
@@ -334,7 +334,11 @@ namespace Barotrauma.Sounds
return;
}
Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.ALMuffledBuffer : (int)Sound.ALBuffer);
if (Sound.Buffers.RequestAlBuffers())
{
Sound.FillBuffers();
}
Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.Buffers.AlMuffledBuffer : (int)Sound.Buffers.AlBuffer);
alError = Al.GetError();
if (alError != Al.NoError)
@@ -487,8 +491,10 @@ namespace Barotrauma.Sounds
mutex = new object();
}
#if !DEBUG
try
{
#endif
if (mutex != null) { Monitor.Enter(mutex); }
if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances)
{
@@ -506,17 +512,17 @@ namespace Barotrauma.Sounds
throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
}
if (!Al.IsBuffer(sound.ALBuffer))
if (Sound.Buffers.RequestAlBuffers())
{
throw new Exception(sound.Filename + " has an invalid buffer!");
Sound.FillBuffers();
}
uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffle ? sound.ALMuffledBuffer : sound.ALBuffer;
uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer;
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
alError = Al.GetError();
if (alError != Al.NoError)
{
throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + sound.ALBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError));
throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError));
}
Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex));
@@ -528,7 +534,7 @@ namespace Barotrauma.Sounds
}
else
{
uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffle ? sound.ALMuffledBuffer : sound.ALBuffer;
uint alBuffer = 0;
Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
int alError = Al.GetError();
if (alError != Al.NoError)
@@ -574,6 +580,7 @@ namespace Barotrauma.Sounds
this.Near = near;
this.Far = far;
this.Category = category;
#if !DEBUG
}
catch
{
@@ -581,8 +588,11 @@ namespace Barotrauma.Sounds
}
finally
{
#endif
if (mutex != null) { Monitor.Exit(mutex); }
#if !DEBUG
}
#endif
Sound.Owner.Update();
}
@@ -736,11 +746,6 @@ namespace Barotrauma.Sounds
if (FilledByNetwork)
{
if (Sound is VoipSound voipSound)
{
voipSound.ApplyFilters(buffer, readSamples);
}
if (readSamples <= 0)
{
streamAmplitude *= 0.5f;
@@ -752,13 +757,18 @@ namespace Barotrauma.Sounds
}
else
{
if (Sound is VoipSound voipSound)
{
voipSound.ApplyFilters(buffer, readSamples);
}
decayTimer = 0;
}
}
else if (Sound.StreamsReliably)
{
streamSeekPos += readSamples;
if (readSamples < STREAM_BUFFER_SIZE)
streamSeekPos += readSamples * 2;
if (readSamples * 2 < STREAM_BUFFER_SIZE)
{
if (looping)
{
@@ -775,7 +785,7 @@ namespace Barotrauma.Sounds
{
streamBufferAmplitudes[index] = readAmplitude;
Al.BufferData<short>(streamBuffers[index], Sound.ALFormat, buffer, readSamples, Sound.SampleRate);
Al.BufferData<short>(streamBuffers[index], Sound.ALFormat, buffer, readSamples * 2, Sound.SampleRate);
alError = Al.GetError();
if (alError != Al.NoError)
@@ -30,6 +30,8 @@ namespace Barotrauma.Sounds
private readonly SoundSourcePool[] sourcePools;
private readonly List<Sound> loadedSounds;
public IReadOnlyList<Sound> LoadedSounds => loadedSounds;
private readonly SoundChannel[][] playingChannels = new SoundChannel[2][];
private readonly object threadDeathMutex = new object();
@@ -512,6 +514,18 @@ namespace Barotrauma.Sounds
}
}
public void MoveSoundToPosition(Sound sound, int pos)
{
lock (loadedSounds)
{
int index = loadedSounds.IndexOf(sound);
if (index >= 0)
{
loadedSounds.SiftElement(index, pos);
}
}
}
public void SetCategoryGainMultiplier(string category, float gain, int index=0)
{
if (Disabled) { return; }
@@ -706,9 +720,11 @@ namespace Barotrauma.Sounds
}
bool areStreamsPlaying = false;
ManualResetEvent streamMre = null;
void UpdateStreaming()
{
streamMre = new ManualResetEvent(false);
bool killThread = false;
while (!killThread)
{
@@ -745,14 +761,20 @@ namespace Barotrauma.Sounds
}
}
}
streamMre.WaitOne(10);
streamMre.Reset();
lock (threadDeathMutex)
{
areStreamsPlaying = !killThread;
}
Thread.Sleep(10); //TODO: use a separate thread for network audio?
}
}
public void ForceStreamUpdate()
{
streamMre?.Set();
}
private void ReloadSounds()
{
for (int i = loadedSounds.Count - 1; i >= 0; i--)
@@ -788,6 +810,8 @@ namespace Barotrauma.Sounds
}
sourcePools[(int)SourcePoolIndex.Default]?.Dispose();
sourcePools[(int)SourcePoolIndex.Voice]?.Dispose();
SoundBuffers.ClearPool();
}
public void Dispose()
@@ -21,12 +21,14 @@ namespace Barotrauma
public readonly string requiredTag;
public DamageSound(Sound sound, Vector2 damageRange, string damageType, string requiredTag = "")
public bool ignoreMuffling;
public DamageSound(Sound sound, Vector2 damageRange, string damageType, bool ignoreMuffling, string requiredTag = "")
{
this.sound = sound;
this.damageRange = damageRange;
this.damageType = damageType;
this.ignoreMuffling = ignoreMuffling;
this.requiredTag = requiredTag;
}
}
@@ -195,14 +197,21 @@ namespace Barotrauma
{
case "music":
var newMusicClip = new BackgroundMusic(soundElement);
musicClips.AddIfNotNull(newMusicClip);
if (loadedSoundElements != null)
if (File.Exists(newMusicClip.File))
{
if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase))
musicClips.AddIfNotNull(newMusicClip);
if (loadedSoundElements != null)
{
targetMusic[0] = newMusicClip;
if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase))
{
targetMusic[0] = newMusicClip;
}
}
}
else
{
DebugConsole.NewMessage($"Music file \"{newMusicClip.File}\" not found.");
}
break;
case "splash":
SplashSounds.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false));
@@ -262,6 +271,7 @@ namespace Barotrauma
damageSound,
soundElement.GetAttributeVector2("damagerange", Vector2.Zero),
damageSoundType,
soundElement.GetAttributeBool("ignoremuffling", false),
soundElement.GetAttributeString("requiredtag", "")));
break;
@@ -524,7 +534,7 @@ namespace Barotrauma
Vector2 diff = gap.WorldPosition - listenerPos;
if (Math.Abs(diff.X) < FlowSoundRange && Math.Abs(diff.Y) < FlowSoundRange)
{
if (gap.Open < 0.01f) { continue; }
if (gap.Open < 0.01f || gap.LerpedFlowForce.LengthSquared() < 100.0f) { continue; }
float gapFlow = Math.Abs(gap.LerpedFlowForce.X) + Math.Abs(gap.LerpedFlowForce.Y) * 2.5f;
if (!gap.IsRoomToRoom) { gapFlow *= 2.0f; }
if (gapFlow < 10.0f) { continue; }
@@ -887,7 +897,17 @@ namespace Barotrauma
if (currentMusic[i] == null || (musicChannel[i] == null || !musicChannel[i].IsPlaying))
{
DisposeMusicChannel(i);
currentMusic[i] = GameMain.SoundManager.LoadSound(targetMusic[i].File, true);
try
{
currentMusic[i] = GameMain.SoundManager.LoadSound(targetMusic[i].File, true);
}
catch (System.IO.InvalidDataException e)
{
DebugConsole.ThrowError($"Failed to load the music clip \"{targetMusic[i].File}\".", e);
musicClips.Remove(targetMusic[i]);
targetMusic[i] = null;
break;
}
musicChannel[i] = currentMusic[i].Play(0.0f, "music");
if (targetMusic[i].ContinueFromPreviousTime)
{
@@ -983,13 +1003,17 @@ namespace Barotrauma
}
Submarine targetSubmarine = Character.Controlled?.Submarine;
if ((targetSubmarine != null && targetSubmarine.AtDamageDepth) ||
(GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Level.Loaded.RealWorldCrushDepth))
if (targetSubmarine != null && targetSubmarine.AtDamageDepth)
{
return "deep";
}
if (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Submarine.MainSub != null &&
Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Submarine.MainSub.RealWorldCrushDepth)
{
return "deep";
}
if (targetSubmarine != null)
if (targetSubmarine != null)
{
float floodedArea = 0.0f;
float totalArea = 0.0f;
@@ -1033,7 +1057,7 @@ namespace Barotrauma
if (GameMain.GameSession != null)
{
if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndPosition)
if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
return "levelend";
}
@@ -1102,7 +1126,11 @@ namespace Barotrauma
tempList.Add(s);
}
}
tempList.GetRandom().sound?.Play(1.0f, range, position, muffle: ShouldMuffleSound(Character.Controlled, position, range, null));
var damageSound = tempList.GetRandom();
if (damageSound.sound != null)
{
damageSound.sound.Play(1.0f, range, position, muffle: !damageSound.ignoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null));
}
}
public static void PlayUISound(GUISoundType soundType)
@@ -107,7 +107,7 @@ namespace Barotrauma.Sounds
readAmount += buf.Length;
}
}
return readAmount*2;
return readAmount;
}
public override void Dispose()
@@ -1,4 +1,5 @@
using Barotrauma.Networking;
using Barotrauma.IO;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using OpenAL;
using System;
@@ -35,14 +36,15 @@ namespace Barotrauma.Sounds
public float Near { get; private set; }
public float Far { get; private set; }
private static BiQuad[] muffleFilters = new BiQuad[]
private BiQuad[] muffleFilters = new BiQuad[]
{
new LowpassFilter(VoipConfig.FREQUENCY, 800)
};
private static BiQuad[] radioFilters = new BiQuad[]
private BiQuad[] radioFilters = new BiQuad[]
{
new BandpassFilter(VoipConfig.FREQUENCY, 2000)
};
private const float PostRadioFilterBoost = 1.2f;
private float gain;
public float Gain
@@ -61,10 +63,8 @@ namespace Barotrauma.Sounds
get { return soundChannel?.CurrentAmplitude ?? 0.0f; }
}
public VoipSound(string name, SoundManager owner, VoipQueue q) : base(owner, "voip", true, true)
public VoipSound(string name, SoundManager owner, VoipQueue q) : base(owner, $"VoIP ({name})", true, true, getFullPath: false)
{
Filename = $"VoIP ({name})";
VoipConfig.SetupEncoding();
ALFormat = Al.FormatMono16;
@@ -104,7 +104,7 @@ namespace Barotrauma.Sounds
if (gain * GameMain.Config.VoiceChatVolume > 1.0f) //TODO: take distance into account?
{
fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1.0f, 1.0f);
fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1f, 1f);
}
if (UseMuffleFilter)
@@ -118,7 +118,7 @@ namespace Barotrauma.Sounds
{
foreach (var filter in radioFilters)
{
fVal = filter.Process(fVal);
fVal = Math.Clamp(filter.Process(fVal) * PostRadioFilterBoost, -1f, 1f);
}
}
buffer[i] = FloatToShort(fVal);
@@ -154,7 +154,7 @@ namespace Barotrauma.Sounds
{
VoipConfig.Decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE);
bufferID++;
return VoipConfig.BUFFER_SIZE * 2;
return VoipConfig.BUFFER_SIZE;
}
if (bufferID < queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1)) bufferID = queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1);
}