This commit is contained in:
zpc 2025-03-28 14:48:09 +08:00
parent b77c86974a
commit 3702252e00
9 changed files with 727 additions and 348 deletions

View File

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using ShengShengBuXi.ConsoleApp.Models;
using System.Linq;
using System.Transactions;
namespace ShengShengBuXi.ConsoleApp.Services;
@ -867,6 +868,9 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
}
}
public static DateTime _lasHasoudDateTime = DateTime.Now;
public static bool _isSpeaking = false;
/// <summary>
/// 检查是否有声音
/// </summary>
@ -894,9 +898,19 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
}
}
}
bool isSpeaking = maxVolume > _config.Recording.SilenceThreshold;
if (isSpeaking && !_isSpeaking)
{
_isSpeaking = true;
}
if (DateTime.Now.Subtract(_lasHasoudDateTime).TotalSeconds > 1)
{
Console.WriteLine($"当前音量:{maxVolume},设置的音量灵敏度:{_config.Recording.SilenceThreshold},是否已监测到在说话中:{_isSpeaking}");
_lasHasoudDateTime = DateTime.Now;
_isSpeaking = false;
}
// 如果最大音量超过阈值,则认为有声音
return maxVolume > _config.Recording.SilenceThreshold;
return isSpeaking;
}
/// <summary>

View File

@ -1,5 +1,6 @@
{
"SignalRHubUrl": "http://115.159.44.16/audiohub",
"SignalRHubUrl": "http://localhost:81/audiohub",
"ConfigBackupPath": "config.json",
"AutoConnectToServer": true
}

View File

@ -19,7 +19,7 @@
"RecordingDeviceNumber": 0,
"SampleRate": 16000,
"Channels": 1,
"BufferMilliseconds": 100,
"BufferMilliseconds": 50,
"SilenceThreshold": 0.15,
"SilenceTimeoutSeconds": 30,
"AllowUserHangup": true,

View File

@ -480,10 +480,19 @@ namespace ShengShengBuXi.Hubs
{
try
{
_logger.LogDebug($"转发音频数据到{monitoringClients.Count}个监听客户端,数据长度: {audioData.Length}");
if (_configurationService.CurrentConfig.Network.EnableAudioNoiseReduction)
{
var jiangzao = _audioProcessingService.ApplyNoiseReduction(audioData, config.SampleRate, config.Channels);
_logger.LogDebug($"转发音频数据到{monitoringClients.Count}个监听客户端,数据长度: {audioData.Length},降噪后长度:{jiangzao.Length}");
// 尝试直接发送数据
await Clients.Clients(monitoringClients).SendAsync("ReceiveAudioData", jiangzao);
}
else
{
// 尝试直接发送数据
await Clients.Clients(monitoringClients).SendAsync("ReceiveAudioData", audioData);
}
}
catch (Exception ex)
{
_logger.LogError($"转发音频数据到监听端失败: {ex.Message}");

View File

@ -309,6 +309,11 @@ public class NetworkConfig
/// 是否启用实时音频传输
/// </summary>
public bool EnableAudioStreaming { get; set; } = true;
/// <summary>
/// 是否开启音频降噪
/// </summary>
public bool EnableAudioNoiseReduction { get; set; } = false;
/// <summary>
/// 心跳间隔(秒)

View File

@ -1,6 +1,11 @@
using Microsoft.Extensions.Logging;
using NAudio.Dsp;
using NAudio.Utils;
using NAudio.Wave;
using ShengShengBuXi.Models;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -9,13 +14,13 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace ShengShengBuXi.Services
namespace ShengShengBuXi.Services;
/// <summary>
/// 音频处理服务实现
/// </summary>
public class AudioProcessingService : IAudioProcessingService
{
/// <summary>
/// 音频处理服务实现
/// </summary>
public class AudioProcessingService : IAudioProcessingService
{
private readonly ILogger<AudioProcessingService> _logger;
private readonly IConfigurationService _configService;
private readonly ConcurrentDictionary<string, WaveFileWriter> _activeRecordings = new ConcurrentDictionary<string, WaveFileWriter>();
@ -115,6 +120,50 @@ namespace ShengShengBuXi.Services
}
}
/// <summary>
/// 应用噪声消除
/// </summary>
/// <param name="audioData">音频数据</param>
/// <param name="sampleRate">采样率</param>
/// <param name="channels">声道数</param>
/// <param name="noiseThreshold">噪声门限值</param>
/// <param name="attackSeconds">攻击时间</param>
/// <param name="releaseSeconds">释放时间</param>
/// <param name="highPassCutoff">高通滤波器截止频率(Hz)</param>
/// <param name="q">滤波器Q值</param>
/// <returns></returns>
public byte[] ApplyNoiseReduction(byte[] audioData, int sampleRate = 16000, int channels = 1, float noiseThreshold = 0.015f, float attackSeconds = 0.01f, float releaseSeconds = 0.1f, int highPassCutoff = 80, float q = 1.0f)
{
using (var inputStream = new MemoryStream(audioData))
using (var waveStream = new RawSourceWaveStream(inputStream, new WaveFormat(16000, 16, 1)))
{
var sampleProvider = waveStream.ToSampleProvider();
// 改进1更温和的噪声门参数
var noiseGate = new ImprovedNoiseGate(sampleProvider)
{
Threshold = 0.015f, // 降低阈值(原0.02)
AttackSeconds = 0.05f, // 延长Attack时间(原0.01)
ReleaseSeconds = 0.3f, // 延长Release时间(原0.1)
HoldSeconds = 0.2f // 新增保持时间
};
// 改进2更平缓的高通滤波
var highPassFilter = new BiQuadFilterSampleProvider(noiseGate);
highPassFilter.Filter = BiQuadFilter.HighPassFilter(
sampleProvider.WaveFormat.SampleRate,
60, // 降低截止频率(原80)
0.707f); // 使用更平缓的Q值(原1.0)
// 改进3添加平滑处理
var smoothedProvider = new SmoothingSampleProvider(highPassFilter);
var outputStream = new MemoryStream();
WaveFileWriter.WriteWavFileToStream(outputStream, smoothedProvider.ToWaveProvider16());
return outputStream.ToArray();
}
}
/// <summary>
/// 开始新的音频流处理
/// </summary>
@ -396,5 +445,290 @@ namespace ShengShengBuXi.Services
_isDisposed = true;
}
}
// 简单的噪声门实现
public class NoiseGateSampleProvider : ISampleProvider
{
private readonly ISampleProvider source;
private float threshold;
private float attackSeconds;
private float releaseSeconds;
private float envelope;
private float gain;
public NoiseGateSampleProvider(ISampleProvider source)
{
this.source = source;
this.WaveFormat = source.WaveFormat;
}
public float Threshold
{
get => threshold;
set => threshold = Math.Max(0, Math.Min(1, value));
}
public float AttackSeconds
{
get => attackSeconds;
set => attackSeconds = Math.Max(0.001f, value);
}
public float ReleaseSeconds
{
get => releaseSeconds;
set => releaseSeconds = Math.Max(0.001f, value);
}
public WaveFormat WaveFormat { get; }
public int Read(float[] buffer, int offset, int count)
{
int samplesRead = source.Read(buffer, offset, count);
float attackCoeff = (float)Math.Exp(-1.0 / (WaveFormat.SampleRate * attackSeconds));
float releaseCoeff = (float)Math.Exp(-1.0 / (WaveFormat.SampleRate * releaseSeconds));
for (int n = 0; n < samplesRead; n++)
{
float sample = buffer[offset + n];
float absSample = Math.Abs(sample);
// 包络跟踪
if (absSample > envelope)
envelope = absSample;
else
envelope *= (absSample > threshold) ? attackCoeff : releaseCoeff;
// 应用增益
if (envelope > threshold)
gain = 1.0f;
else
gain = 0.0f;
buffer[offset + n] = sample * gain;
}
return samplesRead;
}
}
// 简单的BiQuad滤波器包装
public class BiQuadFilterSampleProvider : ISampleProvider
{
private readonly ISampleProvider source;
private BiQuadFilter filter;
public BiQuadFilterSampleProvider(ISampleProvider source)
{
this.source = source;
this.WaveFormat = source.WaveFormat;
}
public BiQuadFilter Filter
{
get => filter;
set => filter = value;
}
public WaveFormat WaveFormat { get; }
public int Read(float[] buffer, int offset, int count)
{
int samplesRead = source.Read(buffer, offset, count);
if (filter != null)
{
for (int n = 0; n < samplesRead; n++)
{
buffer[offset + n] = filter.Transform(buffer[offset + n]);
}
}
return samplesRead;
}
}
public class ImprovedNoiseGate : ISampleProvider
{
private readonly ISampleProvider source;
private float threshold;
private float attackSeconds;
private float releaseSeconds;
private float holdSeconds;
private float envelope;
private bool gateOpen;
private int holdCountRemaining;
public ImprovedNoiseGate(ISampleProvider source)
{
this.source = source ?? throw new ArgumentNullException(nameof(source));
this.WaveFormat = source.WaveFormat;
// 默认参数
Threshold = 0.015f;
AttackSeconds = 0.05f;
ReleaseSeconds = 0.3f;
HoldSeconds = 0.2f;
}
public WaveFormat WaveFormat { get; }
/// <summary>
/// 噪声门阈值 (0.0-1.0)
/// </summary>
public float Threshold
{
get => threshold;
set => threshold = Math.Max(0.0f, Math.Min(1.0f, value));
}
/// <summary>
/// 启动时间 (秒)
/// </summary>
public float AttackSeconds
{
get => attackSeconds;
set => attackSeconds = Math.Max(0.001f, value);
}
/// <summary>
/// 释放时间 (秒)
/// </summary>
public float ReleaseSeconds
{
get => releaseSeconds;
set => releaseSeconds = Math.Max(0.001f, value);
}
/// <summary>
/// 保持时间 (秒),在信号低于阈值后保持门打开的时间
/// </summary>
public float HoldSeconds
{
get => holdSeconds;
set => holdSeconds = Math.Max(0.0f, value);
}
/// <summary>
/// 当前包络值 (只读)
/// </summary>
public float CurrentEnvelope => envelope;
/// <summary>
/// 当前门状态 (只读)
/// </summary>
public bool IsGateOpen => gateOpen;
public int Read(float[] buffer, int offset, int count)
{
int samplesRead = source.Read(buffer, offset, count);
// 预计算系数
float attackCoeff = CalculateCoefficient(AttackSeconds);
float releaseCoeff = CalculateCoefficient(ReleaseSeconds);
int holdSamples = (int)(WaveFormat.SampleRate * HoldSeconds);
for (int n = 0; n < samplesRead; n++)
{
float sample = buffer[offset + n];
float absSample = Math.Abs(sample);
// 更新包络
if (absSample > envelope)
{
envelope = absSample + (envelope - absSample) * attackCoeff;
}
else
{
envelope = absSample + (envelope - absSample) * releaseCoeff;
}
// 更新门状态
if (envelope > Threshold)
{
gateOpen = true;
holdCountRemaining = holdSamples; // 重置保持计数器
}
else if (holdCountRemaining > 0)
{
holdCountRemaining--;
}
else
{
gateOpen = false;
}
// 应用增益 (带平滑过渡)
float gain = gateOpen ? 1.0f : CalculateSoftGain(envelope);
buffer[offset + n] = sample * gain;
}
return samplesRead;
}
private float CalculateCoefficient(float timeInSeconds)
{
if (timeInSeconds <= 0.0f) return 0.0f;
return (float)Math.Exp(-1.0 / (WaveFormat.SampleRate * timeInSeconds));
}
private float CalculateSoftGain(float env)
{
// 软过渡:当包络接近阈值时逐渐降低增益
if (env >= Threshold) return 1.0f;
// 计算相对阈值的位置 (0.0-1.0)
float relativePosition = env / Threshold;
// 三次方曲线实现平滑过渡
return relativePosition * relativePosition * relativePosition;
}
/// <summary>
/// 重置噪声门状态
/// </summary>
public void Reset()
{
envelope = 0.0f;
gateOpen = false;
holdCountRemaining = 0;
}
}
// 新增平滑处理器
public class SmoothingSampleProvider : ISampleProvider
{
private readonly ISampleProvider source;
private readonly float[] history;
private int historyIndex;
public SmoothingSampleProvider(ISampleProvider source, int windowSize = 5)
{
this.source = source;
this.history = new float[windowSize];
this.WaveFormat = source.WaveFormat;
}
public WaveFormat WaveFormat { get; }
public int Read(float[] buffer, int offset, int count)
{
int samplesRead = source.Read(buffer, offset, count);
for (int n = 0; n < samplesRead; n++)
{
history[historyIndex] = buffer[offset + n];
historyIndex = (historyIndex + 1) % history.Length;
// 简单移动平均平滑
float sum = 0;
for (int i = 0; i < history.Length; i++)
sum += history[i];
buffer[offset + n] = sum / history.Length;
}
return samplesRead;
}
}

View File

@ -76,5 +76,20 @@ namespace ShengShengBuXi.Services
/// <param name="clientId">客户端ID</param>
/// <returns>录音文件路径如果没有则返回null</returns>
string GetRecordingFilePath(string clientId);
/// <summary>
/// 应用噪声消除
/// </summary>
/// <param name="audioData">音频数据</param>
/// <param name="sampleRate">采样率</param>
/// <param name="channels">声道数</param>
/// <param name="noiseThreshold">噪声门限值</param>
/// <param name="attackSeconds">攻击时间</param>
/// <param name="releaseSeconds">释放时间</param>
/// <param name="highPassCutoff">高通滤波器截止频率(Hz)</param>
/// <param name="q">滤波器Q值</param>
/// <returns></returns>
byte[] ApplyNoiseReduction(byte[] audioData, int sampleRate = 16000, int channels = 1, float noiseThreshold = 0.02f, float attackSeconds = 0.01f, float releaseSeconds = 0.1f, int highPassCutoff = 80, float q = 1.0f);
}
}

View File

@ -562,7 +562,7 @@ namespace ShengShengBuXi.Services
await ProcessAudioAsync(audioData, sessionId, token);
// 等待一段时间,模拟实时处理
await Task.Delay(100, token);
await Task.Delay(20, token);
}
// 结束会话

View File

@ -39,6 +39,7 @@
"reconnectDelayMs": 2000,
"enableSpeechToText": true,
"enableAudioStreaming": true,
"EnableAudioNoiseReduction": false,
"heartbeatIntervalSeconds": 15,
"tencentCloudASR": {
"appId": "",