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

@ -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>
@ -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>
@ -114,7 +119,8 @@
<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>
@ -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;
@ -410,6 +442,10 @@
// 接收实时音频数据
connection.on("ReceiveAudioData", (audioData) => {
if (!isActiveShow) {
updateCallStatus(true);
return;
}
if (isAudioStreamEnabled && callInProgress) {
// 确保音频流开启且通话进行中
log("接收到实时音频数据...");
@ -438,13 +474,40 @@
// 更新通话状态
function updateCallStatus(isActive) {
callInProgress = isActive;
isActiveShow = isActive;
if (isActive) {
// 当检测到新通话时,先显示确认对话框
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 = "未检测到通话";
if (isActive) {
// 停止缓冲区处理
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) {
@ -468,15 +531,14 @@
// 创建新的音频上下文
initAudioContext();
}
} else {
indicator.style.backgroundColor = "red";
statusText.textContent = "未检测到通话";
// 停止缓冲区处理
stopBufferProcessing();
// 关闭确认对话框
const confirmDialog = bootstrap.Modal.getInstance(document.getElementById('callConfirmDialog'));
if (confirmDialog) {
confirmDialog.hide();
}
// 有新通话时刷新监控列表
// 刷新监控列表
loadMonitorTextList();
}
@ -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}%`);
@ -1599,4 +1661,4 @@
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,161 +668,175 @@ 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);
float inputSample = buffer[offset + n];
float absInput = Math.Abs(inputSample);
// 更新包络
if (absSample > envelope)
// 包络跟踪
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;
private readonly float[] weights;
public SmoothingSampleProvider(ISampleProvider source, int windowSize = 5)
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; }
@ -828,15 +847,23 @@ public class SmoothingSampleProvider : ISampleProvider
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];
{
sum += history[index] * weights[i];
index = (index - 1 + history.Length) % history.Length;
}
buffer[offset + n] = sum / history.Length;
// 更新样本
buffer[offset + n] = sum;
// 更新历史索引
historyIndex = (historyIndex + 1) % history.Length;
}
return samplesRead;