diff --git a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs
index e2a6c19..4674769 100644
--- a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs
+++ b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs
@@ -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;
+
///
/// 检查是否有声音
///
@@ -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;
}
///
diff --git a/ShengShengBuXi.ConsoleApp/appsettings.json b/ShengShengBuXi.ConsoleApp/appsettings.json
index a3f1396..e5986d1 100644
--- a/ShengShengBuXi.ConsoleApp/appsettings.json
+++ b/ShengShengBuXi.ConsoleApp/appsettings.json
@@ -1,5 +1,6 @@
{
- "SignalRHubUrl": "http://115.159.44.16/audiohub",
+
+ "SignalRHubUrl": "http://localhost:81/audiohub",
"ConfigBackupPath": "config.json",
"AutoConnectToServer": true
}
\ No newline at end of file
diff --git a/ShengShengBuXi.ConsoleApp/config.json b/ShengShengBuXi.ConsoleApp/config.json
index dc64b8f..ca0f75c 100644
--- a/ShengShengBuXi.ConsoleApp/config.json
+++ b/ShengShengBuXi.ConsoleApp/config.json
@@ -19,7 +19,7 @@
"RecordingDeviceNumber": 0,
"SampleRate": 16000,
"Channels": 1,
- "BufferMilliseconds": 100,
+ "BufferMilliseconds": 50,
"SilenceThreshold": 0.15,
"SilenceTimeoutSeconds": 30,
"AllowUserHangup": true,
diff --git a/ShengShengBuXi/Hubs/AudioHub.cs b/ShengShengBuXi/Hubs/AudioHub.cs
index 8f63618..61457e7 100644
--- a/ShengShengBuXi/Hubs/AudioHub.cs
+++ b/ShengShengBuXi/Hubs/AudioHub.cs
@@ -83,7 +83,7 @@ namespace ShengShengBuXi.Hubs
/// 预设句子列表
///
private static List _presetSentences = new List();
-
+
// 配置信息
private static string _displayConfig = "{}";
private static object _displayConfigLock = new object();
@@ -480,9 +480,18 @@ namespace ShengShengBuXi.Hubs
{
try
{
- _logger.LogDebug($"转发音频数据到{monitoringClients.Count}个监听客户端,数据长度: {audioData.Length}");
- // 尝试直接发送数据
- await Clients.Clients(monitoringClients).SendAsync("ReceiveAudioData", audioData);
+ 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)
{
diff --git a/ShengShengBuXi/Models/PhoneBoothConfig.cs b/ShengShengBuXi/Models/PhoneBoothConfig.cs
index 463b444..8768b2d 100644
--- a/ShengShengBuXi/Models/PhoneBoothConfig.cs
+++ b/ShengShengBuXi/Models/PhoneBoothConfig.cs
@@ -309,6 +309,11 @@ public class NetworkConfig
/// 是否启用实时音频传输
///
public bool EnableAudioStreaming { get; set; } = true;
+ ///
+ /// 是否开启音频降噪
+ ///
+
+ public bool EnableAudioNoiseReduction { get; set; } = false;
///
/// 心跳间隔(秒)
diff --git a/ShengShengBuXi/Services/AudioProcessingService.cs b/ShengShengBuXi/Services/AudioProcessingService.cs
index 4335f7c..e28653d 100644
--- a/ShengShengBuXi/Services/AudioProcessingService.cs
+++ b/ShengShengBuXi/Services/AudioProcessingService.cs
@@ -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,392 +14,721 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-namespace ShengShengBuXi.Services
+namespace ShengShengBuXi.Services;
+
+///
+/// 音频处理服务实现
+///
+public class AudioProcessingService : IAudioProcessingService
{
+ private readonly ILogger _logger;
+ private readonly IConfigurationService _configService;
+ private readonly ConcurrentDictionary _activeRecordings = new ConcurrentDictionary();
+ private readonly ConcurrentDictionary _recordingFilePaths = new ConcurrentDictionary();
+ private bool _isDisposed;
+
///
- /// 音频处理服务实现
+ /// 当有新的语音转文字结果时触发
///
- public class AudioProcessingService : IAudioProcessingService
+ public event EventHandler SpeechToTextResultReceived;
+
+ ///
+ /// 当音频数据被保存为文件时触发
+ ///
+ public event EventHandler AudioSavedToFile;
+
+ ///
+ /// 初始化音频处理服务
+ ///
+ /// 日志记录器
+ /// 配置服务
+ public AudioProcessingService(ILogger logger, IConfigurationService configService)
{
- private readonly ILogger _logger;
- private readonly IConfigurationService _configService;
- private readonly ConcurrentDictionary _activeRecordings = new ConcurrentDictionary();
- private readonly ConcurrentDictionary _recordingFilePaths = new ConcurrentDictionary();
- private bool _isDisposed;
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _configService = configService ?? throw new ArgumentNullException(nameof(configService));
+ }
- ///
- /// 当有新的语音转文字结果时触发
- ///
- public event EventHandler SpeechToTextResultReceived;
-
- ///
- /// 当音频数据被保存为文件时触发
- ///
- public event EventHandler AudioSavedToFile;
-
- ///
- /// 初始化音频处理服务
- ///
- /// 日志记录器
- /// 配置服务
- public AudioProcessingService(ILogger logger, IConfigurationService configService)
+ ///
+ /// 初始化音频处理服务
+ ///
+ /// 初始化是否成功
+ public bool Initialize()
+ {
+ try
{
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _configService = configService ?? throw new ArgumentNullException(nameof(configService));
+ _logger.LogInformation("初始化音频处理服务");
+
+ // 确保录音文件夹存在
+ var recordingsFolder = Path.Combine(Directory.GetCurrentDirectory(), _configService.CurrentConfig.Recording.RecordingsFolder);
+ if (!Directory.Exists(recordingsFolder))
+ {
+ Directory.CreateDirectory(recordingsFolder);
+ _logger.LogInformation($"已创建录音文件夹: {recordingsFolder}");
+ }
+
+ // 如果配置了自动清理,清理旧录音
+ if (_configService.CurrentConfig.Recording.AutoCleanupOldRecordings)
+ {
+ CleanupOldRecordings();
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"初始化音频处理服务失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// 处理接收到的音频数据
+ ///
+ /// 音频数据
+ /// 采样率
+ /// 声道数
+ /// 客户端ID
+ /// 取消令牌
+ /// 异步任务
+ public async Task ProcessAudioDataAsync(byte[] audioData, int sampleRate, int channels, string clientId, CancellationToken token = default)
+ {
+ if (audioData == null || audioData.Length == 0)
+ {
+ return;
}
- ///
- /// 初始化音频处理服务
- ///
- /// 初始化是否成功
- public bool Initialize()
+ if (string.IsNullOrEmpty(clientId))
{
- try
- {
- _logger.LogInformation("初始化音频处理服务");
-
- // 确保录音文件夹存在
- var recordingsFolder = Path.Combine(Directory.GetCurrentDirectory(), _configService.CurrentConfig.Recording.RecordingsFolder);
- if (!Directory.Exists(recordingsFolder))
- {
- Directory.CreateDirectory(recordingsFolder);
- _logger.LogInformation($"已创建录音文件夹: {recordingsFolder}");
- }
-
- // 如果配置了自动清理,清理旧录音
- if (_configService.CurrentConfig.Recording.AutoCleanupOldRecordings)
- {
- CleanupOldRecordings();
- }
-
- return true;
- }
- catch (Exception ex)
- {
- _logger.LogError($"初始化音频处理服务失败: {ex.Message}");
- return false;
- }
+ throw new ArgumentNullException(nameof(clientId));
}
- ///
- /// 处理接收到的音频数据
- ///
- /// 音频数据
- /// 采样率
- /// 声道数
- /// 客户端ID
- /// 取消令牌
- /// 异步任务
- public async Task ProcessAudioDataAsync(byte[] audioData, int sampleRate, int channels, string clientId, CancellationToken token = default)
+ // 检查是否有活动的录音
+ if (!_activeRecordings.TryGetValue(clientId, out var writer))
{
- if (audioData == null || audioData.Length == 0)
- {
- return;
- }
-
- if (string.IsNullOrEmpty(clientId))
- {
- throw new ArgumentNullException(nameof(clientId));
- }
-
- // 检查是否有活动的录音
- if (!_activeRecordings.TryGetValue(clientId, out var writer))
- {
- _logger.LogWarning($"客户端没有活动的录音会话: {clientId}");
- return;
- }
-
- try
- {
- // 异步写入音频数据
- await Task.Run(() => { writer.Write(audioData, 0, audioData.Length); writer.Flush(); }, token);
- }
- catch (Exception ex)
- {
- _logger.LogError($"处理音频数据失败: {ex.Message}");
- }
+ _logger.LogWarning($"客户端没有活动的录音会话: {clientId}");
+ return;
}
- ///
- /// 开始新的音频流处理
- ///
- /// 客户端ID
- /// 采样率
- /// 声道数
- /// 取消令牌
- /// 异步任务
- public Task StartAudioStreamAsync(string clientId, int sampleRate, int channels, CancellationToken token = default)
+ try
{
- if (string.IsNullOrEmpty(clientId))
- {
- throw new ArgumentNullException(nameof(clientId));
- }
+ // 异步写入音频数据
+ await Task.Run(() => { writer.Write(audioData, 0, audioData.Length); writer.Flush(); }, token);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"处理音频数据失败: {ex.Message}");
+ }
+ }
- // 如果已经有一个活动的录音,先结束它
- if (_activeRecordings.TryGetValue(clientId, out var existingWriter))
- {
- _logger.LogWarning($"客户端已有活动的录音会话,先结束它: {clientId}");
- EndAudioStreamAsync(clientId).Wait(token);
- }
-
- try
- {
- // 创建录音文件路径
- var config = _configService.CurrentConfig.Recording;
- var recordingsFolder = Path.Combine(Directory.GetCurrentDirectory(), config.RecordingsFolder);
- if (!Directory.Exists(recordingsFolder))
- {
- Directory.CreateDirectory(recordingsFolder);
- }
-
- // 创建文件名
- var fileName = string.Format(config.FileNameFormat, DateTime.Now);
- var filePath = Path.Combine(recordingsFolder, fileName);
-
- // 创建WaveFormat
- var waveFormat = new WaveFormat(sampleRate, channels);
-
- // 创建音频文件写入器
- var writer = new WaveFileWriter(filePath, waveFormat);
-
- // 添加到活动录音
- if (_activeRecordings.TryAdd(clientId, writer))
- {
- _recordingFilePaths[clientId] = filePath;
- _logger.LogInformation($"开始音频流处理: {clientId}, 文件: {filePath}");
- }
- else
- {
- _logger.LogError($"无法开始音频流处理,添加到活动录音失败: {clientId}");
- writer.Dispose();
- }
- }
- catch (Exception ex)
- {
- _logger.LogError($"开始音频流处理失败: {ex.Message}");
- }
-
- return Task.CompletedTask;
+ ///
+ /// 应用噪声消除
+ ///
+ /// 音频数据
+ /// 采样率
+ /// 声道数
+ /// 噪声门限值
+ /// 攻击时间
+ /// 释放时间
+ /// 高通滤波器截止频率(Hz)
+ /// 滤波器Q值
+ ///
+ 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();
+ }
+ }
+ ///
+ /// 开始新的音频流处理
+ ///
+ /// 客户端ID
+ /// 采样率
+ /// 声道数
+ /// 取消令牌
+ /// 异步任务
+ public Task StartAudioStreamAsync(string clientId, int sampleRate, int channels, CancellationToken token = default)
+ {
+ if (string.IsNullOrEmpty(clientId))
+ {
+ throw new ArgumentNullException(nameof(clientId));
}
- ///
- /// 结束音频流处理
- ///
- /// 客户端ID
- /// 取消令牌
- /// 异步任务
- public Task EndAudioStreamAsync(string clientId, CancellationToken token = default)
+ // 如果已经有一个活动的录音,先结束它
+ if (_activeRecordings.TryGetValue(clientId, out var existingWriter))
{
- if (string.IsNullOrEmpty(clientId))
- {
- throw new ArgumentNullException(nameof(clientId));
- }
-
- try
- {
- // 移除并处理现有的录音
- if (_activeRecordings.TryRemove(clientId, out var writer))
- {
- _recordingFilePaths.TryGetValue(clientId, out var filePath);
-
- // 关闭和释放写入器
- writer.Close();
- writer.Dispose();
-
- _logger.LogInformation($"结束音频流: {clientId}");
-
- // 如果有文件路径,触发事件
- if (!string.IsNullOrEmpty(filePath))
- {
- // 触发音频保存到文件事件
- OnAudioSavedToFile(filePath);
- }
- }
- }
- catch (Exception ex)
- {
- _logger.LogError($"结束音频流处理失败: {ex.Message}");
- }
-
- return Task.CompletedTask;
+ _logger.LogWarning($"客户端已有活动的录音会话,先结束它: {clientId}");
+ EndAudioStreamAsync(clientId).Wait(token);
}
- ///
- /// 获取会话的录音文件路径
- ///
- /// 客户端ID
- /// 录音文件路径,如果没有则返回null
- public string GetRecordingFilePath(string clientId)
+ try
{
- if (string.IsNullOrEmpty(clientId))
+ // 创建录音文件路径
+ var config = _configService.CurrentConfig.Recording;
+ var recordingsFolder = Path.Combine(Directory.GetCurrentDirectory(), config.RecordingsFolder);
+ if (!Directory.Exists(recordingsFolder))
{
- return null;
+ Directory.CreateDirectory(recordingsFolder);
}
-
- if (_recordingFilePaths.TryGetValue(clientId, out var filePath))
+
+ // 创建文件名
+ var fileName = string.Format(config.FileNameFormat, DateTime.Now);
+ var filePath = Path.Combine(recordingsFolder, fileName);
+
+ // 创建WaveFormat
+ var waveFormat = new WaveFormat(sampleRate, channels);
+
+ // 创建音频文件写入器
+ var writer = new WaveFileWriter(filePath, waveFormat);
+
+ // 添加到活动录音
+ if (_activeRecordings.TryAdd(clientId, writer))
{
- return filePath;
+ _recordingFilePaths[clientId] = filePath;
+ _logger.LogInformation($"开始音频流处理: {clientId}, 文件: {filePath}");
}
-
+ else
+ {
+ _logger.LogError($"无法开始音频流处理,添加到活动录音失败: {clientId}");
+ writer.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"开始音频流处理失败: {ex.Message}");
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// 结束音频流处理
+ ///
+ /// 客户端ID
+ /// 取消令牌
+ /// 异步任务
+ public Task EndAudioStreamAsync(string clientId, CancellationToken token = default)
+ {
+ if (string.IsNullOrEmpty(clientId))
+ {
+ throw new ArgumentNullException(nameof(clientId));
+ }
+
+ try
+ {
+ // 移除并处理现有的录音
+ if (_activeRecordings.TryRemove(clientId, out var writer))
+ {
+ _recordingFilePaths.TryGetValue(clientId, out var filePath);
+
+ // 关闭和释放写入器
+ writer.Close();
+ writer.Dispose();
+
+ _logger.LogInformation($"结束音频流: {clientId}");
+
+ // 如果有文件路径,触发事件
+ if (!string.IsNullOrEmpty(filePath))
+ {
+ // 触发音频保存到文件事件
+ OnAudioSavedToFile(filePath);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"结束音频流处理失败: {ex.Message}");
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// 获取会话的录音文件路径
+ ///
+ /// 客户端ID
+ /// 录音文件路径,如果没有则返回null
+ public string GetRecordingFilePath(string clientId)
+ {
+ if (string.IsNullOrEmpty(clientId))
+ {
return null;
}
- ///
- /// 获取当前正在处理的音频流
- ///
- /// 客户端ID
- /// 音频流
- public Stream GetCurrentAudioStream(string clientId)
+ if (_recordingFilePaths.TryGetValue(clientId, out var filePath))
{
- if (string.IsNullOrEmpty(clientId))
- {
- throw new ArgumentNullException(nameof(clientId));
- }
-
- if (!_recordingFilePaths.TryGetValue(clientId, out var filePath) || !File.Exists(filePath))
- {
- return null;
- }
-
- try
- {
- // 由于录音正在进行中,我们返回一个只读的流
- return new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- }
- catch (Exception ex)
- {
- _logger.LogError($"获取当前音频流失败: {ex.Message}");
- return null;
- }
+ return filePath;
}
- ///
- /// 获取最近的录音文件
- ///
- /// 文件数量
- /// 录音文件信息数组(文件名)
- public string[] GetRecentRecordings(int count = 10)
+ return null;
+ }
+
+ ///
+ /// 获取当前正在处理的音频流
+ ///
+ /// 客户端ID
+ /// 音频流
+ public Stream GetCurrentAudioStream(string clientId)
+ {
+ if (string.IsNullOrEmpty(clientId))
{
- try
- {
- var recordingsFolder = Path.Combine(Directory.GetCurrentDirectory(), _configService.CurrentConfig.Recording.RecordingsFolder);
- _logger.LogInformation($"查找录音文件夹: {recordingsFolder}");
-
- if (!Directory.Exists(recordingsFolder))
- {
- _logger.LogWarning($"录音文件夹不存在: {recordingsFolder}");
- Directory.CreateDirectory(recordingsFolder);
- _logger.LogInformation($"已创建录音文件夹: {recordingsFolder}");
- return Array.Empty();
- }
+ throw new ArgumentNullException(nameof(clientId));
+ }
- // 获取所有WAV文件并按最近修改时间排序
- var files = Directory.GetFiles(recordingsFolder, "*.wav")
- .Select(f => new FileInfo(f))
- .OrderByDescending(f => f.CreationTime)
- .Take(count)
- .ToArray();
+ if (!_recordingFilePaths.TryGetValue(clientId, out var filePath) || !File.Exists(filePath))
+ {
+ return null;
+ }
- _logger.LogInformation($"找到 {files.Length} 个录音文件");
-
- // 只返回文件名,不包含路径
- var fileNames = files.Select(f => f.Name).ToArray();
-
- _logger.LogInformation($"返回录音文件: {string.Join(", ", fileNames)}");
- return fileNames;
- }
- catch (Exception ex)
+ try
+ {
+ // 由于录音正在进行中,我们返回一个只读的流
+ return new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"获取当前音频流失败: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// 获取最近的录音文件
+ ///
+ /// 文件数量
+ /// 录音文件信息数组(文件名)
+ public string[] GetRecentRecordings(int count = 10)
+ {
+ try
+ {
+ var recordingsFolder = Path.Combine(Directory.GetCurrentDirectory(), _configService.CurrentConfig.Recording.RecordingsFolder);
+ _logger.LogInformation($"查找录音文件夹: {recordingsFolder}");
+
+ if (!Directory.Exists(recordingsFolder))
{
- _logger.LogError($"获取最近录音失败: {ex.Message}");
+ _logger.LogWarning($"录音文件夹不存在: {recordingsFolder}");
+ Directory.CreateDirectory(recordingsFolder);
+ _logger.LogInformation($"已创建录音文件夹: {recordingsFolder}");
return Array.Empty();
}
+
+ // 获取所有WAV文件并按最近修改时间排序
+ var files = Directory.GetFiles(recordingsFolder, "*.wav")
+ .Select(f => new FileInfo(f))
+ .OrderByDescending(f => f.CreationTime)
+ .Take(count)
+ .ToArray();
+
+ _logger.LogInformation($"找到 {files.Length} 个录音文件");
+
+ // 只返回文件名,不包含路径
+ var fileNames = files.Select(f => f.Name).ToArray();
+
+ _logger.LogInformation($"返回录音文件: {string.Join(", ", fileNames)}");
+ return fileNames;
}
-
- ///
- /// 清理旧录音
- ///
- private void CleanupOldRecordings()
+ catch (Exception ex)
{
- try
- {
- var config = _configService.CurrentConfig.Recording;
- var recordingsFolder = Path.Combine(Directory.GetCurrentDirectory(), config.RecordingsFolder);
- if (!Directory.Exists(recordingsFolder))
- {
- return;
- }
-
- var threshold = DateTime.Now.AddDays(-config.KeepRecordingsDays);
- var oldFiles = Directory.GetFiles(recordingsFolder, "*.wav")
- .Select(f => new FileInfo(f))
- .Where(f => f.LastWriteTime < threshold)
- .ToArray();
-
- foreach (var file in oldFiles)
- {
- try
- {
- file.Delete();
- _logger.LogInformation($"已删除旧录音: {file.FullName}");
- }
- catch (Exception ex)
- {
- _logger.LogWarning($"删除旧录音失败: {file.FullName}, 错误: {ex.Message}");
- }
- }
-
- _logger.LogInformation($"清理旧录音完成,删除了 {oldFiles.Length} 个文件");
- }
- catch (Exception ex)
- {
- _logger.LogError($"清理旧录音失败: {ex.Message}");
- }
+ _logger.LogError($"获取最近录音失败: {ex.Message}");
+ return Array.Empty();
}
+ }
- ///
- /// 触发音频保存事件
- ///
- /// 文件路径
- protected virtual void OnAudioSavedToFile(string filePath)
+ ///
+ /// 清理旧录音
+ ///
+ private void CleanupOldRecordings()
+ {
+ try
{
- AudioSavedToFile?.Invoke(this, filePath);
- }
-
- ///
- /// 触发语音转文字结果事件
- ///
- /// 结果
- protected virtual void OnSpeechToTextResultReceived(SpeechToTextResult result)
- {
- SpeechToTextResultReceived?.Invoke(this, result);
- }
-
- ///
- /// 释放资源
- ///
- public void Dispose()
- {
- if (_isDisposed)
+ var config = _configService.CurrentConfig.Recording;
+ var recordingsFolder = Path.Combine(Directory.GetCurrentDirectory(), config.RecordingsFolder);
+ if (!Directory.Exists(recordingsFolder))
{
return;
}
- _logger.LogInformation("释放音频处理服务资源");
+ var threshold = DateTime.Now.AddDays(-config.KeepRecordingsDays);
+ var oldFiles = Directory.GetFiles(recordingsFolder, "*.wav")
+ .Select(f => new FileInfo(f))
+ .Where(f => f.LastWriteTime < threshold)
+ .ToArray();
- // 结束所有活动的录音
- foreach (var clientId in _activeRecordings.Keys.ToArray())
+ foreach (var file in oldFiles)
{
try
{
- EndAudioStreamAsync(clientId).Wait();
+ file.Delete();
+ _logger.LogInformation($"已删除旧录音: {file.FullName}");
}
catch (Exception ex)
{
- _logger.LogError($"结束活动录音失败: {clientId}, 错误: {ex.Message}");
+ _logger.LogWarning($"删除旧录音失败: {file.FullName}, 错误: {ex.Message}");
}
}
- _isDisposed = true;
+ _logger.LogInformation($"清理旧录音完成,删除了 {oldFiles.Length} 个文件");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"清理旧录音失败: {ex.Message}");
}
}
-}
\ No newline at end of file
+
+ ///
+ /// 触发音频保存事件
+ ///
+ /// 文件路径
+ protected virtual void OnAudioSavedToFile(string filePath)
+ {
+ AudioSavedToFile?.Invoke(this, filePath);
+ }
+
+ ///
+ /// 触发语音转文字结果事件
+ ///
+ /// 结果
+ protected virtual void OnSpeechToTextResultReceived(SpeechToTextResult result)
+ {
+ SpeechToTextResultReceived?.Invoke(this, result);
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _logger.LogInformation("释放音频处理服务资源");
+
+ // 结束所有活动的录音
+ foreach (var clientId in _activeRecordings.Keys.ToArray())
+ {
+ try
+ {
+ EndAudioStreamAsync(clientId).Wait();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"结束活动录音失败: {clientId}, 错误: {ex.Message}");
+ }
+ }
+
+ _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; }
+
+ ///
+ /// 噪声门阈值 (0.0-1.0)
+ ///
+ public float Threshold
+ {
+ get => threshold;
+ set => threshold = Math.Max(0.0f, Math.Min(1.0f, 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 float HoldSeconds
+ {
+ get => holdSeconds;
+ set => holdSeconds = Math.Max(0.0f, value);
+ }
+
+ ///
+ /// 当前包络值 (只读)
+ ///
+ public float CurrentEnvelope => envelope;
+
+ ///
+ /// 当前门状态 (只读)
+ ///
+ 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;
+ }
+
+ ///
+ /// 重置噪声门状态
+ ///
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/ShengShengBuXi/Services/IAudioProcessingService.cs b/ShengShengBuXi/Services/IAudioProcessingService.cs
index 7b588da..d8e8f0d 100644
--- a/ShengShengBuXi/Services/IAudioProcessingService.cs
+++ b/ShengShengBuXi/Services/IAudioProcessingService.cs
@@ -15,18 +15,18 @@ namespace ShengShengBuXi.Services
/// 当有新的语音转文字结果时触发
///
event EventHandler SpeechToTextResultReceived;
-
+
///
/// 当音频数据被保存为文件时触发
///
event EventHandler AudioSavedToFile;
-
+
///
/// 初始化音频处理服务
///
/// 初始化是否成功
bool Initialize();
-
+
///
/// 处理接收到的音频数据
///
@@ -37,7 +37,7 @@ namespace ShengShengBuXi.Services
/// 取消令牌
/// 异步任务
Task ProcessAudioDataAsync(byte[] audioData, int sampleRate, int channels, string clientId, CancellationToken token = default);
-
+
///
/// 开始新的音频流处理
///
@@ -47,7 +47,7 @@ namespace ShengShengBuXi.Services
/// 取消令牌
/// 异步任务
Task StartAudioStreamAsync(string clientId, int sampleRate, int channels, CancellationToken token = default);
-
+
///
/// 结束音频流处理
///
@@ -55,26 +55,41 @@ namespace ShengShengBuXi.Services
/// 取消令牌
/// 异步任务
Task EndAudioStreamAsync(string clientId, CancellationToken token = default);
-
+
///
/// 获取当前正在处理的音频流
///
/// 客户端ID
/// 音频流
Stream GetCurrentAudioStream(string clientId);
-
+
///
/// 获取最近的录音文件
///
/// 文件数量
/// 录音文件路径列表
string[] GetRecentRecordings(int count = 10);
-
+
///
/// 获取会话的录音文件路径
///
/// 客户端ID
/// 录音文件路径,如果没有则返回null
string GetRecordingFilePath(string clientId);
+
+
+ ///
+ /// 应用噪声消除
+ ///
+ /// 音频数据
+ /// 采样率
+ /// 声道数
+ /// 噪声门限值
+ /// 攻击时间
+ /// 释放时间
+ /// 高通滤波器截止频率(Hz)
+ /// 滤波器Q值
+ ///
+ 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);
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/ShengShengBuXi/Services/SpeechToTextService.cs b/ShengShengBuXi/Services/SpeechToTextService.cs
index b16f2e2..d764d6d 100644
--- a/ShengShengBuXi/Services/SpeechToTextService.cs
+++ b/ShengShengBuXi/Services/SpeechToTextService.cs
@@ -562,7 +562,7 @@ namespace ShengShengBuXi.Services
await ProcessAudioAsync(audioData, sessionId, token);
// 等待一段时间,模拟实时处理
- await Task.Delay(100, token);
+ await Task.Delay(20, token);
}
// 结束会话
diff --git a/ShengShengBuXi/config.json b/ShengShengBuXi/config.json
index 52ecda0..24f5904 100644
--- a/ShengShengBuXi/config.json
+++ b/ShengShengBuXi/config.json
@@ -39,6 +39,7 @@
"reconnectDelayMs": 2000,
"enableSpeechToText": true,
"enableAudioStreaming": true,
+ "EnableAudioNoiseReduction": false,
"heartbeatIntervalSeconds": 15,
"tencentCloudASR": {
"appId": "",