提交
This commit is contained in:
parent
63a0438406
commit
71db19a4be
|
|
@ -126,6 +126,31 @@ public class AudioFilesConfig
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MinKeyTonePlayTimeMs { get; set; } = 200;
|
public int MinKeyTonePlayTimeMs { get; set; } = 200;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按键音音量倍数 (正常音量为1.0)
|
||||||
|
/// </summary>
|
||||||
|
public float KeyToneVolume { get; set; } = 1.0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待机嘟声音量倍数 (正常音量为1.0)
|
||||||
|
/// </summary>
|
||||||
|
public float WaitingToneVolume { get; set; } = 1.0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 拨出嘟声音量倍数 (正常音量为1.0)
|
||||||
|
/// </summary>
|
||||||
|
public float DialToneVolume { get; set; } = 0.9f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提示留言音量倍数 (正常音量为1.0)
|
||||||
|
/// </summary>
|
||||||
|
public float PromptAfterBeepVolume { get; set; } = 1.0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 风铃提示音文件名
|
||||||
|
/// </summary>
|
||||||
|
public string WindChimeFile { get; set; } = "风铃.wav";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取完整的音频文件路径
|
/// 获取完整的音频文件路径
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -234,6 +259,16 @@ public class RecordingConfig
|
||||||
/// 背景音乐音量 (0.0-1.0)
|
/// 背景音乐音量 (0.0-1.0)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float BackgroundMusicVolume { get; set; } = 0.1f;
|
public float BackgroundMusicVolume { get; set; } = 0.1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无声音播放风铃提示的时间(秒)
|
||||||
|
/// </summary>
|
||||||
|
public float WindChimePromptSeconds { get; set; } = 7.5f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 风铃声淡出的时间(毫秒)
|
||||||
|
/// </summary>
|
||||||
|
public int WindChimeFadeOutMs { get; set; } = 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,15 @@
|
||||||
"WaitingToneFile": "等待嘟音.wav", // 等待嘟音文件
|
"WaitingToneFile": "等待嘟音.wav", // 等待嘟音文件
|
||||||
"WaitForPickupFile": "等待接电话.mp3", // 等待接电话音频文件
|
"WaitForPickupFile": "等待接电话.mp3", // 等待接电话音频文件
|
||||||
"PhonePickupFile": "电话接起.mp3", // 电话接起音频文件
|
"PhonePickupFile": "电话接起.mp3", // 电话接起音频文件
|
||||||
"PromptUserRecordFile": "提示用户录音.mp3", // 提示用户录音音频文件
|
"PromptUserRecordFile": "提示用户录音.mp3", // 提示用户录音音频文件
|
||||||
"BeepPromptFile": "滴提示音.wav", // 滴提示音文件
|
"BeepPromptFile": "滴提示音.wav", // 滴提示音文件
|
||||||
"DigitToneFileTemplate": "{0}.mp3" // 数字按键音频文件模板
|
"DigitToneFileTemplate": "{0}.mp3", // 数字按键音频文件模板
|
||||||
|
"MinKeyTonePlayTimeMs": 200, // 按键音的最小播放时间(毫秒)
|
||||||
|
"KeyToneVolume": 1.0, // 按键音量倍数 (0.0-1.0)
|
||||||
|
"WaitingToneVolume": 1.0, // 待机嘟声音量倍数 (0.0-1.0)
|
||||||
|
"DialToneVolume": 0.9, // 拨出嘟声音量倍数 (0.0-1.0)
|
||||||
|
"PromptAfterBeepVolume": 1.0, // 提示留言音量倍数 (0.0-1.0)
|
||||||
|
"WindChimeFile": "风铃.wav" // 风铃提示音文件名
|
||||||
},
|
},
|
||||||
"Dial": {
|
"Dial": {
|
||||||
"MinDigitsToDialOut": 8, // 拨号所需的最小位数
|
"MinDigitsToDialOut": 8, // 拨号所需的最小位数
|
||||||
|
|
@ -39,15 +45,22 @@
|
||||||
"RecordingDeviceNumber": 0, // 录音设备编号
|
"RecordingDeviceNumber": 0, // 录音设备编号
|
||||||
"SampleRate": 16000, // 录音采样率
|
"SampleRate": 16000, // 录音采样率
|
||||||
"Channels": 1, // 录音通道数(1=单声道,2=立体声)
|
"Channels": 1, // 录音通道数(1=单声道,2=立体声)
|
||||||
"BufferMilliseconds": 100, // 录音缓冲区大小(毫秒)
|
"BufferMilliseconds": 50, // 录音缓冲区大小(毫秒)
|
||||||
"SilenceThreshold": 0.02, // 静音检测阈值
|
"SilenceThreshold": 0.05, // 静音检测阈值
|
||||||
"SilenceTimeoutSeconds": 30, // 无声音自动挂断的时间(秒)
|
"SilenceTimeoutSeconds": 30, // 无声音自动挂断的时间(秒)
|
||||||
"AllowUserHangup": true // 是否允许用户手动挂断(按回车键)
|
"AllowUserHangup": true, // 是否允许用户手动挂断(按回车键)
|
||||||
|
"UploadRecordingToServer": false, // 是否上传录音文件到服务器
|
||||||
|
"EnableBackgroundMusic": true, // 是否在录音时播放背景音乐
|
||||||
|
"BackgroundMusicFile": "bj.mp3", // 录音背景音乐文件名
|
||||||
|
"BackgroundMusicVolume": 0.1, // 背景音乐音量 (0.0-1.0)
|
||||||
|
"WindChimePromptSeconds": 7.5, // 无声音播放风铃提示的时间(秒)
|
||||||
|
"WindChimeFadeOutMs": 3000 // 风铃声淡出的时间(毫秒)
|
||||||
},
|
},
|
||||||
"CallFlow": {
|
"CallFlow": {
|
||||||
"WaitForPickupMinSeconds": 3, // 等待接电话音频的最小持续时间(秒)
|
"WaitForPickupMinSeconds": 3, // 等待接电话音频的最小持续时间(秒)
|
||||||
"WaitForPickupMaxSeconds": 6, // 等待接电话音频的最大持续时间(秒)
|
"WaitForPickupMaxSeconds": 6, // 等待接电话音频的最大持续时间(秒)
|
||||||
"PlayPickupProbability": 0.15 // 播放电话接起音频的概率(0-1之间的小数)
|
"PlayPickupProbability": 0.15, // 播放电话接起音频的概率(0-1之间的小数)
|
||||||
|
"ResetSystemAfterHangup": false // 用户挂断后是否重置系统,false表示直接退出程序
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,14 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
private CancellationTokenSource? _backgroundMusicCts = null;
|
private CancellationTokenSource? _backgroundMusicCts = null;
|
||||||
private volatile bool _isBackgroundMusicPlaying = false;
|
private volatile bool _isBackgroundMusicPlaying = false;
|
||||||
|
|
||||||
|
// 风铃提示音相关
|
||||||
|
private WaveOutEvent? _windChimeDevice = null;
|
||||||
|
private AudioFileReader? _windChimeReader = null;
|
||||||
|
private CancellationTokenSource? _windChimeCts = null;
|
||||||
|
private volatile bool _isWindChimePlaying = false;
|
||||||
|
private Timer? _windChimeTimer = null;
|
||||||
|
private DateTime _lastSpeechTime = DateTime.MinValue;
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
private struct KBDLLHOOKSTRUCT
|
private struct KBDLLHOOKSTRUCT
|
||||||
{
|
{
|
||||||
|
|
@ -282,13 +290,12 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
_waitingToneDevice = new WaveOutEvent();
|
_waitingToneDevice = new WaveOutEvent();
|
||||||
_waitingToneReader = new AudioFileReader(_config.AudioFiles.GetFullPath(_config.AudioFiles.WaitingToneFile));
|
_waitingToneReader = new AudioFileReader(_config.AudioFiles.GetFullPath(_config.AudioFiles.WaitingToneFile));
|
||||||
_waitingToneDevice.Init(_waitingToneReader);
|
_waitingToneDevice.Init(_waitingToneReader);
|
||||||
|
// 设置待机嘟声音量,确保在0.0-1.0范围内
|
||||||
Console.WriteLine("音频设备初始化成功");
|
_waitingToneDevice.Volume = Math.Clamp(_config.AudioFiles.WaitingToneVolume > 0 ? _config.AudioFiles.WaitingToneVolume : 1.0f, 0.0f, 1.0f);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"初始化音频设备失败: {ex.Message}");
|
Console.WriteLine($"初始化音频设备失败: {ex.Message}");
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,6 +341,28 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
|
|
||||||
_isBackgroundMusicPlaying = false;
|
_isBackgroundMusicPlaying = false;
|
||||||
|
|
||||||
|
// 释放风铃提示音设备
|
||||||
|
_windChimeCts?.Cancel();
|
||||||
|
_windChimeCts?.Dispose();
|
||||||
|
_windChimeCts = null;
|
||||||
|
|
||||||
|
if (_windChimeDevice != null)
|
||||||
|
{
|
||||||
|
_windChimeDevice.Stop();
|
||||||
|
_windChimeDevice.Dispose();
|
||||||
|
_windChimeDevice = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_windChimeReader != null)
|
||||||
|
{
|
||||||
|
_windChimeReader.Dispose();
|
||||||
|
_windChimeReader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isWindChimePlaying = false;
|
||||||
|
_windChimeTimer?.Dispose();
|
||||||
|
_windChimeTimer = null;
|
||||||
|
|
||||||
foreach (var (device, reader) in _keyToneDevices.Values)
|
foreach (var (device, reader) in _keyToneDevices.Values)
|
||||||
{
|
{
|
||||||
device.Stop();
|
device.Stop();
|
||||||
|
|
@ -552,14 +581,18 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
|
|
||||||
// 启动静音检测计时器
|
// 启动静音检测计时器
|
||||||
_lastSoundTime = DateTime.Now;
|
_lastSoundTime = DateTime.Now;
|
||||||
|
_lastSpeechTime = DateTime.Now; // 初始化最后说话时间
|
||||||
_silenceTimer = new Timer(CheckSilence, recordingCompletionSource, 1000, 1000);
|
_silenceTimer = new Timer(CheckSilence, recordingCompletionSource, 1000, 1000);
|
||||||
|
|
||||||
|
// 启动风铃提示计时器
|
||||||
|
_windChimeTimer = new Timer(CheckWindChimePrompt, null, 1000, 1000);
|
||||||
|
|
||||||
Console.WriteLine("播放滴提示音...");
|
Console.WriteLine("播放滴提示音...");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await PlayAudioAndWait(
|
await PlayAudioAndWait(
|
||||||
_config.AudioFiles.GetFullPath(_config.AudioFiles.BeepPromptFile),
|
_config.AudioFiles.GetFullPath(_config.AudioFiles.BeepPromptFile),
|
||||||
null, false, recordingCts.Token, 0.1f);
|
null, false, recordingCts.Token);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|
@ -838,6 +871,14 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
_silenceTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
_silenceTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||||
_silenceTimer?.Dispose();
|
_silenceTimer?.Dispose();
|
||||||
_silenceTimer = null;
|
_silenceTimer = null;
|
||||||
|
|
||||||
|
// 停止风铃提示音计时器
|
||||||
|
_windChimeTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||||
|
_windChimeTimer?.Dispose();
|
||||||
|
_windChimeTimer = null;
|
||||||
|
|
||||||
|
// 停止风铃提示音
|
||||||
|
_ = StopWindChime(false);
|
||||||
|
|
||||||
// 停止录音
|
// 停止录音
|
||||||
if (_waveIn != null)
|
if (_waveIn != null)
|
||||||
|
|
@ -909,6 +950,19 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
_lasHasoudDateTime = DateTime.Now;
|
_lasHasoudDateTime = DateTime.Now;
|
||||||
_isSpeaking = false;
|
_isSpeaking = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果监测到说话,更新最后说话时间
|
||||||
|
if (isSpeaking)
|
||||||
|
{
|
||||||
|
_lastSpeechTime = DateTime.Now;
|
||||||
|
|
||||||
|
// 如果风铃声正在播放,平滑停止它
|
||||||
|
if (_isWindChimePlaying)
|
||||||
|
{
|
||||||
|
_ = StopWindChime(true); // 平滑淡出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果最大音量超过阈值,则认为有声音
|
// 如果最大音量超过阈值,则认为有声音
|
||||||
return isSpeaking;
|
return isSpeaking;
|
||||||
}
|
}
|
||||||
|
|
@ -925,6 +979,9 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
|
|
||||||
// 确保背景音乐被停止
|
// 确保背景音乐被停止
|
||||||
StopBackgroundMusic();
|
StopBackgroundMusic();
|
||||||
|
|
||||||
|
// 确保风铃提示音被停止
|
||||||
|
_ = StopWindChime(false);
|
||||||
|
|
||||||
// 清除录音状态
|
// 清除录音状态
|
||||||
_isRecording = false;
|
_isRecording = false;
|
||||||
|
|
@ -1238,6 +1295,8 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
var device = new WaveOutEvent();
|
var device = new WaveOutEvent();
|
||||||
var reader = new AudioFileReader(_config.AudioFiles.GetDigitToneFilePath(digit));
|
var reader = new AudioFileReader(_config.AudioFiles.GetDigitToneFilePath(digit));
|
||||||
device.Init(reader);
|
device.Init(reader);
|
||||||
|
// 设置按键音量,确保在0.0-1.0范围内
|
||||||
|
device.Volume = Math.Clamp(_config.AudioFiles.KeyToneVolume > 0 ? _config.AudioFiles.KeyToneVolume : 1.0f, 0.0f, 1.0f);
|
||||||
|
|
||||||
// 记录开始播放时间
|
// 记录开始播放时间
|
||||||
var startTime = DateTime.Now;
|
var startTime = DateTime.Now;
|
||||||
|
|
@ -1245,6 +1304,7 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
|
|
||||||
if (_keyToneDevices.TryAdd(digit, (device, reader)))
|
if (_keyToneDevices.TryAdd(digit, (device, reader)))
|
||||||
{
|
{
|
||||||
|
|
||||||
device.Play();
|
device.Play();
|
||||||
|
|
||||||
// 确保至少播放最小时长
|
// 确保至少播放最小时长
|
||||||
|
|
@ -1301,17 +1361,44 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
{
|
{
|
||||||
device = new WaveOutEvent();
|
device = new WaveOutEvent();
|
||||||
reader = new AudioFileReader(audioPath);
|
reader = new AudioFileReader(audioPath);
|
||||||
//device.Volume
|
|
||||||
if (volume != null)
|
// 如果没有指定音量,根据音频文件类型设置音量
|
||||||
|
if (volume == null)
|
||||||
{
|
{
|
||||||
device.Volume = volume.Value;
|
// 根据文件名判断音频类型并设置对应的音量
|
||||||
|
if (audioPath.Contains(_config.AudioFiles.WaitForPickupFile))
|
||||||
|
{
|
||||||
|
// 拨出嘟声音量
|
||||||
|
device.Volume = Math.Clamp(_config.AudioFiles.DialToneVolume > 0 ? _config.AudioFiles.DialToneVolume : 1.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
else if (audioPath.Contains(_config.AudioFiles.PromptUserRecordFile))
|
||||||
|
{
|
||||||
|
// 提示用户录音的音量
|
||||||
|
device.Volume = Math.Clamp(_config.AudioFiles.PromptAfterBeepVolume > 0 ? _config.AudioFiles.PromptAfterBeepVolume : 1.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
else if (audioPath.Contains(_config.AudioFiles.BeepPromptFile))
|
||||||
|
{
|
||||||
|
// 滴提示音的音量
|
||||||
|
device.Volume = Math.Clamp(_config.AudioFiles.PromptAfterBeepVolume > 0 ? _config.AudioFiles.PromptAfterBeepVolume : 1.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
else if (audioPath.Contains(_config.AudioFiles.WaitingToneFile))
|
||||||
|
{
|
||||||
|
// 待机嘟声音量 (虽然这个可能不会通过这个方法播放)
|
||||||
|
device.Volume = Math.Clamp(_config.AudioFiles.WaitingToneVolume > 0 ? _config.AudioFiles.WaitingToneVolume : 1.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 默认音量为1.0
|
||||||
|
device.Volume = 1.0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
device.Volume = Math.Clamp(volume.Value, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
device.Init(reader);
|
device.Init(reader);
|
||||||
//device.Volume
|
|
||||||
if (volume != null)
|
|
||||||
{
|
|
||||||
device.Volume = volume.Value;
|
|
||||||
}
|
|
||||||
// 如果需要循环播放,创建一个循环播放的任务
|
// 如果需要循环播放,创建一个循环播放的任务
|
||||||
if (loop && maxDuration.HasValue)
|
if (loop && maxDuration.HasValue)
|
||||||
{
|
{
|
||||||
|
|
@ -1518,4 +1605,182 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||||||
Console.WriteLine($"停止背景音乐失败: {ex.Message}");
|
Console.WriteLine($"停止背景音乐失败: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查是否需要播放风铃提示音
|
||||||
|
/// </summary>
|
||||||
|
private void CheckWindChimePrompt(object? state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 如果用户已挂断或者录音已停止,不执行操作
|
||||||
|
if (_isHangUpKeyPressed || !_isRecording)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否超过配置的无声音时间但还未达到挂断时间
|
||||||
|
float windChimePromptSeconds = _config.Recording.WindChimePromptSeconds;
|
||||||
|
int silenceTimeoutSeconds = _config.Recording.SilenceTimeoutSeconds;
|
||||||
|
double secondsSinceLastSpeech = (DateTime.Now - _lastSpeechTime).TotalSeconds;
|
||||||
|
|
||||||
|
// 如果超过风铃提示时间但未达到挂断时间,并且风铃没在播放,开始播放风铃
|
||||||
|
if (secondsSinceLastSpeech >= windChimePromptSeconds &&
|
||||||
|
secondsSinceLastSpeech < silenceTimeoutSeconds &&
|
||||||
|
!_isWindChimePlaying)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"检测到{windChimePromptSeconds}秒无声音,播放风铃提示音...");
|
||||||
|
_ = PlayWindChime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"检查风铃提示音错误: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 播放风铃提示音
|
||||||
|
/// </summary>
|
||||||
|
private async Task PlayWindChime()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 如果已经在播放,先停止
|
||||||
|
if (_isWindChimePlaying)
|
||||||
|
{
|
||||||
|
await StopWindChime(true); // 平滑淡出
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止重复启动
|
||||||
|
if (_isWindChimePlaying)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_windChimeCts?.Cancel();
|
||||||
|
_windChimeCts?.Dispose();
|
||||||
|
_windChimeCts = CancellationTokenSource.CreateLinkedTokenSource(_programCts.Token);
|
||||||
|
var token = _windChimeCts.Token;
|
||||||
|
|
||||||
|
string windChimeFilePath = _config.AudioFiles.GetFullPath(_config.AudioFiles.WindChimeFile);
|
||||||
|
if (!File.Exists(windChimeFilePath))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"风铃提示音文件不存在: {windChimeFilePath}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化播放设备
|
||||||
|
_windChimeDevice = new WaveOutEvent();
|
||||||
|
_windChimeReader = new AudioFileReader(windChimeFilePath);
|
||||||
|
_windChimeDevice.Init(_windChimeReader);
|
||||||
|
_windChimeDevice.Volume = Math.Clamp(1.0f, 0.0f, 1.0f); // 初始音量设为最大
|
||||||
|
|
||||||
|
_isWindChimePlaying = true;
|
||||||
|
Console.WriteLine("开始播放风铃提示音");
|
||||||
|
|
||||||
|
// 开始播放
|
||||||
|
_windChimeDevice.Play();
|
||||||
|
|
||||||
|
// 单独开启一个任务检测是否有声音,如有则淡出停止
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (_isWindChimePlaying && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// 如果检测到有声音,淡出停止
|
||||||
|
if ((DateTime.Now - _lastSpeechTime).TotalSeconds < 1.0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("检测到用户说话,风铃提示音淡出...");
|
||||||
|
await StopWindChime(true); // 平滑淡出
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果到达音频文件末尾,循环播放
|
||||||
|
if (_windChimeReader != null && _windChimeReader.Position >= _windChimeReader.Length)
|
||||||
|
{
|
||||||
|
_windChimeReader.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(100, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 正常取消
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"风铃提示音监控错误: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"播放风铃提示音错误: {ex.Message}");
|
||||||
|
await StopWindChime(false); // 出错时直接停止,不淡出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 停止风铃提示音
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fadeOut">是否平滑淡出</param>
|
||||||
|
private async Task StopWindChime(bool fadeOut)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_isWindChimePlaying)
|
||||||
|
{
|
||||||
|
return; // 如果没有播放,不需要停止
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要淡出
|
||||||
|
if (fadeOut && _windChimeDevice != null)
|
||||||
|
{
|
||||||
|
int fadeOutMs = _config.Recording.WindChimeFadeOutMs;
|
||||||
|
float startVolume = _windChimeDevice.Volume;
|
||||||
|
int steps = 20; // 淡出的步骤数
|
||||||
|
int stepDelay = fadeOutMs / steps;
|
||||||
|
|
||||||
|
// 逐步降低音量实现淡出效果
|
||||||
|
for (int i = 0; i < steps; i++)
|
||||||
|
{
|
||||||
|
if (_windChimeDevice == null || !_isWindChimePlaying)
|
||||||
|
{
|
||||||
|
break; // 如果设备已释放或播放已停止,直接退出
|
||||||
|
}
|
||||||
|
|
||||||
|
float ratio = 1.0f - ((float)i / steps);
|
||||||
|
_windChimeDevice.Volume = startVolume * ratio;
|
||||||
|
await Task.Delay(stepDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正式停止
|
||||||
|
_windChimeCts?.Cancel();
|
||||||
|
|
||||||
|
if (_windChimeDevice != null)
|
||||||
|
{
|
||||||
|
_windChimeDevice.Stop();
|
||||||
|
_windChimeDevice.Dispose();
|
||||||
|
_windChimeDevice = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_windChimeReader != null)
|
||||||
|
{
|
||||||
|
_windChimeReader.Dispose();
|
||||||
|
_windChimeReader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isWindChimePlaying = false;
|
||||||
|
Console.WriteLine("风铃提示音已停止");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"停止风铃提示音错误: {ex.Message}");
|
||||||
|
_isWindChimePlaying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"SignalRHubUrl": "http://localhost:81/audiohub",
|
"SignalRHubUrl": "http://115.159.44.16/audiohub",
|
||||||
"ConfigBackupPath": "config.json",
|
"ConfigBackupPath": "config.json",
|
||||||
"AutoConnectToServer": true,
|
"AutoConnectToServer": true,
|
||||||
"AllowOfflineStart": false
|
"AllowOfflineStart": false
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@
|
||||||
"PromptUserRecordFile": "\u63D0\u793A\u7528\u6237\u5F55\u97F3.mp3",
|
"PromptUserRecordFile": "\u63D0\u793A\u7528\u6237\u5F55\u97F3.mp3",
|
||||||
"BeepPromptFile": "\u6EF4\u63D0\u793A\u97F3.wav",
|
"BeepPromptFile": "\u6EF4\u63D0\u793A\u97F3.wav",
|
||||||
"DigitToneFileTemplate": "{0}.mp3",
|
"DigitToneFileTemplate": "{0}.mp3",
|
||||||
"MinKeyTonePlayTimeMs": 200
|
"MinKeyTonePlayTimeMs": 200,
|
||||||
|
"KeyToneVolume": 1.0,
|
||||||
|
"WaitingToneVolume": 1.0,
|
||||||
|
"DialToneVolume": 0.9,
|
||||||
|
"PromptAfterBeepVolume": 1.0,
|
||||||
|
"WindChimeFile": "\u98CE\u94C3.wav"
|
||||||
},
|
},
|
||||||
"Dial": {
|
"Dial": {
|
||||||
"MinDigitsToDialOut": 8,
|
"MinDigitsToDialOut": 8,
|
||||||
|
|
@ -20,13 +25,15 @@
|
||||||
"SampleRate": 16000,
|
"SampleRate": 16000,
|
||||||
"Channels": 1,
|
"Channels": 1,
|
||||||
"BufferMilliseconds": 50,
|
"BufferMilliseconds": 50,
|
||||||
"SilenceThreshold": 0.15,
|
"SilenceThreshold": 0.1,
|
||||||
"SilenceTimeoutSeconds": 30,
|
"SilenceTimeoutSeconds": 15,
|
||||||
"AllowUserHangup": true,
|
"AllowUserHangup": true,
|
||||||
"UploadRecordingToServer": false,
|
"UploadRecordingToServer": false,
|
||||||
"EnableBackgroundMusic": true,
|
"EnableBackgroundMusic": true,
|
||||||
"BackgroundMusicFile": "bj.mp3",
|
"BackgroundMusicFile": "bj.mp3",
|
||||||
"BackgroundMusicVolume": 0.1
|
"BackgroundMusicVolume": 0.3,
|
||||||
|
"WindChimePromptSeconds": 7.5,
|
||||||
|
"WindChimeFadeOutMs": 3000
|
||||||
},
|
},
|
||||||
"CallFlow": {
|
"CallFlow": {
|
||||||
"WaitForPickupMinSeconds": 3,
|
"WaitForPickupMinSeconds": 3,
|
||||||
|
|
|
||||||
|
|
@ -137,10 +137,13 @@
|
||||||
},
|
},
|
||||||
// 右侧容器配置
|
// 右侧容器配置
|
||||||
rightContainer: {
|
rightContainer: {
|
||||||
fontSize: '40px', // 右侧文字大小
|
fontSize: '40px', // 右侧文字大小
|
||||||
fontWeight: '700', // 右侧文字粗细
|
fontWeight: '700', // 右侧文字粗细
|
||||||
fontStyle: 'italic', // 右侧文字样式
|
fontStyle: 'italic', // 右侧文字样式
|
||||||
typewriterSpeed: 2000 // 右侧文字打字机速度(毫秒),增加到2000毫秒
|
typewriterSpeed: 2000, // 右侧文字打字机速度(毫秒),增加到2000毫秒
|
||||||
|
fadeChars: 4, // 渐变字符数量,值越大渐变效果越长
|
||||||
|
fadeStepTime: 500, // 字符透明度过渡时间(毫秒)
|
||||||
|
fadeDelayFactor: 0.25 // 每个字符的透明度递减因子(0-1)
|
||||||
},
|
},
|
||||||
// 水波纹效果配置
|
// 水波纹效果配置
|
||||||
waterEffect: {
|
waterEffect: {
|
||||||
|
|
@ -199,34 +202,92 @@
|
||||||
if (isAnimating) return;
|
if (isAnimating) return;
|
||||||
isAnimating = true;
|
isAnimating = true;
|
||||||
|
|
||||||
var i = 0;
|
// 获取渐变相关配置
|
||||||
$(selector).html('').css('opacity', 1);
|
const fadeChars = CONFIG.rightContainer.fadeChars || 4; // 渐变字符数量
|
||||||
|
const fadeStepTime = CONFIG.rightContainer.fadeStepTime || 500; // 过渡时间
|
||||||
|
const fadeDelayFactor = CONFIG.rightContainer.fadeDelayFactor || 0.25; // 透明度递减因子
|
||||||
|
|
||||||
function typeWriter() {
|
// 清空容器并设置初始可见度
|
||||||
if (i < text.length) {
|
$(selector).html('').css('opacity', 1);
|
||||||
// 创建一个新的 span 元素包含当前字符
|
|
||||||
const charSpan = $('<span></span>').text(text.charAt(i));
|
// 创建所有字符元素,但初始透明度为0
|
||||||
charSpan.css('opacity', 0); // 初始透明度为0
|
for (let i = 0; i < text.length; i++) {
|
||||||
$(selector).append(charSpan); // 添加到容器
|
const charSpan = $('<span></span>').text(text.charAt(i));
|
||||||
|
charSpan.css({
|
||||||
// 对这个字符应用淡入效果
|
'opacity': 0,
|
||||||
charSpan.animate({opacity: 1}, speed * 0.8, function() {
|
'display': 'inline-block', // 确保每个字符是独立的块
|
||||||
// 淡入完成后继续下一个字符
|
'transition': `opacity ${fadeStepTime}ms ease-in-out` // 平滑的透明度过渡效果
|
||||||
i++;
|
});
|
||||||
setTimeout(typeWriter, speed * 0.2);
|
$(selector).append(charSpan);
|
||||||
});
|
|
||||||
} else {
|
|
||||||
isAnimating = false;
|
|
||||||
if (callback) callback();
|
|
||||||
|
|
||||||
// 打字效果完成后,等待随机4-7秒,然后请求下一条文本
|
|
||||||
const delay = Math.floor(Math.random() * 3000) + 4000; // 4000-7000毫秒
|
|
||||||
console.log(`文本显示完成,将在${delay/1000}秒后请求下一条文本`);
|
|
||||||
|
|
||||||
setTimeout(requestNextText, delay);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
typeWriter();
|
|
||||||
|
// 获取所有字符span元素
|
||||||
|
const chars = $(selector).find('span');
|
||||||
|
const totalChars = chars.length;
|
||||||
|
|
||||||
|
// 设置初始不透明度,形成从左到右的渐变效果
|
||||||
|
// 左侧字符较深,右侧字符较浅
|
||||||
|
const setInitialOpacity = () => {
|
||||||
|
for (let i = 0; i < totalChars; i++) {
|
||||||
|
// 计算不透明度:第一个字符不透明度最高,往后逐渐降低
|
||||||
|
let position = i / totalChars; // 字符在文本中的相对位置 (0-1)
|
||||||
|
let opacity = Math.max(0, 0.8 - (position * fadeChars * 0.2));
|
||||||
|
|
||||||
|
// 超出可见范围的字符完全透明
|
||||||
|
if (i >= fadeChars) {
|
||||||
|
opacity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置不透明度
|
||||||
|
$(chars[i]).css('opacity', opacity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行渐变动画
|
||||||
|
const animateText = () => {
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
const step = () => {
|
||||||
|
if (progress >= 1) {
|
||||||
|
// 完成动画
|
||||||
|
chars.css('opacity', 1);
|
||||||
|
isAnimating = false;
|
||||||
|
if (callback) callback();
|
||||||
|
|
||||||
|
// 等待随机4-7秒后请求下一条文本
|
||||||
|
const delay = Math.floor(Math.random() * 3000) + 4000; // 4000-7000毫秒
|
||||||
|
console.log(`文本显示完成,将在${delay/1000}秒后请求下一条文本`);
|
||||||
|
setTimeout(requestNextText, delay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新每个字符的透明度
|
||||||
|
for (let i = 0; i < totalChars; i++) {
|
||||||
|
let targetOpacity = 1; // 目标不透明度(完全显示)
|
||||||
|
let currentPosition = i / totalChars; // 字符的相对位置
|
||||||
|
|
||||||
|
// 计算当前动画进度下该位置的字符应有的透明度
|
||||||
|
// 小于当前进度的字符应该更不透明
|
||||||
|
let opacity = targetOpacity * Math.max(0, 1 - Math.max(0, (currentPosition - progress) * 3));
|
||||||
|
|
||||||
|
// 设置不透明度
|
||||||
|
$(chars[i]).css('opacity', opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加进度
|
||||||
|
progress += 0.02;
|
||||||
|
|
||||||
|
// 继续下一步动画
|
||||||
|
setTimeout(step, speed / 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始动画
|
||||||
|
step();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置初始不透明度并开始动画
|
||||||
|
setInitialOpacity();
|
||||||
|
setTimeout(animateText, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -422,6 +483,17 @@
|
||||||
CONFIG.rightContainer.fontStyle = newConfig.rightContainer.fontStyle || CONFIG.rightContainer.fontStyle;
|
CONFIG.rightContainer.fontStyle = newConfig.rightContainer.fontStyle || CONFIG.rightContainer.fontStyle;
|
||||||
CONFIG.rightContainer.typewriterSpeed = newConfig.rightContainer.typewriterSpeed || CONFIG.rightContainer.typewriterSpeed;
|
CONFIG.rightContainer.typewriterSpeed = newConfig.rightContainer.typewriterSpeed || CONFIG.rightContainer.typewriterSpeed;
|
||||||
|
|
||||||
|
// 更新渐显效果相关的配置
|
||||||
|
if (newConfig.rightContainer.fadeChars !== undefined) {
|
||||||
|
CONFIG.rightContainer.fadeChars = newConfig.rightContainer.fadeChars;
|
||||||
|
}
|
||||||
|
if (newConfig.rightContainer.fadeStepTime !== undefined) {
|
||||||
|
CONFIG.rightContainer.fadeStepTime = newConfig.rightContainer.fadeStepTime;
|
||||||
|
}
|
||||||
|
if (newConfig.rightContainer.fadeDelayFactor !== undefined) {
|
||||||
|
CONFIG.rightContainer.fadeDelayFactor = newConfig.rightContainer.fadeDelayFactor;
|
||||||
|
}
|
||||||
|
|
||||||
// 应用到CSS
|
// 应用到CSS
|
||||||
document.documentElement.style.setProperty('--right-font-size', CONFIG.rightContainer.fontSize);
|
document.documentElement.style.setProperty('--right-font-size', CONFIG.rightContainer.fontSize);
|
||||||
document.documentElement.style.setProperty('--right-font-weight', CONFIG.rightContainer.fontWeight);
|
document.documentElement.style.setProperty('--right-font-weight', CONFIG.rightContainer.fontWeight);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@
|
||||||
"fontSize": "40px",
|
"fontSize": "40px",
|
||||||
"fontWeight": "bolder",
|
"fontWeight": "bolder",
|
||||||
"fontStyle": "italic",
|
"fontStyle": "italic",
|
||||||
"typewriterSpeed": 500
|
"typewriterSpeed": 6000,
|
||||||
|
"fadeChars": 1,
|
||||||
|
"fadeStepTime": 1000,
|
||||||
|
"fadeDelayFactor": 0.20
|
||||||
},
|
},
|
||||||
"waterEffect": {
|
"waterEffect": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user