1786 lines
60 KiB
C#
1786 lines
60 KiB
C#
using NAudio.Wave;
|
||
using System.Collections.Concurrent;
|
||
using System.Runtime.InteropServices;
|
||
using ShengShengBuXi.ConsoleApp.Models;
|
||
using System.Linq;
|
||
using System.Transactions;
|
||
|
||
namespace ShengShengBuXi.ConsoleApp.Services;
|
||
|
||
/// <summary>
|
||
/// 电话亭服务实现
|
||
/// </summary>
|
||
public class PhoneBoothService : IPhoneBoothService, IDisposable
|
||
{
|
||
// 依赖的服务
|
||
private readonly ISignalRService _signalRService;
|
||
private readonly IAudioFileService _audioFileService;
|
||
|
||
// 配置
|
||
private PhoneBoothConfig _config;
|
||
|
||
// 音频设备相关
|
||
private readonly ConcurrentDictionary<int, (WaveOutEvent Device, AudioFileReader Reader)> _keyToneDevices = new();
|
||
private WaveOutEvent? _waitingToneDevice;
|
||
private AudioFileReader? _waitingToneReader;
|
||
|
||
// 状态相关
|
||
private volatile bool _isRecording = false;
|
||
private DateTime _lastKeyPressTime = DateTime.MinValue;
|
||
private readonly List<int> _pressedKeys = new();
|
||
private volatile bool _isWaitingTonePlaying = false;
|
||
private CancellationTokenSource? _waitingToneCts;
|
||
private readonly CancellationTokenSource _programCts = new();
|
||
private readonly HashSet<int> _currentPressedKeys = new();
|
||
private Timer? _resetTimer;
|
||
private Timer? _dialOutTimer;
|
||
private readonly object _timerLock = new();
|
||
|
||
// Windows API
|
||
private const int WH_KEYBOARD_LL = 13;
|
||
private const int WM_KEYDOWN = 0x0100;
|
||
private const int WM_KEYUP = 0x0101;
|
||
private IntPtr _hookHandle = IntPtr.Zero;
|
||
private HookProc? _hookProc;
|
||
|
||
// 数字键盘的虚拟键码 (VK_NUMPAD0 - VK_NUMPAD9)
|
||
// 数字键盘的虚拟键码 (VK_NUMPAD0 - VK_NUMPAD9) + VK_DECIMAL + VK_MULTIPLY
|
||
private readonly int[] _numpadKeys = Enumerable.Range(0x60, 10) // 0x60~0x69 (NumPad 0~9)
|
||
.Concat(new[] { VK_DECIMAL, VK_MULTIPLY }) // 追加 VK_DECIMAL (.) 和 VK_MULTIPLY (*)
|
||
.ToArray();
|
||
|
||
// 回车键的虚拟键码
|
||
private const int VK_RETURN = 0x0D;
|
||
//#
|
||
private const int VK_DECIMAL = 0x6E; // 0x6E (110) -> NumPad .
|
||
// *
|
||
private const int VK_MULTIPLY = 0x6A; // 0x6A (106) -> NumPad *
|
||
|
||
// 录音相关
|
||
private WaveInEvent? _waveIn = null;
|
||
private WaveFileWriter? _waveWriter = null;
|
||
private string _recordingFilePath = "";
|
||
private DateTime _lastSoundTime = DateTime.MinValue;
|
||
private Timer? _silenceTimer = null;
|
||
private volatile bool _isHangUpKeyPressed = false;
|
||
|
||
// 背景音乐相关
|
||
private WaveOutEvent? _backgroundMusicDevice = null;
|
||
private AudioFileReader? _backgroundMusicReader = null;
|
||
private CancellationTokenSource? _backgroundMusicCts = null;
|
||
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)]
|
||
private struct KBDLLHOOKSTRUCT
|
||
{
|
||
public uint vkCode;
|
||
public uint scanCode;
|
||
public uint flags;
|
||
public uint time;
|
||
public IntPtr dwExtraInfo;
|
||
}
|
||
|
||
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
|
||
|
||
[DllImport("kernel32.dll")]
|
||
private static extern IntPtr GetModuleHandle(string? lpModuleName);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern short GetAsyncKeyState(int vKey);
|
||
|
||
/// <summary>
|
||
/// 构造函数
|
||
/// </summary>
|
||
/// <param name="signalRService">SignalR服务</param>
|
||
/// <param name="audioFileService">音频文件服务</param>
|
||
public PhoneBoothService(ISignalRService signalRService, IAudioFileService audioFileService)
|
||
{
|
||
_signalRService = signalRService;
|
||
_audioFileService = audioFileService;
|
||
|
||
// 获取初始配置
|
||
_config = _signalRService.GetConfigAsync().GetAwaiter().GetResult();
|
||
|
||
// 注册事件处理
|
||
//_signalRService.ConfigUpdated += SignalRService_ConfigUpdated;
|
||
//_signalRService.AudioFileUpdated += SignalRService_AudioFileUpdated;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理配置更新事件
|
||
/// </summary>
|
||
private void SignalRService_ConfigUpdated(object? sender, PhoneBoothConfig e)
|
||
{
|
||
ReloadConfigAsync(e).ConfigureAwait(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理音频文件更新事件
|
||
/// </summary>
|
||
private void SignalRService_AudioFileUpdated(object? sender, (string FileName, byte[] FileData) e)
|
||
{
|
||
_audioFileService.UpdateAudioFileAsync(e.FileName, e.FileData).ConfigureAwait(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化服务
|
||
/// </summary>
|
||
public async Task InitializeAsync()
|
||
{
|
||
// 初始化定时器
|
||
_resetTimer = new Timer(CheckResetTimeout, null, Timeout.Infinite, Timeout.Infinite);
|
||
_dialOutTimer = new Timer(CheckDialOutTimeout, null, Timeout.Infinite, Timeout.Infinite);
|
||
|
||
// 确保音频目录存在
|
||
_audioFileService.EnsureAudioDirectoryExists(_config.AudioFiles.AudioBasePath);
|
||
|
||
// 初始化音频设备
|
||
InitializeAudioDevice();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动服务
|
||
/// </summary>
|
||
public async Task StartAsync()
|
||
{
|
||
try
|
||
{
|
||
Console.WriteLine("电话亭服务启动中...");
|
||
|
||
// 启动键盘监听
|
||
_ = StartKeyboardListener();
|
||
|
||
// 开始播放等待音
|
||
await StartPlayingWaitingTone();
|
||
|
||
Console.WriteLine("电话亭服务已启动");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"启动服务失败: {ex.Message}");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止服务
|
||
/// </summary>
|
||
public async Task StopAsync()
|
||
{
|
||
try
|
||
{
|
||
Console.WriteLine("正在停止电话亭服务...");
|
||
|
||
// 取消所有操作
|
||
_programCts.Cancel();
|
||
|
||
// 停止等待音
|
||
if (_isWaitingTonePlaying)
|
||
{
|
||
_waitingToneCts?.Cancel();
|
||
await Task.Delay(100); // 给一点时间让等待音停止
|
||
}
|
||
|
||
// 停止背景音乐
|
||
if (_isBackgroundMusicPlaying)
|
||
{
|
||
StopBackgroundMusic();
|
||
}
|
||
|
||
// 停止录音
|
||
if (_isRecording)
|
||
{
|
||
StopAudioRecording();
|
||
}
|
||
|
||
// 卸载键盘钩子
|
||
if (_hookHandle != IntPtr.Zero)
|
||
{
|
||
UnhookWindowsHookEx(_hookHandle);
|
||
_hookHandle = IntPtr.Zero;
|
||
}
|
||
|
||
// 释放音频设备
|
||
DisposeAudioDevices();
|
||
|
||
// 停止和释放定时器
|
||
_resetTimer?.Dispose();
|
||
_resetTimer = null;
|
||
|
||
_dialOutTimer?.Dispose();
|
||
_dialOutTimer = null;
|
||
|
||
_silenceTimer?.Dispose();
|
||
_silenceTimer = null;
|
||
|
||
Console.WriteLine("电话亭服务已停止");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"停止服务失败: {ex.Message}");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重新加载配置
|
||
/// </summary>
|
||
public async Task ReloadConfigAsync(PhoneBoothConfig config)
|
||
{
|
||
// 保存新配置
|
||
_config = config;
|
||
|
||
Console.WriteLine("已重新加载配置");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放资源
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
try
|
||
{
|
||
// 停止服务
|
||
StopAsync().GetAwaiter().GetResult();
|
||
|
||
// 取消订阅事件
|
||
if (_signalRService != null)
|
||
{
|
||
_signalRService.ConfigUpdated -= SignalRService_ConfigUpdated;
|
||
_signalRService.AudioFileUpdated -= SignalRService_AudioFileUpdated;
|
||
}
|
||
|
||
// 释放取消令牌源
|
||
_programCts.Dispose();
|
||
_waitingToneCts?.Dispose();
|
||
_backgroundMusicCts?.Dispose();
|
||
|
||
GC.SuppressFinalize(this);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"释放资源失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化音频设备
|
||
/// </summary>
|
||
private void InitializeAudioDevice()
|
||
{
|
||
try
|
||
{
|
||
_waitingToneDevice = new WaveOutEvent();
|
||
_waitingToneReader = new AudioFileReader(_config.AudioFiles.GetFullPath(_config.AudioFiles.WaitingToneFile));
|
||
_waitingToneDevice.Init(_waitingToneReader);
|
||
// 设置待机嘟声音量,确保在0.0-1.0范围内
|
||
_waitingToneDevice.Volume = Math.Clamp(_config.AudioFiles.WaitingToneVolume > 0 ? _config.AudioFiles.WaitingToneVolume : 1.0f, 0.0f, 1.0f);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"初始化音频设备失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放音频设备
|
||
/// </summary>
|
||
private void DisposeAudioDevices()
|
||
{
|
||
_waitingToneCts?.Cancel();
|
||
_waitingToneCts?.Dispose();
|
||
_waitingToneCts = null;
|
||
|
||
if (_waitingToneDevice != null)
|
||
{
|
||
_waitingToneDevice.Stop();
|
||
_waitingToneDevice.Dispose();
|
||
_waitingToneDevice = null;
|
||
}
|
||
|
||
if (_waitingToneReader != null)
|
||
{
|
||
_waitingToneReader.Dispose();
|
||
_waitingToneReader = null;
|
||
}
|
||
|
||
// 释放背景音乐设备
|
||
_backgroundMusicCts?.Cancel();
|
||
_backgroundMusicCts?.Dispose();
|
||
_backgroundMusicCts = null;
|
||
|
||
if (_backgroundMusicDevice != null)
|
||
{
|
||
_backgroundMusicDevice.Stop();
|
||
_backgroundMusicDevice.Dispose();
|
||
_backgroundMusicDevice = null;
|
||
}
|
||
|
||
if (_backgroundMusicReader != null)
|
||
{
|
||
_backgroundMusicReader.Dispose();
|
||
_backgroundMusicReader = null;
|
||
}
|
||
|
||
_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)
|
||
{
|
||
device.Stop();
|
||
device.Dispose();
|
||
reader.Dispose();
|
||
}
|
||
_keyToneDevices.Clear();
|
||
|
||
Console.WriteLine("音频设备已释放");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查重置超时
|
||
/// </summary>
|
||
private void CheckResetTimeout(object? state)
|
||
{
|
||
try
|
||
{
|
||
// 如果正在录音中,不执行重置操作
|
||
if (_isRecording)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_pressedKeys.Count > 0 && _pressedKeys.Count < _config.Dial.MinDigitsToDialOut &&
|
||
(DateTime.Now - _lastKeyPressTime).TotalSeconds >= _config.Dial.ResetTimeoutSeconds)
|
||
{
|
||
Console.WriteLine($"{_config.Dial.ResetTimeoutSeconds}秒内未完成{_config.Dial.MinDigitsToDialOut}位数字输入,重置等待...");
|
||
_pressedKeys.Clear();
|
||
_keyToneDevices.Clear();
|
||
_ = StartPlayingWaitingTone();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"检查超时重置失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查拨出超时
|
||
/// </summary>
|
||
private void CheckDialOutTimeout(object? state)
|
||
{
|
||
try
|
||
{
|
||
// 如果正在录音中,不执行拨出操作
|
||
if (_isRecording)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 检查是否已经过了配置的秒数且按键数大于等于配置的最小位数
|
||
int minDigits = _config.Dial.MinDigitsToDialOut;
|
||
int autoDialSeconds = _config.Dial.AutoDialOutAfterSeconds;
|
||
|
||
if (_pressedKeys.Count >= minDigits &&
|
||
(DateTime.Now - _lastKeyPressTime).TotalSeconds >= autoDialSeconds)
|
||
{
|
||
Console.WriteLine($"{autoDialSeconds}秒内无新按键,开始拨出...");
|
||
// 非阻塞方式启动录音
|
||
_ = StartRecording();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"检查拨出超时失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始录音流程
|
||
/// </summary>
|
||
private async Task StartRecording()
|
||
{
|
||
// 创建一个CancellationTokenSource用于取消整个录音流程
|
||
using var recordingCts = CancellationTokenSource.CreateLinkedTokenSource(_programCts.Token);
|
||
|
||
// 创建一个单独的任务来监视挂断信号
|
||
var hangupMonitorTask = Task.Run(async () =>
|
||
{
|
||
while (!recordingCts.Token.IsCancellationRequested)
|
||
{
|
||
// 检查是否收到挂断信号
|
||
if (_isHangUpKeyPressed)
|
||
{
|
||
Console.WriteLine("录音流程中检测到挂断信号,即将终止流程...");
|
||
recordingCts.Cancel();
|
||
break;
|
||
}
|
||
await Task.Delay(50);
|
||
}
|
||
});
|
||
|
||
try
|
||
{
|
||
// 进入录音状态,停止所有定时器
|
||
_isRecording = true;
|
||
_isHangUpKeyPressed = false;
|
||
lock (_timerLock)
|
||
{
|
||
_resetTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||
_dialOutTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||
}
|
||
|
||
// 确保等待音已停止
|
||
if (_isWaitingTonePlaying)
|
||
{
|
||
_waitingToneCts?.Cancel();
|
||
// 等待等待音实际停止
|
||
while (_isWaitingTonePlaying && !recordingCts.Token.IsCancellationRequested)
|
||
{
|
||
await Task.Delay(10);
|
||
}
|
||
}
|
||
|
||
// 如果已经收到取消信号,立即结束
|
||
if (recordingCts.Token.IsCancellationRequested)
|
||
{
|
||
throw new OperationCanceledException("录音流程被用户取消");
|
||
}
|
||
|
||
Console.WriteLine("开始录音流程...");
|
||
|
||
// 随机播放指定秒数的等待接电话音频
|
||
int waitMin = _config.CallFlow.WaitForPickupMinSeconds;
|
||
int waitMax = _config.CallFlow.WaitForPickupMaxSeconds;
|
||
int waitTime = new Random().Next(waitMin, waitMax + 1);
|
||
Console.WriteLine($"播放等待接电话音频,持续{waitTime}秒...");
|
||
|
||
try
|
||
{
|
||
await PlayAudioAndWait(
|
||
_config.AudioFiles.GetFullPath(_config.AudioFiles.WaitForPickupFile),
|
||
waitTime * 1000, true, recordingCts.Token);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
throw;
|
||
}
|
||
|
||
// 如果已经收到取消信号,立即结束
|
||
if (recordingCts.Token.IsCancellationRequested)
|
||
{
|
||
throw new OperationCanceledException("录音流程被用户取消");
|
||
}
|
||
|
||
// 根据配置的概率播放电话接起音频
|
||
bool playPickup = new Random().NextDouble() < _config.CallFlow.PlayPickupProbability;
|
||
if (playPickup)
|
||
{
|
||
Console.WriteLine("播放电话接起音频...");
|
||
try
|
||
{
|
||
await PlayAudioAndWait(
|
||
_config.AudioFiles.GetFullPath(_config.AudioFiles.PhonePickupFile),
|
||
null, false, recordingCts.Token);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
throw;
|
||
}
|
||
|
||
if (recordingCts.Token.IsCancellationRequested)
|
||
{
|
||
throw new OperationCanceledException("录音流程被用户取消");
|
||
}
|
||
}
|
||
|
||
// 播放提示用户录音的音频
|
||
Console.WriteLine("播放提示用户录音音频...");
|
||
try
|
||
{
|
||
await PlayAudioAndWait(
|
||
_config.AudioFiles.GetFullPath(_config.AudioFiles.PromptUserRecordFile),
|
||
null, false, recordingCts.Token);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
throw;
|
||
}
|
||
|
||
if (recordingCts.Token.IsCancellationRequested)
|
||
{
|
||
throw new OperationCanceledException("录音流程被用户取消");
|
||
}
|
||
|
||
// 开始实际录音逻辑
|
||
Console.WriteLine("开始初始化录音设备...");
|
||
|
||
// 创建录音文件路径
|
||
string recordingsFolder = Path.Combine(
|
||
AppDomain.CurrentDomain.BaseDirectory,
|
||
_config.Recording.RecordingFolder);
|
||
|
||
if (!Directory.Exists(recordingsFolder))
|
||
{
|
||
Directory.CreateDirectory(recordingsFolder);
|
||
}
|
||
|
||
_recordingFilePath = Path.Combine(
|
||
recordingsFolder,
|
||
$"recording_{DateTime.Now:yyyyMMdd_HHmmss}.wav");
|
||
|
||
Console.WriteLine("正在初始化录音设备...");
|
||
// 初始化录音设备
|
||
await StartAudioRecording(_recordingFilePath);
|
||
Console.WriteLine("正在录音中...");
|
||
if (recordingCts.Token.IsCancellationRequested)
|
||
{
|
||
throw new OperationCanceledException("录音流程被用户取消");
|
||
}
|
||
|
||
// 创建一个等待完成的任务源
|
||
var recordingCompletionSource = new TaskCompletionSource<bool>();
|
||
|
||
// 启动静音检测计时器
|
||
_lastSoundTime = DateTime.Now;
|
||
_lastSpeechTime = DateTime.Now; // 初始化最后说话时间
|
||
_silenceTimer = new Timer(CheckSilence, recordingCompletionSource, 1000, 1000);
|
||
|
||
// 启动风铃提示计时器
|
||
_windChimeTimer = new Timer(CheckWindChimePrompt, null, 1000, 1000);
|
||
|
||
Console.WriteLine("播放滴提示音...");
|
||
try
|
||
{
|
||
await PlayAudioAndWait(
|
||
_config.AudioFiles.GetFullPath(_config.AudioFiles.BeepPromptFile),
|
||
null, false, recordingCts.Token);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
throw;
|
||
}
|
||
|
||
if (recordingCts.Token.IsCancellationRequested)
|
||
{
|
||
throw new OperationCanceledException("录音流程被用户取消");
|
||
}
|
||
Console.WriteLine("提示音播放完成,等待用户说话结束...");
|
||
|
||
// 创建一个任务等待录音结束(通过静音检测、挂断按键或取消)
|
||
var recordingTask = Task.Run(async () =>
|
||
{
|
||
try
|
||
{
|
||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(recordingCts.Token);
|
||
|
||
// 注册取消处理程序
|
||
linkedCts.Token.Register(() =>
|
||
{
|
||
recordingCompletionSource.TrySetResult(true);
|
||
});
|
||
|
||
// 启动背景音乐播放(不等待它完成)
|
||
if (_config.Recording.EnableBackgroundMusic)
|
||
{
|
||
_ = StartPlayingBackgroundMusic(linkedCts.Token);
|
||
}
|
||
|
||
// 等待录音完成
|
||
await recordingCompletionSource.Task;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 捕获所有异常,确保不会中断主流程
|
||
Console.WriteLine($"录音任务异常: {ex.Message}");
|
||
}
|
||
});
|
||
|
||
// 等待录音完成或取消
|
||
try
|
||
{
|
||
await Task.WhenAny(recordingTask, Task.Delay(Timeout.Infinite, recordingCts.Token));
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// 预期的取消异常,可以忽略
|
||
}
|
||
|
||
// 停止录音
|
||
StopAudioRecording();
|
||
|
||
// 确保背景音乐停止
|
||
StopBackgroundMusic();
|
||
|
||
Console.WriteLine("录音结束,处理中...");
|
||
|
||
// 尝试上传录音文件到服务器
|
||
if (File.Exists(_recordingFilePath))
|
||
{
|
||
_ = Task.Run(async () =>
|
||
{
|
||
try
|
||
{
|
||
Console.WriteLine("正在上传录音文件到服务器...");
|
||
bool success = await _signalRService.UploadRecordingAsync(_recordingFilePath);
|
||
if (success)
|
||
{
|
||
Console.WriteLine("录音文件上传成功");
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine("录音文件上传失败");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"上传录音文件时发生错误: {ex.Message}");
|
||
}
|
||
});
|
||
}
|
||
|
||
// 检查是否由用户挂断或静音触发的结束
|
||
bool isUserHangup = _isHangUpKeyPressed || (DateTime.Now - _lastSoundTime).TotalSeconds >= _config.Recording.SilenceTimeoutSeconds;
|
||
|
||
// 根据配置决定是重置系统还是退出程序
|
||
if (isUserHangup && !_config.CallFlow.ResetSystemAfterHangup)
|
||
{
|
||
Console.WriteLine("用户挂断或静音超时,根据配置将退出程序...");
|
||
// 完全重置系统状态
|
||
//CompletelyResetState();
|
||
// 确保所有资源被释放
|
||
StopAsync().Wait();
|
||
|
||
// 退出程序
|
||
Environment.Exit(0);
|
||
}
|
||
else
|
||
{
|
||
// 完全重置系统状态
|
||
CompletelyResetState();
|
||
Console.WriteLine("系统已重置,可以继续使用...");
|
||
}
|
||
}
|
||
catch (OperationCanceledException ex)
|
||
{
|
||
Console.WriteLine($"录音流程被取消: {ex.Message}");
|
||
|
||
// 确保录音设备被释放
|
||
StopAudioRecording();
|
||
|
||
// 检查是否需要退出程序
|
||
if (_isHangUpKeyPressed && !_config.CallFlow.ResetSystemAfterHangup)
|
||
{
|
||
Console.WriteLine("用户挂断,根据配置将退出程序...");
|
||
|
||
// 确保所有资源被释放
|
||
StopAsync().Wait();
|
||
|
||
// 退出程序
|
||
Environment.Exit(0);
|
||
}
|
||
else
|
||
{
|
||
// 取消时也完全重置系统状态
|
||
CompletelyResetState();
|
||
Console.WriteLine("系统已重置,可以继续使用...");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"录音过程发生错误: {ex.Message}");
|
||
|
||
// 确保录音设备被释放
|
||
StopAudioRecording();
|
||
|
||
// 发生错误时也完全重置系统状态
|
||
CompletelyResetState();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查静音
|
||
/// </summary>
|
||
private void CheckSilence(object? state)
|
||
{
|
||
try
|
||
{
|
||
var completionSource = state as TaskCompletionSource<bool>;
|
||
|
||
// 如果用户按了回车键,且配置允许用户挂断,立即结束录音
|
||
if (_isHangUpKeyPressed && _config.Recording.AllowUserHangup)
|
||
{
|
||
Console.WriteLine("定时器检测到用户手动挂断");
|
||
completionSource?.TrySetResult(true);
|
||
return;
|
||
}
|
||
|
||
// 检查是否超过配置秒数没有声音
|
||
int silenceTimeout = _config.Recording.SilenceTimeoutSeconds;
|
||
if ((DateTime.Now - _lastSoundTime).TotalSeconds >= silenceTimeout)
|
||
{
|
||
Console.WriteLine($"检测到{silenceTimeout}秒无声音,自动挂断");
|
||
// 将静音超时也视为用户挂断的一种形式
|
||
_isHangUpKeyPressed = true;
|
||
completionSource?.TrySetResult(true);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"静音检测错误: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动音频录制
|
||
/// </summary>
|
||
private async Task StartAudioRecording(string filePath)
|
||
{
|
||
try
|
||
{
|
||
// 创建录音设备
|
||
_waveIn = new WaveInEvent
|
||
{
|
||
DeviceNumber = _config.Recording.RecordingDeviceNumber,
|
||
WaveFormat = new WaveFormat(
|
||
_config.Recording.SampleRate,
|
||
_config.Recording.Channels),
|
||
BufferMilliseconds = _config.Recording.BufferMilliseconds
|
||
};
|
||
|
||
// 创建文件写入器
|
||
_waveWriter = new WaveFileWriter(filePath, _waveIn.WaveFormat);
|
||
|
||
// 如果SignalR连接可用,通知服务器开始接收音频流
|
||
if (_signalRService.IsConnected)
|
||
{
|
||
await _signalRService.StartAudioStreamAsync(_waveIn.WaveFormat.SampleRate, _waveIn.WaveFormat.Channels);
|
||
}
|
||
|
||
// 处理录音数据
|
||
_waveIn.DataAvailable += (s, e) =>
|
||
{
|
||
try
|
||
{
|
||
// 将数据写入文件
|
||
_waveWriter.Write(e.Buffer, 0, e.BytesRecorded);
|
||
|
||
// 检查是否有声音(仅用于更新最后音频时间,不影响上传)
|
||
if (HasSound(e.Buffer, e.BytesRecorded))
|
||
{
|
||
_lastSoundTime = DateTime.Now;
|
||
}
|
||
|
||
// 实时上传音频数据到服务器(无论是否有声音都上传)
|
||
if (_signalRService.IsConnected)
|
||
{
|
||
// 创建音频数据的副本以避免并发问题
|
||
byte[] audioDataCopy = new byte[e.BytesRecorded];
|
||
Buffer.BlockCopy(e.Buffer, 0, audioDataCopy, 0, e.BytesRecorded);
|
||
|
||
// 非阻塞方式上传音频数据
|
||
_ = Task.Run(async () =>
|
||
{
|
||
try
|
||
{
|
||
await _signalRService.UploadAudioDataRealtimeAsync(audioDataCopy);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"实时音频数据上传出错: {ex.Message}");
|
||
}
|
||
});
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"录音数据处理错误: {ex.Message}");
|
||
}
|
||
};
|
||
|
||
// 录音完成事件
|
||
_waveIn.RecordingStopped += (s, e) =>
|
||
{
|
||
// 在这里处理录音停止后的逻辑
|
||
Console.WriteLine("录音已停止");
|
||
|
||
// 通知服务器结束音频流
|
||
if (_signalRService.IsConnected)
|
||
{
|
||
_ = _signalRService.EndAudioStreamAsync();
|
||
}
|
||
};
|
||
|
||
// 开始录音
|
||
_waveIn.StartRecording();
|
||
Console.WriteLine($"开始录音,保存到文件: {filePath}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"启动录音失败: {ex.Message}");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止音频录制
|
||
/// </summary>
|
||
private void StopAudioRecording()
|
||
{
|
||
try
|
||
{
|
||
// 停止静音检测计时器
|
||
_silenceTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||
_silenceTimer?.Dispose();
|
||
_silenceTimer = null;
|
||
|
||
// 停止风铃提示音计时器
|
||
_windChimeTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||
_windChimeTimer?.Dispose();
|
||
_windChimeTimer = null;
|
||
|
||
// 停止风铃提示音
|
||
_ = StopWindChime(false);
|
||
|
||
// 停止录音
|
||
if (_waveIn != null)
|
||
{
|
||
_waveIn.StopRecording();
|
||
_waveIn.Dispose();
|
||
_waveIn = null;
|
||
}
|
||
|
||
// 关闭文件
|
||
if (_waveWriter != null)
|
||
{
|
||
_waveWriter.Dispose();
|
||
_waveWriter = null;
|
||
|
||
Console.WriteLine($"录音已保存到: {_recordingFilePath}");
|
||
}
|
||
|
||
// 确保结束音频流
|
||
if (_signalRService.IsConnected)
|
||
{
|
||
_ = _signalRService.EndAudioStreamAsync();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"停止录音失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
public static DateTime _lasHasoudDateTime = DateTime.Now;
|
||
public static bool _isSpeaking = false;
|
||
|
||
/// <summary>
|
||
/// 检查是否有声音
|
||
/// </summary>
|
||
private bool HasSound(byte[] buffer, int bytesRecorded)
|
||
{
|
||
// 将字节数组转换为浮点数数组以计算音量
|
||
float maxVolume = 0;
|
||
|
||
// 对于16位PCM数据
|
||
for (int i = 0; i < bytesRecorded; i += 2)
|
||
{
|
||
if (i + 1 < bytesRecorded)
|
||
{
|
||
// 将两个字节转换为short(16位)
|
||
short sample = (short)((buffer[i + 1] << 8) | buffer[i]);
|
||
|
||
// 转换为-1.0到1.0范围内的浮点数
|
||
float normSample = sample / 32768.0f;
|
||
|
||
// 取绝对值并更新最大音量
|
||
float absSample = Math.Abs(normSample);
|
||
if (absSample > maxVolume)
|
||
{
|
||
maxVolume = absSample;
|
||
}
|
||
}
|
||
}
|
||
bool isSpeaking = maxVolume > _config.Recording.SilenceThreshold;
|
||
if (isSpeaking && !_isSpeaking)
|
||
{
|
||
_isSpeaking = true;
|
||
}
|
||
if (DateTime.Now.Subtract(_lasHasoudDateTime).TotalSeconds > 1)
|
||
{
|
||
Console.WriteLine($"当前音量:{maxVolume},设置的音量灵敏度:{_config.Recording.SilenceThreshold},是否已监测到在说话中:{_isSpeaking}");
|
||
_lasHasoudDateTime = DateTime.Now;
|
||
_isSpeaking = false;
|
||
}
|
||
|
||
// 如果监测到说话,更新最后说话时间
|
||
if (isSpeaking)
|
||
{
|
||
_lastSpeechTime = DateTime.Now;
|
||
|
||
// 如果风铃声正在播放,平滑停止它
|
||
if (_isWindChimePlaying)
|
||
{
|
||
_ = StopWindChime(true); // 平滑淡出
|
||
}
|
||
}
|
||
|
||
// 如果最大音量超过阈值,则认为有声音
|
||
return isSpeaking;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 完全重置状态
|
||
/// </summary>
|
||
private void CompletelyResetState()
|
||
{
|
||
try
|
||
{
|
||
// 确保录音设备被释放
|
||
StopAudioRecording();
|
||
|
||
// 确保背景音乐被停止
|
||
StopBackgroundMusic();
|
||
|
||
// 确保风铃提示音被停止
|
||
_ = StopWindChime(false);
|
||
|
||
// 清除录音状态
|
||
_isRecording = false;
|
||
_isHangUpKeyPressed = false;
|
||
|
||
// 清除按键记录
|
||
_pressedKeys.Clear();
|
||
_currentPressedKeys.Clear();
|
||
|
||
// 停止所有按键音
|
||
foreach (var (digit, deviceInfo) in _keyToneDevices.ToArray())
|
||
{
|
||
try
|
||
{
|
||
_keyToneDevices.TryRemove(digit, out _);
|
||
deviceInfo.Device.Stop();
|
||
deviceInfo.Device.Dispose();
|
||
deviceInfo.Reader.Dispose();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"清理按键音设备失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 重新开始播放等待音
|
||
_ = StartPlayingWaitingTone();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"重置状态失败: {ex.Message}");
|
||
|
||
// 最后的保障措施
|
||
_isRecording = false;
|
||
_isHangUpKeyPressed = false;
|
||
_pressedKeys.Clear();
|
||
_currentPressedKeys.Clear();
|
||
_keyToneDevices.Clear();
|
||
|
||
// 尝试停止背景音乐
|
||
try
|
||
{
|
||
StopBackgroundMusic();
|
||
}
|
||
catch
|
||
{
|
||
// 忽略任何错误
|
||
}
|
||
|
||
// 尝试重新开始播放等待音
|
||
try
|
||
{
|
||
_ = StartPlayingWaitingTone();
|
||
}
|
||
catch
|
||
{
|
||
// 忽略任何错误
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始播放等待音
|
||
/// </summary>
|
||
private async Task StartPlayingWaitingTone()
|
||
{
|
||
try
|
||
{
|
||
_waitingToneCts?.Cancel();
|
||
_waitingToneCts?.Dispose();
|
||
_waitingToneCts = CancellationTokenSource.CreateLinkedTokenSource(_programCts.Token);
|
||
|
||
var token = _waitingToneCts.Token;
|
||
_isWaitingTonePlaying = true;
|
||
|
||
while (!token.IsCancellationRequested)
|
||
{
|
||
try
|
||
{
|
||
if (_waitingToneReader?.Position >= _waitingToneReader?.Length)
|
||
{
|
||
_waitingToneReader.Position = 0;
|
||
}
|
||
_waitingToneDevice?.Play();
|
||
await Task.Delay(100, token);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"播放等待音失败: {ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
_isWaitingTonePlaying = false;
|
||
_waitingToneDevice?.Stop();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动键盘监听
|
||
/// </summary>
|
||
private async Task StartKeyboardListener()
|
||
{
|
||
while (!_programCts.Token.IsCancellationRequested)
|
||
{
|
||
try
|
||
{
|
||
// 检查回车键
|
||
bool isEnterPressed = (GetAsyncKeyState(VK_RETURN) & 0x8000) != 0;
|
||
if (isEnterPressed)
|
||
{
|
||
// 添加一点延迟,防止连续检测到按键
|
||
await Task.Delay(20, _programCts.Token);
|
||
|
||
// 再次检查回车键状态,确保不是误触或抖动
|
||
bool stillPressed = (GetAsyncKeyState(VK_RETURN) & 0x8000) != 0;
|
||
if (!stillPressed)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (_isRecording)
|
||
{
|
||
// 检查是否允许用户挂断
|
||
if (_config.Recording.AllowUserHangup)
|
||
{
|
||
// 如果正在录音中,回车键被视为挂断信号
|
||
Console.WriteLine("检测到回车键,用户挂断...");
|
||
_isHangUpKeyPressed = true;
|
||
|
||
// 等待一段时间,防止重复触发
|
||
await Task.Delay(200, _programCts.Token);
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine("当前配置不允许用户手动挂断");
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 如果等待音正在播放,停止它
|
||
if (_isWaitingTonePlaying)
|
||
{
|
||
_waitingToneCts?.Cancel();
|
||
// 确保等待音停止
|
||
while (_isWaitingTonePlaying)
|
||
{
|
||
await Task.Delay(10);
|
||
}
|
||
}
|
||
|
||
Console.WriteLine("检测到回车键,开始拨出...");
|
||
|
||
// 非阻塞方式启动录音,不等待其完成
|
||
_ = StartRecording();
|
||
|
||
// 延迟防止重复触发
|
||
await Task.Delay(500, _programCts.Token);
|
||
continue;
|
||
}
|
||
|
||
foreach (int key in _numpadKeys)
|
||
{
|
||
int digit = key - 0x60; // 将虚拟键码转换为数字
|
||
bool isKeyDown = (GetAsyncKeyState(key) & 0x8000) != 0;
|
||
|
||
if (isKeyDown)
|
||
{
|
||
if (_isRecording)
|
||
{
|
||
Console.WriteLine("正在录音中,无法点击按键...");
|
||
await Task.Delay(1000, _programCts.Token);
|
||
continue;
|
||
}
|
||
if (!_currentPressedKeys.Contains(digit))
|
||
{
|
||
Console.WriteLine($"按下数字键: {digit}");
|
||
_currentPressedKeys.Add(digit);
|
||
await HandleDigitKeyPress(digit);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (_currentPressedKeys.Contains(digit))
|
||
{
|
||
Console.WriteLine($"释放数字键: {digit}");
|
||
_currentPressedKeys.Remove(digit);
|
||
await HandleDigitKeyRelease(digit);
|
||
}
|
||
}
|
||
}
|
||
|
||
await Task.Delay(10, _programCts.Token); // 降低CPU使用率
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
break;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"键盘监听出错: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理数字按键按下
|
||
/// </summary>
|
||
private async Task HandleDigitKeyPress(int digit)
|
||
{
|
||
try
|
||
{
|
||
// 记录按键
|
||
_pressedKeys.Add(digit);
|
||
_lastKeyPressTime = DateTime.Now;
|
||
Console.WriteLine($"按下数字键: {digit}, 当前已按键数: {_pressedKeys.Count}");
|
||
|
||
// 如果是第一个按键,停止等待音
|
||
if (_pressedKeys.Count == 1 && _isWaitingTonePlaying)
|
||
{
|
||
_waitingToneCts?.Cancel();
|
||
}
|
||
|
||
// 播放按键音
|
||
await PlayKeyTone(digit);
|
||
|
||
// 重置定时器
|
||
lock (_timerLock)
|
||
{
|
||
_resetTimer?.Change(_config.Dial.ResetTimeoutSeconds * 1000, Timeout.Infinite);
|
||
}
|
||
|
||
// 检查是否需要拨出(达到配置的最小位数且指定秒数内无新按键)
|
||
int minDigits = _config.Dial.MinDigitsToDialOut;
|
||
if (_pressedKeys.Count >= minDigits)
|
||
{
|
||
// 重置拨出定时器,指定秒数后检查是否需要拨出
|
||
lock (_timerLock)
|
||
{
|
||
_dialOutTimer?.Change(_config.Dial.AutoDialOutAfterSeconds * 1000, Timeout.Infinite);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 如果位数不足最小位数,停止拨出定时器
|
||
lock (_timerLock)
|
||
{
|
||
_dialOutTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"处理按键 {digit} 失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理数字按键释放
|
||
/// </summary>
|
||
private async Task HandleDigitKeyRelease(int digit)
|
||
{
|
||
try
|
||
{
|
||
// 当按键释放时,我们不做特殊处理
|
||
// 按键音的播放和结束完全由PlayKeyTone中的逻辑控制
|
||
// 这样确保了即使按键时间很短,声音也能够完整播放
|
||
|
||
// 我们只需要确认按键已经从当前按下状态列表中移除即可
|
||
// 实际的音频资源释放由PlayKeyTone中的PlaybackStopped事件处理
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"处理按键 {digit} 释放失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放按键音
|
||
/// </summary>
|
||
private async Task PlayKeyTone(int digit)
|
||
{
|
||
try
|
||
{
|
||
// 如果该按键声音正在播放中,不再重新开始播放
|
||
if (_keyToneDevices.ContainsKey(digit))
|
||
{
|
||
// 如果已经在播放中,可以选择重新开始播放,或者保持当前播放
|
||
// 这里我们选择重新开始播放
|
||
if (_keyToneDevices.TryRemove(digit, out var oldDeviceInfo))
|
||
{
|
||
// 安全地停止并释放旧设备
|
||
try
|
||
{
|
||
oldDeviceInfo.Device.Stop();
|
||
oldDeviceInfo.Device.Dispose();
|
||
oldDeviceInfo.Reader.Dispose();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"停止旧按键音 {digit} 失败: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
|
||
var device = new WaveOutEvent();
|
||
var reader = new AudioFileReader(_config.AudioFiles.GetDigitToneFilePath(digit));
|
||
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;
|
||
int minPlayTime = _config.AudioFiles.MinKeyTonePlayTimeMs;
|
||
|
||
if (_keyToneDevices.TryAdd(digit, (device, reader)))
|
||
{
|
||
|
||
device.Play();
|
||
|
||
// 确保至少播放最小时长
|
||
device.PlaybackStopped += async (s, e) =>
|
||
{
|
||
// 计算已经播放的时间
|
||
var playedTime = (int)(DateTime.Now - startTime).TotalMilliseconds;
|
||
|
||
// 如果播放时间不足最小时间,延迟处理
|
||
if (playedTime < minPlayTime)
|
||
{
|
||
try
|
||
{
|
||
// 如果播放已经停止但时间不够,先不删除设备
|
||
// 等待剩余的时间后再处理
|
||
var remainingTime = minPlayTime - playedTime;
|
||
await Task.Delay(remainingTime);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"按键音 {digit} 最小播放时间延迟失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 播放完成后(包括可能的延迟),移除并释放资源
|
||
if (_keyToneDevices.TryRemove(digit, out var deviceInfo) && deviceInfo.Device == device)
|
||
{
|
||
deviceInfo.Device.Dispose();
|
||
deviceInfo.Reader.Dispose();
|
||
}
|
||
};
|
||
}
|
||
else
|
||
{
|
||
device.Dispose();
|
||
reader.Dispose();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"播放按键音 {digit} 失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 播放音频并等待
|
||
/// </summary>
|
||
private async Task PlayAudioAndWait(string audioPath, int? maxDuration = null, bool loop = false, CancellationToken token = default, float? volume = null)
|
||
{
|
||
WaveOutEvent? device = null;
|
||
AudioFileReader? reader = null;
|
||
|
||
try
|
||
{
|
||
device = new WaveOutEvent();
|
||
reader = new AudioFileReader(audioPath);
|
||
|
||
// 如果没有指定音量,根据音频文件类型设置音量
|
||
if (volume == null)
|
||
{
|
||
// 根据文件名判断音频类型并设置对应的音量
|
||
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);
|
||
|
||
// 如果需要循环播放,创建一个循环播放的任务
|
||
if (loop && maxDuration.HasValue)
|
||
{
|
||
var startTime = DateTime.Now;
|
||
var endTime = startTime.AddMilliseconds(maxDuration.Value);
|
||
|
||
// 开始播放
|
||
device.Play();
|
||
|
||
// 循环播放直到达到指定时间
|
||
while (DateTime.Now < endTime && !token.IsCancellationRequested)
|
||
{
|
||
// 如果到达文件末尾,重新开始播放
|
||
if (reader.Position >= reader.Length)
|
||
{
|
||
reader.Position = 0;
|
||
}
|
||
|
||
// 如果没有在播放中,重新开始播放
|
||
if (device.PlaybackState != PlaybackState.Playing)
|
||
{
|
||
reader.Position = 0;
|
||
device.Play();
|
||
}
|
||
|
||
// 短暂等待,避免CPU占用过高
|
||
await Task.Delay(50, token);
|
||
}
|
||
|
||
// 时间到,停止播放
|
||
device.Stop();
|
||
}
|
||
else if (maxDuration.HasValue)
|
||
{
|
||
// 开始播放
|
||
device.Play();
|
||
|
||
// 等待指定时间
|
||
await Task.Delay(maxDuration.Value, token);
|
||
|
||
// 时间到,停止播放
|
||
device.Stop();
|
||
}
|
||
else
|
||
{
|
||
// 创建一个TaskCompletionSource等待播放完成
|
||
var completionSource = new TaskCompletionSource<bool>();
|
||
|
||
// 一次性事件处理,播放完成后设置结果
|
||
EventHandler<StoppedEventArgs> handler = null!;
|
||
handler = (s, e) =>
|
||
{
|
||
device.PlaybackStopped -= handler;
|
||
completionSource.TrySetResult(true);
|
||
};
|
||
|
||
device.PlaybackStopped += handler;
|
||
device.Play();
|
||
|
||
// 注册取消操作
|
||
using var registration = token.Register(() =>
|
||
{
|
||
device.Stop();
|
||
completionSource.TrySetCanceled(token);
|
||
});
|
||
|
||
// 等待播放完成或取消
|
||
await completionSource.Task;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (token.IsCancellationRequested)
|
||
{
|
||
// 如果是取消引起的异常,重新抛出OperationCanceledException
|
||
throw new OperationCanceledException("播放音频被用户取消", ex, token);
|
||
}
|
||
Console.WriteLine($"播放音频失败: {ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
device?.Stop();
|
||
device?.Dispose();
|
||
reader?.Dispose();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始播放背景音乐
|
||
/// </summary>
|
||
private async Task StartPlayingBackgroundMusic(CancellationToken token)
|
||
{
|
||
try
|
||
{
|
||
// 如果配置不启用背景音乐,则直接返回
|
||
if (!_config.Recording.EnableBackgroundMusic)
|
||
{
|
||
Console.WriteLine("背景音乐功能已禁用");
|
||
return;
|
||
}
|
||
|
||
// 获取背景音乐文件路径
|
||
string musicFilePath = _config.AudioFiles.GetFullPath(_config.Recording.BackgroundMusicFile);
|
||
|
||
// 检查文件是否存在
|
||
if (!File.Exists(musicFilePath))
|
||
{
|
||
Console.WriteLine($"背景音乐文件不存在: {musicFilePath}");
|
||
return;
|
||
}
|
||
|
||
// 取消可能正在播放的背景音乐
|
||
_backgroundMusicCts?.Cancel();
|
||
_backgroundMusicCts?.Dispose();
|
||
_backgroundMusicCts = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||
|
||
// 初始化背景音乐设备
|
||
_backgroundMusicDevice = new WaveOutEvent();
|
||
_backgroundMusicReader = new AudioFileReader(musicFilePath);
|
||
|
||
// 设置音量(默认为10%)
|
||
_backgroundMusicReader.Volume = _config.Recording.BackgroundMusicVolume;
|
||
|
||
_backgroundMusicDevice.Init(_backgroundMusicReader);
|
||
|
||
// 标记背景音乐开始播放
|
||
_isBackgroundMusicPlaying = true;
|
||
Console.WriteLine($"开始播放背景音乐,音量: {_backgroundMusicReader.Volume * 100}%");
|
||
|
||
// 循环播放背景音乐
|
||
while (!_backgroundMusicCts.Token.IsCancellationRequested)
|
||
{
|
||
try
|
||
{
|
||
// 检查是否到达文件末尾,是则重置
|
||
if (_backgroundMusicReader.Position >= _backgroundMusicReader.Length)
|
||
{
|
||
_backgroundMusicReader.Position = 0;
|
||
}
|
||
|
||
// 如果没有在播放,则开始播放
|
||
if (_backgroundMusicDevice.PlaybackState != PlaybackState.Playing)
|
||
{
|
||
_backgroundMusicDevice.Play();
|
||
}
|
||
|
||
// 短暂等待,避免CPU占用过高
|
||
await Task.Delay(100, _backgroundMusicCts.Token);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// 正常取消,退出循环
|
||
break;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"背景音乐播放出错: {ex.Message}");
|
||
// 短暂等待后继续尝试
|
||
await Task.Delay(1000, _backgroundMusicCts.Token);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"启动背景音乐失败: {ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
// 停止并释放音乐设备
|
||
StopBackgroundMusic();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止背景音乐
|
||
/// </summary>
|
||
private void StopBackgroundMusic()
|
||
{
|
||
try
|
||
{
|
||
// 取消播放任务
|
||
_backgroundMusicCts?.Cancel();
|
||
|
||
// 停止并释放设备
|
||
if (_backgroundMusicDevice != null)
|
||
{
|
||
_backgroundMusicDevice.Stop();
|
||
_backgroundMusicDevice.Dispose();
|
||
_backgroundMusicDevice = null;
|
||
}
|
||
|
||
if (_backgroundMusicReader != null)
|
||
{
|
||
_backgroundMusicReader.Dispose();
|
||
_backgroundMusicReader = null;
|
||
}
|
||
|
||
// 标记背景音乐已停止
|
||
_isBackgroundMusicPlaying = false;
|
||
Console.WriteLine("背景音乐已停止");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
} |