From e6816d49b7c3e7eac43c4775cf30fa5c38869409 Mon Sep 17 00:00:00 2001 From: zpc Date: Fri, 28 Mar 2025 15:55:39 +0800 Subject: [PATCH] 321 --- .../Services/PhoneBoothService.cs | 8 +- ShengShengBuXi.ConsoleApp/appsettings.json | 2 +- ShengShengBuXi/Hubs/AudioHub.cs | 87 +++++- ShengShengBuXi/Pages/Monitor.cshtml | 258 ++++++++++++++++- .../Services/AudioProcessingService.cs | 264 +++++++++++++----- 5 files changed, 515 insertions(+), 104 deletions(-) diff --git a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs index 4674769..0158810 100644 --- a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs +++ b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs @@ -559,7 +559,7 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable { await PlayAudioAndWait( _config.AudioFiles.GetFullPath(_config.AudioFiles.BeepPromptFile), - null, false, recordingCts.Token, 0.3f); + null, false, recordingCts.Token, 0.1f); } catch (OperationCanceledException) { @@ -1307,7 +1307,11 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable device.Volume = volume.Value; } device.Init(reader); - + //device.Volume + if (volume != null) + { + device.Volume = volume.Value; + } // 如果需要循环播放,创建一个循环播放的任务 if (loop && maxDuration.HasValue) { diff --git a/ShengShengBuXi.ConsoleApp/appsettings.json b/ShengShengBuXi.ConsoleApp/appsettings.json index be2a092..df6159d 100644 --- a/ShengShengBuXi.ConsoleApp/appsettings.json +++ b/ShengShengBuXi.ConsoleApp/appsettings.json @@ -1,5 +1,5 @@ { - "SignalRHubUrl": "http://115.159.44.16/audiohub", + "SignalRHubUrl": "http://localhost:81/audiohub", "ConfigBackupPath": "config.json", "AutoConnectToServer": true } diff --git a/ShengShengBuXi/Hubs/AudioHub.cs b/ShengShengBuXi/Hubs/AudioHub.cs index 61457e7..d24914c 100644 --- a/ShengShengBuXi/Hubs/AudioHub.cs +++ b/ShengShengBuXi/Hubs/AudioHub.cs @@ -92,6 +92,17 @@ namespace ShengShengBuXi.Hubs private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config"); private static readonly string DisplayConfigPath = Path.Combine(ConfigDirectory, "display.json"); + private static DateTime _lastRealTimeResultSentTime = DateTime.MinValue; + private static int _realTimeResultCounter = 0; + private static readonly TimeSpan _realTimeResultThrottleWindow = TimeSpan.FromSeconds(0.3); + private static readonly int _maxRealTimeResultsPerSecond = 1; + + // 增加对最终结果的限流变量 + private static DateTime _lastFinalResultSentTime = DateTime.MinValue; + private static int _finalResultCounter = 0; + private static TimeSpan _finalResultThrottleWindow = TimeSpan.FromSeconds(0.3); + private static readonly int _maxFinalResultsPerSecond = 1; + // 用于初始化配置 static AudioHub() { @@ -480,18 +491,21 @@ namespace ShengShengBuXi.Hubs { try { + byte[] dataToSend; + 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); + dataToSend = _audioProcessingService.ApplyNoiseReduction(audioData, config.SampleRate, config.Channels); + _logger.LogDebug($"转发音频数据到{monitoringClients.Count}个监听客户端,数据长度: {audioData.Length},降噪后长度:{dataToSend.Length}"); } else { - // 尝试直接发送数据 - await Clients.Clients(monitoringClients).SendAsync("ReceiveAudioData", audioData); + dataToSend = audioData; + _logger.LogDebug($"转发音频数据到{monitoringClients.Count}个监听客户端,数据长度: {audioData.Length}"); } + + // 始终使用二进制格式发送数据,避免字符串转换 + await Clients.Clients(monitoringClients).SendAsync("ReceiveAudioData", dataToSend); } catch (Exception ex) { @@ -736,19 +750,62 @@ namespace ShengShengBuXi.Hubs { _logger.LogInformation($"接收到语音识别最终结果: {e.Text}"); - // 将结果添加到显示文本队列 - //AddRecognizedTextToDisplay(e.Text, true, e.Id); + // 发送频率限制:每秒最多发送3次最终识别结果 + DateTime now = DateTime.Now; + TimeSpan elapsed = now - _lastFinalResultSentTime; - // 将最终结果发送给所有监听中的客户端 - await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" }) - .SendAsync("ReceiveSpeechToEndTextResult", e.Text); + // 检查是否已进入新的时间窗口 + if (elapsed >= _finalResultThrottleWindow) + { + // 重置计数器和时间窗口 + _finalResultCounter = 0; + _lastFinalResultSentTime = now; + } + + // 检查当前时间窗口内是否已达到发送上限 + if (_finalResultCounter < _maxFinalResultsPerSecond) + { + // 将最终结果发送给所有监听中的客户端 + await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" }) + .SendAsync("ReceiveSpeechToEndTextResult", e.Text); + + // 增加计数器 + _finalResultCounter++; + } + else + { + _logger.LogDebug($"已达到最终结果发送频率限制,跳过发送: {e.Text}"); + } } else { - // 发送实时识别结果给客户端 - _logger.LogInformation($"接收到语音识别实时结果: {e.Text}"); - await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" }) - .SendAsync("ReceiveSpeechToTextResult", e.Text); + // 发送频率限制:每秒最多发送3次实时识别结果 + DateTime now = DateTime.Now; + TimeSpan elapsed = now - _lastRealTimeResultSentTime; + + // 检查是否已进入新的时间窗口 + if (elapsed >= _realTimeResultThrottleWindow) + { + // 重置计数器和时间窗口 + _realTimeResultCounter = 0; + _lastRealTimeResultSentTime = now; + } + + // 检查当前时间窗口内是否已达到发送上限 + if (_realTimeResultCounter < _maxRealTimeResultsPerSecond) + { + // 发送实时识别结果给客户端 + _logger.LogInformation($"接收到语音识别实时结果: {e.Text}"); + await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" }) + .SendAsync("ReceiveSpeechToTextResult", e.Text); + + // 增加计数器 + _realTimeResultCounter++; + } + else + { + _logger.LogDebug($"已达到实时结果发送频率限制,跳过发送: {e.Text}"); + } } } catch (Exception ex) diff --git a/ShengShengBuXi/Pages/Monitor.cshtml b/ShengShengBuXi/Pages/Monitor.cshtml index a48f471..e3e2034 100644 --- a/ShengShengBuXi/Pages/Monitor.cshtml +++ b/ShengShengBuXi/Pages/Monitor.cshtml @@ -187,6 +187,14 @@ let isAudioStreamEnabled = false; let audioGainNode = null; let currentVolume = 1.0; // 默认音量为1.0 (100%) + + // 添加音频缓冲区变量 + let audioBufferQueue = []; + const MAX_BUFFER_SIZE = 15; // 最大缓冲队列大小,调整为15帧以适应网络延迟 + let isAudioPlaying = false; + let audioBufferTimeout = null; + const BUFFER_PROCESS_INTERVAL = 33; // 缓冲区处理间隔(毫秒),调整为约30fps的处理速率 + let bufferStartSizeThreshold = 5; // 开始播放所需的最小缓冲数据量,增加到5帧以减少卡顿 // 调试日志 function log(message) { @@ -357,15 +365,30 @@ // 接收实时语音识别结果 connection.on("ReceiveSpeechToTextResult", (result) => { log(`接收到实时语音识别结果: ${result.substring(0, 30)}${result.length > 30 ? '...' : ''}`); - // 显示实时识别结果 - showRealtimeTextResult(result); + + // 使用防抖动技术显示实时识别结果,避免频繁更新导致页面卡顿 + if (window.realtimeTextDebounceTimer) { + clearTimeout(window.realtimeTextDebounceTimer); + } + window.realtimeTextDebounceTimer = setTimeout(() => { + showRealtimeTextResult(result); + }, 300); // 300毫秒的防抖动延迟 }); // 接收最终语音识别结果 connection.on("ReceiveSpeechToEndTextResult", (result) => { log(`接收到最终语音识别结果: ${result.substring(0, 30)}${result.length > 30 ? '...' : ''}`); + + // 清除任何正在进行的实时文本更新定时器 + if (window.realtimeTextDebounceTimer) { + clearTimeout(window.realtimeTextDebounceTimer); + } + // 显示最终识别结果 showFinalTextResult(result); + + // 同时添加到监控列表的顶部(如果不存在于列表中) + addToMonitorList(result); }); // 显示模式更新消息 @@ -424,12 +447,33 @@ statusText.textContent = "正在通话中"; // 初始化音频上下文(如果需要且启用了音频流) - if (isAudioStreamEnabled && !audioContext) { + if (isAudioStreamEnabled) { + // 如果存在音频上下文,则先尝试关闭,然后重新创建 + if (audioContext) { + log("重置音频上下文以确保新的通话正常播放"); + try { + // 释放旧的增益节点 + if (audioGainNode) { + audioGainNode.disconnect(); + audioGainNode = null; + } + // 关闭旧的音频上下文 + audioContext.close().catch(e => log("关闭音频上下文失败: " + e)); + audioContext = null; + } catch (e) { + log("重置音频上下文失败: " + e); + } + } + + // 创建新的音频上下文 initAudioContext(); } } else { indicator.style.backgroundColor = "red"; statusText.textContent = "未检测到通话"; + + // 停止缓冲区处理 + stopBufferProcessing(); } // 有新通话时刷新监控列表 @@ -845,9 +889,13 @@ clearInterval(refreshDisplayInterval); } + // 停止缓冲区处理 + stopBufferProcessing(); + // 关闭音频上下文 if (audioContext) { audioContext.close().catch(e => console.log("关闭音频上下文失败: " + e)); + audioContext = null; } }); @@ -1143,7 +1191,22 @@ // 初始化音频上下文 function initAudioContext() { - if (audioContext) return; // 避免重复初始化 + if (audioContext) { + log("音频上下文已存在,使用现有上下文"); + + // 确保音频上下文已激活 + if (audioContext.state === 'suspended') { + audioContext.resume().then(() => { + log("已恢复暂停的音频上下文"); + }).catch(err => { + log("恢复音频上下文失败: " + err); + }); + } + + // 启动缓冲处理 + startBufferProcessing(); + return; + } try { // 创建音频上下文 @@ -1174,12 +1237,80 @@ } log("音频上下文已初始化,状态: " + audioContext.state + ", 采样率: " + audioContext.sampleRate); + + // 启动音频缓冲处理 + startBufferProcessing(); } catch (e) { log("无法创建音频上下文: " + e); showMessage("无法初始化音频播放: " + e, "danger"); } } + // 启动缓冲区处理 + function startBufferProcessing() { + if (audioBufferTimeout) { + clearInterval(audioBufferTimeout); + } + audioBufferTimeout = setInterval(processAudioBuffer, BUFFER_PROCESS_INTERVAL); + log("音频缓冲处理已启动"); + } + + // 停止缓冲区处理 + function stopBufferProcessing() { + if (audioBufferTimeout) { + clearInterval(audioBufferTimeout); + audioBufferTimeout = null; + } + // 清空缓冲区 + audioBufferQueue = []; + isAudioPlaying = false; + log("音频缓冲处理已停止"); + } + + // 处理音频缓冲区 + function processAudioBuffer() { + if (!audioContext || !isAudioStreamEnabled || !callInProgress) { + return; + } + + // 没有足够的缓冲数据 + if (audioBufferQueue.length === 0) { + if (isAudioPlaying) { + log("缓冲区已空,等待更多数据..."); + isAudioPlaying = false; + } + return; + } + + // 初始播放需要达到阈值 + if (!isAudioPlaying && audioBufferQueue.length < bufferStartSizeThreshold) { + log(`缓冲中,当前数据量: ${audioBufferQueue.length}/${bufferStartSizeThreshold}`); + return; + } + + // 从队列取出一个音频数据并播放 + const audioData = audioBufferQueue.shift(); + playBufferedAudio(audioData); + isAudioPlaying = true; + + // 自适应调整缓冲区大小 + if (audioBufferQueue.length > MAX_BUFFER_SIZE * 0.8) { + log("缓冲区接近上限,增加处理频率"); + // 增加处理频率 + if (audioBufferTimeout) { + clearInterval(audioBufferTimeout); + } + audioBufferTimeout = setInterval(processAudioBuffer, BUFFER_PROCESS_INTERVAL / 2); + } else if (audioBufferQueue.length < MAX_BUFFER_SIZE * 0.2 && audioBufferQueue.length > 0) { + log("缓冲区数据较少,减少处理频率"); + // 减少处理频率 + if (audioBufferTimeout) { + clearInterval(audioBufferTimeout); + } + audioBufferTimeout = setInterval(processAudioBuffer, BUFFER_PROCESS_INTERVAL * 1.5); + } + } + // 播放实时音频 function playRealTimeAudio(audioData) { if (!audioContext || !isAudioStreamEnabled) return; @@ -1226,15 +1357,30 @@ log("字符串数据(前20字符): " + audioData.substring(0, 20)); // 尝试从Base64字符串解码 try { - const binary = atob(audioData); + // 尝试使用更健壮的Base64解码,先规范化字符串格式 + const base64Str = audioData.trim().replace(/^data:[^;]+;base64,/, ''); + + // 创建具有适当长度的Uint8Array + const binary = atob(base64Str); pcmData = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { pcmData[i] = binary.charCodeAt(i); } log("已从Base64字符串转换为Uint8Array"); - } catch (e) { + } catch (e) { log("Base64转换失败: " + e); - return; + + // 尝试直接解码二进制字符串 + try { + pcmData = new Uint8Array(audioData.length); + for (let i = 0; i < audioData.length; i++) { + pcmData[i] = audioData.charCodeAt(i); + } + log("已从二进制字符串转换为Uint8Array"); + } catch (e2) { + log("二进制字符串转换也失败: " + e2); + return; + } } } else { return; @@ -1256,6 +1402,25 @@ pcmData = pcmData.slice(0, validLength); } + // 添加到缓冲队列 + if (audioBufferQueue.length < MAX_BUFFER_SIZE) { + audioBufferQueue.push(pcmData); + log(`已添加到缓冲区,当前缓冲数量: ${audioBufferQueue.length}`); + } else { + log("缓冲区已满,丢弃数据"); + // 队列满时,移除最早的,添加最新的 + audioBufferQueue.shift(); + audioBufferQueue.push(pcmData); + } + + } catch (e) { + log("处理实时音频失败: " + e); + } + } + + // 播放缓冲区中的音频 + async function playBufferedAudio(pcmData) { + try { // 获取有效的DataView let dataView; try { @@ -1322,11 +1487,16 @@ audioGainNode.gain.value = currentVolume; } + // 播放前检查音频上下文状态 + if (audioContext.state === 'suspended') { + log("音频上下文处于暂停状态,尝试恢复..."); + await audioContext.resume(); + } + // 播放 source.start(0); - log(`实时音频播放中...音量: ${currentVolume * 100}%`); } catch (e) { - log("处理实时音频失败: " + e); + log("播放缓冲音频失败: " + e); } } @@ -1358,5 +1528,75 @@ log("显示最终语音识别结果"); } + + // 添加文本到监控列表 + function addToMonitorList(text) { + if (!text || text.trim() === "") return; + + // 检查是否已经存在相同内容 + const existingItems = document.querySelectorAll("#monitor-text-list .list-group-item"); + for (let i = 0; i < existingItems.length; i++) { + if (existingItems[i].dataset.fullText === text) { + // 已存在相同内容,不需要添加 + return; + } + } + + // 创建新的列表项 + const container = document.getElementById("monitor-text-list"); + const listItem = document.createElement("div"); + listItem.className = "list-group-item"; + // 使用临时ID,服务器端会分配真正的ID + listItem.dataset.id = "temp-" + Date.now(); + listItem.dataset.fullText = text; + + // 添加点击事件 + listItem.addEventListener('click', function (e) { + // 如果点击的是按钮,不触发选择 + if (e.target.tagName === 'BUTTON' || e.target.tagName === 'I' || + e.target.closest('button')) { + return; + } + + // 移除其他项的active类 + document.querySelectorAll("#monitor-text-list .list-group-item").forEach(item => { + item.classList.remove("active"); + }); + + // 添加active类到当前项 + this.classList.add("active"); + + // 显示文本 + displayTextInCenter(this.dataset.fullText); + }); + + // 获取当前时间 + const date = new Date(); + const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`; + + // 截取前10个字符,若不足10个则全部显示 + const shortText = text.length > 10 ? text.substring(0, 10) : text; + + listItem.innerHTML = ` +
+ 【${shortText}】 +
+ +
+
+
${formattedDate} (本地)
+ `; + + // 添加到列表顶部 + if (container.firstChild) { + container.insertBefore(listItem, container.firstChild); + } else { + container.appendChild(listItem); + } + + log(`已添加文本到监控列表: ${shortText}...`); + } } diff --git a/ShengShengBuXi/Services/AudioProcessingService.cs b/ShengShengBuXi/Services/AudioProcessingService.cs index e28653d..cad1b15 100644 --- a/ShengShengBuXi/Services/AudioProcessingService.cs +++ b/ShengShengBuXi/Services/AudioProcessingService.cs @@ -102,15 +102,23 @@ public class AudioProcessingService : IAudioProcessingService throw new ArgumentNullException(nameof(clientId)); } - // 检查是否有活动的录音 - if (!_activeRecordings.TryGetValue(clientId, out var writer)) - { - _logger.LogWarning($"客户端没有活动的录音会话: {clientId}"); - return; - } - try { + // 判断采样率,如果高于16kHz则降采样 + if (sampleRate > 16000) + { + _logger.LogInformation($"原始采样率为{sampleRate}Hz,进行降采样到16000Hz"); + audioData = ResampleAudio(audioData, sampleRate, 16000, 16, channels); + sampleRate = 16000; // 更新采样率为降采样后的值 + } + + // 检查是否有活动的录音 + if (!_activeRecordings.TryGetValue(clientId, out var writer)) + { + _logger.LogWarning($"客户端没有活动的录音会话: {clientId}"); + return; + } + // 异步写入音频数据 await Task.Run(() => { writer.Write(audioData, 0, audioData.Length); writer.Flush(); }, token); } @@ -121,7 +129,67 @@ public class AudioProcessingService : IAudioProcessingService } /// - /// 应用噪声消除 + /// 对音频数据进行降采样 + /// + /// 原始音频数据 + /// 原始采样率 + /// 目标采样率 + /// 采样位深 + /// 声道数 + /// 降采样后的音频数据 + private byte[] ResampleAudio(byte[] audioData, int originalSampleRate, int targetSampleRate, int bitsPerSample, int channels) + { + if (originalSampleRate == targetSampleRate) + { + return audioData; + } + + try + { + // 简单的降采样算法 - 采样率转换比例 + double ratio = (double)originalSampleRate / targetSampleRate; + + // 计算新音频数据的字节数 + int bytesPerSample = bitsPerSample / 8; + int samplesPerChannel = audioData.Length / (bytesPerSample * channels); + int newSamplesPerChannel = (int)(samplesPerChannel / ratio); + int newDataLength = newSamplesPerChannel * bytesPerSample * channels; + + byte[] result = new byte[newDataLength]; + + // 对每个声道执行降采样 + for (int i = 0; i < newSamplesPerChannel; i++) + { + int originalIndex = Math.Min((int)(i * ratio), samplesPerChannel - 1); + + for (int ch = 0; ch < channels; ch++) + { + // 复制每个采样点的所有字节 + int originalOffset = (originalIndex * channels + ch) * bytesPerSample; + int newOffset = (i * channels + ch) * bytesPerSample; + + for (int b = 0; b < bytesPerSample; b++) + { + if (originalOffset + b < audioData.Length && newOffset + b < result.Length) + { + result[newOffset + b] = audioData[originalOffset + b]; + } + } + } + } + + _logger.LogInformation($"音频降采样完成: {audioData.Length}字节 -> {result.Length}字节"); + return result; + } + catch (Exception ex) + { + _logger.LogError($"音频降采样失败: {ex.Message}"); + return audioData; // 如果降采样失败,返回原始数据 + } + } + + /// + /// 应用降噪处理 /// /// 音频数据 /// 采样率 @@ -129,41 +197,53 @@ public class AudioProcessingService : IAudioProcessingService /// 噪声门限值 /// 攻击时间 /// 释放时间 - /// 高通滤波器截止频率(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))) + // 调用内部实现 + return ApplyNoiseReductionInternal(audioData, noiseThreshold, attackSeconds, releaseSeconds, highPassCutoff, q); + } + + private byte[] ApplyNoiseReductionInternal( + byte[] audioData, + float noiseThreshold = 0.02f, // 噪声门限值 + float attackSeconds = 0.01f, // 攻击时间 + float releaseSeconds = 0.1f, // 释放时间 + int highPassCutoff = 80, // 高通滤波器截止频率(Hz) + float q = 1.0f) // 滤波器Q值 { - var sampleProvider = waveStream.ToSampleProvider(); - - // 改进1:更温和的噪声门参数 - var noiseGate = new ImprovedNoiseGate(sampleProvider) + // 1. 将字节数组转换为 WaveStream + using (var inputStream = new MemoryStream(audioData)) + using (var waveStream = new RawSourceWaveStream(inputStream, new WaveFormat(16000, 16, 1))) { - 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(); - } + // 2. 转换为浮点样本便于处理 + var sampleProvider = waveStream.ToSampleProvider(); + + // 3. 应用噪声门(Noise Gate) + var noiseGate = new NoiseGateSampleProvider(sampleProvider) + { + Threshold = noiseThreshold, + AttackSeconds = attackSeconds, + ReleaseSeconds = releaseSeconds + }; + + // 4. 应用高通滤波器去除低频噪音 + var highPassFilter = new BiQuadFilterSampleProvider(noiseGate); + highPassFilter.Filter = BiQuadFilter.HighPassFilter( + sampleProvider.WaveFormat.SampleRate, + highPassCutoff, + q); + + // 5. 处理后的音频转回字节数组 + var outputStream = new MemoryStream(); + WaveFileWriter.WriteWavFileToStream(outputStream, highPassFilter.ToWaveProvider16()); + + return outputStream.ToArray(); + } } + /// /// 开始新的音频流处理 /// @@ -447,67 +527,97 @@ public class AudioProcessingService : IAudioProcessingService } } -// 简单的噪声门实现 +// 自定义噪声门实现 public class NoiseGateSampleProvider : ISampleProvider { private readonly ISampleProvider source; - private float threshold; - private float attackSeconds; - private float releaseSeconds; - private float envelope; - private float gain; + private float threshold = 0.02f; + private float attackSeconds = 0.01f; + private float releaseSeconds = 0.1f; + private float currentLevel = 0; + private float attackRate; + private float releaseRate; + private bool open = false; public NoiseGateSampleProvider(ISampleProvider source) { this.source = source; - this.WaveFormat = source.WaveFormat; + CalculateAttackAndReleaseRates(); } - public float Threshold + public WaveFormat WaveFormat => source.WaveFormat; + + public float Threshold + { + get => threshold; + set + { + threshold = value; + } + } + + public float AttackSeconds + { + get => attackSeconds; + set + { + attackSeconds = value; + CalculateAttackAndReleaseRates(); + } + } + + public float ReleaseSeconds + { + get => releaseSeconds; + set + { + releaseSeconds = value; + CalculateAttackAndReleaseRates(); + } + } + + private void CalculateAttackAndReleaseRates() { - get => threshold; - set => threshold = Math.Max(0, Math.Min(1, value)); + attackRate = 1.0f / (WaveFormat.SampleRate * attackSeconds); + releaseRate = 1.0f / (WaveFormat.SampleRate * releaseSeconds); } - 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++) + for (int i = 0; i < samplesRead; i++) { - float sample = buffer[offset + n]; - float absSample = Math.Abs(sample); - - // 包络跟踪 - if (absSample > envelope) - envelope = absSample; + float currentSample = Math.Abs(buffer[offset + i]); + + // 更新当前电平 + if (currentSample > currentLevel) + { + // 攻击:升高比较快 + currentLevel += attackRate * (currentSample - currentLevel); + } else - envelope *= (absSample > threshold) ? attackCoeff : releaseCoeff; + { + // 释放:降低比较慢 + currentLevel -= releaseRate * (currentLevel - currentSample); + if (currentLevel < 0) currentLevel = 0; + } - // 应用增益 - if (envelope > threshold) - gain = 1.0f; - else - gain = 0.0f; + // 应用噪声门 + if (currentLevel >= threshold) + { + open = true; + } + else if (open && currentLevel < threshold * 0.5f) // 加入一点滞后效应 + { + open = false; + } - buffer[offset + n] = sample * gain; + // 根据噪声门状态保留或衰减信号 + if (!open) + { + buffer[offset + i] *= 0.05f; // 不完全消除,仅保留5%的信号强度 + } } return samplesRead;