Public Access
131 lines
3.9 KiB
C#
131 lines
3.9 KiB
C#
namespace MeetingAssistant.Recording;
|
|
|
|
public interface IAcousticEchoCanceller
|
|
{
|
|
void Reset();
|
|
|
|
AudioChunk CleanMicrophone(AudioChunk microphone, AudioChunk systemAudio);
|
|
}
|
|
|
|
public interface IAcousticEchoCancellerFactory
|
|
{
|
|
IAcousticEchoCanceller Create();
|
|
}
|
|
|
|
public sealed class AdaptiveFilterAcousticEchoCancellerFactory : IAcousticEchoCancellerFactory
|
|
{
|
|
public IAcousticEchoCanceller Create()
|
|
{
|
|
return new AdaptiveFilterAcousticEchoCanceller();
|
|
}
|
|
}
|
|
|
|
public sealed class NoopAcousticEchoCanceller : IAcousticEchoCanceller
|
|
{
|
|
public void Reset()
|
|
{
|
|
}
|
|
|
|
public AudioChunk CleanMicrophone(AudioChunk microphone, AudioChunk systemAudio)
|
|
{
|
|
return microphone;
|
|
}
|
|
}
|
|
|
|
public sealed class AdaptiveFilterAcousticEchoCanceller : IAcousticEchoCanceller
|
|
{
|
|
private const int DefaultFilterLength = 256;
|
|
private const double DefaultAdaptationStep = 0.25;
|
|
private const double Epsilon = 1.0;
|
|
private readonly double[] coefficients;
|
|
private readonly double[] renderHistory;
|
|
private int historyPosition;
|
|
|
|
public AdaptiveFilterAcousticEchoCanceller()
|
|
: this(DefaultFilterLength)
|
|
{
|
|
}
|
|
|
|
public AdaptiveFilterAcousticEchoCanceller(int filterLength)
|
|
{
|
|
var effectiveFilterLength = Math.Max(16, filterLength);
|
|
coefficients = new double[effectiveFilterLength];
|
|
renderHistory = new double[effectiveFilterLength];
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
Array.Clear(coefficients);
|
|
Array.Clear(renderHistory);
|
|
historyPosition = 0;
|
|
}
|
|
|
|
public AudioChunk CleanMicrophone(AudioChunk microphone, AudioChunk systemAudio)
|
|
{
|
|
if (microphone.Pcm.Length == 0 ||
|
|
systemAudio.Pcm.Length == 0 ||
|
|
microphone.SampleRate != systemAudio.SampleRate ||
|
|
microphone.Channels != 1 ||
|
|
systemAudio.Channels != 1)
|
|
{
|
|
return microphone;
|
|
}
|
|
|
|
var byteCount = Math.Min(microphone.Pcm.Length, systemAudio.Pcm.Length);
|
|
byteCount -= byteCount % sizeof(short);
|
|
if (byteCount <= 0)
|
|
{
|
|
return microphone;
|
|
}
|
|
|
|
var cleaned = new byte[microphone.Pcm.Length];
|
|
for (var byteIndex = 0; byteIndex < byteCount; byteIndex += sizeof(short))
|
|
{
|
|
var render = BitConverter.ToInt16(systemAudio.Pcm, byteIndex) / 32768.0;
|
|
var capture = BitConverter.ToInt16(microphone.Pcm, byteIndex) / 32768.0;
|
|
renderHistory[historyPosition] = render;
|
|
|
|
var estimatedEcho = 0.0;
|
|
var energy = Epsilon;
|
|
for (var tap = 0; tap < coefficients.Length; tap++)
|
|
{
|
|
var sample = renderHistory[HistoryIndex(tap)];
|
|
estimatedEcho += coefficients[tap] * sample;
|
|
energy += sample * sample;
|
|
}
|
|
|
|
var error = capture - estimatedEcho;
|
|
var updateScale = DefaultAdaptationStep * error / energy;
|
|
for (var tap = 0; tap < coefficients.Length; tap++)
|
|
{
|
|
coefficients[tap] += updateScale * renderHistory[HistoryIndex(tap)];
|
|
}
|
|
|
|
WriteSample(cleaned, byteIndex, error);
|
|
historyPosition = historyPosition == 0 ? renderHistory.Length - 1 : historyPosition - 1;
|
|
}
|
|
|
|
if (byteCount < microphone.Pcm.Length)
|
|
{
|
|
Buffer.BlockCopy(microphone.Pcm, byteCount, cleaned, byteCount, microphone.Pcm.Length - byteCount);
|
|
}
|
|
|
|
return microphone with { Pcm = cleaned };
|
|
}
|
|
|
|
private int HistoryIndex(int offset)
|
|
{
|
|
var index = historyPosition + offset;
|
|
return index >= renderHistory.Length ? index - renderHistory.Length : index;
|
|
}
|
|
|
|
private static void WriteSample(byte[] target, int byteIndex, double sample)
|
|
{
|
|
var scaled = Math.Clamp(
|
|
(int)Math.Round(sample * 32768.0),
|
|
short.MinValue,
|
|
short.MaxValue);
|
|
BitConverter.GetBytes((short)scaled).CopyTo(target, byteIndex);
|
|
}
|
|
}
|