0
$\begingroup$

I'm trying to wright a program to compare, two sound signals, output (speakers) and input (mic) streams and verify/adjust the volume. I'm using Microsoft Visual Studio C# forms project with the MauiFft package, but I’m open to any programming language. I’ve found a way to use the NAudio capture the input signal from the mic and using WasapiLoopbackCapture to capture the output going to the speakers. The pattern that I’m getting in the SkiaSharp SKGControl looks a lot different. Is there a better way to do this?

FttProcessor.cs

    namespace MauiFft;

using NAudio.CoreAudioApi;
using NAudio.Wave;

public class FftProcessor : IDisposable
{
    readonly List<double> Values = new();
    readonly List<double> ValuesOut = new();

    readonly NAudio.Wave.WaveInEvent WaveIn;
    readonly WasapiCapture WaveOut;

    public readonly MMDevice[] AudioDevices = new MMDeviceEnumerator()
    .EnumerateAudioEndPoints(DataFlow.All, DeviceState.Active)
    .ToArray();

    public FftProcessor(int device = 0, int deviceOut = 0)
    {
        // input signal from NAudio
        WaveIn = new()
        {
            DeviceNumber = device,
            WaveFormat = new NAudio.Wave.WaveFormat(rate: 12_000, bits: 16, channels: 1),
            BufferMilliseconds = 10,
        };
        WaveIn.DataAvailable += WaveIn_DataAvailable; ;
        WaveIn.StartRecording();

        // output signal from WasapiLoopbackCapture
        WaveOut = new WasapiLoopbackCapture(AudioDevices[0]);
        WaveOut.DataAvailable += WaveOut_DataAvailable; ;
        WaveOut.StartRecording();
    }

    public void Dispose()
    {
        WaveIn.DataAvailable -= WaveIn_DataAvailable;
        WaveIn.StopRecording();
        WaveIn.Dispose();
        WaveOut.DataAvailable -= WaveOut_DataAvailable;
        WaveOut.StopRecording();
        WaveOut.Dispose();
    }

    private void WaveIn_DataAvailable(object? sender, NAudio.Wave.WaveInEventArgs e)
    {
        for (int index = 0; index < e.BytesRecorded; index += 2)
        {
            double value = BitConverter.ToInt16(e.Buffer, index);
            Values.Add(value);
        }
    }

    private void WaveOut_DataAvailable(object? sender, NAudio.Wave.WaveInEventArgs e)
    {
        for (int index = 0; index < e.BytesRecorded; index += 2)
        {
            double value = BitConverter.ToInt16(e.Buffer, index);
            ValuesOut.Add(value);
        }
    }

    public double[]? GetFft(int pow = 10, double stepFrac = 0.1)
    {
        int sampleCount = 1 << pow;
        if (Values.Count < sampleCount)
            return null;

        double[] values = new double[sampleCount];
        Values.CopyTo(Values.Count - sampleCount, values, 0, sampleCount);

        int pointsToKeep = (int)((1 - stepFrac) * sampleCount);
        Values.RemoveRange(0, Values.Count - pointsToKeep);

        double[] fft = FftSharp.Transform.FFTmagnitude(values);
        return fft;
    }

    public double[]? GetFftOut(int pow = 10, double stepFrac = 0.1)
    {
        int sampleCount = 1 << pow;
        if (ValuesOut.Count < sampleCount)
            return null;

        double[] values = new double[sampleCount];
        ValuesOut.CopyTo(ValuesOut.Count - sampleCount, values, 0, sampleCount);

        int pointsToKeep = (int)((1 - stepFrac) * sampleCount);
        ValuesOut.RemoveRange(0, ValuesOut.Count - pointsToKeep);

        double[] fft = FftSharp.Transform.FFTmagnitude(values);
        return fft;
    }

}

Form1.cs

    using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;

namespace MauiFft;

public partial class Form1 : Form
{
    double[]? LastFft;
    double[]? LastFftOut;

    double MaxFft = 1;
    double MaxFftOut = 1;

    readonly FftProcessor FftProc = new();

    public Form1()
    {
        InitializeComponent();
    }

    private void skglControl1_PaintSurface(object sender, SkiaSharp.Views.Desktop.SKPaintGLSurfaceEventArgs e)
    {
        if (skglControl1 is null) { return; }

        float width = skglControl1.Width;
        float height = skglControl1.Height;

        ICanvas canvas = new SkiaCanvas() { Canvas = e.Surface.Canvas };
        canvas.FillColor = Microsoft.Maui.Graphics.Color.FromArgb("#003366");
        canvas.FillRectangle(0, 0, width, height);

        List<RedLine> redlines = new List<RedLine>();
        List<GreenLine> greenlines = new List<GreenLine>();

        if (LastFft != null)
        {
            double lastFftMax = LastFft.Max();
            MaxFft = Math.Max(MaxFft, lastFftMax);
            float[] ys = LastFft.Select(x => (float)(x / MaxFft) * skglControl1.Height).ToArray();
            float[] xs = Enumerable.Range(0, ys.Length).Select(x => (float)x / ys.Length * skglControl1.Width).ToArray();
            var points = LastFft.Select((mag, i) => new Microsoft.Maui.Graphics.PointF(xs[i], skglControl1.Height - ys[i])).ToArray();
            for (int i = 0; i < points.Length - 1; i++)
            {
                greenlines.Add(new GreenLine(points[i], points[i + 1]));
                canvas.StrokeColor = greenlines[i].PenColor;
                canvas.DrawLine(greenlines[i].Point0, greenlines[i].Point1);
            }
        }
        if (LastFftOut != null)
        {
            double lastFftMaxOut = LastFftOut.Max();
            MaxFftOut = Math.Max(MaxFftOut, lastFftMaxOut);
            float[] ys = LastFftOut.Select(x => (float)(x / MaxFftOut) * skglControl1.Height).ToArray();
            float[] xs = Enumerable.Range(0, ys.Length).Select(x => (float)x / ys.Length * skglControl1.Width).ToArray();
            var pointsOut = LastFftOut.Select((mag, i) => new Microsoft.Maui.Graphics.PointF(xs[i], skglControl1.Height - ys[i])).ToArray();
            for (int i = 0; i < pointsOut.Length - 1; i++)
            {
                redlines.Add(new RedLine(pointsOut[i], pointsOut[i + 1]));
                canvas.StrokeColor = redlines[i].PenColor;
                canvas.DrawLine(redlines[i].Point0, redlines[i].Point1);

            }
        }
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        if(FreezeCheckBox.Checked) { return; }
        double[]? fft = FftProc.GetFft();
        bool redraw = false;
        if (fft is not null)
        {
            LastFft = fft;
            redraw = true;
        }
        double[]? fftOut = FftProc.GetFftOut();
        if (fftOut != null)
        {
            LastFftOut = fftOut;
            redraw = true;

        }
        if (redraw)
        {
            skglControl1.Invalidate();
        }

    }
}

600Hz tone generator

$\endgroup$
1
  • 1
    $\begingroup$ You could first "sketch" your prog by using Octave/Matlab? Does it make any difference if you do everything using equal sample-rate and bit-depth (sample-rate by your audio interface (onboard audio is usually 48kHz based and bit-depth defaulted to 16-bits)). $\endgroup$
    – Juha P
    Commented Apr 3 at 8:44

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.