Files
codex 740f93f185
PR and Push Build/Test / build-and-test (push) Successful in 11m22s
Improve meeting audio mixing pipeline
2026-05-29 13:47:21 +02:00

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);
}
}