ASP.NET那点不为人知的事(一)
放踊融胸前面几篇文章已经把机器人硬件控制部分的开发讲得差不多了包括屏幕控制、舵机驱动、语音交互等功能。但是之前的外形太过简单可动角度不够多所以我就新改进了一个版本叫VerdiBot阿荫详细视频介绍地址请点击链接。ESP32社区最火的AI对话机器人非小智AI莫属了所以为了让自己做的机器人对话部分也足够的生动我就重新实现了一个.NET版本的小智客户端打算后期集成更多的功能并整理成了一个完整的开源项目——Verdure Assistant绿荫助手这是一个基于.NET 9.0的多平台AI语音助手支持Windows桌面、Android移动端、命令行以及Web API等多种使用方式。这篇文章主要是给大家讲讲这个对话机器人项目的一些代码方便想尝试的小伙伴快速上手体验。项目代码已经开源了大家可以自己研究遇到问题也欢迎提Issue讨论。机器人图片GitHub项目地址https://github.com/maker-community/Verdure.Assistant问题解答Q: 之前为什么特意做树莓派wifi配网的功能A: 之前的博客有网友说我浪费生命开发wifi配网功能我在评论区也有讲过原因现在我在这里再讲一遍因为有时候我们拿着设备到新环境的时候并不能时刻有可用的显示器和鼠标键盘但是又需要联网这时就可以使用wifi配网了。然后ssh连接到设备上就可以像服务器一样控制了。Q: 支持哪些AI服务A: 目前主要对接的是小智AI服务后续计划支持更多AI服务的接入包括OpenAI等。项目采用了抽象设计扩展起来比较方便。Q: 项目使用什么技术栈A: 核心使用.NET 9.0跨平台UI用.NET MAUIWindows桌面使用的WinUI 3。网络音频编解码用的OpusSharp库音频录制播放使用的最近社区刚有人开源的的SoundFlow库这个库功能完善使用方便并且内置了多种音频格式解码的播放所以我用它替换了之前的PortAudioSharp2网络通信基于WebSocket和MQTT未测试。详细的技术点在GitHub的README里都有说明。Q: 为什么要重新实现这个项目A: 目前小智AI机器人有免费的服务端可以使用而且整个架构都很优雅对比我之前的实现优点很多所以重新实现一个客户端对于用户体验有很大的帮助并且协议是公开的以后如果想自己拓展实现服务端也是很轻松的。项目整体架构目录结构项目采用清晰的分层架构便于理解和扩展Verdure.Assistant/├── src/ # 源代码│ ├── Verdure.Assistant.Core/ # 核心库音频、网络、服务│ ├── Verdure.Assistant.ViewModels/ # 共享视图模型MVVM│ ├── Verdure.Assistant.Console/ # 控制台应用│ ├── Verdure.Assistant.WinUI/ # WinUI桌面应用│ ├── Verdure.Assistant.MAUI/ # MAUI移动应用│ └── Verdure.Assistant.Api/ # Web API服务├── tests/ # 测试项目├── docs/ # 技术文档└── scripts/ # 构建脚本GitHub项目地址https://github.com/maker-community/Verdure.Assistant核心功能模块语音交互模块使用微软的语音认知服务的关键词唤醒加载关键词唤醒模型文件不需要Azure订阅你好小电/你好小娜src/Verdure.Assistant.Core/Services/WakeWords/KeywordSpottingService.cs音频处理模块Opus编解码、SoundFlow音频播放、跨平台音频录制src/Verdure.Assistant.Core/Services/Audio/AudioDataDistributor.cssrc/Verdure.Assistant.Core/Services/Audio/OpusSharpAudioCodec.cssrc/Verdure.Assistant.Core/Services/Audio/SoundFlowAudioPlayer.cssrc/Verdure.Assistant.Core/Services/Audio/SoundFlowAudioRecorder.cs网络通信模块WebSocket实时通信、MQTT物联网协议src/Verdure.Assistant.Core/Services/Protocols/WebSocketClient.cs状态管理模块设备状态机、会话状态控制src/Verdure.Assistant.Core/Services/StateMachine/ConversationStateMachine.cssrc/Verdure.Assistant.Core/Services/StateMachine/ConversationStateMachineContext.cs音乐播放模块集成酷狗/酷我API、在线播放和缓存src/Verdure.Assistant.Core/Services/KuwoMusicService?? 应用截图与演示??? WinUI 桌面应用WinUI Application Screenshot - 点击查看演示视频?? 演示视频点击在新标签页播放 ↗现代化的 Windows 桌面应用界面支持语音交互和实时状态显示?? MAUI 移动应用AndroidMAUI Application Screenshot - 点击查看演示视频?? 演示视频点击在新标签页播放 ↗基于 .NET MAUI 的 Android 移动应用支持后台语音处理和音乐播放? MAUI 安卓手表应用Android WatchMAUI Android Watch Application Screenshot - 点击查看演示视频?? 演示视频点击在新标签页播放 ↗基于 .NET MAUI 的安卓手表应用适配圆形/方形表盘支持语音助手核心功能?? Web API 服务Console Application Screenshot - 点击查看演示视频?? 演示视频点击在新标签页播放 ↗适合树莓派机器人和普通的测试使用快速开始环境准备基础要求.NET 9.0 SDK - 下载地址Visual Studio 2022 (17.8) 或 Visual Studio Code克隆项目git clone https://github.com/maker-community/Verdure.Assistant.gitcd Verdure.Assistant各平台使用指南1. Windows桌面版WinUI运行方式在Visual Studio中直接设置为启动项目运行。使用流程启动应用后界面会显示连接状态如果没有在小智后台绑定会提示进行绑定绑定完成说出你好小电开启对话说再见会再次进入等待状态功能特性自动模式自动持续监听无需重复唤醒实时状态显示连接状态、语音识别状态可视化音乐控制搜索、播放、暂停音乐主题切换支持深色/浅色主题2. Android移动版MAUI运行方式使用Visual Studio打开解决方案选择Android设备或模拟器使用流程安装APK到Android设备授予录音和通知权限使用唤醒词开启对话3. 命令行版Console运行方式cd src/Verdure.Assistant.Consoledotnet restoredotnet run使用场景服务器端部署Linux/Windows Server开发调试和测试查看详细日志输出自动化脚本集成4. Web API服务树莓派/服务器运行方式cd src/Verdure.Assistant.Apidotnet restoredotnet run主要API端点音乐相关# 搜索音乐GET /api/music/search?songName青花瓷# 播放音乐POST /api/music/search-and-playContent-Type: application/json{songName: 青花瓷}# 播放控制POST /api/music/pausePOST /api/music/resumePOST /api/music/stop树莓派部署适合部署在树莓派等嵌入式设备上配合VerdiBot硬件机器人使用。详细部署步骤参考项目中的API文档。核心技术详解1. 会话状态机项目使用状态机管理设备状态主要状态包括IDLE空闲等待唤醒LISTENING监听正在录音SPEAKING说话播放回复状态转换逻辑清晰避免混乱的条件判断。核心代码如下请求状态变更代码////// 请求状态转换//////触发事件///上下文信息/// 是否成功转换public bool RequestTransition(ConversationTrigger trigger, string? context null){lock (_stateLock){var fromState _currentState;var toState GetNextState(_currentState, trigger);if (toState null){_logger?.LogWarning(Invalid state transition: {FromState} - {Trigger} (context: {Context}),fromState, trigger, context);return false;}if (fromState toState.Value){_logger?.LogDebug(State transition ignored (already in target state): {State} - {Trigger},fromState, trigger);return true;}_logger?.LogInformation(State transition: {FromState} - {ToState} (trigger: {Trigger}, context: {Context}),fromState, toState.Value, trigger, context);_currentState toState.Value;_previousState fromState;// Fire state change eventvar eventArgs new StateTransitionEventArgs{FromState fromState,ToState toState.Value,Trigger trigger,Context context};try{StateChanged?.Invoke(this, eventArgs);}catch (Exception ex){_logger?.LogError(ex, Error in state change event handler);}return true;}}状态处理代码private void InitializeStateMachine(){_stateMachine new ConversationStateMachine();_stateMachineContext new ConversationStateMachineContext(_stateMachine){// Set up state machine actionsOnEnterListening async () {await StartListeningInternalAsync();},OnExitListening async () {await StopListeningInternalAsync();},OnEnterSpeaking async () {// 进入说话状态 - 保持录音以检测用户打断// 不需要停止录音继续监听用户的打断_logger?.LogDebug(进入说话状态保持录音以检测打断);await Task.CompletedTask;},OnExitSpeaking async () {await StopSpeakingInternalAsync();},OnEnterIdle async () {await EnterIdleStateAsync();},OnEnterConnecting async () {await EnterConnectingStateAsync();}};// Subscribe to state changes to sync with legacy state property_stateMachine.StateChanged OnStateMachineStateChanged;}2. Opus编解码使用Opus编解码器进行音频压缩特点低延迟适合实时语音通信高质量保证语音清晰度带宽节省有效降低网络传输压力项目中封装了OpusCodec类简化了编解码操作。完整代码如下using OpusSharp.Core;using Verdure.Assistant.Core.Interfaces;namespace Verdure.Assistant.Core.Services;////// OpusSharp音频编解码器实现///public class OpusSharpAudioCodec : IAudioCodec{private OpusEncoder? _encoder;private OpusDecoder? _decoder;private readonly object _lock new();private int _currentSampleRate;private int _currentChannels;public byte[] Encode(byte[] pcmData, int sampleRate, int channels){lock (_lock){// 验证输入参数是否符合官方规格if (sampleRate ! 16000){System.Console.WriteLine($警告: 编码采样率 {sampleRate} 不符合官方规格 16000Hz);}if (channels ! 1){System.Console.WriteLine($警告: 编码声道数 {channels} 不符合官方规格 1单声道);}if (_encoder null || _currentSampleRate ! sampleRate || _currentChannels ! channels){_encoder?.Dispose();_encoder new OpusEncoder(sampleRate, channels, OpusPredefinedValues.OPUS_APPLICATION_AUDIO);_currentSampleRate sampleRate;_currentChannels channels;System.Console.WriteLine($Opus编码器已初始化: {sampleRate}Hz, {channels}声道);}try{// 计算帧大小 (采样数不是字节数) - 严格按照官方60ms规格int frameSize sampleRate * 60 / 1000; // 对于16kHz 960样本// 确保输入数据长度正确 (16位音频 2字节/样本)int expectedBytes frameSize * channels * 2;//System.Console.WriteLine($编码PCM数据: 输入长度{pcmData.Length}字节, 期望长度{expectedBytes}字节, 帧大小{frameSize}样本);if (pcmData.Length ! expectedBytes){//System.Console.WriteLine($调整PCM数据长度: 从{pcmData.Length}字节到{expectedBytes}字节);// 调整数据长度或填充零byte[] adjustedData new byte[expectedBytes];if (pcmData.Length expectedBytes){// 数据不足复制现有数据并填充零Array.Copy(pcmData, adjustedData, pcmData.Length);//System.Console.WriteLine($PCM数据不足已填充{expectedBytes - pcmData.Length}字节的零);}else{// 数据过多截断Array.Copy(pcmData, adjustedData, expectedBytes);//System.Console.WriteLine($PCM数据过多已截断{pcmData.Length - expectedBytes}字节);}pcmData adjustedData;}// 转换为16位短整型数组short[] pcmShorts new short[frameSize * channels];for (int i 0; i pcmShorts.Length i * 2 1 pcmData.Length; i){pcmShorts[i] BitConverter.ToInt16(pcmData, i * 2);}// 可选添加输入音频质量检查//CheckAudioQuality(pcmData, $编码输入PCM长度{pcmData.Length}字节);// OpusSharp编码 - 使用正确的APIbyte[] outputBuffer new byte[4000]; // Opus最大包大小int encodedLength _encoder.Encode(pcmShorts, frameSize, outputBuffer, outputBuffer.Length);//System.Console.WriteLine($编码结果: 输出长度{encodedLength}字节);if (encodedLength 0){// 返回实际编码的数据byte[] result new byte[encodedLength];Array.Copy(outputBuffer, result, encodedLength);return result;}else{//System.Console.WriteLine($编码失败: 返回长度为 {encodedLength});}return Array.Empty();}catch (Exception ex){System.Console.WriteLine($OpusSharp编码失败: {ex.Message});System.Console.WriteLine($堆栈跟踪: {ex.StackTrace});return Array.Empty();}}}public byte[] Decode(byte[] encodedData, int sampleRate, int channels){lock (_lock){// 验证输入参数是否符合官方规格if (sampleRate ! 16000){System.Console.WriteLine($警告: 采样率 {sampleRate} 不符合官方规格 16000Hz);}if (channels ! 1){System.Console.WriteLine($警告: 声道数 {channels} 不符合官方规格 1单声道);}if (_decoder null || _currentSampleRate ! sampleRate || _currentChannels ! channels){_decoder?.Dispose();_decoder new OpusDecoder(sampleRate, channels);_currentSampleRate sampleRate;_currentChannels channels;System.Console.WriteLine($Opus解码器已初始化: {sampleRate}Hz, {channels}声道);}// 检查输入数据有效性if (encodedData null || encodedData.Length 0){System.Console.WriteLine(警告: 接收到空的Opus数据包);int frameSize sampleRate * 60 / 1000; // 60ms帧符合官方规格byte[] silenceData new byte[frameSize * channels * 2];return silenceData;}try{// 计算帧大小 (采样数不是字节数) - 严格按照官方60ms规格int frameSize sampleRate * 60 / 1000; // 对于16kHz 960样本// 为解码输出分配缓冲区确保有足够空间// Opus可能解码出不同长度的帧所以使用最大可能的帧大小int maxFrameSize sampleRate * 120 / 1000; // 最大120ms帧作为安全缓冲short[] outputBuffer new short[maxFrameSize * channels];System.Console.WriteLine($解码Opus数据: 输入长度{encodedData.Length}字节, 期望帧大小{frameSize}样本);// OpusSharp解码 - 使用正确的API让解码器自动确定帧大小int decodedSamples _decoder.Decode(encodedData, encodedData.Length, outputBuffer, maxFrameSize, false);System.Console.WriteLine($解码结果: 解码了{decodedSamples}样本);if (decodedSamples 0){// 验证解码出的样本数是否合理if (decodedSamples maxFrameSize){System.Console.WriteLine($警告: 解码样本数({decodedSamples})超出最大帧大小({maxFrameSize}));decodedSamples maxFrameSize;}// 转换为字节数组 - 确保正确的字节序byte[] pcmBytes new byte[decodedSamples * channels * 2];for (int i 0; i decodedSamples * channels; i){var bytes BitConverter.GetBytes(outputBuffer[i]);pcmBytes[i * 2] bytes[0]; // 低字节pcmBytes[i * 2 1] bytes[1]; // 高字节}// 可选添加简单的音频质量检查CheckAudioQuality(pcmBytes, $解码输出PCM长度{pcmBytes.Length}字节);return pcmBytes;}else{System.Console.WriteLine($解码失败: 返回的样本数为 {decodedSamples});}// 返回静音数据而不是空数组保持音频流连续性int silenceFrameSize frameSize * channels * 2;byte[] silenceData new byte[silenceFrameSize];System.Console.WriteLine($返回静音数据: {silenceFrameSize}字节);return silenceData;}catch (Exception ex){System.Console.WriteLine($OpusSharp解码失败: {ex.Message});System.Console.WriteLine($堆栈跟踪: {ex.StackTrace});// 返回静音数据而不是空数组保持音频流连续性int frameSize sampleRate * 60 / 1000; // 60ms帧byte[] silenceData new byte[frameSize * channels * 2];return silenceData;}}}////// 简单的音频质量检查帮助诊断音频问题///private void CheckAudioQuality(byte[] pcmData, string context){if (pcmData.Length 4) return;// 转换为16位样本进行分析var samples new short[pcmData.Length / 2];Buffer.BlockCopy(pcmData, 0, samples, 0, pcmData.Length);// 计算音频统计信息double sum 0;double sumSquares 0;short min short.MaxValue;short max short.MinValue;int zeroCount 0;foreach (short sample in samples){sum sample;sumSquares sample * sample;min Math.Min(min, sample);max Math.Max(max, sample);if (sample 0) zeroCount;}double mean sum / samples.Length;double rms Math.Sqrt(sumSquares / samples.Length);double zeroPercent (double)zeroCount / samples.Length * 100;// 检测潜在问题bool hasIssues false;var issues new List();// 检查是否全为零静音if (zeroPercent 95){issues.Add(几乎全为静音);hasIssues true;}// 检查是否有削波饱和if (max 32760 || min -32760){issues.Add(可能存在音频削波);hasIssues true;}// 检查是否有异常的DC偏移if (Math.Abs(mean) 1000){issues.Add($异常的DC偏移: {mean:F1});hasIssues true;}// 检查RMS是否异常低可能的损坏信号if (rms 10 zeroPercent 50){issues.Add($异常低的RMS: {rms:F1});hasIssues true;} if (hasIssues){//System.Console.WriteLine($音频质量警告 ({context}): {string.Join(, , issues)});//System.Console.WriteLine($ 统计: 样本数{samples.Length}, RMS{rms:F1}, 范围[{min}, {max}], 零值比例{zeroPercent:F1}%);}else{//System.Console.WriteLine($音频质量正常 ({context}): RMS{rms:F1}, 范围[{min}, {max}]);}}public void Dispose(){lock (_lock){_encoder?.Dispose();_decoder?.Dispose();}}}3. SoundFlow音频框架跨平台音频播放框架提供统一的音频播放接口屏蔽平台差异。录音初始化代码private static SoundFlowAudioRecorder? _instance;private static readonly object _instanceLock new();private AudioEngine? _engine;private AudioCaptureDevice? _captureDevice;private Recorder? _recorder;private readonly object _streamLock new();private readonly AudioDataDistributor _audioDistributor; // 使用 Channel 优化的音频分发器private bool _isRecording false;private bool _isDisposed false;private int _sampleRate 16000;private int _channels 1;private readonly ILogger? _logger;// 设备配置 - 优化为低延迟录音private static readonly MiniAudioDeviceConfig DeviceConfig new(){PeriodSizeInFrames 960, // 60ms 16kHz 960 samplesPeriodSizeInMilliseconds 0,Periods 3,NoPreSilencedOutputBuffer true,NoClip false,NoDisableDenormals false,NoFixedSizedCallback false,Capture new DeviceSubConfig{ShareMode ShareMode.Shared},Wasapi new WasapiSettings{Usage WasapiUsage.ProAudio,NoAutoConvertSRC false, // 允许自动采样率转换NoDefaultQualitySRC false, // 允许高质量重采样NoAutoStreamRouting false,NoHardwareOffloading false}};// 参考 py-xiaozhi 的事件系统public event EventHandler? DataAvailable;public event EventHandler? RecordingStopped;public bool IsRecording _isRecording;private SoundFlowAudioRecorder(ILogger? logger null){_logger logger;_audioDistributor new AudioDataDistributor(logger);InitializeAudioEngine();}////// 在构造函数中初始化音频引擎和基础组件///private void InitializeAudioEngine(){try{// 在构造时就初始化引擎_engine new MiniAudioEngine();// 显示可用的录音设备调试模式if (_logger ! null _logger.IsEnabled(LogLevel.Debug)){_logger.LogDebug(SoundFlow录音引擎初始化完成);_logger.LogDebug(可用SoundFlow录音设备:);for (int i 0; i _engine.CaptureDevices.Length; i){var device _engine.CaptureDevices[i];var marker device.IsDefault ? (默认) : ;_logger.LogDebug( [{Index}] {Name}{Marker}, i, device.Name, marker);}}}catch (Exception ex){_logger?.LogError(ex, 初始化SoundFlow录音引擎失败);throw;}}播放器初始化代码private readonly ILogger? _logger;private AudioEngine? _engine;private AudioPlaybackDevice? _playbackDevice;private SoundPlayer? _soundPlayer;private QueueDataProvider? _dataProvider;private readonly object _lock new();private bool _isPlaying false;private bool _isDisposed false;private int _sampleRate 16000;private int _channels 1;// 设备配置 - 优化为更低延迟播放减少断断续续private static readonly MiniAudioDeviceConfig DeviceConfig new(){PeriodSizeInFrames 480, // 30ms 16kHz 480 samples (减少到30ms提高响应性)PeriodSizeInMilliseconds 0,Periods 4, // 增加到4个周期提供更好的缓冲NoPreSilencedOutputBuffer false,NoClip false,NoDisableDenormals false,NoFixedSizedCallback false,Playback new DeviceSubConfig{ShareMode ShareMode.Shared},Wasapi new WasapiSettings{Usage WasapiUsage.ProAudio, // 专业音频模式降低延迟NoAutoConvertSRC false, // 允许自动采样率转换NoDefaultQualitySRC false, // 允许高质量重采样NoAutoStreamRouting false,NoHardwareOffloading false}};public event EventHandler? PlaybackStopped;public bool IsPlaying _isPlaying;public SoundFlowAudioPlayer(ILogger? logger null){_logger logger;// 创建无界通道用于音频数据缓冲避免阻塞问题var options new UnboundedChannelOptions{SingleReader true, // 只有播放任务读取SingleWriter false, // 多个来源可能写入音频数据AllowSynchronousContinuations false // 避免死锁};InitializeAudioEngine();// 以默认参数预初始化播放设备与播放器便于后续快速切换/播放try{InitializePlaybackDevice(_sampleRate, _channels);}catch (Exception ex){// 预初始化失败不致命延迟到首次播放再初始化_logger?.LogWarning(ex, SoundFlow预初始化失败将在首次播放时重试);}}////// 在构造函数中初始化音频引擎和基础组件///private void InitializeAudioEngine(){try{// 在构造时就初始化引擎_engine new MiniAudioEngine();// 显示可用的播放设备调试模式if (_logger ! null _logger.IsEnabled(LogLevel.Debug)){_logger.LogDebug(SoundFlow播放引擎初始化完成);_logger.LogDebug(可用SoundFlow播放设备:);for (int i 0; i _engine.PlaybackDevices.Length; i){var device _engine.PlaybackDevices[i];var status device.IsDefault ? (默认) : ;_logger.LogDebug( [{Index}] {Name}{Status}, i, device.Name, status);}}if (_engine.PlaybackDevices.Length 0){throw new InvalidOperationException(未找到SoundFlow音频播放设备);}}catch (Exception ex){_logger?.LogError(ex, 初始化SoundFlow播放引擎失败);throw;}}////// 初始化播放设备仅在参数变化时调用///private void InitializePlaybackDevice(int sampleRate, int channels){if (!ValidateAudioParameters(sampleRate, channels)){throw new ArgumentException(Invalid audio parameters);}// 如果参数相同且设备已初始化直接返回if (_playbackDevice ! null _sampleRate sampleRate _channels channels){return;}// 清理现有设备if (_playbackDevice ! null){try{_playbackDevice.Stop();_playbackDevice null;}catch (Exception ex){_logger?.LogDebug(ex, 清理旧播放设备时出错);}}_sampleRate sampleRate;_channels channels;try{// 引擎已在构造函数中初始化if (_engine null){throw new InvalidOperationException(SoundFlow引擎未正确初始化);}// 修复统一使用F32格式与QueueDataProvider保持一致var format new AudioFormat{SampleRate sampleRate,Channels channels,Format SampleFormat.F32 // 改为F32与播放器格式一致};_playbackDevice _engine.InitializePlaybackDevice(null, format, DeviceConfig);_logger?.LogDebug(已选择SoundFlow播放设备: {DeviceName}, _playbackDevice.Info?.Name ?? 默认设备);_logger?.LogDebug(播放设备格式: {Format}, {Channels}ch, {SampleRate}Hz,_playbackDevice.Format.Format, _playbackDevice.Format.Channels, _playbackDevice.Format.SampleRate);_logger?.LogInformation(SoundFlow音频播放器设备初始化成功: {SampleRate}Hz, {Channels}声道,sampleRate, channels);}catch (Exception ex){throw new Exception($初始化SoundFlow音频播放设备失败: {ex.Message}, ex);}}////// 初始化SoundPlayer与QueueDataProvider仅在参数变化时调用///private async Task InitializePlayer(int sampleRate, int channels){if (_engine null || _playbackDevice null){throw new InvalidOperationException(SoundFlow引擎或播放设备未初始化);}_sampleRate sampleRate;_channels channels;try{// 创建音频格式 - 匹配测试项目的要求var format new AudioFormat{SampleRate sampleRate,Channels channels,Format SampleFormat.F32 // QueueDataProvider使用Float32格式};// 清理旧播放器if (_soundPlayer ! null){try{_soundPlayer.Stop();_playbackDevice.MasterMixer.RemoveComponent(_soundPlayer);_soundPlayer.Dispose();}catch (Exception ex){_logger?.LogDebug(ex, 清理旧播放器时出错);}}_dataProvider?.Dispose();// 创建QueueDataProvider - 专为流式数据设计_dataProvider new QueueDataProvider(format);_dataProvider.EndOfStreamReached (s, e) {_logger?.LogDebug(SoundFlow数据提供者已到达流末尾);PlaybackStopped?.Invoke(this, EventArgs.Empty);};// 创建播放器_soundPlayer new SoundPlayer(_engine, format, _dataProvider);// 添加到播放设备的混音器_playbackDevice.MasterMixer.AddComponent(_soundPlayer);_logger?.LogDebug(SoundFlow播放器初始化完成: {SampleRate}Hz, {Channels}ch, sampleRate, channels);await Task.CompletedTask;}catch (Exception ex){_logger?.LogError(ex, SoundFlow播放器初始化错误);throw;}}4. 关键词唤醒支持两种唤醒词模型xiaodian小电你好小电cortana小娜你好小娜基于音频流实时检测CPU占用低响应速度快。关键词唤醒核心逻辑////// 初始化语音配置离线模式无需订阅密钥///private void InitializeSpeechConfig(){try{// 创建离线语音配置// 对于关键词检测可以使用空的配置因为我们使用本地.table文件_speechConfig SpeechConfig.FromSubscription(dummy, dummy);// 设置为离线模式_speechConfig.SetProperty(SPEECH-UseOfflineRecognition, true);_logger?.LogInformation(语音配置初始化成功离线模式);}catch (Exception ex){_logger?.LogError(ex, 初始化语音配置失败);_isEnabled false;}}////// 启动关键词检测对应py-xiaozhi的start方法///public async Task StartAsync(IAudioRecorder? audioRecorder null){if (!_isEnabled){_logger?.LogWarning(关键词检测功能未启用);return false;}if (_isRunning){_logger?.LogWarning(关键词检测已在运行);return true;}try{await _semaphore.WaitAsync();_cancellationTokenSource new CancellationTokenSource();// 设置音频源对应py-xiaozhi的多种启动模式if (audioRecorder ! null){_audioRecorder audioRecorder;_useExternalAudioSource true;_logger?.LogInformation(使用外部音频源启动关键词检测);}else{_useExternalAudioSource false;_logger?.LogInformation(使用独立音频模式启动关键词检测);}// 加载关键词模型if (!await LoadKeywordModelsAsync()){_logger?.LogError(加载关键词模型失败);return false;}// 配置音频输入 - 使用共享音频流管理器var audioConfig await ConfigureSharedAudioInput();if (audioConfig null){_logger?.LogError(配置音频输入失败);return false;}// 创建关键词识别器 - 确保每次启动都是全新实例_keywordRecognizer new KeywordRecognizer(audioConfig);// 订阅事件SubscribeToRecognizerEvents();// 在后台任务中启动关键词识别避免阻塞主流程_ Task.Run(async () {try{if (_keywordModel ! null _keywordRecognizer ! null){await _keywordRecognizer.RecognizeOnceAsync(_keywordModel);_logger?.LogInformation(关键词识别已启动后台任务);}}catch (Exception ex)

相关新闻

浅谈SQL Server 对于内存的管理

浅谈SQL Server 对于内存的管理

堂辞剿秸在前面的文章《10min搭建一个大模型智能客服助手》中,我们详细介绍了搭建大模型智能客服的详细过程。 但是,这种基于工作流知识库的模式的方案只能解决通用的问答,还不够""智能""。 所以,我继续增加&…

2026/7/7 3:49:32 阅读更多 →
C# 温故而知新: 线程篇(一)

C# 温故而知新: 线程篇(一)

痘靥诤俳2.1 DeepSeek V3.2 自问世以来,Transformer架构始终是大语言模型(LLM)发展中无可争议的核心引擎。其关键创新——自注意力机制,赋予模型捕捉海量数据序列中长距离依赖与上下文细微差别的卓越能力。然而这种强大能力伴随着…

2026/7/6 11:36:49 阅读更多 →
使用jQuery Ajax功能的时候需要注意的一个问题

使用jQuery Ajax功能的时候需要注意的一个问题

被涯旅购上述每次divstep迭代可以表示为矩阵乘法,对向量[f g]和[d e]应用如下的转换矩阵(1/2*t): image 其中根据迭代中分支的不同,(u, v, q, r)分别有不同的取值(0, 2, -1, 1),(2, 0, 1, 1),(2…

2026/7/3 1:03:48 阅读更多 →

最新新闻

2026最新6款免费AI编程工具全生命周期实测学生党平替权威合集

2026最新6款免费AI编程工具全生命周期实测学生党平替权威合集

一、开篇引言很多人选 AI 编程工具只看一个指标:补全速度快不快。但真正影响开发效率的是全流程的支持能力。我按项目生命周期的每个阶段逐个对比。我是一名外企远程办公全栈开发者,常年在外企云平台维护车联网数据采集平台V4.1,日常大量编写…

2026/7/7 12:21:30 阅读更多 →
终极指南:3分钟掌握Switch破解神器TegraRcmGUI

终极指南:3分钟掌握Switch破解神器TegraRcmGUI

终极指南:3分钟掌握Switch破解神器TegraRcmGUI 【免费下载链接】TegraRcmGUI C GUI for TegraRcmSmash (Fuse Gele exploit for Nintendo Switch) 项目地址: https://gitcode.com/gh_mirrors/te/TegraRcmGUI 你是否曾经看着手中的Switch,想象过它…

2026/7/7 12:17:29 阅读更多 →
数据产业服务分类(36)——原则、目标与方法——分类设计目标

数据产业服务分类(36)——原则、目标与方法——分类设计目标

数据产业分类设计的目的是明确界定数据产业服务的范围和内容,有效识别和区分各种服务内容,为数据产业服务的获取与提供、政策制定及行业发展提供坚实基础、提供支撑。构建数据产业服务分类的核心目标是为数据产业服务的标准体系建立奠定基础。达到覆盖数据全生命周期…

2026/7/7 12:15:29 阅读更多 →
小白程序员必看:大模型系统开发10层深度解析,从基础到实战

小白程序员必看:大模型系统开发10层深度解析,从基础到实战

本文深入剖析了大模型系统开发的十个关键层次,涵盖模型语言理解、模型表现优化、提示工程、知识库问答、Agent技能、推理成本控制、系统评估与安全、上线后运维等核心领域。文章以实际案例为引,详细解读了每个层次中的63个核心概念,旨在帮助读…

2026/7/7 12:13:28 阅读更多 →
鱼叉式钓鱼攻击:从社会工程学到多层防御的实战解析

鱼叉式钓鱼攻击:从社会工程学到多层防御的实战解析

1. 项目概述:从“广撒网”到“精准狙击”的威胁演进在网络安全领域,钓鱼攻击早已不是什么新鲜事。但如果你还停留在“恭喜您中奖了,请点击链接领取”这种群发邮件的印象里,那可能已经落后于攻击者的步伐了。今天要深入探讨的“鱼叉…

2026/7/7 12:11:28 阅读更多 →
基于ComfyUI的AI漫剧制作:8G显存下的零人工干预方案

基于ComfyUI的AI漫剧制作:8G显存下的零人工干预方案

🚀 30款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度 最近在B站刷到不少AI生成的漫画剧视频,从角色设计到分镜再到视频合成,全程自动化,效果相当惊艳。但…

2026/7/7 12:09:27 阅读更多 →

日新闻

鸿蒙新特性:图片画廊与轮播导航——构建沉浸式图片浏览体验

鸿蒙新特性:图片画廊与轮播导航——构建沉浸式图片浏览体验

图片浏览是移动应用中最高频的场景之一。从社交应用的照片流到电商平台的商品图集,从旅游应用的景点相册到摄影作品展示——用户对图片浏览的体验要求不断提高:流畅的切换动画、直观的缩略图导航、便捷的收藏操作、自动播放模式。HarmonyOS NEXT ArkUI 虽…

2026/7/7 0:05:16 阅读更多 →
24V DC-DC降压芯片PW2312B/PW2815,SOT23-6到SOP8-EP方案对比

24V DC-DC降压芯片PW2312B/PW2815,SOT23-6到SOP8-EP方案对比

24V稳压芯片完整选型指南 PW8600 PW75XX PW2815 PW2312B LDODC/DC全方案 一、24V稳压方案概述 24V直流电源在工业自动化、门禁系统、电梯控制、汽车电子、LED驱动、监控设备等场景中应用极广,是最常见的中压直流母线电压。要将24V母线稳定降压至下游MCU、传感器…

2026/7/7 0:05:16 阅读更多 →
RAG+知识图谱混合检索与Graph RAG核心对比

RAG+知识图谱混合检索与Graph RAG核心对比

做企业RAG落地的团队,往往容易卡在一容易踩坑的选型难题: 当需求单纯靠向量RAG搞不定、单纯靠知识图谱也搞不定,必须同时依赖「文本语义理解 实体关系推理」时,到底是做「向量图谱混合检索」就够了,还是必须上「Grap…

2026/7/7 0:07:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/6 8:11:50 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/6 8:11:52 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/6 6:52:56 阅读更多 →

月新闻