ShengShengBuXi/ShengShengBuXi.ConsoleApp/Program.cs
2025-03-28 20:03:52 +08:00

1006 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<int, (WaveOutEvent Device, AudioFileReader Reader)> 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<int> pressedKeys = new();
private static volatile bool isWaitingTonePlaying = false;
private static CancellationTokenSource? waitingToneCts;
private static readonly CancellationTokenSource programCts = new();
private static readonly HashSet<int> 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<IPhoneBoothService>();
var signalRService = services.GetRequiredService<ISignalRService>();
// 初始化电话亭服务
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();
}
}
/// <summary>
/// 配置依赖注入服务
/// </summary>
private static ServiceProvider ConfigureServices(AppSettings appSettings)
{
var services = new ServiceCollection();
// 注册配置
services.AddSingleton(appSettings);
// 注册服务
services.AddSingleton<IAudioFileService, AudioFileService>();
services.AddSingleton<ISignalRService>(provider =>
new SignalRService(appSettings.ConfigBackupPath));
services.AddSingleton<IPhoneBoothService, PhoneBoothService>();
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<bool>();
// 启动静音检测计时器
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<bool>;
// 如果用户按了回车键,立即结束录音
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)
{
// 将两个字节转换为short16位
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<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();
}
}
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();
}
}