using NAudio.Wave; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Diagnostics; using System.Threading; using Microsoft.Extensions.DependencyInjection; using ShengShengBuXi.ConsoleApp.Models; using ShengShengBuXi.ConsoleApp.Services; namespace ShengShengBuXi.ConsoleApp; public class Program { private static readonly string AudioPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mp3"); private static readonly ConcurrentDictionary KeyToneDevices = new(); private static WaveOutEvent? waitingToneDevice; private static AudioFileReader? waitingToneReader; private static volatile bool isRecording = false; private static DateTime lastKeyPressTime = DateTime.MinValue; private static List pressedKeys = new(); private static volatile bool isWaitingTonePlaying = false; private static CancellationTokenSource? waitingToneCts; private static readonly CancellationTokenSource programCts = new(); private static readonly HashSet currentPressedKeys = new(); private static readonly Timer resetTimer; private static readonly Timer dialOutTimer; // Windows API private const int WH_KEYBOARD_LL = 13; private const int WM_KEYDOWN = 0x0100; private const int WM_KEYUP = 0x0101; private static IntPtr hookHandle = IntPtr.Zero; private static HookProc? hookProc; // 数字键盘的虚拟键码 (VK_NUMPAD0 - VK_NUMPAD9) private static readonly int[] numpadKeys = Enumerable.Range(0x60, 10).ToArray(); // 回车键的虚拟键码 private const int VK_RETURN = 0x0D; // 新增录音相关变量 private static WaveInEvent? waveIn = null; private static WaveFileWriter? waveWriter = null; private static string recordingFilePath = ""; private static DateTime lastSoundTime = DateTime.MinValue; private static Timer? silenceTimer = null; private static readonly float silenceThreshold = 0.02f; // 静音阈值 private static volatile bool isHangUpKeyPressed = false; static Program() { resetTimer = new Timer(CheckResetTimeout, null, Timeout.Infinite, Timeout.Infinite); dialOutTimer = new Timer(CheckDialOutTimeout, null, Timeout.Infinite, Timeout.Infinite); } [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); public static async Task Main(string[] args) { Console.WriteLine("生生不息电话亭启动中..."); try { // 加载应用程序设置 var appSettings = AppSettings.LoadFromFile("appsettings.json"); // 配置依赖注入 var services = ConfigureServices(appSettings); // 获取服务 var phoneBoothService = services.GetRequiredService(); var signalRService = services.GetRequiredService(); // 初始化电话亭服务 await phoneBoothService.InitializeAsync(); // 如果配置为自动连接,则连接到SignalR服务器 if (appSettings.AutoConnectToServer) { Console.WriteLine($"正在连接到服务器: {appSettings.SignalRHubUrl}"); int retryCount = 0; bool connected = false; while (retryCount < 10) { try { await signalRService.StartConnectionAsync(appSettings.SignalRHubUrl); connected = true; break; } catch (Exception ex) { retryCount++; Console.WriteLine($"连接失败 (第{retryCount}次): {ex.Message}"); if (retryCount >= 10) { if (!appSettings.AllowOfflineStart) { Console.WriteLine("达到最大重试次数,程序将退出"); return; } else { Console.WriteLine("达到最大重试次数,但配置允许离线启动,将继续运行"); break; } } await Task.Delay(1000); } } if (!connected && !appSettings.AllowOfflineStart) { Console.WriteLine("无法连接到服务器且不允许离线启动,程序将退出"); return; } } // 启动电话亭服务 await phoneBoothService.StartAsync(); // 注册Ctrl+C处理 var cts = new CancellationTokenSource(); Console.CancelKeyPress += (s, e) => { e.Cancel = true; cts.Cancel(); }; // 等待程序退出信号 try { await Task.Delay(-1, cts.Token); } catch (OperationCanceledException) { // 正常退出 } // 停止服务 await phoneBoothService.StopAsync(); await signalRService.StopConnectionAsync(); } catch (Exception ex) { Console.WriteLine($"程序启动失败: {ex.Message}"); Console.WriteLine("按任意键退出..."); Console.ReadKey(); } } /// /// 配置依赖注入服务 /// private static ServiceProvider ConfigureServices(AppSettings appSettings) { var services = new ServiceCollection(); // 注册配置 services.AddSingleton(appSettings); // 注册服务 services.AddSingleton(); services.AddSingleton(provider => new SignalRService(appSettings.ConfigBackupPath)); services.AddSingleton(); return services.BuildServiceProvider(); } private static void InitializeAudioDevice() { try { waitingToneDevice = new WaveOutEvent(); waitingToneReader = new AudioFileReader(Path.Combine(AudioPath, "等待嘟音.wav")); waitingToneDevice.Init(waitingToneReader); } catch (Exception ex) { Console.WriteLine($"初始化音频设备失败: {ex.Message}"); throw; } } private static async Task StartKeyboardListener() { while (!programCts.Token.IsCancellationRequested) { try { // 检查回车键 bool isEnterPressed = (GetAsyncKeyState(VK_RETURN) & 0x8000) != 0; if (isEnterPressed) { // 添加一点延迟,防止连续检测到按键 await Task.Delay(100, programCts.Token); // 再次检查回车键状态,确保不是误触或抖动 bool stillPressed = (GetAsyncKeyState(VK_RETURN) & 0x8000) != 0; if (!stillPressed) { continue; } if (isRecording) { // 如果正在录音中,回车键被视为挂断信号 Console.WriteLine("检测到回车键,用户挂断..."); isHangUpKeyPressed = true; // 等待一段时间,防止重复触发 await Task.Delay(500, programCts.Token); 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 static 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 static 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); // 重置定时器 resetTimer.Change(10000, Timeout.Infinite); // 10秒后检查是否需要重置 // 检查是否需要拨出(8位以上且5秒内无新按键) if (pressedKeys.Count >= 8) { // 重置拨出定时器,5秒后检查是否需要拨出 dialOutTimer.Change(5000, Timeout.Infinite); } else { // 如果位数不足8位,停止拨出定时器 dialOutTimer.Change(Timeout.Infinite, Timeout.Infinite); } } catch (Exception ex) { Console.WriteLine($"处理按键 {digit} 失败: {ex.Message}"); } } private static void CheckResetTimeout(object? state) { try { // 如果正在录音中,不执行重置操作 if (isRecording) { return; } if (pressedKeys.Count > 0 && pressedKeys.Count < 8 && (DateTime.Now - lastKeyPressTime).TotalSeconds >= 10) { Console.WriteLine("10秒内未完成8位数字输入,重置等待..."); pressedKeys.Clear(); KeyToneDevices.Clear(); _ = StartPlayingWaitingTone(); } } catch (Exception ex) { Console.WriteLine($"检查超时重置失败: {ex.Message}"); } } private static void CheckDialOutTimeout(object? state) { try { // 如果正在录音中,不执行拨出操作 if (isRecording) { return; } // 检查是否已经过了5秒且按键数大于等于8 if (pressedKeys.Count >= 8 && (DateTime.Now - lastKeyPressTime).TotalSeconds >= 5) { Console.WriteLine("5秒内无新按键,开始拨出..."); // 非阻塞方式启动录音 _ = StartRecording(); } } catch (Exception ex) { Console.WriteLine($"检查拨出超时失败: {ex.Message}"); } } private static async Task HandleDigitKeyRelease(int digit) { try { if (KeyToneDevices.TryRemove(digit, out var deviceInfo)) { deviceInfo.Device.Stop(); deviceInfo.Device.Dispose(); deviceInfo.Reader.Dispose(); } } catch (Exception ex) { Console.WriteLine($"处理按键 {digit} 释放失败: {ex.Message}"); } } private static async Task PlayKeyTone(int digit) { try { if (KeyToneDevices.ContainsKey(digit)) { return; // 已经在播放中 } var device = new WaveOutEvent(); var reader = new AudioFileReader(Path.Combine(AudioPath, $"{digit}.mp3")); device.Init(reader); if (KeyToneDevices.TryAdd(digit, (device, reader))) { device.Play(); } else { device.Dispose(); reader.Dispose(); } } catch (Exception ex) { Console.WriteLine($"播放按键音 {digit} 失败: {ex.Message}"); } } private static 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; 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("开始录音流程..."); // 随机播放3-6秒的等待接电话音频 int waitTime = new Random().Next(3, 7); // 3到6秒 Console.WriteLine($"播放等待接电话音频,持续{waitTime}秒..."); try { await PlayAudioAndWait(Path.Combine(AudioPath, "等待接电话.mp3"), waitTime * 1000, true, recordingCts.Token); } catch (OperationCanceledException) { throw; } // 如果已经收到取消信号,立即结束 if (recordingCts.Token.IsCancellationRequested) { throw new OperationCanceledException("录音流程被用户取消"); } // 15%概率播放电话接起音频 bool playPickup = new Random().NextDouble() < 0.15; if (playPickup) { Console.WriteLine("播放电话接起音频..."); try { await PlayAudioAndWait(Path.Combine(AudioPath, "电话接起.mp3"), null, false, recordingCts.Token); } catch (OperationCanceledException) { throw; } if (recordingCts.Token.IsCancellationRequested) { throw new OperationCanceledException("录音流程被用户取消"); } } // 播放提示用户录音的音频 Console.WriteLine("播放提示用户录音音频..."); try { await PlayAudioAndWait(Path.Combine(AudioPath, "提示用户录音.mp3"), null, false, recordingCts.Token); } catch (OperationCanceledException) { throw; } if (recordingCts.Token.IsCancellationRequested) { throw new OperationCanceledException("录音流程被用户取消"); } // 开始实际录音逻辑 Console.WriteLine("开始初始化录音设备..."); // 创建录音文件路径 string recordingsFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "recordings"); if (!Directory.Exists(recordingsFolder)) { Directory.CreateDirectory(recordingsFolder); } recordingFilePath = Path.Combine(recordingsFolder, $"recording_{DateTime.Now:yyyyMMdd_HHmmss}.wav"); // 初始化录音设备 await StartAudioRecording(recordingFilePath); if (recordingCts.Token.IsCancellationRequested) { throw new OperationCanceledException("录音流程被用户取消"); } // 创建一个等待完成的任务源 var recordingCompletionSource = new TaskCompletionSource(); // 启动静音检测计时器 lastSoundTime = DateTime.Now; silenceTimer = new Timer(CheckSilence, recordingCompletionSource, 1000, 1000); Console.WriteLine("播放滴提示音..."); try { await PlayAudioAndWait(Path.Combine(AudioPath, "滴提示音.wav"), 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); }); // 等待录音完成 await recordingCompletionSource.Task; } catch (Exception) { // 捕获所有异常,确保不会中断主流程 } }); // 等待录音完成或取消 try { await Task.WhenAny(recordingTask, Task.Delay(Timeout.Infinite, recordingCts.Token)); } catch (OperationCanceledException) { // 预期的取消异常,可以忽略 } // 停止录音 StopAudioRecording(); Console.WriteLine("录音结束,重置状态..."); // 完全重置系统状态 CompletelyResetState(); Console.WriteLine("系统已重置,可以继续使用..."); } catch (OperationCanceledException ex) { Console.WriteLine($"录音流程被取消: {ex.Message}"); // 确保录音设备被释放 StopAudioRecording(); // 取消时也完全重置系统状态 CompletelyResetState(); Console.WriteLine("系统已重置,可以继续使用..."); } catch (Exception ex) { Console.WriteLine($"录音过程发生错误: {ex.Message}"); // 确保录音设备被释放 StopAudioRecording(); // 发生错误时也完全重置系统状态 CompletelyResetState(); } } private static void CheckSilence(object? state) { try { var completionSource = state as TaskCompletionSource; // 如果用户按了回车键,立即结束录音 if (isHangUpKeyPressed) { Console.WriteLine("定时器检测到用户手动挂断"); completionSource?.TrySetResult(true); return; } // 检查是否超过30秒没有声音 if ((DateTime.Now - lastSoundTime).TotalSeconds >= 30) { Console.WriteLine("检测到30秒无声音,自动挂断"); completionSource?.TrySetResult(true); } } catch (Exception ex) { Console.WriteLine($"静音检测错误: {ex.Message}"); } } private static async Task StartAudioRecording(string filePath) { try { // 创建录音设备 waveIn = new WaveInEvent { DeviceNumber = 0, // 使用默认录音设备 WaveFormat = new WaveFormat(16000, 1), // 16 kHz, 单声道 BufferMilliseconds = 100 //音频缓冲区的大小(以毫秒为单位)。这个参数控制着音频数据从麦克风读取并触发 DataAvailable 事件的频率。 }; // 创建文件写入器 waveWriter = new WaveFileWriter(filePath, waveIn.WaveFormat); // 处理录音数据 waveIn.DataAvailable += (s, e) => { try { // 将数据写入文件 waveWriter.Write(e.Buffer, 0, e.BytesRecorded); // 检查是否有声音 if (HasSound(e.Buffer, e.BytesRecorded)) { lastSoundTime = DateTime.Now; } } catch (Exception ex) { Console.WriteLine($"录音数据处理错误: {ex.Message}"); } }; // 录音完成事件 waveIn.RecordingStopped += (s, e) => { // 在这里处理录音停止后的逻辑 Console.WriteLine("录音已停止"); }; // 开始录音 waveIn.StartRecording(); Console.WriteLine($"开始录音,保存到文件: {filePath}"); } catch (Exception ex) { Console.WriteLine($"启动录音失败: {ex.Message}"); throw; } } private static void StopAudioRecording() { try { // 停止静音检测计时器 silenceTimer?.Change(Timeout.Infinite, Timeout.Infinite); silenceTimer?.Dispose(); silenceTimer = null; // 停止录音 if (waveIn != null) { waveIn.StopRecording(); waveIn.Dispose(); waveIn = null; } // 关闭文件 if (waveWriter != null) { waveWriter.Dispose(); waveWriter = null; Console.WriteLine($"录音已保存到: {recordingFilePath}"); } } catch (Exception ex) { Console.WriteLine($"停止录音失败: {ex.Message}"); } } private static 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; } } } // 如果最大音量超过阈值,则认为有声音 return maxVolume > silenceThreshold; } private static async void CompletelyResetState() { try { // 确保录音设备被释放 StopAudioRecording(); // 清除录音状态 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 { _ = StartPlayingWaitingTone(); } catch { // 忽略任何错误 } } } private static async Task PlayAudioAndWait(string audioPath, int? maxDuration = null, bool loop = false, CancellationToken token = default) { WaveOutEvent device = null; AudioFileReader reader = null; try { device = new WaveOutEvent(); reader = new AudioFileReader(audioPath); 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 static void DisposeAudioDevices() { waitingToneCts?.Cancel(); waitingToneCts?.Dispose(); waitingToneDevice?.Dispose(); waitingToneReader?.Dispose(); foreach (var (device, reader) in KeyToneDevices.Values) { device.Dispose(); reader.Dispose(); } KeyToneDevices.Clear(); } }