a50deb73d3
Removing if (length < 3000) check fixes the issue because it ensures small buffers are not prematurely discarded. The original check was likely added with a misguided assumption that small buffers are insignificant, but this breaks microphone functionality where small buffers are common.
217 lines
7.7 KiB
C#
217 lines
7.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using ManagedBass;
|
|
using ManagedBass.Wasapi;
|
|
using T3.Core.Animation;
|
|
using T3.Core.Logging;
|
|
using T3.Core.Operator;
|
|
|
|
namespace T3.Core.Audio
|
|
{
|
|
/// <summary>
|
|
/// Uses the windows Wasapi audio API to get audio reaction from devices like speakers and microphones
|
|
/// </summary>
|
|
public static class WasapiAudioInput
|
|
{
|
|
/// <summary>
|
|
/// Needs to be called once a frame
|
|
/// Switches input device required
|
|
/// </summary>
|
|
public static void StartFrame(PlaybackSettings settings)
|
|
{
|
|
_fftUpdatesSinceLastFrame = 0;
|
|
|
|
if (settings == null)
|
|
return;
|
|
|
|
if (settings.AudioSource != PlaybackSettings.AudioSources.ExternalDevice)
|
|
{
|
|
if (!string.IsNullOrEmpty(ActiveInputDeviceName))
|
|
{
|
|
Stop();
|
|
}
|
|
return ;
|
|
}
|
|
|
|
var deviceName = settings.AudioInputDeviceName;
|
|
if (ActiveInputDeviceName == deviceName)
|
|
return;
|
|
|
|
if (string.IsNullOrEmpty(deviceName))
|
|
{
|
|
if (_complainedOnce)
|
|
return ;
|
|
|
|
Log.Warning("Can't switch to WASAPI device without a name");
|
|
_complainedOnce = true;
|
|
return ;
|
|
}
|
|
|
|
|
|
var device = InputDevices.FirstOrDefault(d => d.DeviceInfo.Name == deviceName);
|
|
if (device == null)
|
|
{
|
|
Log.Warning($"Can't find input device {deviceName}");
|
|
_complainedOnce = true;
|
|
return ;
|
|
}
|
|
|
|
StartInputCapture(device);
|
|
_complainedOnce = false;
|
|
}
|
|
|
|
|
|
public static List<WasapiInputDevice> InputDevices
|
|
{
|
|
get
|
|
{
|
|
if (_inputDevices == null)
|
|
InitializeInputDeviceList();
|
|
|
|
return _inputDevices;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// If device is null we will attempt default input index
|
|
/// </summary>
|
|
private static void StartInputCapture(WasapiInputDevice device)
|
|
//public static void StartInputCapture(string deviceName)
|
|
{
|
|
int inputDeviceIndex = BassWasapi.DefaultInputDevice;
|
|
|
|
if (device == null)
|
|
{
|
|
if (_inputDevices.Count == 0)
|
|
{
|
|
Log.Error("No wasapi input devices found");
|
|
return;
|
|
}
|
|
|
|
Log.Error($"Attempting default input {BassWasapi.DefaultInputDevice}.");
|
|
device = _inputDevices[0];
|
|
}
|
|
else
|
|
{
|
|
Log.Debug($"Initializing WASAPI audio input for {device.DeviceInfo.Name}... ");
|
|
inputDeviceIndex = device.WasapiDeviceIndex;
|
|
}
|
|
|
|
//Bass.Configure(Configuration.UpdateThreads, false);
|
|
// Bass.Configure(Configuration.DeviceBufferLength, 1024);
|
|
|
|
BassWasapi.Stop();
|
|
BassWasapi.Free();
|
|
if (!BassWasapi.Init(inputDeviceIndex,
|
|
Frequency: 0,
|
|
Channels: 0,
|
|
//Flags: WasapiInitFlags.Buffer | WasapiInitFlags.Exclusive,
|
|
Flags: WasapiInitFlags.Buffer,
|
|
Buffer: (float)device.DeviceInfo.DefaultUpdatePeriod,
|
|
Period: (float)device.DeviceInfo.DefaultUpdatePeriod,
|
|
Procedure: _wasapiProcedure,
|
|
User: IntPtr.Zero))
|
|
{
|
|
Log.Error("Can't initialize WASAPI:" + Bass.LastError);
|
|
return;
|
|
}
|
|
|
|
ActiveInputDeviceName = device.DeviceInfo.Name;
|
|
var result = BassWasapi.Start();
|
|
//Log.Debug("Wasapi.StartInputCapture() -> BassWasapi.Start():" + result);
|
|
}
|
|
|
|
private static void Stop()
|
|
{
|
|
//Log.Debug("Wasapi.Stop()");
|
|
BassWasapi.Stop();
|
|
BassWasapi.Free();
|
|
ActiveInputDeviceName = null;
|
|
}
|
|
|
|
private static bool _complainedOnce;
|
|
|
|
|
|
private static void InitializeInputDeviceList()
|
|
{
|
|
_inputDevices = new List<WasapiInputDevice>();
|
|
|
|
// Keep in local variable to avoid double evaluation
|
|
var deviceCount = BassWasapi.DeviceCount;
|
|
|
|
for (var deviceIndex = 0; deviceIndex < deviceCount; deviceIndex++)
|
|
{
|
|
var deviceInfo = BassWasapi.GetDeviceInfo(deviceIndex);
|
|
var isValidInputDevice = deviceInfo.IsEnabled && (deviceInfo.IsLoopback || deviceInfo.IsInput);
|
|
|
|
if (!isValidInputDevice)
|
|
continue;
|
|
|
|
Log.Debug($"Found Wasapi input ID:{_inputDevices.Count} {deviceInfo.Name} LoopBack:{deviceInfo.IsLoopback} IsInput:{deviceInfo.IsInput} (at {deviceIndex})");
|
|
_inputDevices.Add(new WasapiInputDevice()
|
|
{
|
|
WasapiDeviceIndex = deviceIndex,
|
|
DeviceInfo = deviceInfo,
|
|
});
|
|
}
|
|
}
|
|
|
|
private static int Process(IntPtr buffer, int length, IntPtr user)
|
|
{
|
|
|
|
//Log.Debug($"Wasapi.Process() called with buffer length: {length}");
|
|
var level = BassWasapi.GetLevel();
|
|
|
|
_lastUpdateTime = Playback.RunTimeInSecs;
|
|
|
|
int resultCode;
|
|
if (_fftUpdatesSinceLastFrame == 0)
|
|
{
|
|
resultCode = BassWasapi.GetData(AudioAnalysis.FftGainBuffer, (int)(AudioAnalysis.BassFlagForFftBufferSize | DataFlags.FFTRemoveDC));
|
|
//Log.Debug("Wasapi.Process() Result code after first update:" +resultCode);
|
|
}
|
|
else
|
|
{
|
|
resultCode = BassWasapi.GetData(_fftIntermediate, (int)(AudioAnalysis.BassFlagForFftBufferSize | DataFlags.FFTRemoveDC));
|
|
//Log.Debug("Wasapi.Process() Result code after another update:" +resultCode);
|
|
if (resultCode >= 0)
|
|
{
|
|
for (var i = 0; i < AudioAnalysis.FftHalfSize; i++)
|
|
{
|
|
AudioAnalysis.FftGainBuffer[i] = MathF.Max(_fftIntermediate[i], AudioAnalysis.FftGainBuffer[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (resultCode < 0)
|
|
{
|
|
Log.Debug($"Can't get Wasapi FFT-Data: {Bass.LastError}");
|
|
}
|
|
|
|
_lastAudioLevel = (float)(level * 0.00001);
|
|
_fftUpdatesSinceLastFrame++;
|
|
//Log.Debug($"Process with {length} #{_fftUpdatesSinceLastFrame} L:{audioLevel:0.0} DevBufLen:{BassWasapi.Info.BufferLength}");
|
|
return length;
|
|
}
|
|
|
|
private static int _fftUpdatesSinceLastFrame;
|
|
|
|
public class WasapiInputDevice
|
|
{
|
|
public int WasapiDeviceIndex;
|
|
public WasapiDeviceInfo DeviceInfo;
|
|
}
|
|
|
|
private static List<WasapiInputDevice> _inputDevices;
|
|
private static readonly float[] _fftIntermediate = new float[AudioAnalysis.FftHalfSize];
|
|
private static readonly WasapiProcedure _wasapiProcedure = Process;
|
|
private static double _lastUpdateTime;
|
|
|
|
public static string ActiveInputDeviceName { get; private set; }
|
|
private static float _lastAudioLevel;
|
|
public static float DecayingAudioLevel => (float)(_lastAudioLevel / Math.Max(1, (Playback.RunTimeInSecs - _lastUpdateTime) * 100));
|
|
}
|
|
} |