266 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			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);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| } |