ShengShengBuXi/ShengShengBuXi.ConsoleApp/Services/PhoneBoothService.cs
2025-03-29 03:43:31 +08:00

1805 lines
60 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 ShengShengBuXi.ConsoleApp.Models;
using System.Linq;
using System.Transactions;
namespace ShengShengBuXi.ConsoleApp.Services;
/// <summary>
/// 电话亭服务实现
/// </summary>
public class PhoneBoothService : IPhoneBoothService, IDisposable
{
// 依赖的服务
private readonly ISignalRService _signalRService;
private readonly IAudioFileService _audioFileService;
// 配置
private PhoneBoothConfig _config;
// 音频设备相关
private readonly ConcurrentDictionary<int, (WaveOutEvent Device, AudioFileReader Reader)> _keyToneDevices = new();
private WaveOutEvent? _waitingToneDevice;
private AudioFileReader? _waitingToneReader;
// 状态相关
private volatile bool _isRecording = false;
private DateTime _lastKeyPressTime = DateTime.MinValue;
private readonly List<int> _pressedKeys = new();
private volatile bool _isWaitingTonePlaying = false;
private CancellationTokenSource? _waitingToneCts;
private readonly CancellationTokenSource _programCts = new();
private readonly HashSet<int> _currentPressedKeys = new();
private Timer? _resetTimer;
private Timer? _dialOutTimer;
private readonly object _timerLock = new();
// Windows API
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private IntPtr _hookHandle = IntPtr.Zero;
private HookProc? _hookProc;
// 数字键盘的虚拟键码 (VK_NUMPAD0 - VK_NUMPAD9)
// 数字键盘的虚拟键码 (VK_NUMPAD0 - VK_NUMPAD9) + VK_DECIMAL + VK_MULTIPLY
private readonly int[] _numpadKeys = Enumerable.Range(0x60, 10) // 0x60~0x69 (NumPad 0~9)
.Concat(new[] { VK_DECIMAL, VK_MULTIPLY }) // 追加 VK_DECIMAL (.) 和 VK_MULTIPLY (*)
.ToArray();
// 回车键的虚拟键码
private const int VK_RETURN = 0x0D;
//#
private const int VK_DECIMAL = 0x6E; // 0x6E (110) -> NumPad .
// *
private const int VK_MULTIPLY = 0x6A; // 0x6A (106) -> NumPad *
// 录音相关
private WaveInEvent? _waveIn = null;
private WaveFileWriter? _waveWriter = null;
private string _recordingFilePath = "";
private DateTime _lastSoundTime = DateTime.MinValue;
private Timer? _silenceTimer = null;
private volatile bool _isHangUpKeyPressed = false;
// 背景音乐相关
private WaveOutEvent? _backgroundMusicDevice = null;
private AudioFileReader? _backgroundMusicReader = null;
private CancellationTokenSource? _backgroundMusicCts = null;
private volatile bool _isBackgroundMusicPlaying = false;
// 风铃提示音相关
private WaveOutEvent? _windChimeDevice = null;
private AudioFileReader? _windChimeReader = null;
private CancellationTokenSource? _windChimeCts = null;
private volatile bool _isWindChimePlaying = false;
private Timer? _windChimeTimer = null;
private DateTime _lastSpeechTime = DateTime.MinValue;
[StructLayout(LayoutKind.Sequential)]
private struct KBDLLHOOKSTRUCT
{
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string? lpModuleName);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
/// <summary>
/// 构造函数
/// </summary>
/// <param name="signalRService">SignalR服务</param>
/// <param name="audioFileService">音频文件服务</param>
public PhoneBoothService(ISignalRService signalRService, IAudioFileService audioFileService)
{
_signalRService = signalRService;
_audioFileService = audioFileService;
// 获取初始配置
_config = _signalRService.GetConfigAsync().GetAwaiter().GetResult();
// 注册事件处理
//_signalRService.ConfigUpdated += SignalRService_ConfigUpdated;
//_signalRService.AudioFileUpdated += SignalRService_AudioFileUpdated;
}
/// <summary>
/// 处理配置更新事件
/// </summary>
private void SignalRService_ConfigUpdated(object? sender, PhoneBoothConfig e)
{
ReloadConfigAsync(e).ConfigureAwait(false);
}
/// <summary>
/// 处理音频文件更新事件
/// </summary>
private void SignalRService_AudioFileUpdated(object? sender, (string FileName, byte[] FileData) e)
{
_audioFileService.UpdateAudioFileAsync(e.FileName, e.FileData).ConfigureAwait(false);
}
/// <summary>
/// 初始化服务
/// </summary>
public async Task InitializeAsync()
{
// 初始化定时器
_resetTimer = new Timer(CheckResetTimeout, null, Timeout.Infinite, Timeout.Infinite);
_dialOutTimer = new Timer(CheckDialOutTimeout, null, Timeout.Infinite, Timeout.Infinite);
// 确保音频目录存在
_audioFileService.EnsureAudioDirectoryExists(_config.AudioFiles.AudioBasePath);
// 初始化音频设备
InitializeAudioDevice();
}
/// <summary>
/// 启动服务
/// </summary>
public async Task StartAsync()
{
try
{
Console.WriteLine("电话亭服务启动中...");
// 启动键盘监听
_ = StartKeyboardListener();
// 开始播放等待音
await StartPlayingWaitingTone();
Console.WriteLine("电话亭服务已启动");
}
catch (Exception ex)
{
Console.WriteLine($"启动服务失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 停止服务
/// </summary>
public async Task StopAsync()
{
try
{
Console.WriteLine("正在停止电话亭服务...");
// 取消所有操作
_programCts.Cancel();
// 停止等待音
if (_isWaitingTonePlaying)
{
_waitingToneCts?.Cancel();
await Task.Delay(100); // 给一点时间让等待音停止
}
// 停止背景音乐
if (_isBackgroundMusicPlaying)
{
StopBackgroundMusic();
}
// 停止录音
if (_isRecording)
{
StopAudioRecording();
}
// 卸载键盘钩子
if (_hookHandle != IntPtr.Zero)
{
UnhookWindowsHookEx(_hookHandle);
_hookHandle = IntPtr.Zero;
}
// 释放音频设备
DisposeAudioDevices();
// 停止和释放定时器
_resetTimer?.Dispose();
_resetTimer = null;
_dialOutTimer?.Dispose();
_dialOutTimer = null;
_silenceTimer?.Dispose();
_silenceTimer = null;
Console.WriteLine("电话亭服务已停止");
}
catch (Exception ex)
{
Console.WriteLine($"停止服务失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 重新加载配置
/// </summary>
public async Task ReloadConfigAsync(PhoneBoothConfig config)
{
// 保存新配置
_config = config;
Console.WriteLine("已重新加载配置");
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
try
{
// 停止服务
StopAsync().GetAwaiter().GetResult();
// 取消订阅事件
if (_signalRService != null)
{
_signalRService.ConfigUpdated -= SignalRService_ConfigUpdated;
_signalRService.AudioFileUpdated -= SignalRService_AudioFileUpdated;
}
// 释放取消令牌源
_programCts.Dispose();
_waitingToneCts?.Dispose();
_backgroundMusicCts?.Dispose();
GC.SuppressFinalize(this);
}
catch (Exception ex)
{
Console.WriteLine($"释放资源失败: {ex.Message}");
}
}
/// <summary>
/// 初始化音频设备
/// </summary>
private void InitializeAudioDevice()
{
try
{
_waitingToneDevice = new WaveOutEvent();
_waitingToneReader = new AudioFileReader(_config.AudioFiles.GetFullPath(_config.AudioFiles.WaitingToneFile));
_waitingToneDevice.Init(_waitingToneReader);
// 设置待机嘟声音量确保在0.0-1.0范围内
_waitingToneDevice.Volume = Math.Clamp(_config.AudioFiles.WaitingToneVolume > 0 ? _config.AudioFiles.WaitingToneVolume : 1.0f, 0.0f, 1.0f);
}
catch (Exception ex)
{
Console.WriteLine($"初始化音频设备失败: {ex.Message}");
}
}
/// <summary>
/// 释放音频设备
/// </summary>
private void DisposeAudioDevices()
{
_waitingToneCts?.Cancel();
_waitingToneCts?.Dispose();
_waitingToneCts = null;
if (_waitingToneDevice != null)
{
_waitingToneDevice.Stop();
_waitingToneDevice.Dispose();
_waitingToneDevice = null;
}
if (_waitingToneReader != null)
{
_waitingToneReader.Dispose();
_waitingToneReader = null;
}
// 释放背景音乐设备
_backgroundMusicCts?.Cancel();
_backgroundMusicCts?.Dispose();
_backgroundMusicCts = null;
if (_backgroundMusicDevice != null)
{
_backgroundMusicDevice.Stop();
_backgroundMusicDevice.Dispose();
_backgroundMusicDevice = null;
}
if (_backgroundMusicReader != null)
{
_backgroundMusicReader.Dispose();
_backgroundMusicReader = null;
}
_isBackgroundMusicPlaying = false;
// 释放风铃提示音设备
_windChimeCts?.Cancel();
_windChimeCts?.Dispose();
_windChimeCts = null;
if (_windChimeDevice != null)
{
_windChimeDevice.Stop();
_windChimeDevice.Dispose();
_windChimeDevice = null;
}
if (_windChimeReader != null)
{
_windChimeReader.Dispose();
_windChimeReader = null;
}
_isWindChimePlaying = false;
_windChimeTimer?.Dispose();
_windChimeTimer = null;
foreach (var (device, reader) in _keyToneDevices.Values)
{
device.Stop();
device.Dispose();
reader.Dispose();
}
_keyToneDevices.Clear();
Console.WriteLine("音频设备已释放");
}
/// <summary>
/// 检查重置超时
/// </summary>
private void CheckResetTimeout(object? state)
{
try
{
// 如果正在录音中,不执行重置操作
if (_isRecording)
{
return;
}
if (_pressedKeys.Count > 0 && _pressedKeys.Count < _config.Dial.MinDigitsToDialOut &&
(DateTime.Now - _lastKeyPressTime).TotalSeconds >= _config.Dial.ResetTimeoutSeconds)
{
Console.WriteLine($"{_config.Dial.ResetTimeoutSeconds}秒内未完成{_config.Dial.MinDigitsToDialOut}位数字输入,重置等待...");
_pressedKeys.Clear();
_keyToneDevices.Clear();
_ = StartPlayingWaitingTone();
}
}
catch (Exception ex)
{
Console.WriteLine($"检查超时重置失败: {ex.Message}");
}
}
/// <summary>
/// 检查拨出超时
/// </summary>
private void CheckDialOutTimeout(object? state)
{
try
{
// 如果正在录音中,不执行拨出操作
if (_isRecording)
{
return;
}
// 检查是否已经过了配置的秒数且按键数大于等于配置的最小位数
int minDigits = _config.Dial.MinDigitsToDialOut;
int autoDialSeconds = _config.Dial.AutoDialOutAfterSeconds;
if (_pressedKeys.Count >= minDigits &&
(DateTime.Now - _lastKeyPressTime).TotalSeconds >= autoDialSeconds)
{
Console.WriteLine($"{autoDialSeconds}秒内无新按键,开始拨出...");
// 非阻塞方式启动录音
_ = StartRecording();
}
}
catch (Exception ex)
{
Console.WriteLine($"检查拨出超时失败: {ex.Message}");
}
}
/// <summary>
/// 开始录音流程
/// </summary>
private async Task StartRecording()
{
// 创建一个CancellationTokenSource用于取消整个录音流程
using var recordingCts = CancellationTokenSource.CreateLinkedTokenSource(_programCts.Token);
// 创建一个单独的任务来监视挂断信号
var hangupMonitorTask = Task.Run(async () =>
{
while (!recordingCts.Token.IsCancellationRequested)
{
// 检查是否收到挂断信号
if (_isHangUpKeyPressed)
{
Console.WriteLine("录音流程中检测到挂断信号,即将终止流程...");
recordingCts.Cancel();
break;
}
await Task.Delay(50);
}
});
try
{
// 进入录音状态,停止所有定时器
_isRecording = true;
_isHangUpKeyPressed = false;
lock (_timerLock)
{
_resetTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_dialOutTimer?.Change(Timeout.Infinite, Timeout.Infinite);
}
// 确保等待音已停止
if (_isWaitingTonePlaying)
{
_waitingToneCts?.Cancel();
// 等待等待音实际停止
while (_isWaitingTonePlaying && !recordingCts.Token.IsCancellationRequested)
{
await Task.Delay(10);
}
}
// 如果已经收到取消信号,立即结束
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
Console.WriteLine("开始录音流程...");
// 随机播放指定秒数的等待接电话音频
int waitMin = _config.CallFlow.WaitForPickupMinSeconds;
int waitMax = _config.CallFlow.WaitForPickupMaxSeconds;
int waitTime = new Random().Next(waitMin, waitMax + 1);
Console.WriteLine($"播放等待接电话音频,持续{waitTime}秒...");
try
{
await PlayAudioAndWait(
_config.AudioFiles.GetFullPath(_config.AudioFiles.WaitForPickupFile),
waitTime * 1000, true, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
// 如果已经收到取消信号,立即结束
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
// 根据配置的概率播放电话接起音频
bool playPickup = new Random().NextDouble() < _config.CallFlow.PlayPickupProbability;
if (playPickup)
{
Console.WriteLine("播放电话接起音频...");
try
{
await PlayAudioAndWait(
_config.AudioFiles.GetFullPath(_config.AudioFiles.PhonePickupFile),
null, false, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
}
// 播放提示用户录音的音频
Console.WriteLine("播放提示用户录音音频...");
try
{
await PlayAudioAndWait(
_config.AudioFiles.GetFullPath(_config.AudioFiles.PromptUserRecordFile),
null, false, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
// 开始实际录音逻辑
Console.WriteLine("开始初始化录音设备...");
// 创建录音文件路径
string recordingsFolder = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
_config.Recording.RecordingFolder);
if (!Directory.Exists(recordingsFolder))
{
Directory.CreateDirectory(recordingsFolder);
}
_recordingFilePath = Path.Combine(
recordingsFolder,
$"recording_{DateTime.Now:yyyyMMdd_HHmmss}.wav");
Console.WriteLine("正在初始化录音设备...");
// 初始化录音设备
await StartAudioRecording(_recordingFilePath);
Console.WriteLine("正在录音中...");
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
// 创建一个等待完成的任务源
var recordingCompletionSource = new TaskCompletionSource<bool>();
// 启动静音检测计时器
_lastSoundTime = DateTime.Now;
_lastSpeechTime = DateTime.Now; // 初始化最后说话时间
_silenceTimer = new Timer(CheckSilence, recordingCompletionSource, 1000, 1000);
// 启动风铃提示计时器
_windChimeTimer = new Timer(CheckWindChimePrompt, null, 1000, 1000);
Console.WriteLine("播放滴提示音...");
try
{
await PlayAudioAndWait(
_config.AudioFiles.GetFullPath(_config.AudioFiles.BeepPromptFile),
null, false, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
Console.WriteLine("提示音播放完成,等待用户说话结束...");
// 创建一个任务等待录音结束(通过静音检测、挂断按键或取消)
var recordingTask = Task.Run(async () =>
{
try
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(recordingCts.Token);
// 注册取消处理程序
linkedCts.Token.Register(() =>
{
recordingCompletionSource.TrySetResult(true);
});
// 启动背景音乐播放(不等待它完成)
if (_config.Recording.EnableBackgroundMusic)
{
_ = StartPlayingBackgroundMusic(linkedCts.Token);
}
// 等待录音完成
await recordingCompletionSource.Task;
}
catch (Exception ex)
{
// 捕获所有异常,确保不会中断主流程
Console.WriteLine($"录音任务异常: {ex.Message}");
}
});
// 等待录音完成或取消
try
{
await Task.WhenAny(recordingTask, Task.Delay(Timeout.Infinite, recordingCts.Token));
}
catch (OperationCanceledException)
{
// 预期的取消异常,可以忽略
}
// 停止录音
StopAudioRecording();
// 确保背景音乐停止
StopBackgroundMusic();
Console.WriteLine("录音结束,处理中...");
// 尝试上传录音文件到服务器
if (File.Exists(_recordingFilePath))
{
_ = Task.Run(async () =>
{
try
{
Console.WriteLine("正在上传录音文件到服务器...");
bool success = await _signalRService.UploadRecordingAsync(_recordingFilePath);
if (success)
{
Console.WriteLine("录音文件上传成功");
}
else
{
Console.WriteLine("录音文件上传失败");
}
}
catch (Exception ex)
{
Console.WriteLine($"上传录音文件时发生错误: {ex.Message}");
}
});
}
// 检查是否由用户挂断或静音触发的结束
bool isUserHangup = _isHangUpKeyPressed || (DateTime.Now - _lastSoundTime).TotalSeconds >= _config.Recording.SilenceTimeoutSeconds;
// 根据配置决定是重置系统还是退出程序
if (isUserHangup && !_config.CallFlow.ResetSystemAfterHangup)
{
Console.WriteLine("用户挂断或静音超时,根据配置将退出程序...");
// 完全重置系统状态
//CompletelyResetState();
// 确保所有资源被释放
StopAsync().Wait();
// 退出程序
Environment.Exit(0);
}
else
{
// 完全重置系统状态
CompletelyResetState();
Console.WriteLine("系统已重置,可以继续使用...");
}
}
catch (OperationCanceledException ex)
{
Console.WriteLine($"录音流程被取消: {ex.Message}");
// 确保录音设备被释放
StopAudioRecording();
// 检查是否需要退出程序
if (_isHangUpKeyPressed && !_config.CallFlow.ResetSystemAfterHangup)
{
Console.WriteLine("用户挂断,根据配置将退出程序...");
// 确保所有资源被释放
StopAsync().Wait();
// 退出程序
Environment.Exit(0);
}
else
{
// 取消时也完全重置系统状态
CompletelyResetState();
Console.WriteLine("系统已重置,可以继续使用...");
}
}
catch (Exception ex)
{
Console.WriteLine($"录音过程发生错误: {ex.Message}");
// 确保录音设备被释放
StopAudioRecording();
// 发生错误时也完全重置系统状态
CompletelyResetState();
}
}
/// <summary>
/// 检查静音
/// </summary>
private void CheckSilence(object? state)
{
try
{
var completionSource = state as TaskCompletionSource<bool>;
// 如果用户按了回车键,且配置允许用户挂断,立即结束录音
if (_isHangUpKeyPressed && _config.Recording.AllowUserHangup)
{
Console.WriteLine("定时器检测到用户手动挂断");
completionSource?.TrySetResult(true);
return;
}
// 检查是否超过配置秒数没有声音
int silenceTimeout = _config.Recording.SilenceTimeoutSeconds;
if ((DateTime.Now - _lastSoundTime).TotalSeconds >= silenceTimeout)
{
Console.WriteLine($"检测到{silenceTimeout}秒无声音,自动挂断");
// 将静音超时也视为用户挂断的一种形式
_isHangUpKeyPressed = true;
completionSource?.TrySetResult(true);
}
}
catch (Exception ex)
{
Console.WriteLine($"静音检测错误: {ex.Message}");
}
}
/// <summary>
/// 启动音频录制
/// </summary>
private async Task StartAudioRecording(string filePath)
{
try
{
// 创建录音设备
_waveIn = new WaveInEvent
{
DeviceNumber = _config.Recording.RecordingDeviceNumber,
WaveFormat = new WaveFormat(
_config.Recording.SampleRate,
_config.Recording.Channels),
BufferMilliseconds = _config.Recording.BufferMilliseconds
};
// 创建文件写入器
_waveWriter = new WaveFileWriter(filePath, _waveIn.WaveFormat);
// 如果SignalR连接可用通知服务器开始接收音频流
if (_signalRService.IsConnected)
{
await _signalRService.StartAudioStreamAsync(_waveIn.WaveFormat.SampleRate, _waveIn.WaveFormat.Channels);
}
// 处理录音数据
_waveIn.DataAvailable += (s, e) =>
{
try
{
// 将数据写入文件
_waveWriter.Write(e.Buffer, 0, e.BytesRecorded);
// 检查是否有声音(仅用于更新最后音频时间,不影响上传)
if (HasSound(e.Buffer, e.BytesRecorded))
{
_lastSoundTime = DateTime.Now;
}
// 实时上传音频数据到服务器(无论是否有声音都上传)
if (_signalRService.IsConnected)
{
// 创建音频数据的副本以避免并发问题
byte[] audioDataCopy = new byte[e.BytesRecorded];
Buffer.BlockCopy(e.Buffer, 0, audioDataCopy, 0, e.BytesRecorded);
// 非阻塞方式上传音频数据
_ = Task.Run(async () =>
{
try
{
await _signalRService.UploadAudioDataRealtimeAsync(audioDataCopy);
}
catch (Exception ex)
{
Console.WriteLine($"实时音频数据上传出错: {ex.Message}");
}
});
}
}
catch (Exception ex)
{
Console.WriteLine($"录音数据处理错误: {ex.Message}");
}
};
// 录音完成事件
_waveIn.RecordingStopped += (s, e) =>
{
// 在这里处理录音停止后的逻辑
Console.WriteLine("录音已停止");
// 通知服务器结束音频流
if (_signalRService.IsConnected)
{
_ = _signalRService.EndAudioStreamAsync();
}
};
// 开始录音
_waveIn.StartRecording();
Console.WriteLine($"开始录音,保存到文件: {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"启动录音失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 停止音频录制
/// </summary>
private void StopAudioRecording()
{
try
{
// 停止静音检测计时器
_silenceTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_silenceTimer?.Dispose();
_silenceTimer = null;
// 停止风铃提示音计时器
_windChimeTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_windChimeTimer?.Dispose();
_windChimeTimer = null;
// 停止风铃提示音
_ = StopWindChime(false);
// 停止录音
if (_waveIn != null)
{
_waveIn.StopRecording();
_waveIn.Dispose();
_waveIn = null;
}
// 关闭文件
if (_waveWriter != null)
{
_waveWriter.Dispose();
_waveWriter = null;
Console.WriteLine($"录音已保存到: {_recordingFilePath}");
}
// 确保结束音频流
if (_signalRService.IsConnected)
{
_ = _signalRService.EndAudioStreamAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"停止录音失败: {ex.Message}");
}
}
public static DateTime _lasHasoudDateTime = DateTime.Now;
public static bool _isSpeaking = false;
/// <summary>
/// 检查是否有声音
/// </summary>
private 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;
}
}
}
bool isSpeaking = maxVolume > _config.Recording.SilenceThreshold;
if (isSpeaking && !_isSpeaking)
{
_isSpeaking = true;
}
if (DateTime.Now.Subtract(_lasHasoudDateTime).TotalSeconds > 1)
{
Console.WriteLine($"当前音量:{maxVolume},设置的音量灵敏度:{_config.Recording.SilenceThreshold},是否已监测到在说话中:{_isSpeaking},风铃提示音:{_windChimeDevice?.Volume ?? 0}bgm:{_backgroundMusicDevice?.Volume ?? 0}");
_lasHasoudDateTime = DateTime.Now;
_isSpeaking = false;
}
// 如果监测到说话,更新最后说话时间
if (isSpeaking)
{
_lastSpeechTime = DateTime.Now;
// 如果风铃声正在播放,先停止风铃声
if (_isWindChimePlaying)
{
Console.WriteLine("检测到用户说话,停止风铃声...");
_ = StopWindChime(true); // 平滑淡出
// 风铃声停止后,重新启动背景音乐
if (!_isBackgroundMusicPlaying && _config.Recording.EnableBackgroundMusic)
{
Console.WriteLine("重新启动背景音乐...");
_ = StartPlayingBackgroundMusic(_programCts.Token);
}
}
}
// 如果最大音量超过阈值,则认为有声音
return isSpeaking;
}
/// <summary>
/// 完全重置状态
/// </summary>
private void CompletelyResetState()
{
try
{
// 确保录音设备被释放
StopAudioRecording();
// 确保背景音乐被停止
StopBackgroundMusic();
// 确保风铃提示音被停止
_ = StopWindChime(false);
// 清除录音状态
_isRecording = false;
_isHangUpKeyPressed = false;
// 清除按键记录
_pressedKeys.Clear();
_currentPressedKeys.Clear();
// 停止所有按键音
foreach (var (digit, deviceInfo) in _keyToneDevices.ToArray())
{
try
{
_keyToneDevices.TryRemove(digit, out _);
deviceInfo.Device.Stop();
deviceInfo.Device.Dispose();
deviceInfo.Reader.Dispose();
}
catch (Exception ex)
{
Console.WriteLine($"清理按键音设备失败: {ex.Message}");
}
}
// 重新开始播放等待音
_ = StartPlayingWaitingTone();
}
catch (Exception ex)
{
Console.WriteLine($"重置状态失败: {ex.Message}");
// 最后的保障措施
_isRecording = false;
_isHangUpKeyPressed = false;
_pressedKeys.Clear();
_currentPressedKeys.Clear();
_keyToneDevices.Clear();
// 尝试停止背景音乐
try
{
StopBackgroundMusic();
}
catch
{
// 忽略任何错误
}
// 尝试重新开始播放等待音
try
{
_ = StartPlayingWaitingTone();
}
catch
{
// 忽略任何错误
}
}
}
/// <summary>
/// 开始播放等待音
/// </summary>
private async Task StartPlayingWaitingTone()
{
try
{
_waitingToneCts?.Cancel();
_waitingToneCts?.Dispose();
_waitingToneCts = CancellationTokenSource.CreateLinkedTokenSource(_programCts.Token);
var token = _waitingToneCts.Token;
_isWaitingTonePlaying = true;
while (!token.IsCancellationRequested)
{
try
{
if (_waitingToneReader?.Position >= _waitingToneReader?.Length)
{
_waitingToneReader.Position = 0;
}
_waitingToneDevice?.Play();
await Task.Delay(100, token);
}
catch (OperationCanceledException)
{
break;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"播放等待音失败: {ex.Message}");
}
finally
{
_isWaitingTonePlaying = false;
_waitingToneDevice?.Stop();
}
}
/// <summary>
/// 启动键盘监听
/// </summary>
private async Task StartKeyboardListener()
{
while (!_programCts.Token.IsCancellationRequested)
{
try
{
// 检查回车键
bool isEnterPressed = (GetAsyncKeyState(VK_RETURN) & 0x8000) != 0;
if (isEnterPressed)
{
// 添加一点延迟,防止连续检测到按键
await Task.Delay(20, _programCts.Token);
// 再次检查回车键状态,确保不是误触或抖动
bool stillPressed = (GetAsyncKeyState(VK_RETURN) & 0x8000) != 0;
if (!stillPressed)
{
continue;
}
if (_isRecording)
{
// 检查是否允许用户挂断
if (_config.Recording.AllowUserHangup)
{
// 如果正在录音中,回车键被视为挂断信号
Console.WriteLine("检测到回车键,用户挂断...");
_isHangUpKeyPressed = true;
// 等待一段时间,防止重复触发
await Task.Delay(200, _programCts.Token);
}
else
{
Console.WriteLine("当前配置不允许用户手动挂断");
}
continue;
}
// 如果等待音正在播放,停止它
if (_isWaitingTonePlaying)
{
_waitingToneCts?.Cancel();
// 确保等待音停止
while (_isWaitingTonePlaying)
{
await Task.Delay(10);
}
}
Console.WriteLine("检测到回车键,开始拨出...");
// 非阻塞方式启动录音,不等待其完成
_ = StartRecording();
// 延迟防止重复触发
await Task.Delay(500, _programCts.Token);
continue;
}
foreach (int key in _numpadKeys)
{
int digit = key - 0x60; // 将虚拟键码转换为数字
bool isKeyDown = (GetAsyncKeyState(key) & 0x8000) != 0;
if (isKeyDown)
{
if (_isRecording)
{
Console.WriteLine("正在录音中,无法点击按键...");
await Task.Delay(1000, _programCts.Token);
continue;
}
if (!_currentPressedKeys.Contains(digit))
{
Console.WriteLine($"按下数字键: {digit}");
_currentPressedKeys.Add(digit);
await HandleDigitKeyPress(digit);
}
}
else
{
if (_currentPressedKeys.Contains(digit))
{
Console.WriteLine($"释放数字键: {digit}");
_currentPressedKeys.Remove(digit);
await HandleDigitKeyRelease(digit);
}
}
}
await Task.Delay(10, _programCts.Token); // 降低CPU使用率
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Console.WriteLine($"键盘监听出错: {ex.Message}");
}
}
}
/// <summary>
/// 处理数字按键按下
/// </summary>
private async Task HandleDigitKeyPress(int digit)
{
try
{
// 记录按键
_pressedKeys.Add(digit);
_lastKeyPressTime = DateTime.Now;
Console.WriteLine($"按下数字键: {digit}, 当前已按键数: {_pressedKeys.Count}");
// 如果是第一个按键,停止等待音
if (_pressedKeys.Count == 1 && _isWaitingTonePlaying)
{
_waitingToneCts?.Cancel();
}
// 播放按键音
await PlayKeyTone(digit);
// 重置定时器
lock (_timerLock)
{
_resetTimer?.Change(_config.Dial.ResetTimeoutSeconds * 1000, Timeout.Infinite);
}
// 检查是否需要拨出(达到配置的最小位数且指定秒数内无新按键)
int minDigits = _config.Dial.MinDigitsToDialOut;
if (_pressedKeys.Count >= minDigits)
{
// 重置拨出定时器,指定秒数后检查是否需要拨出
lock (_timerLock)
{
_dialOutTimer?.Change(_config.Dial.AutoDialOutAfterSeconds * 1000, Timeout.Infinite);
}
}
else
{
// 如果位数不足最小位数,停止拨出定时器
lock (_timerLock)
{
_dialOutTimer?.Change(Timeout.Infinite, Timeout.Infinite);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"处理按键 {digit} 失败: {ex.Message}");
}
}
/// <summary>
/// 处理数字按键释放
/// </summary>
private async Task HandleDigitKeyRelease(int digit)
{
try
{
// 当按键释放时,我们不做特殊处理
// 按键音的播放和结束完全由PlayKeyTone中的逻辑控制
// 这样确保了即使按键时间很短,声音也能够完整播放
// 我们只需要确认按键已经从当前按下状态列表中移除即可
// 实际的音频资源释放由PlayKeyTone中的PlaybackStopped事件处理
}
catch (Exception ex)
{
Console.WriteLine($"处理按键 {digit} 释放失败: {ex.Message}");
}
}
/// <summary>
/// 播放按键音
/// </summary>
private async Task PlayKeyTone(int digit)
{
try
{
// 如果该按键声音正在播放中,不再重新开始播放
if (_keyToneDevices.ContainsKey(digit))
{
// 如果已经在播放中,可以选择重新开始播放,或者保持当前播放
// 这里我们选择重新开始播放
if (_keyToneDevices.TryRemove(digit, out var oldDeviceInfo))
{
// 安全地停止并释放旧设备
try
{
oldDeviceInfo.Device.Stop();
oldDeviceInfo.Device.Dispose();
oldDeviceInfo.Reader.Dispose();
}
catch (Exception ex)
{
Console.WriteLine($"停止旧按键音 {digit} 失败: {ex.Message}");
}
}
}
var device = new WaveOutEvent();
var reader = new AudioFileReader(_config.AudioFiles.GetDigitToneFilePath(digit));
device.Init(reader);
// 设置按键音量确保在0.0-1.0范围内
device.Volume = Math.Clamp(_config.AudioFiles.KeyToneVolume > 0 ? _config.AudioFiles.KeyToneVolume : 1.0f, 0.0f, 1.0f);
// 记录开始播放时间
var startTime = DateTime.Now;
int minPlayTime = _config.AudioFiles.MinKeyTonePlayTimeMs;
if (_keyToneDevices.TryAdd(digit, (device, reader)))
{
device.Play();
// 确保至少播放最小时长
device.PlaybackStopped += async (s, e) =>
{
// 计算已经播放的时间
var playedTime = (int)(DateTime.Now - startTime).TotalMilliseconds;
// 如果播放时间不足最小时间,延迟处理
if (playedTime < minPlayTime)
{
try
{
// 如果播放已经停止但时间不够,先不删除设备
// 等待剩余的时间后再处理
var remainingTime = minPlayTime - playedTime;
await Task.Delay(remainingTime);
}
catch (Exception ex)
{
Console.WriteLine($"按键音 {digit} 最小播放时间延迟失败: {ex.Message}");
}
}
// 播放完成后(包括可能的延迟),移除并释放资源
if (_keyToneDevices.TryRemove(digit, out var deviceInfo) && deviceInfo.Device == device)
{
deviceInfo.Device.Dispose();
deviceInfo.Reader.Dispose();
}
};
}
else
{
device.Dispose();
reader.Dispose();
}
}
catch (Exception ex)
{
Console.WriteLine($"播放按键音 {digit} 失败: {ex.Message}");
}
}
/// <summary>
/// 播放音频并等待
/// </summary>
private async Task PlayAudioAndWait(string audioPath, int? maxDuration = null, bool loop = false, CancellationToken token = default, float? volume = null)
{
WaveOutEvent? device = null;
AudioFileReader? reader = null;
try
{
device = new WaveOutEvent();
reader = new AudioFileReader(audioPath);
// 如果没有指定音量,根据音频文件类型设置音量
if (volume == null)
{
// 根据文件名判断音频类型并设置对应的音量
if (audioPath.Contains(_config.AudioFiles.WaitForPickupFile))
{
// 拨出嘟声音量
device.Volume = Math.Clamp(_config.AudioFiles.DialToneVolume > 0 ? _config.AudioFiles.DialToneVolume : 1.0f, 0.0f, 1.0f);
}
else if (audioPath.Contains(_config.AudioFiles.PromptUserRecordFile))
{
// 提示用户录音的音量
device.Volume = Math.Clamp(_config.AudioFiles.PromptAfterBeepVolume > 0 ? _config.AudioFiles.PromptAfterBeepVolume : 1.0f, 0.0f, 1.0f);
}
else if (audioPath.Contains(_config.AudioFiles.BeepPromptFile))
{
// 滴提示音的音量
device.Volume = Math.Clamp(_config.AudioFiles.BeepPromptVolume > 0 ? _config.AudioFiles.BeepPromptVolume : 1.0f, 0.0f, 1.0f);
}
else if (audioPath.Contains(_config.AudioFiles.WaitingToneFile))
{
// 待机嘟声音量 (虽然这个可能不会通过这个方法播放)
device.Volume = Math.Clamp(_config.AudioFiles.WaitingToneVolume > 0 ? _config.AudioFiles.WaitingToneVolume : 1.0f, 0.0f, 1.0f);
}
else
{
// 默认音量为1.0
device.Volume = 1.0f;
}
}
else
{
device.Volume = Math.Clamp(volume.Value, 0.0f, 1.0f);
}
device.Init(reader);
// 如果需要循环播放,创建一个循环播放的任务
if (loop && maxDuration.HasValue)
{
var startTime = DateTime.Now;
var endTime = startTime.AddMilliseconds(maxDuration.Value);
// 开始播放
device.Play();
// 循环播放直到达到指定时间
while (DateTime.Now < endTime && !token.IsCancellationRequested)
{
// 如果到达文件末尾,重新开始播放
if (reader.Position >= reader.Length)
{
reader.Position = 0;
}
// 如果没有在播放中,重新开始播放
if (device.PlaybackState != PlaybackState.Playing)
{
reader.Position = 0;
device.Play();
}
// 短暂等待避免CPU占用过高
await Task.Delay(50, token);
}
// 时间到,停止播放
device.Stop();
}
else if (maxDuration.HasValue)
{
// 开始播放
device.Play();
// 等待指定时间
await Task.Delay(maxDuration.Value, token);
// 时间到,停止播放
device.Stop();
}
else
{
// 创建一个TaskCompletionSource等待播放完成
var completionSource = new TaskCompletionSource<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();
}
}
/// <summary>
/// 开始播放背景音乐
/// </summary>
private async Task StartPlayingBackgroundMusic(CancellationToken token)
{
try
{
// 如果配置不启用背景音乐,则直接返回
if (!_config.Recording.EnableBackgroundMusic)
{
Console.WriteLine("背景音乐功能已禁用");
return;
}
// 获取背景音乐文件路径
string musicFilePath = _config.AudioFiles.GetFullPath(_config.Recording.BackgroundMusicFile);
// 检查文件是否存在
if (!File.Exists(musicFilePath))
{
Console.WriteLine($"背景音乐文件不存在: {musicFilePath}");
return;
}
// 取消可能正在播放的背景音乐
_backgroundMusicCts?.Cancel();
_backgroundMusicCts?.Dispose();
_backgroundMusicCts = CancellationTokenSource.CreateLinkedTokenSource(token);
// 初始化背景音乐设备
_backgroundMusicDevice = new WaveOutEvent();
_backgroundMusicReader = new AudioFileReader(musicFilePath);
// 初始化设备
_backgroundMusicDevice.Init(_backgroundMusicReader);
// 设置音量到设备而非Reader对象确保独立控制默认为10%
_backgroundMusicDevice.Volume = Math.Clamp(_config.Recording.BackgroundMusicVolume, 0.0f, 1.0f);
// 标记背景音乐开始播放
_isBackgroundMusicPlaying = true;
Console.WriteLine($"开始播放背景音乐,音量: {_backgroundMusicDevice.Volume * 100}%");
// 循环播放背景音乐
while (!_backgroundMusicCts.Token.IsCancellationRequested)
{
try
{
// 检查是否到达文件末尾,是则重置
if (_backgroundMusicReader.Position >= _backgroundMusicReader.Length)
{
_backgroundMusicReader.Position = 0;
}
// 如果没有在播放,则开始播放
if (_backgroundMusicDevice.PlaybackState != PlaybackState.Playing)
{
_backgroundMusicDevice.Play();
}
// 短暂等待避免CPU占用过高
await Task.Delay(100, _backgroundMusicCts.Token);
}
catch (OperationCanceledException)
{
// 正常取消,退出循环
break;
}
catch (Exception ex)
{
Console.WriteLine($"背景音乐播放出错: {ex.Message}");
// 短暂等待后继续尝试
await Task.Delay(1000, _backgroundMusicCts.Token);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"启动背景音乐失败: {ex.Message}");
}
finally
{
// 停止并释放音乐设备
StopBackgroundMusic();
}
}
/// <summary>
/// 停止背景音乐
/// </summary>
private void StopBackgroundMusic()
{
try
{
// 取消播放任务
_backgroundMusicCts?.Cancel();
// 停止并释放设备
if (_backgroundMusicDevice != null)
{
_backgroundMusicDevice.Stop();
_backgroundMusicDevice.Dispose();
_backgroundMusicDevice = null;
}
if (_backgroundMusicReader != null)
{
_backgroundMusicReader.Dispose();
_backgroundMusicReader = null;
}
// 标记背景音乐已停止
_isBackgroundMusicPlaying = false;
Console.WriteLine("背景音乐已停止");
}
catch (Exception ex)
{
Console.WriteLine($"停止背景音乐失败: {ex.Message}");
}
}
/// <summary>
/// 检查是否需要播放风铃提示音
/// </summary>
private void CheckWindChimePrompt(object? state)
{
try
{
// 如果用户已挂断或者录音已停止,不执行操作
if (_isHangUpKeyPressed || !_isRecording)
{
return;
}
// 检查是否超过配置的无声音时间但还未达到挂断时间
float windChimePromptSeconds = _config.Recording.WindChimePromptSeconds;
int silenceTimeoutSeconds = _config.Recording.SilenceTimeoutSeconds;
double secondsSinceLastSpeech = (DateTime.Now - _lastSpeechTime).TotalSeconds;
// 如果超过风铃提示时间但未达到挂断时间,并且风铃没在播放,开始播放风铃
if (secondsSinceLastSpeech >= windChimePromptSeconds &&
secondsSinceLastSpeech < silenceTimeoutSeconds &&
!_isWindChimePlaying)
{
Console.WriteLine($"检测到{windChimePromptSeconds}秒无声音,先停止背景音乐,再播放风铃提示音...");
// 先停止背景音乐
if (_isBackgroundMusicPlaying)
{
StopBackgroundMusic();
}
// 然后播放风铃提示音
_ = PlayWindChime();
}
}
catch (Exception ex)
{
Console.WriteLine($"检查风铃提示音错误: {ex.Message}");
}
}
/// <summary>
/// 播放风铃提示音
/// </summary>
private async Task PlayWindChime()
{
try
{
// 如果已经在播放,先停止
if (_isWindChimePlaying)
{
await StopWindChime(true); // 平滑淡出
}
// 防止重复启动
if (_isWindChimePlaying)
{
return;
}
_windChimeCts?.Cancel();
_windChimeCts?.Dispose();
_windChimeCts = CancellationTokenSource.CreateLinkedTokenSource(_programCts.Token);
var token = _windChimeCts.Token;
string windChimeFilePath = _config.AudioFiles.GetFullPath(_config.AudioFiles.WindChimeFile);
if (!File.Exists(windChimeFilePath))
{
Console.WriteLine($"风铃提示音文件不存在: {windChimeFilePath}");
return;
}
// 初始化播放设备(创建完全独立的音频流)
_windChimeDevice = new WaveOutEvent();
_windChimeReader = new AudioFileReader(windChimeFilePath);
_windChimeDevice.Init(_windChimeReader);
// 设置初始音量
float initialVolume = 0.8f; // 使用稍低于最大的音量,避免音量混合问题
_windChimeDevice.Volume = Math.Clamp(initialVolume, 0.0f, 1.0f);
_isWindChimePlaying = true;
Console.WriteLine($"开始播放风铃提示音,初始音量: {_windChimeDevice.Volume * 100}%");
// 开始播放
_windChimeDevice.Play();
// 启动循环播放任务
_ = Task.Run(async () =>
{
try
{
while (_isWindChimePlaying && !token.IsCancellationRequested)
{
// 如果到达音频文件末尾,循环播放
if (_windChimeReader != null && _windChimeReader.Position >= _windChimeReader.Length)
{
_windChimeReader.Position = 0;
}
// 确保风铃声仍在播放
if (_windChimeDevice != null && _windChimeDevice.PlaybackState != PlaybackState.Playing)
{
_windChimeDevice.Play();
}
await Task.Delay(100, token);
}
}
catch (OperationCanceledException)
{
// 正常取消
}
catch (Exception ex)
{
Console.WriteLine($"风铃提示音播放循环错误: {ex.Message}");
}
});
}
catch (Exception ex)
{
Console.WriteLine($"播放风铃提示音错误: {ex.Message}");
await StopWindChime(false); // 出错时直接停止,不淡出
}
}
/// <summary>
/// 停止风铃提示音
/// </summary>
/// <param name="fadeOut">是否平滑淡出</param>
private async Task StopWindChime(bool fadeOut)
{
try
{
if (!_isWindChimePlaying)
{
return; // 如果没有播放,不需要停止
}
// 如果需要淡出
if (fadeOut && _windChimeDevice != null)
{
int fadeOutMs = _config.Recording.WindChimeFadeOutMs;
float startVolume = _windChimeDevice.Volume;
int steps = 20; // 淡出的步骤数
int stepDelay = fadeOutMs / steps;
// 逐步降低音量实现淡出效果
for (int i = 0; i < steps; i++)
{
if (_windChimeDevice == null || !_isWindChimePlaying)
{
break; // 如果设备已释放或播放已停止,直接退出
}
float ratio = 1.0f - ((float)i / steps);
_windChimeDevice.Volume = startVolume * ratio;
await Task.Delay(stepDelay);
}
}
// 正式停止
_windChimeCts?.Cancel();
if (_windChimeDevice != null)
{
_windChimeDevice.Stop();
_windChimeDevice.Dispose();
_windChimeDevice = null;
}
if (_windChimeReader != null)
{
_windChimeReader.Dispose();
_windChimeReader = null;
}
_isWindChimePlaying = false;
Console.WriteLine("风铃提示音已停止");
}
catch (Exception ex)
{
Console.WriteLine($"停止风铃提示音错误: {ex.Message}");
_isWindChimePlaying = false;
}
}
}