diff --git a/ShengShengBuXi.ConsoleApp/Program.cs b/ShengShengBuXi.ConsoleApp/Program.cs index 09b0fd5..54fab91 100644 --- a/ShengShengBuXi.ConsoleApp/Program.cs +++ b/ShengShengBuXi.ConsoleApp/Program.cs @@ -11,74 +11,6 @@ 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) { @@ -88,17 +20,17 @@ public class Program { // 加载应用程序设置 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) { @@ -133,17 +65,17 @@ public class Program await Task.Delay(1000); } } - + if (!connected && !appSettings.AllowOfflineStart) { Console.WriteLine("无法连接到服务器且不允许离线启动,程序将退出"); return; } } - + // 启动电话亭服务 await phoneBoothService.StartAsync(); - + // 注册Ctrl+C处理 var cts = new CancellationTokenSource(); Console.CancelKeyPress += (s, e) => @@ -151,7 +83,7 @@ public class Program e.Cancel = true; cts.Cancel(); }; - + // 等待程序退出信号 try { @@ -161,7 +93,7 @@ public class Program { // 正常退出 } - + // 停止服务 await phoneBoothService.StopAsync(); await signalRService.StopConnectionAsync(); @@ -173,833 +105,24 @@ public class Program Console.ReadKey(); } } - + /// /// 配置依赖注入服务 /// private static ServiceProvider ConfigureServices(AppSettings appSettings) { var services = new ServiceCollection(); - + // 注册配置 services.AddSingleton(appSettings); - + // 注册服务 services.AddSingleton(); - services.AddSingleton(provider => + 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(); - } } diff --git a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs index f9a01c8..328d455 100644 --- a/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs +++ b/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs @@ -531,25 +531,6 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable 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("开始初始化录音设备..."); @@ -570,7 +551,27 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable Console.WriteLine("正在初始化录音设备..."); // 初始化录音设备 await StartAudioRecording(_recordingFilePath); - Console.WriteLine("正在录音中..."); + Console.WriteLine("录音开始..."); + + 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("录音流程被用户取消"); @@ -961,7 +962,7 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable { Console.WriteLine("检测到用户说话,停止风铃声..."); _ = StopWindChime(true); // 平滑淡出 - + // 风铃声停止后,重新启动背景音乐 if (!_isBackgroundMusicPlaying && _config.Recording.EnableBackgroundMusic) { @@ -1547,7 +1548,7 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable { _backgroundMusicReader.Position = 0; } - + // 如果没有在播放,则开始播放 if (_backgroundMusicDevice.PlaybackState != PlaybackState.Playing) { @@ -1639,13 +1640,13 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable !_isWindChimePlaying) { Console.WriteLine($"检测到{windChimePromptSeconds}秒无声音,先停止背景音乐,再播放风铃提示音..."); - + // 先停止背景音乐 if (_isBackgroundMusicPlaying) { StopBackgroundMusic(); } - + // 然后播放风铃提示音 _ = PlayWindChime(); } @@ -1772,14 +1773,14 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable float ratio = 1.0f - ((float)i / steps); _windChimeDevice.Volume = startVolume * ratio; - + await Task.Delay(stepDelay); } } // 正式停止 _windChimeCts?.Cancel(); - + if (_windChimeDevice != null) { _windChimeDevice.Stop(); diff --git a/ShengShengBuXi.ConsoleApp/appsettings.json b/ShengShengBuXi.ConsoleApp/appsettings.json index 7700478..f1dd3dd 100644 --- a/ShengShengBuXi.ConsoleApp/appsettings.json +++ b/ShengShengBuXi.ConsoleApp/appsettings.json @@ -1,5 +1,5 @@ { - "SignalRHubUrl": "http://115.159.44.16/audiohub", + "SignalRHubUrl": "http://localhost:81/audiohub", "ConfigBackupPath": "config.json", "AutoConnectToServer": true, "AllowOfflineStart": false diff --git a/ShengShengBuXi/Hubs/AudioHub.cs b/ShengShengBuXi/Hubs/AudioHub.cs index 7dc97f8..bb923dc 100644 --- a/ShengShengBuXi/Hubs/AudioHub.cs +++ b/ShengShengBuXi/Hubs/AudioHub.cs @@ -29,6 +29,9 @@ namespace ShengShengBuXi.Hubs private readonly IAudioProcessingService _audioProcessingService; private readonly IConfigurationService _configurationService; private readonly ISpeechToTextService _speechToTextService; + /// + /// 客户端列表 + /// private static readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); private readonly ILogger _logger; private readonly IHubContext _hubContext; @@ -42,6 +45,8 @@ namespace ShengShengBuXi.Hubs /// 用于清理过期记录的计时器 /// private static Timer _cleanupTimer; + // 管理员配置保存路径 + private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config"); private static bool isInitialized = false; @@ -78,30 +83,48 @@ namespace ShengShengBuXi.Hubs /// /// 监控文本队列的持久化文件路径 /// - private static readonly string _monitorTextQueueFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/monitor_text_queue.json"); + private static readonly string _monitorTextQueueFilePath = Path.Combine(ConfigDirectory, "monitor_text_queue.json"); /// /// 预设句子文件路径 /// - private static readonly string _sentencesFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/sentences.txt"); + private static readonly string _sentencesFilePath = Path.Combine(ConfigDirectory, "sentences.txt"); /// /// 真实用户显示记录的持久化文件路径 /// - private static readonly string _realUserDisplayLogsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/real_user_displays.log"); + private static readonly string _realUserDisplayLogsPath = Path.Combine(ConfigDirectory, "real_user_displays.log"); /// /// 预设句子列表 /// private static List _presetSentences = new List(); + /// + /// 预设句子2文件路径 + /// + private static readonly string _sentences2FilePath = Path.Combine(ConfigDirectory, "sentences2.txt"); + + /// + /// 预设句子2列表 + /// + private static List _presetSentences2 = new List(); + + /// + /// 当前使用的预设句子类型 (0: 默认预设句子, 1: 预设句子2) + /// + private static int _currentPresetSentenceType = 0; + /// + /// 预设句子类型配置文件路径 + /// + private static readonly string _presetSentenceConfigPath = Path.Combine(ConfigDirectory, "newconfig.js"); + // 配置信息 private static string _displayConfig = "{}"; private static object _displayConfigLock = new object(); // 控屏开关设置 private static bool _manualScreenControlEnabled = false; - private static readonly string _screenControlSettingPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/screen_control.json"); + private static readonly string _screenControlSettingPath = Path.Combine(ConfigDirectory, "screen_control.json"); + - // 管理员配置保存路径 - private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config"); private static readonly string DisplayConfigPath = Path.Combine(ConfigDirectory, "display.json"); private static DateTime _lastRealTimeResultSentTime = DateTime.MinValue; @@ -129,6 +152,15 @@ namespace ShengShengBuXi.Hubs // 加载控屏设置 LoadScreenControlSetting(); + + // 加载预设句子类型设置 + LoadPresetSentenceTypeSetting(); + + // 加载预设句子 + LoadPresetSentencesFromFile(); + + // 加载预设句子2 + LoadPresetSentences2FromFile(); } // 加载显示配置 @@ -235,6 +267,51 @@ namespace ShengShengBuXi.Hubs } } + // 加载预设句子类型设置 + private static void LoadPresetSentenceTypeSetting() + { + try + { + if (File.Exists(_presetSentenceConfigPath)) + { + string json = File.ReadAllText(_presetSentenceConfigPath); + var setting = JsonConvert.DeserializeObject(json); + _currentPresetSentenceType = setting?.Type ?? 0; + Console.WriteLine($"加载预设句子类型设置成功,当前类型: {_currentPresetSentenceType}"); + } + else + { + // 默认使用类型0 + _currentPresetSentenceType = 0; + + // 保存默认设置 + SavePresetSentenceTypeSetting(); + Console.WriteLine("创建默认预设句子类型设置成功"); + } + } + catch (Exception ex) + { + Console.WriteLine($"加载预设句子类型设置出错: {ex.Message}"); + _currentPresetSentenceType = 0; + } + } + + // 保存预设句子类型设置 + private static bool SavePresetSentenceTypeSetting() + { + try + { + string json = JsonConvert.SerializeObject(new { Type = _currentPresetSentenceType }); + File.WriteAllText(_presetSentenceConfigPath, json); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"保存预设句子类型设置出错: {ex.Message}"); + return false; + } + } + /// /// 初始化音频Hub /// @@ -591,7 +668,8 @@ namespace ShengShengBuXi.Hubs // 添加音频数据发送任务,使用单独的客户端连接 // 使用清晰的对象格式,并添加WAV头以确保格式正确 var wavData = _audioProcessingService.AddWavHeader(dataToSend, config.SampleRate, config.Channels); - tasks.Add(Clients.Client(clientId).SendAsync("ReceiveAudioData", new { + tasks.Add(Clients.Client(clientId).SendAsync("ReceiveAudioData", new + { format = "WAV", sampleRate = config.SampleRate, channels = config.Channels, @@ -841,6 +919,8 @@ namespace ShengShengBuXi.Hubs Text = text, Timestamp = DateTime.Now }); + _presetSentences.Add(text); + _presetSentences2.Add(text); return false; } @@ -1090,6 +1170,13 @@ namespace ShengShengBuXi.Hubs return; } + if (highestPriority.Value.Text.Contains("青竹园")) + { + if (_displayTextQueue.TryRemove(highestPriority.Key, out _)) + { + } + return; + } // 在手动控屏模式下,只处理真实用户的消息 if (_manualScreenControlEnabled && !highestPriority.Value.IsRealUser) @@ -1137,19 +1224,44 @@ namespace ShengShengBuXi.Hubs { try { - // 确保预设句子列表不为空 - if (_presetSentences.Count == 0) - { - LoadPresetSentencesFromFile(); + // 根据当前预设句子类型选择使用哪个预设句子列表 + List currentPresetSentences; - // 如果加载后仍然为空,使用默认句子 + if (_currentPresetSentenceType == 1) + { + // 使用预设句子2 + if (_presetSentences2.Count == 0) + { + LoadPresetSentences2FromFile(); + + // 如果加载后仍然为空,使用默认句子 + if (_presetSentences2.Count == 0) + { + _presetSentences2.Add("每一次跨越山川,都是为了与你重逢。"); + _presetSentences2.Add("星光璀璨的夜晚,我想起了你的笑容。"); + } + } + currentPresetSentences = _presetSentences2; + } + else + { + // 使用默认预设句子 if (_presetSentences.Count == 0) { - _presetSentences.Add("记得每到夏天傍晚,您就摇着蒲扇坐在藤椅里,把切好的西瓜最甜那块硬塞给我。"); - _presetSentences.Add("时光匆匆流逝,思念却越来越深。"); + LoadPresetSentencesFromFile(); + + // 如果加载后仍然为空,使用默认句子 + if (_presetSentences.Count == 0) + { + _presetSentences.Add("记得每到夏天傍晚,您就摇着蒲扇坐在藤椅里,把切好的西瓜最甜那块硬塞给我。"); + _presetSentences.Add("时光匆匆流逝,思念却越来越深。"); + } } + currentPresetSentences = _presetSentences; } - _presetSentences.OrderBy(it => Guid.NewGuid()).ToList().ForEach(item => + + // 随机排序并添加到显示队列 + currentPresetSentences.OrderBy(it => Guid.NewGuid()).ToList().ForEach(item => { var displayText = new DisplayText { @@ -1173,7 +1285,7 @@ namespace ShengShengBuXi.Hubs /// /// 从文件加载预设句子 /// - private void LoadPresetSentencesFromFile() + private static void LoadPresetSentencesFromFile() { try { @@ -1182,7 +1294,7 @@ namespace ShengShengBuXi.Hubs // 检查文件是否存在 if (!File.Exists(_sentencesFilePath)) { - _logger.LogWarning($"预设句子文件不存在: {_sentencesFilePath},将创建默认文件"); + Console.WriteLine($"预设句子文件不存在: {_sentencesFilePath},将创建默认文件"); // 创建目录(如果不存在) Directory.CreateDirectory(Path.GetDirectoryName(_sentencesFilePath)); @@ -1204,11 +1316,53 @@ namespace ShengShengBuXi.Hubs } } _presetSentences = _presetSentences.OrderBy(x => Guid.NewGuid()).ToList(); - _logger.LogInformation($"成功从文件加载预设句子: {_presetSentences.Count} 条"); + Console.WriteLine($"成功从文件加载预设句子: {_presetSentences.Count} 条"); } catch (Exception ex) { - _logger.LogError($"加载预设句子失败: {ex.Message}"); + Console.WriteLine($"加载预设句子失败: {ex.Message}"); + } + } + + /// + /// 从文件加载预设句子2 + /// + private static void LoadPresetSentences2FromFile() + { + try + { + _presetSentences2.Clear(); + + // 检查文件是否存在 + if (!File.Exists(_sentences2FilePath)) + { + Console.WriteLine($"预设句子2文件不存在: {_sentences2FilePath},将创建默认文件"); + + // 创建目录(如果不存在) + Directory.CreateDirectory(Path.GetDirectoryName(_sentences2FilePath)); + + // 写入默认的预设句子2 + File.WriteAllLines(_sentences2FilePath, new string[] { + + }); + } + + // 读取文件中的每一行作为一个预设句子 + string[] lines = File.ReadAllLines(_sentences2FilePath); + foreach (string line in lines) + { + // 忽略空行 + if (!string.IsNullOrWhiteSpace(line)) + { + _presetSentences2.Add(line.Trim()); + } + } + _presetSentences2 = _presetSentences2.OrderBy(x => Guid.NewGuid()).ToList(); + Console.WriteLine($"成功从文件加载预设句子2: {_presetSentences2.Count} 条"); + } + catch (Exception ex) + { + Console.WriteLine($"加载预设句子2失败: {ex.Message}"); } } @@ -1833,5 +1987,148 @@ namespace ShengShengBuXi.Hubs return records; } + + /// + /// 同时保存两个预设句子列表到文件 + /// + private static void SavePresetSentencesToFile() + { + try + { + // 保存预设句子 + File.WriteAllLines(_sentencesFilePath, _presetSentences); + Console.WriteLine($"成功保存预设句子到文件: {_presetSentences.Count} 条"); + + // 保存预设句子2 + File.WriteAllLines(_sentences2FilePath, _presetSentences2); + Console.WriteLine($"成功保存预设句子2到文件: {_presetSentences2.Count} 条"); + } + catch (Exception ex) + { + Console.WriteLine($"保存预设句子失败: {ex.Message}"); + } + } + + /// + /// 添加预设句子 + /// + /// 要添加的预设句子文本 + /// 处理任务 + public async Task AddPresetSentence(string text) + { + if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo)) + { + _logger.LogWarning($"未注册的客户端尝试添加预设句子: {Context.ConnectionId}"); + await Clients.Caller.SendAsync("Error", "请先注册客户端"); + return false; + } + + if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin) + { + _logger.LogWarning($"非监控或管理端客户端尝试添加预设句子: {Context.ConnectionId}, 类型: {clientInfo.ClientType}"); + await Clients.Caller.SendAsync("Error", "只有监控或管理端客户端可以添加预设句子"); + return false; + } + + if (string.IsNullOrWhiteSpace(text)) + { + _logger.LogWarning("尝试添加空预设句子"); + await Clients.Caller.SendAsync("Error", "预设句子不能为空"); + return false; + } + + _logger.LogInformation($"添加预设句子: {text}"); + + try + { + // 同时添加到两个列表,确保同步 + _presetSentences.Add(text.Trim()); + _presetSentences2.Add(text.Trim()); + + // 保存到文件 + SavePresetSentencesToFile(); + + // 通知其他客户端预设句子已更新 + await Clients.Groups(new[] { "webadmin", "monitor" }) + .SendAsync("PresetSentencesUpdated"); + + return true; + } + catch (Exception ex) + { + _logger.LogError($"添加预设句子失败: {ex.Message}"); + await Clients.Caller.SendAsync("Error", $"添加预设句子失败: {ex.Message}"); + return false; + } + } + + /// + /// 获取当前预设句子类型 + /// + /// 当前预设句子类型 (0: 默认, 1: 预设句子2) + public async Task GetPresetSentenceType() + { + if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo)) + { + _logger.LogWarning($"未注册的客户端尝试获取预设句子类型: {Context.ConnectionId}"); + throw new HubException("请先注册客户端"); + } + + _logger.LogInformation($"客户端获取当前预设句子类型: {Context.ConnectionId}, 类型: {clientInfo.ClientType}"); + return _currentPresetSentenceType; + } + + /// + /// 更新预设句子类型 + /// + /// 预设句子类型 (0: 默认, 1: 预设句子2) + /// 处理任务 + public async Task UpdatePresetSentenceType(int type) + { + if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo)) + { + _logger.LogWarning($"未注册的客户端尝试更新预设句子类型: {Context.ConnectionId}"); + await Clients.Caller.SendAsync("Error", "请先注册客户端"); + return false; + } + + if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin) + { + _logger.LogWarning($"非监控或管理端客户端尝试更新预设句子类型: {Context.ConnectionId}, 类型: {clientInfo.ClientType}"); + await Clients.Caller.SendAsync("Error", "只有监控或管理端客户端可以更新预设句子类型"); + return false; + } + + if (type < 0 || type > 1) + { + _logger.LogWarning($"客户端尝试设置无效的预设句子类型: {type}"); + await Clients.Caller.SendAsync("Error", "无效的预设句子类型,有效值为0或1"); + return false; + } + + _logger.LogInformation($"更新预设句子类型: {Context.ConnectionId}, 新类型: {type}"); + + // 更新配置 + _currentPresetSentenceType = type; + + // 保存设置 + if (SavePresetSentenceTypeSetting()) + { + _displayTextQueue.Clear(); + // 通知其他客户端预设句子类型已更改 + await Clients.Groups(new[] { "webadmin", "monitor" }) + .SendAsync("PresetSentenceTypeChanged", type); + + return true; + } + + return false; + } + + public async Task GetClientList() + { + var clients = _clients.Values.ToList(); + await Clients.Caller.SendAsync("ClientList", clients); + } } } \ No newline at end of file diff --git a/ShengShengBuXi/Monitor.cshtml.bak b/ShengShengBuXi/Monitor.cshtml.bak new file mode 100644 index 0000000..5437e73 --- /dev/null +++ b/ShengShengBuXi/Monitor.cshtml.bak @@ -0,0 +1,2042 @@ +@page +@model ShengShengBuXi.Pages.MonitorModel +@{ + ViewData["Title"] = "清竹园-中控页面"; +} + + + + +
+ +
+
+
+
+
+

+ 清竹园-中控页面 连接中... +

+ + +
+
+ 未检测到通话 +
+ + +
+ 预设句子类型: + + + + +
+ + +
+ 控屏模式: + + + + +
+ + + +
+
+
+
+ + +
+ +
+
+
+
语音列表
+
+
+
+ +
加载中...
+
+
+
+
+ + +
+
+
+
+ +
+ +
+
+
+ + + + +
+
+
+
+ 文本编辑 +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+
+
+
+
+ + +
+
+
+
显示文本列表
+
+
+
+ +
加载中...
+
+
+
+
+
+ + +
+
+
+
+
预设句子控制
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+ 调试信息 +
+ + +
+
+
+

+                    
+
+
+
+
+ + +
+ + + + + @section Scripts { + + + + + + + + } diff --git a/ShengShengBuXi/Pages/Admin.cshtml b/ShengShengBuXi/Pages/Admin.cshtml index 6c79a10..d19e542 100644 --- a/ShengShengBuXi/Pages/Admin.cshtml +++ b/ShengShengBuXi/Pages/Admin.cshtml @@ -20,7 +20,7 @@
-
+
@@ -28,33 +28,43 @@
-
+
- - - + + +
@@ -63,44 +73,53 @@
控制台配置
- +
WebSocket服务器地址
- +
- +
- +
- +
- +
拨号配置
- +
拨号所需的最小位数
- +
无新按键后自动拨出的等待秒数
- +
位数不足时,无操作重置的等待秒数
@@ -110,57 +129,72 @@
录音配置
- +
- +
- +
- +
1=单声道,2=立体声
- +
- +
- +
无声音自动挂断的时间
- +
- - + +
- +
- +
- - + +
- +
启用自动清理时有效
@@ -172,31 +206,38 @@
音频文件配置
- +
- +
- +
- +
- +
- +
- +
例如: {0}.mp3,用于数字0-9的按键音
@@ -206,17 +247,20 @@
电话流程配置
- +
等待接电话音频的最小持续时间
- +
等待接电话音频的最大持续时间
- +
播放电话接起音频的概率(0-1之间)
@@ -225,7 +269,7 @@
-
+
@@ -247,15 +291,16 @@ -
+
- +