312321
This commit is contained in:
parent
e6816d49b7
commit
ee18fd55a5
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"SignalRHubUrl": "http://localhost:81/audiohub",
|
||||
"SignalRHubUrl": "http://115.159.44.16/audiohub",
|
||||
"ConfigBackupPath": "config.json",
|
||||
"AutoConnectToServer": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user