Public Access
Improve meeting audio mixing pipeline
PR and Push Build/Test / build-and-test (push) Successful in 11m22s
PR and Push Build/Test / build-and-test (push) Successful in 11m22s
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user