From 08462dad0b9639a5201518c5244044cbbf13bd53 Mon Sep 17 00:00:00 2001 From: zpc Date: Sat, 29 Mar 2025 12:14:34 +0800 Subject: [PATCH] 312321 --- ShengShengBuXi/Hubs/AudioHub.cs | 9 +- ShengShengBuXi/Pages/Monitor.cshtml | 506 ++++++++---------- .../Services/AudioProcessingService.cs | 147 +++-- .../Services/IAudioProcessingService.cs | 9 + 4 files changed, 338 insertions(+), 333 deletions(-) diff --git a/ShengShengBuXi/Hubs/AudioHub.cs b/ShengShengBuXi/Hubs/AudioHub.cs index 4eb21d5..cae5f73 100644 --- a/ShengShengBuXi/Hubs/AudioHub.cs +++ b/ShengShengBuXi/Hubs/AudioHub.cs @@ -589,7 +589,14 @@ namespace ShengShengBuXi.Hubs foreach (var clientId in monitoringClients) { // 添加音频数据发送任务,使用单独的客户端连接 - tasks.Add(Clients.Client(clientId).SendAsync("ReceiveAudioData", dataToSend)); + // 使用清晰的对象格式,并添加WAV头以确保格式正确 + var wavData = _audioProcessingService.AddWavHeader(dataToSend, config.SampleRate, config.Channels); + tasks.Add(Clients.Client(clientId).SendAsync("ReceiveAudioData", new { + format = "WAV", + sampleRate = config.SampleRate, + channels = config.Channels, + data = wavData + })); } // 只在日志级别为Debug时输出详细信息 diff --git a/ShengShengBuXi/Pages/Monitor.cshtml b/ShengShengBuXi/Pages/Monitor.cshtml index c201189..cc58458 100644 --- a/ShengShengBuXi/Pages/Monitor.cshtml +++ b/ShengShengBuXi/Pages/Monitor.cshtml @@ -115,12 +115,12 @@
- 文本编辑 (最多输入30个文字) + 文本编辑 (最多输入100个文字)
+ maxlength="100">
@@ -269,22 +269,6 @@ let currentVolume = 1.0; // 默认音量 let volumeBoost = 3.0; // 音量增益倍数,提高接收到的音频音量 - // 添加音频缓冲区变量 - let audioBufferQueue = []; - const MAX_BUFFER_SIZE = 400; // 增加缓冲队列大小,提供更充足的数据缓冲 - let isAudioPlaying = false; - let audioBufferTimeout = null; - const BUFFER_PROCESS_INTERVAL = 85; // 显著增加处理间隔,确保音频帧有足够的播放时间 - let bufferStartSizeThreshold = 25; // 增加开始阈值,确保有足够数据开始播放 - let lastAudioTimestamp = 0; - let audioJitterBuffer = 60; // 增加抖动缓冲,避免播放过快 - let sampleRate = 16000; // 采样率固定为16kHz - let frameSize = 320; // 每帧样本数 (20ms @@ 16kHz) - let targetFrameDuration = 20; // 目标帧时长(毫秒) - let receivedPacketCounter = 0; - let lastPacketTime = 0; - let receivedPacketRate = 0; - // 调试日志 function log(message) { const timestamp = new Date().toLocaleTimeString(); @@ -562,9 +546,6 @@ indicator.style.backgroundColor = "red"; statusText.textContent = "未检测到通话"; - // 停止缓冲区处理 - stopBufferProcessing(); - // 如果确认对话框还在显示(用户未点击接听),则自动关闭 const confirmDialog = bootstrap.Modal.getInstance(document.getElementById('callConfirmDialog')); if (confirmDialog) { @@ -1109,9 +1090,6 @@ clearInterval(refreshDisplayInterval); } - // 停止缓冲区处理 - stopBufferProcessing(); - // 关闭音频上下文 if (audioContext) { audioContext.close().catch(e => console.log("关闭音频上下文失败: " + e)); @@ -1441,7 +1419,6 @@ // 创建新的音频上下文 const AudioContext = window.AudioContext || window.webkitAudioContext; audioContext = new AudioContext({ - sampleRate: sampleRate, // 固定使用16kHz采样率 latencyHint: 'interactive' // 低延迟设置 }); @@ -1452,9 +1429,6 @@ log(`音频上下文已初始化: 采样率=${audioContext.sampleRate}Hz, 状态=${audioContext.state}, 音量增益=${volumeBoost}倍`); - // 重置音频缓冲处理 - startBufferProcessing(); - // 恢复音频上下文 if (audioContext.state === 'suspended') { const resumeAudio = function () { @@ -1481,182 +1455,241 @@ } } - // 启动缓冲区处理 - function startBufferProcessing() { - // 停止现有的处理 - if (audioBufferTimeout) { - clearInterval(audioBufferTimeout); - } + // 播放实时音频 - 适应新格式 + function playRealTimeAudio(audioPacket) { + if (!audioContext || !isAudioStreamEnabled || !callInProgress) return; - // 重置状态 - audioBufferQueue = []; - isAudioPlaying = false; - lastAudioTimestamp = 0; - receivedPacketCounter = 0; - lastPacketTime = 0; - - // 启动处理间隔 - audioBufferTimeout = setInterval(processAudioBuffer, BUFFER_PROCESS_INTERVAL); - log(`音频处理已启动: 间隔=${BUFFER_PROCESS_INTERVAL}ms, 缓冲阈值=${bufferStartSizeThreshold}帧`); - } - - // 停止缓冲区处理 - function stopBufferProcessing() { - if (audioBufferTimeout) { - clearInterval(audioBufferTimeout); - audioBufferTimeout = null; - } - - // 清空状态 - audioBufferQueue = []; - isAudioPlaying = false; - lastAudioTimestamp = 0; - log("音频处理已停止"); - } - - // 处理音频缓冲区 - function processAudioBuffer() { - if (!audioContext || !isAudioStreamEnabled || !callInProgress) { - return; - } - - // 没有数据时等待 - if (audioBufferQueue.length === 0) { - if (isAudioPlaying) { - log("缓冲区已空,等待更多数据..."); - isAudioPlaying = false; - } - return; - } - - // 初始播放需要达到阈值 - if (!isAudioPlaying && audioBufferQueue.length < bufferStartSizeThreshold) { - // 只在达到特定比例时记录 - if (audioBufferQueue.length === 1 || - audioBufferQueue.length === Math.floor(bufferStartSizeThreshold / 2) || - audioBufferQueue.length === bufferStartSizeThreshold - 1) { - log(`缓冲中: ${audioBufferQueue.length}/${bufferStartSizeThreshold}`); - } - return; - } - - // 当前时间和上次播放的时间间隔检查 - const now = Date.now(); - const elapsed = now - lastAudioTimestamp; - - // 如果上次播放时间太近,等待足够的时间间隔 - if (isAudioPlaying && elapsed < audioJitterBuffer) { - return; - } - - // 从队列获取下一个音频包 - const packet = audioBufferQueue.shift(); - if (packet && !packet.processed) { - playBufferedAudio(packet.data, packet.estimatedDuration); - packet.processed = true; - isAudioPlaying = true; - lastAudioTimestamp = now; - - // 自适应调整缓冲区处理间隔 - const bufferRatio = audioBufferQueue.length / MAX_BUFFER_SIZE; - - if (bufferRatio > 0.7) { - // 数据充足,可以稍微加快处理 - if (audioBufferTimeout) { - clearInterval(audioBufferTimeout); - const newInterval = Math.max(BUFFER_PROCESS_INTERVAL * 0.8, 60); - audioBufferTimeout = setInterval(processAudioBuffer, newInterval); - log(`缓冲区数据充足 (${audioBufferQueue.length}/${MAX_BUFFER_SIZE}), 调整间隔为 ${newInterval.toFixed(0)}ms`); - } - } else if (bufferRatio < 0.1 && audioBufferQueue.length > 0) { - // 数据不足,需要减缓处理速度 - if (audioBufferTimeout) { - clearInterval(audioBufferTimeout); - const newInterval = BUFFER_PROCESS_INTERVAL * 1.5; - audioBufferTimeout = setInterval(processAudioBuffer, newInterval); - log(`缓冲区数据不足 (${audioBufferQueue.length}/${MAX_BUFFER_SIZE}), 调整间隔为 ${newInterval.toFixed(0)}ms`); - } - } - } - } - - // 播放缓冲区中的音频 - async function playBufferedAudio(pcmData, estimatedDuration) { try { - // 确保音频上下文正常 - if (!audioContext || audioContext.state === 'closed') { - initAudioContext(); - if (!audioContext) return; + // 解析音频元数据和数据 + const { format, sampleRate, channels, data } = audioPacket; + + // 确保格式正确 + if (!format || !data) { + log("音频格式或数据无效"); + return; } - if (audioContext.state === 'suspended') { - try { - await audioContext.resume(); - } catch (e) { + log(`接收到音频数据: 格式=${format}, 采样率=${sampleRate}, 声道=${channels}, 数据长度=${Array.isArray(data) ? data.length : '未知'}`); + + // 根据不同的音频格式处理 + if (format === "WAV") { + // 处理WAV格式数据 - 异步解码 + processWavData(data, sampleRate, channels) + .then(audioBuffer => { + if (audioBuffer) { + playAudioBuffer(audioBuffer); + } else { + log("无法创建WAV音频缓冲区"); + } + }) + .catch(err => { + log("WAV处理错误: " + err); + }); + } else if (format === "16bit_PCM") { + // 处理PCM格式数据 - 同步处理 + const audioBuffer = processPcmData(data, sampleRate, channels); + if (audioBuffer) { + playAudioBuffer(audioBuffer); + } else { + log("无法创建PCM音频缓冲区"); + } + } else { + log("不支持的音频格式: " + format); + } + } catch (e) { + log("处理实时音频失败: " + e); + } + } + + // 播放音频缓冲区 + function playAudioBuffer(audioBuffer) { + // 确保音频上下文活跃 + if (audioContext.state === 'suspended') { + try { + audioContext.resume(); + } catch (e) { + log("恢复音频上下文失败: " + e); + return; + } + } + + // 创建音频源并连接 + const source = audioContext.createBufferSource(); + source.buffer = audioBuffer; + + // 应用音量控制 + source.connect(audioGainNode); + if (audioGainNode) { + audioGainNode.gain.value = currentVolume * volumeBoost; + } + + // 立即播放 + source.start(0); + log("开始播放音频"); + } + + // 处理WAV格式的数据 - 返回Promise + function processWavData(data, sampleRate, channels) { + return new Promise((resolve, reject) => { + try { + // 确保音频上下文存在 + if (!audioContext || audioContext.state === 'closed') { + initAudioContext(); + if (!audioContext) { + reject(new Error("无法初始化音频上下文")); + return; + } + } + + // 将数据转换为ArrayBuffer + let arrayBuffer; + if (data instanceof Uint8Array) { + arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); + } else if (Array.isArray(data)) { + // 转换数组为Uint8Array + const uint8Array = new Uint8Array(data); + arrayBuffer = uint8Array.buffer; + } else if (typeof data === 'string') { + // 处理Base64编码 + try { + const base64Str = data.trim().replace(/^data:[^;]+;base64,/, ''); + const binary = atob(base64Str); + const uint8Array = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + uint8Array[i] = binary.charCodeAt(i); + } + arrayBuffer = uint8Array.buffer; + } catch (e) { + log("WAV数据Base64解码失败: " + e); + reject(e); + return; + } + } else if (data.buffer) { + arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); + } else { + const error = new Error("无法处理的WAV数据类型"); + log(error.message); + reject(error); return; } - } - // 数据准备 - let dataView; - try { - dataView = new DataView(pcmData.buffer, pcmData.byteOffset, pcmData.byteLength); + // 使用Web Audio API解码音频 + audioContext.decodeAudioData( + arrayBuffer, + (buffer) => { + log("WAV数据解码成功, 时长: " + buffer.duration.toFixed(2) + "秒"); + resolve(buffer); + }, + (err) => { + log("解码WAV数据失败: " + err); + reject(err); + } + ); } catch (e) { - const newBuffer = new ArrayBuffer(pcmData.length); - const newBufferView = new Uint8Array(newBuffer); - newBufferView.set(pcmData); - dataView = new DataView(newBuffer); + log("处理WAV数据失败: " + e); + reject(e); + } + }); + } + + // 处理PCM格式的数据 + function processPcmData(data, sampleRate, channels) { + try { + // 确保音频上下文存在 + if (!audioContext || audioContext.state === 'closed') { + initAudioContext(); + if (!audioContext) return null; } - // 转换为音频数据并增强音量 - const floatData = new Float32Array(pcmData.length / 2); - for (let i = 0, j = 0; i < pcmData.length; i += 2, j++) { - if (i + 1 < pcmData.length) { - const int16 = dataView.getInt16(i, true); - // 标准化16位PCM数据到-1.0到1.0,但不进行音量增益 - // 音量增益由audioGainNode处理,避免信号失真 - floatData[j] = int16 / 32768.0; + // 转换数据为适合的格式 + let pcmData; + if (data instanceof Uint8Array) { + pcmData = data; + } else if (Array.isArray(data)) { + pcmData = new Uint8Array(data); + } else if (typeof data === 'object' && data.buffer) { + pcmData = new Uint8Array(data.buffer); + } else if (typeof data === 'string') { + try { + // 处理Base64编码 + const base64Str = data.trim().replace(/^data:[^;]+;base64,/, ''); + const binary = atob(base64Str); + pcmData = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + pcmData[i] = binary.charCodeAt(i); + } + } catch (e) { + log("PCM数据Base64解码失败: " + e); + return null; } + } else { + log("不支持的PCM数据类型"); + return null; } - // 实际帧长度计算和调整 - const actualFrameDuration = (floatData.length / sampleRate) * 1000; // 毫秒 - let outputFloatData = floatData; - - // 确保音频帧至少有目标时长 - if (actualFrameDuration < targetFrameDuration && floatData.length > 0) { - const targetLength = Math.ceil(targetFrameDuration * sampleRate / 1000); - outputFloatData = new Float32Array(targetLength); - outputFloatData.set(floatData); - // 其余部分为0,即静音填充 + // 确保有效的数据 + if (!pcmData || pcmData.length < 2) { + log("PCM数据无效或太短"); + return null; } - // 创建音频缓冲区 - const buffer = audioContext.createBuffer(1, outputFloatData.length, sampleRate); - const channel = buffer.getChannelData(0); - channel.set(outputFloatData); - - // 创建音频源 - const source = audioContext.createBufferSource(); - source.buffer = buffer; - - // 应用音量控制 - source.connect(audioGainNode); - if (audioGainNode) { - audioGainNode.gain.value = currentVolume * volumeBoost; // 应用音量增益 + // 确保数据长度是偶数(16位PCM) + const validLength = Math.floor(pcmData.length / 2) * 2; + if (validLength < pcmData.length) { + pcmData = pcmData.slice(0, validLength); } - // 确保音频播放完成时进行清理 - source.onended = () => { - // 在音频片段播放完成时可以执行一些操作 - }; + try { + // 从Uint8Array创建Int16Array视图 + let int16Data; - // 使用精确调度启动播放 - const startTime = audioContext.currentTime; - source.start(startTime); + try { + // 创建DataView以便正确解析16位整数 + const dataView = new DataView(pcmData.buffer, pcmData.byteOffset, pcmData.byteLength); + int16Data = new Int16Array(pcmData.length / 2); + + // 从小端字节序读取16位整数 + for (let i = 0; i < pcmData.length; i += 2) { + if (i + 1 < pcmData.length) { + int16Data[i/2] = dataView.getInt16(i, true); // true表示小端字节序 + } + } + } catch (e) { + log("创建Int16Array失败: " + e); + + // 备用方法 + const newBuffer = new ArrayBuffer(pcmData.length); + const newView = new Uint8Array(newBuffer); + newView.set(pcmData); + const dataView = new DataView(newBuffer); + + int16Data = new Int16Array(pcmData.length / 2); + for (let i = 0; i < pcmData.length; i += 2) { + if (i + 1 < pcmData.length) { + int16Data[i/2] = dataView.getInt16(i, true); + } + } + } + // 创建音频缓冲区,使用实际采样率 + const audioSampleRate = sampleRate || audioContext.sampleRate; + const buffer = audioContext.createBuffer(channels || 1, int16Data.length, audioSampleRate); + + // 将Int16数据转换为Float32数据并存入缓冲区 + const channelData = buffer.getChannelData(0); + for (let i = 0; i < int16Data.length; i++) { + // 将Int16转换为-1.0到1.0的Float32 + channelData[i] = Math.max(-1, Math.min(1, int16Data[i] / 32768.0)); + } + + return buffer; + } catch (e) { + log("PCM数据处理失败: " + e); + return null; + } } catch (e) { - log("播放缓冲音频失败: " + e); + log("处理PCM数据失败: " + e); + return null; } } @@ -1788,110 +1821,5 @@ updateScreenControlUI(false); }); } - - // 播放实时音频 - function playRealTimeAudio(audioData) { - if (!audioContext || !isAudioStreamEnabled) return; - - try { - // 计算数据接收速率 - const now = Date.now(); - receivedPacketCounter++; - if (now - lastPacketTime > 1000) { - receivedPacketRate = receivedPacketCounter; - receivedPacketCounter = 0; - lastPacketTime = now; - log(`音频数据接收速率: ${receivedPacketRate} 包/秒`); - } - - // 处理音频数据 - let pcmData; - - // 不同类型的音频数据处理 - if (audioData instanceof Uint8Array) { - pcmData = audioData; - } else if (audioData instanceof ArrayBuffer) { - pcmData = new Uint8Array(audioData); - } else if (Array.isArray(audioData)) { - pcmData = new Uint8Array(audioData); - } else if (typeof audioData === 'object' && audioData !== null) { - if (audioData.data && (audioData.data instanceof Uint8Array || audioData.data instanceof ArrayBuffer)) { - pcmData = audioData.data instanceof Uint8Array ? audioData.data : new Uint8Array(audioData.data); - } else if (audioData.buffer && audioData.buffer instanceof ArrayBuffer) { - pcmData = new Uint8Array(audioData.buffer); - } else { - log("无法识别的对象类型数据"); - return; - } - } else if (typeof audioData === 'string') { - try { - // 移除可能的数据URL前缀 - const base64Str = audioData.trim().replace(/^data:[^;]+;base64,/, ''); - - // Base64解码 - const binary = atob(base64Str); - pcmData = new Uint8Array(binary.length); - - for (let i = 0; i < binary.length; i++) { - pcmData[i] = binary.charCodeAt(i); - } - } catch (e) { - try { - pcmData = new Uint8Array(audioData.length); - for (let i = 0; i < audioData.length; i++) { - pcmData[i] = audioData.charCodeAt(i); - } - } catch (e2) { - return; - } - } - } else { - return; - } - - // 数据有效性检查 - if (!pcmData || pcmData.length < 2) { - return; - } - - // 确保数据长度是偶数(16位样本需要2个字节) - const validLength = Math.floor(pcmData.length / 2) * 2; - if (validLength < pcmData.length) { - pcmData = pcmData.slice(0, validLength); - } - - // 估算当前数据帧的时长 - const numSamples = validLength / 2; // 16位PCM = 2字节/样本 - const frameDuration = (numSamples / sampleRate) * 1000; // 毫秒 - - // 时间戳校准功能 - 固定帧率处理 - const timestamp = Date.now(); - const packet = { - data: pcmData, - timestamp: timestamp, - estimatedDuration: frameDuration, - processed: false - }; - - // 添加到缓冲队列 - if (audioBufferQueue.length < MAX_BUFFER_SIZE) { - audioBufferQueue.push(packet); - - // 只在达到重要阈值时记录 - if (audioBufferQueue.length === bufferStartSizeThreshold || - audioBufferQueue.length % 20 === 0) { - log(`缓冲区状态: ${audioBufferQueue.length}/${MAX_BUFFER_SIZE}, 估计时长: ${frameDuration.toFixed(1)}ms`); - } - } else { - // 缓冲区已满时的处理策略 - const keepFrames = Math.floor(MAX_BUFFER_SIZE * 0.75); - audioBufferQueue.splice(0, MAX_BUFFER_SIZE - keepFrames); - audioBufferQueue.push(packet); - log(`缓冲区已满 (${MAX_BUFFER_SIZE}), 丢弃旧数据, 保留${keepFrames}帧`); - } - } catch (e) { - log("处理实时音频失败: " + e); - } - } } diff --git a/ShengShengBuXi/Services/AudioProcessingService.cs b/ShengShengBuXi/Services/AudioProcessingService.cs index b454289..27147cc 100644 --- a/ShengShengBuXi/Services/AudioProcessingService.cs +++ b/ShengShengBuXi/Services/AudioProcessingService.cs @@ -189,7 +189,7 @@ public class AudioProcessingService : IAudioProcessingService } /// - /// 应用降噪处理 + /// 应用噪声消除 /// /// 音频数据 /// 采样率 @@ -197,55 +197,116 @@ 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) + public 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) { - // 调用内部实现 - return ApplyNoiseReductionInternal(audioData, noiseThreshold, attackSeconds, releaseSeconds, highPassCutoff, q); + // 基本的噪声消除实现(简化版) + // 实际项目中,这里可以使用更复杂的噪声消除算法,如频域滤波、自适应滤波等 + // 为简单起见,我们这里只实现一个简单的高通滤波和噪声门限 + + if (audioData == null || audioData.Length < 4) + { + return audioData; + } + + try + { + // 转换为16位的短整型数据(PCM格式) + short[] samples = new short[audioData.Length / 2]; + for (int i = 0; i < samples.Length; i++) + { + // 假设数据是小端字节序 + samples[i] = (short)(audioData[i * 2] | (audioData[i * 2 + 1] << 8)); + } + + // 应用噪声门限 - 低于门限的声音被静音 + float maxAmplitude = samples.Max(s => Math.Abs(s)) / 32768.0f; // 归一化 + if (maxAmplitude < noiseThreshold) + { + // 整个片段低于门限,认为是噪声,全部静音 + Array.Clear(samples, 0, samples.Length); + } + else + { + // 应用高通滤波(简单的单阶IIR滤波器) + float alpha = (float)Math.Exp(-2 * Math.PI * highPassCutoff / sampleRate); + float filterState = 0; + + for (int i = 0; i < samples.Length; i++) + { + // 高通滤波 + float input = samples[i] / 32768.0f; + float filtered = alpha * (filterState + input - filterState); + filterState = input; + + // 噪声门限 + float absFiltered = Math.Abs(filtered); + if (absFiltered < noiseThreshold) + { + filtered = 0; + } + else + { + // 应用平滑瞬态响应(攻击/释放) + // 这里简化实现 + } + + // 转回16位整数 + samples[i] = (short)(filtered * 32768.0f); + } + } + + // 转回字节数组 + byte[] result = new byte[samples.Length * 2]; + for (int i = 0; i < samples.Length; i++) + { + // 小端字节序 + result[i * 2] = (byte)(samples[i] & 0xFF); + result[i * 2 + 1] = (byte)((samples[i] >> 8) & 0xFF); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "应用噪声消除时发生错误"); + return audioData; // 失败时返回原始数据 + } } - private byte[] ApplyNoiseReductionInternal( - byte[] audioData, - float noiseThreshold = 0.015f, // 降低噪声门限值,使其更温和 - float attackSeconds = 0.05f, // 增加攻击时间,使开启更平滑 - float releaseSeconds = 0.15f, // 增加释放时间,使关闭更平滑 - int highPassCutoff = 60, // 降低高通滤波器截止频率,减少声音失真 - float q = 0.7071f) // 使用更平缓的Q值(巴特沃斯滤波器标准Q值) + /// + /// 为PCM数据添加WAV头 + /// + /// 原始PCM数据 + /// 采样率 + /// 声道数 + /// 包含WAV头的完整WAV格式数据 + public byte[] AddWavHeader(byte[] pcmData, int sampleRate = 16000, int channels = 1) { - // 1. 将字节数组转换为 WaveStream - using (var inputStream = new MemoryStream(audioData)) - using (var waveStream = new RawSourceWaveStream(inputStream, new WaveFormat(16000, 16, 1))) + using (var stream = new MemoryStream()) { - // 2. 转换为浮点样本便于处理 - var sampleProvider = waveStream.ToSampleProvider(); - - // 3. 应用改进的噪声门,使用平滑过渡 - var noiseGate = new ImprovedNoiseGate(sampleProvider) - { - Threshold = noiseThreshold, - AttackSeconds = attackSeconds, - ReleaseSeconds = releaseSeconds, - HoldSeconds = 0.1f, // 添加保持时间,防止快速开关 - SoftKneeDb = 6.0f // 添加软膝,使过渡更平滑 - }; - - // 4. 应用高通滤波器去除低频噪音,使用更温和的设置 - var highPassFilter = new BiQuadFilterSampleProvider(noiseGate); - highPassFilter.Filter = BiQuadFilter.HighPassFilter( - sampleProvider.WaveFormat.SampleRate, - highPassCutoff, - q); - - // 5. 添加平滑处理器 - var smoothedProvider = new SmoothingSampleProvider(highPassFilter, 5); - - // 6. 处理后的音频转回字节数组 - var outputStream = new MemoryStream(); - WaveFileWriter.WriteWavFileToStream(outputStream, smoothedProvider.ToWaveProvider16()); - - return outputStream.ToArray(); + // RIFF头 + stream.Write(System.Text.Encoding.ASCII.GetBytes("RIFF"), 0, 4); + stream.Write(BitConverter.GetBytes(pcmData.Length + 36), 0, 4); + stream.Write(System.Text.Encoding.ASCII.GetBytes("WAVEfmt "), 0, 8); + + // 格式块 + stream.Write(BitConverter.GetBytes(16), 0, 4); // fmt块大小 + stream.Write(BitConverter.GetBytes((ushort)1), 0, 2); // PCM格式 + stream.Write(BitConverter.GetBytes((ushort)channels), 0, 2); + stream.Write(BitConverter.GetBytes(sampleRate), 0, 4); + stream.Write(BitConverter.GetBytes(sampleRate * channels * 2), 0, 4); // 字节率 + stream.Write(BitConverter.GetBytes((ushort)(channels * 2)), 0, 2); // 块对齐 + stream.Write(BitConverter.GetBytes((ushort)16), 0, 2); // 位深 + + // 数据块 + stream.Write(System.Text.Encoding.ASCII.GetBytes("data"), 0, 4); + stream.Write(BitConverter.GetBytes(pcmData.Length), 0, 4); + stream.Write(pcmData, 0, pcmData.Length); + + return stream.ToArray(); } } diff --git a/ShengShengBuXi/Services/IAudioProcessingService.cs b/ShengShengBuXi/Services/IAudioProcessingService.cs index d8e8f0d..552ac77 100644 --- a/ShengShengBuXi/Services/IAudioProcessingService.cs +++ b/ShengShengBuXi/Services/IAudioProcessingService.cs @@ -91,5 +91,14 @@ namespace ShengShengBuXi.Services /// 滤波器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); + + /// + /// 为PCM数据添加WAV头 + /// + /// 原始PCM数据 + /// 采样率 + /// 声道数 + /// 包含WAV头的完整WAV格式数据 + byte[] AddWavHeader(byte[] pcmData, int sampleRate = 16000, int channels = 1); } } \ No newline at end of file