This commit is contained in:
zpc 2025-03-28 16:58:18 +08:00
parent e6816d49b7
commit ee18fd55a5
3 changed files with 324 additions and 235 deletions

View File

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

View File

@ -18,14 +18,14 @@
<!-- 通话状态指示器 -->
<div class="d-flex align-items-center">
<div id="status-indicator" class="rounded-circle me-2"
style="width: 24px; height: 24px; background-color: red;"></div>
style="width: 24px; height: 24px; background-color: red;"></div>
<span id="status-text" class="me-3">未检测到通话</span>
</div>
<!-- 单选按钮组 - 隐藏自动识别显示选项 -->
<div class="btn-group" role="group" style="display: none;">
<input type="radio" class="btn-check" name="displayMode" id="displayMode0" value="0"
checked>
checked>
<label class="btn btn-outline-primary" for="displayMode0">自动识别显示</label>
<input type="radio" class="btn-check" name="displayMode" id="displayMode1" value="1">
@ -34,16 +34,20 @@
<!-- 音频传输开关 - 隐藏关闭音频传输选项 -->
<div class="btn-group ms-3" role="group">
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming1" value="1" checked>
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming1" value="1"
checked>
<label class="btn btn-outline-success" for="audioStreaming1">开启音频传输</label>
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming0" value="0" style="display: none;">
<label class="btn btn-outline-danger" for="audioStreaming0" style="display: none;">关闭音频传输</label>
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming0" value="0"
style="display: none;">
<label class="btn btn-outline-danger" for="audioStreaming0"
style="display: none;">关闭音频传输</label>
<!-- 音量控制滑块 -->
<div class="ms-3 d-flex align-items-center">
<label for="volumeControl" class="me-2"><i class="bi bi-volume-up"></i></label>
<input type="range" class="form-range" min="0" max="1" step="0.1" value="1.0" id="volumeControl" style="width: 100px;">
<input type="range" class="form-range" min="0" max="1" step="0.1" value="1.0"
id="volumeControl" style="width: 100px;">
</div>
</div>
</div>
@ -61,7 +65,7 @@
</div>
<div class="card-body p-0">
<div id="monitor-text-list" class="list-group list-group-flush"
style="max-height: 75vh; overflow-y: auto;">
style="max-height: 75vh; overflow-y: auto;">
<!-- 文本列表内容将通过JS动态填充 -->
<div class="text-center text-muted p-3">加载中...</div>
</div>
@ -95,11 +99,12 @@
<div id="input-text-area" style="height: 40%;">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">文本编辑 <span style="color:#6c757d;font-size:12px;">(最多输入30个文字)</span></h5>
<h5 class="mb-0">文本编辑 <span
style="color:#6c757d;font-size:12px;">(最多输入30个文字)</span></h5>
</div>
<div class="card-body">
<textarea id="text-input" class="form-control h-100"
placeholder="请输入要显示的文本..." maxlength="30"></textarea>
<textarea id="text-input" class="form-control h-100" placeholder="请输入要显示的文本..."
maxlength="30"></textarea>
</div>
</div>
@ -110,11 +115,12 @@
<div class="card h-100">
<div class="card-body d-flex justify-content-center align-items-center">
<button id="add-text-btn" class="btn btn-primary me-3" data-bs-toggle="tooltip"
title="将文本添加到显示队列,并显示在大屏上" onclick="addDisplayText()">
title="将文本添加到显示队列,并显示在大屏上" onclick="addDisplayText()">
<i class="bi bi-plus-circle"></i> 显示到大屏
</button>
<button id="add-and-remove-btn" class="btn btn-warning" data-bs-toggle="tooltip"
title="将文本添加到显示队列,并从监控列表中移除当前选中项" onclick="addDisplayTextAndRemoveMonitor()" style="display: none;">
title="将文本添加到显示队列,并从监控列表中移除当前选中项" onclick="addDisplayTextAndRemoveMonitor()"
style="display: none;">
<i class="bi bi-arrow-right-circle"></i> 添加并移除
</button>
</div>
@ -138,7 +144,7 @@
</div>
<div class="card-body p-0">
<div id="display-text-list" class="list-group list-group-flush"
style="max-height: 75vh; overflow-y: auto;">
style="max-height: 75vh; overflow-y: auto;">
<!-- 文本列表内容将通过JS动态填充 -->
<div class="text-center text-muted p-3">加载中...</div>
</div>
@ -169,14 +175,40 @@
<!-- 消息区域 -->
<div id="message-area" class="position-fixed bottom-0 end-0 p-3" style="z-index: 1050;"></div>
@section Scripts {
<!-- 来电确认对话框 -->
<div class="modal fade" id="callConfirmDialog" tabindex="-1" aria-labelledby="callConfirmDialogLabel"
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="callConfirmDialogLabel">
<i class="bi bi-telephone-inbound"></i> 来电提醒
</h5>
</div>
<div class="modal-body text-center py-4">
<div class="mb-3">
<i class="bi bi-telephone-fill text-primary" style="font-size: 3rem;"></i>
</div>
<h5>检测到新的通话</h5>
<p class="text-muted">请点击接听开始播放音频</p>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-primary" onclick="confirmCall()">
<i class="bi bi-telephone"></i> 接听
</button>
</div>
</div>
</div>
</div>
@section Scripts {
<script src="~/lib/microsoft-signalr/signalr.min.js"></script>
<script>
let connection = null;
let refreshDisplayInterval = null;
let callInProgress = false;
let isActiveShow = false;
// 当前播放的音频元素和按钮引用
let currentAudio = null;
let currentPlayButton = null;
@ -187,7 +219,7 @@
let isAudioStreamEnabled = false;
let audioGainNode = null;
let currentVolume = 1.0; // 默认音量为1.0 (100%)
// 添加音频缓冲区变量
let audioBufferQueue = [];
const MAX_BUFFER_SIZE = 15; // 最大缓冲队列大小调整为15帧以适应网络延迟
@ -228,9 +260,9 @@
const alert = document.createElement("div");
alert.className = `alert alert-${type} alert-dismissible fade show`;
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
messageArea.appendChild(alert);
// 自动移除
@ -365,7 +397,7 @@
// 接收实时语音识别结果
connection.on("ReceiveSpeechToTextResult", (result) => {
log(`接收到实时语音识别结果: ${result.substring(0, 30)}${result.length > 30 ? '...' : ''}`);
// 使用防抖动技术显示实时识别结果,避免频繁更新导致页面卡顿
if (window.realtimeTextDebounceTimer) {
clearTimeout(window.realtimeTextDebounceTimer);
@ -378,15 +410,15 @@
// 接收最终语音识别结果
connection.on("ReceiveSpeechToEndTextResult", (result) => {
log(`接收到最终语音识别结果: ${result.substring(0, 30)}${result.length > 30 ? '...' : ''}`);
// 清除任何正在进行的实时文本更新定时器
if (window.realtimeTextDebounceTimer) {
clearTimeout(window.realtimeTextDebounceTimer);
}
// 显示最终识别结果
showFinalTextResult(result);
// 同时添加到监控列表的顶部(如果不存在于列表中)
addToMonitorList(result);
});
@ -410,6 +442,10 @@
// 接收实时音频数据
connection.on("ReceiveAudioData", (audioData) => {
if (!isActiveShow) {
updateCallStatus(true);
return;
}
if (isAudioStreamEnabled && callInProgress) {
// 确保音频流开启且通话进行中
log("接收到实时音频数据...");
@ -438,48 +474,74 @@
// 更新通话状态
function updateCallStatus(isActive) {
callInProgress = isActive;
const indicator = document.getElementById("status-indicator");
const statusText = document.getElementById("status-text");
isActiveShow = isActive;
if (isActive) {
indicator.style.backgroundColor = "green";
statusText.textContent = "正在通话中";
// 初始化音频上下文(如果需要且启用了音频流)
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();
}
// 当检测到新通话时,先显示确认对话框
const confirmDialog = new bootstrap.Modal(document.getElementById('callConfirmDialog'));
confirmDialog.show();
return; // 等待用户确认后再继续处理
} else {
const indicator = document.getElementById("status-indicator");
const statusText = document.getElementById("status-text");
indicator.style.backgroundColor = "red";
statusText.textContent = "未检测到通话";
// 停止缓冲区处理
stopBufferProcessing();
// 如果确认对话框还在显示(用户未点击接听),则自动关闭
const confirmDialog = bootstrap.Modal.getInstance(document.getElementById('callConfirmDialog'));
if (confirmDialog) {
confirmDialog.hide();
log("通话已结束,自动关闭确认对话框");
}
}
// 有新通话时刷新监控列表
loadMonitorTextList();
}
// 用户确认接听通话
function confirmCall() {
const indicator = document.getElementById("status-indicator");
const statusText = document.getElementById("status-text");
indicator.style.backgroundColor = "green";
statusText.textContent = "正在通话中";
callInProgress = true;
// 初始化音频上下文(如果需要且启用了音频流)
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();
}
// 关闭确认对话框
const confirmDialog = bootstrap.Modal.getInstance(document.getElementById('callConfirmDialog'));
if (confirmDialog) {
confirmDialog.hide();
}
// 刷新监控列表
loadMonitorTextList();
}
// 加载监控文本列表
function loadMonitorTextList() {
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
@ -550,22 +612,22 @@
const shortText = text.length > 10 ? text.substring(0, 10) : text;
listItem.innerHTML = `
<div class="d-flex justify-content-between align-items-start mb-1">
<small class="text-muted">【${shortText}】</small>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary btn-sm" onclick="playAudio('${item.recordingPath || ''}', this)">
<i class="bi bi-play-fill"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="downloadRecording('${item.recordingPath || ''}')">
<i class="bi bi-download"></i>
</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteMonitorText('${item.id}')">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div>${formattedDate}</div>
`;
<div class="d-flex justify-content-between align-items-start mb-1">
<small class="text-muted">【${shortText}】</small>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary btn-sm" onclick="playAudio('${item.recordingPath || ''}', this)">
<i class="bi bi-play-fill"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="downloadRecording('${item.recordingPath || ''}')">
<i class="bi bi-download"></i>
</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteMonitorText('${item.id}')">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div>${formattedDate}</div>
`;
container.appendChild(listItem);
});
@ -771,11 +833,11 @@
const shortText = text.length > 5 ? text.substring(0, 5) : text;
listItem.innerHTML = `
<div class="mb-1">
<small class="text-muted">${formattedDate}</small>
</div>
<div>【${shortText}】</div>
`;
<div class="mb-1">
<small class="text-muted">${formattedDate}</small>
</div>
<div>【${shortText}】</div>
`;
container.appendChild(listItem);
});
@ -792,10 +854,10 @@
// 调用服务器端方法更新显示模式
if (connection && connection.state === signalR.HubConnectionState.Connected) {
connection.invoke("UpdateDisplayType", mode)
.then(() => {
.then(() => {
showMessage(`已切换为${mode === 0 ? "识别立即显示" : "手动显示"}模式`, "success");
})
.catch(err => {
})
.catch(err => {
log(`更新显示模式失败: ${err}`);
showMessage("切换显示模式失败", "danger");
@ -826,12 +888,12 @@
}
// 调用服务器端方法更新音频流设置
if (connection && connection.state === signalR.HubConnectionState.Connected) {
if (connection && connection.state === signalR.HubConnectionState.Connected) {
connection.invoke("UpdateAudioStreaming", enabled)
.then(() => {
.then(() => {
showMessage(`已${enabled ? "开启" : "关闭"}音频传输`, "success");
})
.catch(err => {
})
.catch(err => {
log(`更新音频流设置失败: ${err}`);
showMessage("切换音频传输失败", "danger");
@ -1122,8 +1184,8 @@
// 刷新右侧显示文本列表
loadDisplayTextList();
}
})
.catch(err => {
})
.catch(err => {
log(`删除监控文本失败: ${err}`);
showMessage("文本已添加到显示队列,但从监控列表移除失败", "warning");
@ -1172,7 +1234,7 @@
function setupVolumeControl() {
const volumeControl = document.getElementById('volumeControl');
if (volumeControl) {
volumeControl.addEventListener('input', function(e) {
volumeControl.addEventListener('input', function (e) {
currentVolume = parseFloat(e.target.value);
log(`音量已调整为: ${currentVolume * 100}%`);
@ -1193,7 +1255,7 @@
function initAudioContext() {
if (audioContext) {
log("音频上下文已存在,使用现有上下文");
// 确保音频上下文已激活
if (audioContext.state === 'suspended') {
audioContext.resume().then(() => {
@ -1202,7 +1264,7 @@
log("恢复音频上下文失败: " + err);
});
}
// 启动缓冲处理
startBufferProcessing();
return;
@ -1237,7 +1299,7 @@
}
log("音频上下文已初始化,状态: " + audioContext.state + ", 采样率: " + audioContext.sampleRate);
// 启动音频缓冲处理
startBufferProcessing();
} catch (e) {
@ -1292,7 +1354,7 @@
const audioData = audioBufferQueue.shift();
playBufferedAudio(audioData);
isAudioPlaying = true;
// 自适应调整缓冲区大小
if (audioBufferQueue.length > MAX_BUFFER_SIZE * 0.8) {
log("缓冲区接近上限,增加处理频率");
@ -1359,7 +1421,7 @@
try {
// 尝试使用更健壮的Base64解码先规范化字符串格式
const base64Str = audioData.trim().replace(/^data:[^;]+;base64,/, '');
// 创建具有适当长度的Uint8Array
const binary = atob(base64Str);
pcmData = new Uint8Array(binary.length);
@ -1369,7 +1431,7 @@
log("已从Base64字符串转换为Uint8Array");
} catch (e) {
log("Base64转换失败: " + e);
// 尝试直接解码二进制字符串
try {
pcmData = new Uint8Array(audioData.length);
@ -1417,7 +1479,7 @@
log("处理实时音频失败: " + e);
}
}
// 播放缓冲区中的音频
async function playBufferedAudio(pcmData) {
try {
@ -1532,7 +1594,7 @@
// 添加文本到监控列表
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++) {
@ -1541,7 +1603,7 @@
return;
}
}
// 创建新的列表项
const container = document.getElementById("monitor-text-list");
const listItem = document.createElement("div");
@ -1549,7 +1611,7 @@
// 使用临时ID服务器端会分配真正的ID
listItem.dataset.id = "temp-" + Date.now();
listItem.dataset.fullText = text;
// 添加点击事件
listItem.addEventListener('click', function (e) {
// 如果点击的是按钮,不触发选择
@ -1569,34 +1631,34 @@
// 显示文本
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 = `
<div class="d-flex justify-content-between align-items-start mb-1">
<small class="text-muted">【${shortText}】</small>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-danger btn-sm" onclick="deleteMonitorText('${listItem.dataset.id}')">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div>${formattedDate} (本地)</div>
`;
<div class="d-flex justify-content-between align-items-start mb-1">
<small class="text-muted">【${shortText}】</small>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-danger btn-sm" onclick="deleteMonitorText('${listItem.dataset.id}')">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div>${formattedDate} (本地)</div>
`;
// 添加到列表顶部
if (container.firstChild) {
container.insertBefore(listItem, container.firstChild);
} else {
container.appendChild(listItem);
}
log(`已添加文本到监控列表: ${shortText}...`);
}
</script>
}
}

View File

@ -208,11 +208,11 @@ public class AudioProcessingService : IAudioProcessingService
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值
float noiseThreshold = 0.015f, // 降低噪声门限值,使其更温和
float attackSeconds = 0.05f, // 增加攻击时间,使开启更平滑
float releaseSeconds = 0.15f, // 增加释放时间,使关闭更平滑
int highPassCutoff = 60, // 降低高通滤波器截止频率,减少声音失真
float q = 0.7071f) // 使用更平缓的Q值巴特沃斯滤波器标准Q值
{
// 1. 将字节数组转换为 WaveStream
using (var inputStream = new MemoryStream(audioData))
@ -221,24 +221,29 @@ public class AudioProcessingService : IAudioProcessingService
// 2. 转换为浮点样本便于处理
var sampleProvider = waveStream.ToSampleProvider();
// 3. 应用噪声门(Noise Gate)
var noiseGate = new NoiseGateSampleProvider(sampleProvider)
// 3. 应用改进的噪声门,使用平滑过渡
var noiseGate = new ImprovedNoiseGate(sampleProvider)
{
Threshold = noiseThreshold,
AttackSeconds = attackSeconds,
ReleaseSeconds = releaseSeconds
ReleaseSeconds = releaseSeconds,
HoldSeconds = 0.1f, // 添加保持时间,防止快速开关
SoftKneeDb = 6.0f // 添加软膝,使过渡更平滑
};
// 4. 应用高通滤波器去除低频噪音
// 4. 应用高通滤波器去除低频噪音,使用更温和的设置
var highPassFilter = new BiQuadFilterSampleProvider(noiseGate);
highPassFilter.Filter = BiQuadFilter.HighPassFilter(
sampleProvider.WaveFormat.SampleRate,
highPassCutoff,
q);
// 5. 处理后的音频转回字节数组
// 5. 添加平滑处理器
var smoothedProvider = new SmoothingSampleProvider(highPassFilter, 5);
// 6. 处理后的音频转回字节数组
var outputStream = new MemoryStream();
WaveFileWriter.WriteWavFileToStream(outputStream, highPassFilter.ToWaveProvider16());
WaveFileWriter.WriteWavFileToStream(outputStream, smoothedProvider.ToWaveProvider16());
return outputStream.ToArray();
}
@ -663,182 +668,204 @@ public class BiQuadFilterSampleProvider : ISampleProvider
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;
private float threshold = 0.015f;
private float attackSeconds = 0.05f;
private float releaseSeconds = 0.15f;
private float holdSeconds = 0.1f;
private float softKneeDb = 6.0f;
private float currentGain = 1.0f;
private float envelope = 0.0f;
private int holdSamples;
private int holdCounter;
public ImprovedNoiseGate(ISampleProvider source)
{
this.source = source ?? throw new ArgumentNullException(nameof(source));
this.source = source;
this.WaveFormat = source.WaveFormat;
// 默认参数
Threshold = 0.015f;
AttackSeconds = 0.05f;
ReleaseSeconds = 0.3f;
HoldSeconds = 0.2f;
this.holdSamples = (int)(holdSeconds * WaveFormat.SampleRate);
}
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);
set
{
attackSeconds = Math.Max(0.001f, value);
}
}
/// <summary>
/// 释放时间 (秒)
/// </summary>
public float ReleaseSeconds
{
get => releaseSeconds;
set => releaseSeconds = Math.Max(0.001f, value);
set
{
releaseSeconds = Math.Max(0.001f, value);
}
}
/// <summary>
/// 保持时间 (秒),在信号低于阈值后保持门打开的时间
/// </summary>
public float HoldSeconds
{
get => holdSeconds;
set => holdSeconds = Math.Max(0.0f, value);
set
{
holdSeconds = Math.Max(0.0f, value);
holdSamples = (int)(holdSeconds * WaveFormat.SampleRate);
}
}
/// <summary>
/// 当前包络值 (只读)
/// </summary>
public float CurrentEnvelope => envelope;
/// <summary>
/// 当前门状态 (只读)
/// </summary>
public bool IsGateOpen => gateOpen;
public float SoftKneeDb
{
get => softKneeDb;
set => softKneeDb = Math.Max(0.0f, value);
}
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);
float attackRate = (float)Math.Exp(-1.0 / (WaveFormat.SampleRate * attackSeconds));
float releaseRate = (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)
float inputSample = buffer[offset + n];
float absInput = Math.Abs(inputSample);
// 包络跟踪
if (absInput > envelope)
{
envelope = absSample + (envelope - absSample) * attackCoeff;
envelope = absInput + attackRate * (envelope - absInput);
}
else
{
envelope = absSample + (envelope - absSample) * releaseCoeff;
envelope = absInput + releaseRate * (envelope - absInput);
}
// 更新门状态
if (envelope > Threshold)
// 软膝处理
float softKneeStart = threshold - (softKneeDb / 40.0f); // 将dB转换为线性值
float softKneeEnd = threshold + (softKneeDb / 40.0f);
float targetGain;
if (envelope < softKneeStart)
{
gateOpen = true;
holdCountRemaining = holdSamples; // 重置保持计数器
// 低于软膝起点,完全衰减
holdCounter = 0;
targetGain = 0.0f;
}
else if (holdCountRemaining > 0)
else if (envelope > softKneeEnd)
{
holdCountRemaining--;
// 高于软膝终点,完全开启
holdCounter = holdSamples;
targetGain = 1.0f;
}
else
{
gateOpen = false;
// 在软膝区域内,线性插值
float ratio = (envelope - softKneeStart) / (softKneeEnd - softKneeStart);
targetGain = ratio;
// 更新保持计数器
if (ratio > 0.5f)
{
holdCounter = holdSamples;
}
else if (holdCounter > 0)
{
holdCounter--;
}
}
// 应用增益 (带平滑过渡)
float gain = gateOpen ? 1.0f : CalculateSoftGain(envelope);
buffer[offset + n] = sample * gain;
// 如果在保持时间内,保持门开启
if (holdCounter > 0)
{
targetGain = Math.Max(targetGain, 0.5f);
}
// 平滑增益变化
if (targetGain > currentGain)
{
currentGain = targetGain * attackRate + currentGain * (1 - attackRate);
}
else
{
currentGain = targetGain * releaseRate + currentGain * (1 - releaseRate);
}
// 应用增益
buffer[offset + n] = inputSample * currentGain;
}
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)
private readonly float[] weights;
public SmoothingSampleProvider(ISampleProvider source, int windowSize)
{
this.source = source;
this.history = new float[windowSize];
this.WaveFormat = source.WaveFormat;
this.history = new float[windowSize];
this.weights = new float[windowSize];
// 创建高斯权重
float sigma = windowSize / 6.0f;
float weightSum = 0;
for (int i = 0; i < windowSize; i++)
{
float x = (i - windowSize / 2.0f) / sigma;
weights[i] = (float)Math.Exp(-0.5f * x * x);
weightSum += weights[i];
}
// 归一化权重
for (int i = 0; i < windowSize; i++)
{
weights[i] /= weightSum;
}
}
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;
int index = historyIndex;
for (int i = 0; i < history.Length; i++)
sum += history[i];
buffer[offset + n] = sum / history.Length;
{
sum += history[index] * weights[i];
index = (index - 1 + history.Length) % history.Length;
}
// 更新样本
buffer[offset + n] = sum;
// 更新历史索引
historyIndex = (historyIndex + 1) % history.Length;
}
return samplesRead;
}
}