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; /// /// 电话亭服务实现 /// public class PhoneBoothService : IPhoneBoothService, IDisposable { // 依赖的服务 private readonly ISignalRService _signalRService; private readonly IAudioFileService _audioFileService; // 配置 private PhoneBoothConfig _config; // 音频设备相关 private readonly ConcurrentDictionary _keyToneDevices = new(); private WaveOutEvent? _waitingToneDevice; private AudioFileReader? _waitingToneReader; // 状态相关 private volatile bool _isRecording = false; private DateTime _lastKeyPressTime = DateTime.MinValue; private readonly List _pressedKeys = new(); private volatile bool _isWaitingTonePlaying = false; private CancellationTokenSource? _waitingToneCts; private readonly CancellationTokenSource _programCts = new(); private readonly HashSet _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); /// /// 构造函数 /// /// SignalR服务 /// 音频文件服务 public PhoneBoothService(ISignalRService signalRService, IAudioFileService audioFileService) { _signalRService = signalRService; _audioFileService = audioFileService; // 获取初始配置 _config = _signalRService.GetConfigAsync().GetAwaiter().GetResult(); // 注册事件处理 //_signalRService.ConfigUpdated += SignalRService_ConfigUpdated; //_signalRService.AudioFileUpdated += SignalRService_AudioFileUpdated; } /// /// 处理配置更新事件 /// private void SignalRService_ConfigUpdated(object? sender, PhoneBoothConfig e) { ReloadConfigAsync(e).ConfigureAwait(false); } /// /// 处理音频文件更新事件 /// private void SignalRService_AudioFileUpdated(object? sender, (string FileName, byte[] FileData) e) { _audioFileService.UpdateAudioFileAsync(e.FileName, e.FileData).ConfigureAwait(false); } /// /// 初始化服务 /// 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(); } /// /// 启动服务 /// public async Task StartAsync() { try { Console.WriteLine("电话亭服务启动中..."); // 启动键盘监听 _ = StartKeyboardListener(); // 开始播放等待音 await StartPlayingWaitingTone(); Console.WriteLine("电话亭服务已启动"); } catch (Exception ex) { Console.WriteLine($"启动服务失败: {ex.Message}"); throw; } } /// /// 停止服务 /// 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; } } /// /// 重新加载配置 /// public async Task ReloadConfigAsync(PhoneBoothConfig config) { // 保存新配置 _config = config; Console.WriteLine("已重新加载配置"); } /// /// 释放资源 /// 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}"); } } /// /// 初始化音频设备 /// 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}"); } } /// /// 释放音频设备 /// 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("音频设备已释放"); } /// /// 检查重置超时 /// 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}"); } } /// /// 检查拨出超时 /// 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}"); } } /// /// 开始录音流程 /// 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(); // 启动静音检测计时器 _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(); } } /// /// 检查静音 /// private void CheckSilence(object? state) { try { var completionSource = state as TaskCompletionSource; // 如果用户按了回车键,且配置允许用户挂断,立即结束录音 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}"); } } /// /// 启动音频录制 /// 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; } } /// /// 停止音频录制 /// 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; /// /// 检查是否有声音 /// 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},风铃提示音:{_windChimeDevice?.Volume ?? 0},bgm:{_backgroundMusicDevice?.Volume ?? 0}"); _lasHasoudDateTime = DateTime.Now; _isSpeaking = false; } // 如果监测到说话,更新最后说话时间 if (isSpeaking) { _lastSpeechTime = DateTime.Now; // 如果风铃声正在播放,先停止风铃声 if (_isWindChimePlaying) { Console.WriteLine("检测到用户说话,停止风铃声..."); _ = StopWindChime(true); // 平滑淡出 // 风铃声停止后,重新启动背景音乐 if (!_isBackgroundMusicPlaying && _config.Recording.EnableBackgroundMusic) { Console.WriteLine("重新启动背景音乐..."); _ = StartPlayingBackgroundMusic(_programCts.Token); } } } // 如果最大音量超过阈值,则认为有声音 return isSpeaking; } /// /// 完全重置状态 /// 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 { // 忽略任何错误 } } } /// /// 开始播放等待音 /// 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(); } } /// /// 启动键盘监听 /// 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}"); } } } /// /// 处理数字按键按下 /// 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}"); } } /// /// 处理数字按键释放 /// private async Task HandleDigitKeyRelease(int digit) { try { // 当按键释放时,我们不做特殊处理 // 按键音的播放和结束完全由PlayKeyTone中的逻辑控制 // 这样确保了即使按键时间很短,声音也能够完整播放 // 我们只需要确认按键已经从当前按下状态列表中移除即可 // 实际的音频资源释放由PlayKeyTone中的PlaybackStopped事件处理 } catch (Exception ex) { Console.WriteLine($"处理按键 {digit} 释放失败: {ex.Message}"); } } /// /// 播放按键音 /// 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}"); } } /// /// 播放音频并等待 /// 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.BeepPromptVolume > 0 ? _config.AudioFiles.BeepPromptVolume : 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(); // 一次性事件处理,播放完成后设置结果 EventHandler 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(); } } /// /// 开始播放背景音乐 /// 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); // 初始化设备 _backgroundMusicDevice.Init(_backgroundMusicReader); // 设置音量到设备而非Reader对象,确保独立控制(默认为10%) _backgroundMusicDevice.Volume = Math.Clamp(_config.Recording.BackgroundMusicVolume, 0.0f, 1.0f); // 标记背景音乐开始播放 _isBackgroundMusicPlaying = true; Console.WriteLine($"开始播放背景音乐,音量: {_backgroundMusicDevice.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(); } } /// /// 停止背景音乐 /// 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}"); } } /// /// 检查是否需要播放风铃提示音 /// 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}秒无声音,先停止背景音乐,再播放风铃提示音..."); // 先停止背景音乐 if (_isBackgroundMusicPlaying) { StopBackgroundMusic(); } // 然后播放风铃提示音 _ = PlayWindChime(); } } catch (Exception ex) { Console.WriteLine($"检查风铃提示音错误: {ex.Message}"); } } /// /// 播放风铃提示音 /// 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); // 设置初始音量 float initialVolume = 0.8f; // 使用稍低于最大的音量,避免音量混合问题 _windChimeDevice.Volume = Math.Clamp(initialVolume, 0.0f, 1.0f); _isWindChimePlaying = true; Console.WriteLine($"开始播放风铃提示音,初始音量: {_windChimeDevice.Volume * 100}%"); // 开始播放 _windChimeDevice.Play(); // 启动循环播放任务 _ = Task.Run(async () => { try { while (_isWindChimePlaying && !token.IsCancellationRequested) { // 如果到达音频文件末尾,循环播放 if (_windChimeReader != null && _windChimeReader.Position >= _windChimeReader.Length) { _windChimeReader.Position = 0; } // 确保风铃声仍在播放 if (_windChimeDevice != null && _windChimeDevice.PlaybackState != PlaybackState.Playing) { _windChimeDevice.Play(); } await Task.Delay(100, token); } } catch (OperationCanceledException) { // 正常取消 } catch (Exception ex) { Console.WriteLine($"风铃提示音播放循环错误: {ex.Message}"); } }); } catch (Exception ex) { Console.WriteLine($"播放风铃提示音错误: {ex.Message}"); await StopWindChime(false); // 出错时直接停止,不淡出 } } /// /// 停止风铃提示音 /// /// 是否平滑淡出 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; } } }