971 lines
31 KiB
C#
971 lines
31 KiB
C#
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}");
|
||
await signalRService.StartConnectionAsync(appSettings.SignalRHubUrl);
|
||
}
|
||
|
||
// 启动电话亭服务
|
||
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)
|
||
{
|
||
// 将两个字节转换为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<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();
|
||
}
|
||
}
|