2022-06-29 14:45:17 +03:00

266 lines
10 KiB
C#

using System.Collections.Generic;
namespace Photon.Voice
{
// Keeps buffer size within given bounds (discards or repeats samples) even if numbers of pushed and read samples per second are different
public class AudioSyncBuffer<T> : IAudioOut<T>
{
private int curPlayingFrameSamplePos;
private int sampleRate;
private int channels;
private int frameSamples;
private int frameSize;
private bool started;
private int maxDevPlayDelaySamples;
private int targetPlayDelaySamples;
int playDelayMs;
private readonly ILogger logger;
private readonly string logPrefix;
private readonly bool debugInfo;
private readonly int elementSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(T));
private T[] emptyFrame;
public AudioSyncBuffer(int playDelayMs, ILogger logger, string logPrefix, bool debugInfo)
{
this.playDelayMs = playDelayMs;
this.logger = logger;
this.logPrefix = logPrefix;
this.debugInfo = debugInfo;
}
public int Lag
{
get
{
lock (this)
{
return (int)((float)this.frameQueue.Count * this.frameSamples * 1000 / sampleRate);
}
}
}
public bool IsPlaying
{
get
{
lock (this)
{
return this.started;
}
}
}
// Can be called on runnig AudioSyncBuffer to reuse it for other parameters
public void Start(int sampleRate, int channels, int frameSamples)
{
lock (this)
{
this.started = false;
this.sampleRate = sampleRate;
// this.sampleRate = (int)(sampleRate * 1.2); // underrun test
// this.sampleRate = (int)(sampleRate / 1.2); // overrun test
this.channels = channels;
this.frameSamples = frameSamples;
this.frameSize = frameSamples * channels;
int playDelaySamples = playDelayMs * sampleRate / 1000 + frameSamples;
this.maxDevPlayDelaySamples = playDelaySamples / 2;
this.targetPlayDelaySamples = playDelaySamples + maxDevPlayDelaySamples;
if (this.framePool.Info != this.frameSize)
{
this.framePool.Init(this.frameSize);
}
//frameQueue = new Queue<T[]>();
while (this.frameQueue.Count > 0)
{
dequeueFrameQueue();
}
// it's important to change 'emptyFrame' value after frameQueue cleaned up, otherwise ' != this.emptyFrame' check in dequeueFrameQueue() will not work
this.emptyFrame = new T[this.frameSize];
// initial sync
int framesCnt = targetPlayDelaySamples / this.frameSamples;
this.curPlayingFrameSamplePos = targetPlayDelaySamples % this.frameSamples;
while (this.frameQueue.Count < framesCnt)
{
this.frameQueue.Enqueue(emptyFrame);
}
this.started = true;
}
}
Queue<T[]> frameQueue = new Queue<T[]>();
public const int FRAME_POOL_CAPACITY = 50;
PrimitiveArrayPool<T> framePool = new PrimitiveArrayPool<T>(FRAME_POOL_CAPACITY, "AudioSyncBuffer");
public void Service()
{
}
public void Read(T[] outBuf, int outChannels, int outSampleRate)
{
lock (this)
{
if (this.started)
{
int outPos = 0;
// enough data in remaining frames to fill entire out buffer
// framesElemRem / this.sampleRate >= outElemRem / outSampleRate
while ((this.frameQueue.Count * this.frameSamples - this.curPlayingFrameSamplePos) * this.channels * outSampleRate >= (outBuf.Length - outPos) * this.sampleRate)
{
int playingFramePos = this.curPlayingFrameSamplePos * this.channels;
var frame = frameQueue.Peek();
int outElemRem = outBuf.Length - outPos;
int frameElemRem = frame.Length - playingFramePos;
// enough data in the current frame to fill entire out buffer and some will remain for the next call: keeping this frame
// frameElemRem / (frCh * frRate) > outElemRem / (outCh * outRate)
if (frameElemRem * outChannels * outSampleRate > outElemRem * this.channels * this.sampleRate)
{
// frame remainder is large enough to fill outBuf remainder, keep this frame and return
//int framePosDelta = this.channels * outChannels * this.sampleRate / (outElemRem * outSampleRate);
int framePosDelta = outElemRem * this.channels* this.sampleRate / (outChannels * outSampleRate);
if (this.sampleRate == outSampleRate && this.channels == outChannels)
{
System.Buffer.BlockCopy(frame, playingFramePos * elementSize, outBuf, outPos * elementSize, outElemRem * elementSize);
}
else
{
AudioUtil.Resample(frame, playingFramePos, framePosDelta, this.channels, outBuf, outPos, outElemRem, outChannels);
}
this.curPlayingFrameSamplePos += framePosDelta / this.channels;
return;
}
// discarding current frame because it fills exactly out buffer or next frame required to do so
else
{
int outPosDelta = frameElemRem * outChannels * outSampleRate / (this.channels * this.sampleRate);
if (this.sampleRate == outSampleRate && this.channels == outChannels)
{
System.Buffer.BlockCopy(frame, playingFramePos * elementSize, outBuf, outPos * elementSize, frameElemRem * elementSize);
}
else
{
AudioUtil.Resample(frame, playingFramePos, frameElemRem, this.channels, outBuf, outPos, outPosDelta, outChannels);
}
outPos += outPosDelta;
this.curPlayingFrameSamplePos = 0;
dequeueFrameQueue();
if (outPosDelta == outElemRem)
{
return;
}
}
}
}
}
}
// may be called on any thread
public void Push(T[] frame)
{
lock (this)
{
if (this.started)
{
if (frame.Length == 0)
{
return;
}
if (frame.Length != this.frameSize)
{
logger.LogError("{0} AudioSyncBuffer audio frames are not of size: {1} != {2}", this.logPrefix, frame.Length, frameSize);
return;
}
//TODO: call framePool.AcquireOrCreate(frame.Length) and test
if (framePool.Info != frame.Length)
{
framePool.Init(frame.Length);
}
T[] b = framePool.AcquireOrCreate();
System.Buffer.BlockCopy(frame, 0, b, 0, System.Buffer.ByteLength(frame));
lock (this)
{
frameQueue.Enqueue(b);
syncFrameQueue();
}
}
}
}
public void Flush()
{
}
public void Stop()
{
lock (this)
{
this.started = false;
}
}
// call inside lock (this) { ... }
private void dequeueFrameQueue()
{
var f = this.frameQueue.Dequeue();
if (f != this.emptyFrame)
{
this.framePool.Release(f, f.Length);
}
}
// call inside lock (this) { ... }
private void syncFrameQueue()
{
var lagSamples = this.frameQueue.Count * this.frameSamples - this.curPlayingFrameSamplePos;
if (lagSamples > targetPlayDelaySamples + maxDevPlayDelaySamples)
{
int framesCnt = targetPlayDelaySamples / this.frameSamples;
this.curPlayingFrameSamplePos = targetPlayDelaySamples % this.frameSamples;
while (frameQueue.Count > framesCnt)
{
dequeueFrameQueue();
}
if (this.debugInfo)
{
this.logger.LogWarning("{0} AudioSynctBuffer overrun {1} {2} {3} {4}", this.logPrefix, targetPlayDelaySamples - maxDevPlayDelaySamples, targetPlayDelaySamples + maxDevPlayDelaySamples, lagSamples, framesCnt, this.curPlayingFrameSamplePos);
}
}
else if (lagSamples < targetPlayDelaySamples - maxDevPlayDelaySamples)
{
int framesCnt = targetPlayDelaySamples / this.frameSamples;
this.curPlayingFrameSamplePos = targetPlayDelaySamples % this.frameSamples;
while (frameQueue.Count < framesCnt)
{
frameQueue.Enqueue(emptyFrame);
}
if (this.debugInfo)
{
this.logger.LogWarning("{0} AudioSyncBuffer underrun {1} {2} {3} {4}", this.logPrefix, targetPlayDelaySamples - maxDevPlayDelaySamples, targetPlayDelaySamples + maxDevPlayDelaySamples, lagSamples, framesCnt, this.curPlayingFrameSamplePos);
}
}
}
}
}