<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      用純.NET開發(fā)并制作一個智能桌面機器人(六):使用.NET開發(fā)一個跨平臺功能完善的AI語音對話客戶端

      前言

      前面幾篇文章已經(jīng)把機器人硬件控制部分的開發(fā)講得差不多了,包括屏幕控制、舵機驅(qū)動、語音交互等功能。但是之前的外形太過簡單,可動角度不夠多,所以我就新改進了一個版本,叫VerdiBot(阿蔭),詳細視頻介紹地址請點擊鏈接

      ESP32社區(qū)最火的AI對話機器人非小智AI莫屬了,所以為了讓自己做的機器人對話部分也足夠的生動我就重新實現(xiàn)了一個.NET版本的小智客戶端,打算后期集成更多的功能,并整理成了一個完整的開源項目——Verdure Assistant(綠蔭助手),這是一個基于.NET 9.0的多平臺AI語音助手,支持Windows桌面、Android移動端、命令行以及Web API等多種使用方式。

      這篇文章主要是給大家講講這個對話機器人項目的一些代碼,方便想嘗試的小伙伴快速上手體驗。項目代碼已經(jīng)開源了,大家可以自己研究,遇到問題也歡迎提Issue討論。

      機器人圖片

      GitHub項目地址https://github.com/maker-community/Verdure.Assistant

      問題解答

      Q: 之前為什么特意做樹莓派wifi配網(wǎng)的功能?

      A: 之前的博客有網(wǎng)友說我浪費生命開發(fā)wifi配網(wǎng)功能,我在評論區(qū)也有講過原因,現(xiàn)在我在這里再講一遍,因為有時候我們拿著設(shè)備到新環(huán)境的時候,并不能時刻有可用的顯示器和鼠標鍵盤,但是又需要聯(lián)網(wǎng),這時就可以使用wifi配網(wǎng)了。然后ssh連接到設(shè)備上就可以像服務(wù)器一樣控制了。

      Q: 支持哪些AI服務(wù)?

      A: 目前主要對接的是小智AI服務(wù),后續(xù)計劃支持更多AI服務(wù)的接入,包括OpenAI等。項目采用了抽象設(shè)計,擴展起來比較方便。

      Q: 項目使用什么技術(shù)棧?

      A: 核心使用.NET 9.0,跨平臺UI用.NET MAUI,Windows桌面使用的WinUI 3。網(wǎng)絡(luò)音頻編解碼用的OpusSharp庫,音頻錄制播放使用的最近社區(qū)剛有人開源的的SoundFlow庫,這個庫功能完善,使用方便,并且內(nèi)置了多種音頻格式解碼的播放,所以我用它替換了之前的PortAudioSharp2,網(wǎng)絡(luò)通信基于WebSocket和MQTT(未測試)。詳細的技術(shù)點在GitHub的README里都有說明。

      Q: 為什么要重新實現(xiàn)這個項目?
      A: 目前小智AI機器人有免費的服務(wù)端可以使用,而且整個架構(gòu)都很優(yōu)雅,對比我之前的實現(xiàn)優(yōu)點很多,所以重新實現(xiàn)一個客戶端對于用戶體驗有很大的幫助,并且協(xié)議是公開的,以后如果想自己拓展實現(xiàn)服務(wù)端也是很輕松的。

      項目整體架構(gòu)

      目錄結(jié)構(gòu)

      項目采用清晰的分層架構(gòu),便于理解和擴展:

      Verdure.Assistant/
      ├── src/                              # 源代碼
      │   ├── Verdure.Assistant.Core/       # 核心庫(音頻、網(wǎng)絡(luò)、服務(wù))
      │   ├── Verdure.Assistant.ViewModels/ # 共享視圖模型(MVVM)
      │   ├── Verdure.Assistant.Console/    # 控制臺應(yīng)用
      │   ├── Verdure.Assistant.WinUI/      # WinUI桌面應(yīng)用
      │   ├── Verdure.Assistant.MAUI/       # MAUI移動應(yīng)用
      │   └── Verdure.Assistant.Api/        # Web API服務(wù)
      ├── tests/                            # 測試項目
      ├── docs/                             # 技術(shù)文檔
      └── scripts/                          # 構(gòu)建腳本
      

      GitHub項目地址https://github.com/maker-community/Verdure.Assistant

      核心功能模塊

      • 語音交互模塊:使用微軟的語音認知服務(wù)的關(guān)鍵詞喚醒,加載關(guān)鍵詞喚醒模型文件不需要Azure訂閱("你好小電"/"你好小娜")

        src/Verdure.Assistant.Core/Services/WakeWords/KeywordSpottingService.cs

      • 音頻處理模塊:Opus編解碼、SoundFlow音頻播放、跨平臺音頻錄制

        src/Verdure.Assistant.Core/Services/Audio/AudioDataDistributor.cs
        src/Verdure.Assistant.Core/Services/Audio/OpusSharpAudioCodec.cs
        src/Verdure.Assistant.Core/Services/Audio/SoundFlowAudioPlayer.cs
        src/Verdure.Assistant.Core/Services/Audio/SoundFlowAudioRecorder.cs

      • 網(wǎng)絡(luò)通信模塊:WebSocket實時通信、MQTT物聯(lián)網(wǎng)協(xié)議

        src/Verdure.Assistant.Core/Services/Protocols/WebSocketClient.cs

      • 狀態(tài)管理模塊:設(shè)備狀態(tài)機、會話狀態(tài)控制

        src/Verdure.Assistant.Core/Services/StateMachine/ConversationStateMachine.cs
        src/Verdure.Assistant.Core/Services/StateMachine/ConversationStateMachineContext.cs

      • 音樂播放模塊:集成酷狗/酷我API、在線播放和緩存

        src/Verdure.Assistant.Core/Services/KuwoMusicService

      ?? 應(yīng)用截圖與演示

      ??? WinUI 桌面應(yīng)用

      WinUI Application Screenshot - 點擊查看演示視頻

      ?? 演示視頻:點擊在新標簽頁播放 ↗

      現(xiàn)代化的 Windows 桌面應(yīng)用界面,支持語音交互和實時狀態(tài)顯示


      ?? MAUI 移動應(yīng)用(Android)

      MAUI Application Screenshot - 點擊查看演示視頻

      ?? 演示視頻:點擊在新標簽頁播放 ↗

      基于 .NET MAUI 的 Android 移動應(yīng)用,支持后臺語音處理和音樂播放


      ? MAUI 安卓手表應(yīng)用(Android Watch)

      MAUI Android Watch Application Screenshot - 點擊查看演示視頻

      ?? 演示視頻:點擊在新標簽頁播放 ↗

      基于 .NET MAUI 的安卓手表應(yīng)用,適配圓形/方形表盤,支持語音助手核心功能


      ?? Web API 服務(wù)

      Console Application Screenshot - 點擊查看演示視頻

      ?? 演示視頻:點擊在新標簽頁播放 ↗

      適合樹莓派機器人和普通的測試使用

      快速開始

      環(huán)境準備

      基礎(chǔ)要求

      • .NET 9.0 SDK - 下載地址
      • Visual Studio 2022 (17.8+)Visual Studio Code

      克隆項目

      git clone https://github.com/maker-community/Verdure.Assistant.git
      cd Verdure.Assistant
      

      各平臺使用指南

      1. Windows桌面版(WinUI)

      運行方式

      在Visual Studio中直接設(shè)置為啟動項目運行。

      使用流程

      1. 啟動應(yīng)用后,界面會顯示連接狀態(tài)
      2. 如果沒有在小智后臺綁定,會提示進行綁定
      3. 綁定完成說出你好小電開啟對話
      4. 說再見會再次進入等待狀態(tài)

      功能特性

      • 自動模式:自動持續(xù)監(jiān)聽,無需重復(fù)喚醒
      • 實時狀態(tài)顯示:連接狀態(tài)、語音識別狀態(tài)可視化
      • 音樂控制:搜索、播放、暫停音樂
      • 主題切換:支持深色/淺色主題

      2. Android移動版(MAUI)

      運行方式

      使用Visual Studio打開解決方案,選擇Android設(shè)備或模擬器:

      使用流程

      1. 安裝APK到Android設(shè)備
      2. 授予錄音通知權(quán)限
      3. 使用喚醒詞開啟對話

      3. 命令行版(Console)

      運行方式

      cd src/Verdure.Assistant.Console
      dotnet restore
      dotnet run
      

      使用場景

      • 服務(wù)器端部署(Linux/Windows Server)
      • 開發(fā)調(diào)試和測試
      • 查看詳細日志輸出
      • 自動化腳本集成

      4. Web API服務(wù)(樹莓派/服務(wù)器)

      運行方式

      cd src/Verdure.Assistant.Api
      dotnet restore
      dotnet run
      

      主要API端點

      音樂相關(guān):

      # 搜索音樂
      GET /api/music/search?songName=青花瓷
      
      # 播放音樂
      POST /api/music/search-and-play
      Content-Type: application/json
      {"songName": "青花瓷"}
      
      # 播放控制
      POST /api/music/pause
      POST /api/music/resume
      POST /api/music/stop
      

      樹莓派部署

      適合部署在樹莓派等嵌入式設(shè)備上,配合VerdiBot硬件機器人使用。詳細部署步驟參考項目中的API文檔。

      核心技術(shù)詳解

      1. 會話狀態(tài)機

      項目使用狀態(tài)機管理設(shè)備狀態(tài),主要狀態(tài)包括:

      • IDLE(空閑):等待喚醒
      • LISTENING(監(jiān)聽):正在錄音
      • SPEAKING(說話):播放回復(fù)

      狀態(tài)轉(zhuǎn)換邏輯清晰,避免混亂的條件判斷。

      核心代碼如下:

      請求狀態(tài)變更代碼:

      /// <summary>
          /// 請求狀態(tài)轉(zhuǎn)換
          /// </summary>
          /// <param name="trigger">觸發(fā)事件</param>
          /// <param name="context">上下文信息</param>
          /// <returns>是否成功轉(zhuǎn)換</returns>
          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 event
                  var 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;
              }
          }
      

      狀態(tài)處理代碼:

       private void InitializeStateMachine()
          {
              _stateMachine = new ConversationStateMachine();
              _stateMachineContext = new ConversationStateMachineContext(_stateMachine)
              {
                  // Set up state machine actions
                  OnEnterListening = async () =>
                      {
                          await StartListeningInternalAsync();
                      },
      
                  OnExitListening = async () =>
                      {
                          await StopListeningInternalAsync();
                      },
      
                  OnEnterSpeaking = async () =>
                      {
                          // 進入說話狀態(tài) - 保持錄音以檢測用戶打斷
                          // 不需要停止錄音,繼續(xù)監(jiān)聽用戶的打斷
                          _logger?.LogDebug("進入說話狀態(tài),保持錄音以檢測打斷");
                          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編解碼器進行音頻壓縮,特點:

      • 低延遲:適合實時語音通信
      • 高質(zhì)量:保證語音清晰度
      • 帶寬節(jié)省:有效降低網(wǎng)絡(luò)傳輸壓力

      項目中封裝了OpusCodec類,簡化了編解碼操作。

      完整代碼如下:

      using OpusSharp.Core;
      using Verdure.Assistant.Core.Interfaces;
      
      namespace Verdure.Assistant.Core.Services;
      
      /// <summary>
      /// OpusSharp音頻編解碼器實現(xiàn)
      /// </summary>
      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)
              {
                  // 驗證輸入?yún)?shù)是否符合官方規(guī)格
                  if (sampleRate != 16000)
                  {
                      System.Console.WriteLine($"警告: 編碼采樣率 {sampleRate} 不符合官方規(guī)格 16000Hz");
                  }
                  if (channels != 1)
                  {
                      System.Console.WriteLine($"警告: 編碼聲道數(shù) {channels} 不符合官方規(guī)格 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
                  {
                      // 計算幀大小 (采樣數(shù),不是字節(jié)數(shù)) - 嚴格按照官方60ms規(guī)格
                      int frameSize = sampleRate * 60 / 1000; // 對于16kHz = 960樣本
                      
                      // 確保輸入數(shù)據(jù)長度正確 (16位音頻 = 2字節(jié)/樣本)
                      int expectedBytes = frameSize * channels * 2;
                      
                      //System.Console.WriteLine($"編碼PCM數(shù)據(jù): 輸入長度={pcmData.Length}字節(jié), 期望長度={expectedBytes}字節(jié), 幀大小={frameSize}樣本");
                      
                      if (pcmData.Length != expectedBytes)
                      {
                          //System.Console.WriteLine($"調(diào)整PCM數(shù)據(jù)長度: 從{pcmData.Length}字節(jié)到{expectedBytes}字節(jié)");
                          // 調(diào)整數(shù)據(jù)長度或填充零
                          byte[] adjustedData = new byte[expectedBytes];
                          if (pcmData.Length < expectedBytes)
                          {
                              // 數(shù)據(jù)不足,復(fù)制現(xiàn)有數(shù)據(jù)并填充零
                              Array.Copy(pcmData, adjustedData, pcmData.Length);
                              //System.Console.WriteLine($"PCM數(shù)據(jù)不足,已填充{expectedBytes - pcmData.Length}字節(jié)的零");
                          }
                          else
                          {
                              // 數(shù)據(jù)過多,截斷
                              Array.Copy(pcmData, adjustedData, expectedBytes);
                              //System.Console.WriteLine($"PCM數(shù)據(jù)過多,已截斷{pcmData.Length - expectedBytes}字節(jié)");
                          }
                          pcmData = adjustedData;
                      }
      
                      // 轉(zhuǎn)換為16位短整型數(shù)組
                      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);
                      }
      
                      // 可選:添加輸入音頻質(zhì)量檢查
                      //CheckAudioQuality(pcmData, $"編碼輸入PCM,長度={pcmData.Length}字節(jié)");
      
                      // OpusSharp編碼 - 使用正確的API
                      byte[] outputBuffer = new byte[4000]; // Opus最大包大小
                      int encodedLength = _encoder.Encode(pcmShorts, frameSize, outputBuffer, outputBuffer.Length);
      
                      //System.Console.WriteLine($"編碼結(jié)果: 輸出長度={encodedLength}字節(jié)");
      
                      if (encodedLength > 0)
                      {
                          // 返回實際編碼的數(shù)據(jù)
                          byte[] result = new byte[encodedLength];
                          Array.Copy(outputBuffer, result, encodedLength);
                          return result;
                      }
                      else
                      {
                          //System.Console.WriteLine($"編碼失敗: 返回長度為 {encodedLength}");
                      }
      
                      return Array.Empty<byte>();
                  }
                  catch (Exception ex)
                  {
                      System.Console.WriteLine($"OpusSharp編碼失敗: {ex.Message}");
                      System.Console.WriteLine($"堆棧跟蹤: {ex.StackTrace}");
                      return Array.Empty<byte>();
                  }
              }
          }    
          public byte[] Decode(byte[] encodedData, int sampleRate, int channels)
          {
              lock (_lock)
              {
                  // 驗證輸入?yún)?shù)是否符合官方規(guī)格
                  if (sampleRate != 16000)
                  {
                      System.Console.WriteLine($"警告: 采樣率 {sampleRate} 不符合官方規(guī)格 16000Hz");
                  }
                  if (channels != 1)
                  {
                      System.Console.WriteLine($"警告: 聲道數(shù) {channels} 不符合官方規(guī)格 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}聲道");
                  }
      
                  // 檢查輸入數(shù)據(jù)有效性
                  if (encodedData == null || encodedData.Length == 0)
                  {
                      System.Console.WriteLine("警告: 接收到空的Opus數(shù)據(jù)包");
                      int frameSize = sampleRate * 60 / 1000; // 60ms幀,符合官方規(guī)格
                      byte[] silenceData = new byte[frameSize * channels * 2];
                      return silenceData;
                  }
      
                  try
                  {
                      // 計算幀大小 (采樣數(shù),不是字節(jié)數(shù)) - 嚴格按照官方60ms規(guī)格
                      int frameSize = sampleRate * 60 / 1000; // 對于16kHz = 960樣本
                      
                      // 為解碼輸出分配緩沖區(qū),確保有足夠空間
                      // Opus可能解碼出不同長度的幀,所以使用最大可能的幀大小
                      int maxFrameSize = sampleRate * 120 / 1000; // 最大120ms幀作為安全緩沖
                      short[] outputBuffer = new short[maxFrameSize * channels];
                      
                      System.Console.WriteLine($"解碼Opus數(shù)據(jù): 輸入長度={encodedData.Length}字節(jié), 期望幀大小={frameSize}樣本");
                      
                      // OpusSharp解碼 - 使用正確的API,讓解碼器自動確定幀大小
                      int decodedSamples = _decoder.Decode(encodedData, encodedData.Length, outputBuffer, maxFrameSize, false);
                      
                      System.Console.WriteLine($"解碼結(jié)果: 解碼了{decodedSamples}樣本");
                      
                      if (decodedSamples > 0)
                      {
                          // 驗證解碼出的樣本數(shù)是否合理
                          if (decodedSamples > maxFrameSize)
                          {
                              System.Console.WriteLine($"警告: 解碼樣本數(shù)({decodedSamples})超出最大幀大小({maxFrameSize})");
                              decodedSamples = maxFrameSize;
                          }
                          
                          // 轉(zhuǎn)換為字節(jié)數(shù)組 - 確保正確的字節(jié)序
                          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];     // 低字節(jié)
                              pcmBytes[i * 2 + 1] = bytes[1]; // 高字節(jié)
                          }
                          
                          // 可選:添加簡單的音頻質(zhì)量檢查
                          CheckAudioQuality(pcmBytes, $"解碼輸出PCM,長度={pcmBytes.Length}字節(jié)");
                          
                          return pcmBytes;
                      }
                      else
                      {
                          System.Console.WriteLine($"解碼失敗: 返回的樣本數(shù)為 {decodedSamples}");
                      }
                      
                      // 返回靜音數(shù)據(jù)而不是空數(shù)組,保持音頻流連續(xù)性
                      int silenceFrameSize = frameSize * channels * 2;
                      byte[] silenceData = new byte[silenceFrameSize];
                      System.Console.WriteLine($"返回靜音數(shù)據(jù): {silenceFrameSize}字節(jié)");
                      return silenceData;
                  }
                  catch (Exception ex)
                  {
                      System.Console.WriteLine($"OpusSharp解碼失敗: {ex.Message}");
                      System.Console.WriteLine($"堆棧跟蹤: {ex.StackTrace}");
                      
                      // 返回靜音數(shù)據(jù)而不是空數(shù)組,保持音頻流連續(xù)性
                      int frameSize = sampleRate * 60 / 1000; // 60ms幀
                      byte[] silenceData = new byte[frameSize * channels * 2];
                      return silenceData;
                  }
              }
          }
      
          /// <summary>
          /// 簡單的音頻質(zhì)量檢查,幫助診斷音頻問題
          /// </summary>
          private void CheckAudioQuality(byte[] pcmData, string context)
          {
              if (pcmData.Length < 4) return;
      
              // 轉(zhuǎn)換為16位樣本進行分析
              var samples = new short[pcmData.Length / 2];
              Buffer.BlockCopy(pcmData, 0, samples, 0, pcmData.Length);
      
              // 計算音頻統(tǒng)計信息
              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<string>();
      
              // 檢查是否全為零(靜音)
              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($"音頻質(zhì)量警告 ({context}): {string.Join(", ", issues)}");
                  //System.Console.WriteLine($"  統(tǒng)計: 樣本數(shù)={samples.Length}, RMS={rms:F1}, 范圍=[{min}, {max}], 零值比例={zeroPercent:F1}%");
              }
              else
              {
                  //System.Console.WriteLine($"音頻質(zhì)量正常 ({context}): RMS={rms:F1}, 范圍=[{min}, {max}]");
              }
          }
      
          public void Dispose()
          {
              lock (_lock)
              {
                  _encoder?.Dispose();
                  _decoder?.Dispose();
              }
          }
      }
      

      3. SoundFlow音頻框架

      跨平臺音頻播放框架:

      提供統(tǒng)一的音頻播放接口,屏蔽平臺差異。

      錄音初始化代碼:

          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 優(yōu)化的音頻分發(fā)器
          private bool _isRecording = false;
          private bool _isDisposed = false;
          private int _sampleRate = 16000;
          private int _channels = 1;
          private readonly ILogger<SoundFlowAudioRecorder>? _logger;
      
          // 設(shè)備配置 - 優(yōu)化為低延遲錄音
          private static readonly MiniAudioDeviceConfig DeviceConfig = new()
          {
              PeriodSizeInFrames = 960,   // 60ms @ 16kHz = 960 samples
              PeriodSizeInMilliseconds = 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,     // 允許自動采樣率轉(zhuǎn)換
                  NoDefaultQualitySRC = false,  // 允許高質(zhì)量重采樣
                  NoAutoStreamRouting = false,
                  NoHardwareOffloading = false
              }
          };
      
          // 參考 py-xiaozhi 的事件系統(tǒng)
          public event EventHandler<byte[]>? DataAvailable;
          public event EventHandler? RecordingStopped;
      
          public bool IsRecording => _isRecording;
      
          private SoundFlowAudioRecorder(ILogger<SoundFlowAudioRecorder>? logger = null)
          {
              _logger = logger;
              _audioDistributor = new AudioDataDistributor(logger);
              InitializeAudioEngine();
          }
      
          /// <summary>
          /// 在構(gòu)造函數(shù)中初始化音頻引擎和基礎(chǔ)組件
          /// </summary>
          private void InitializeAudioEngine()
          {
              try
              {
                  // 在構(gòu)造時就初始化引擎
                  _engine = new MiniAudioEngine();
                  
                  // 顯示可用的錄音設(shè)備(調(diào)試模式)
                  if (_logger != null && _logger.IsEnabled(LogLevel.Debug))
                  {
                      _logger.LogDebug("SoundFlow錄音引擎初始化完成");
                      _logger.LogDebug("可用SoundFlow錄音設(shè)備:");
                      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<SoundFlowAudioPlayer>? _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;
          // 設(shè)備配置 - 優(yōu)化為更低延遲播放,減少斷斷續(xù)續(xù)
          private static readonly MiniAudioDeviceConfig DeviceConfig = new()
          {
              PeriodSizeInFrames = 480,   // 30ms @ 16kHz = 480 samples (減少到30ms提高響應(yīng)性)
              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,    // 專業(yè)音頻模式,降低延遲
                  NoAutoConvertSRC = false,        // 允許自動采樣率轉(zhuǎn)換
                  NoDefaultQualitySRC = false,     // 允許高質(zhì)量重采樣
                  NoAutoStreamRouting = false,
                  NoHardwareOffloading = false
              }
          };
      
          public event EventHandler? PlaybackStopped;
          public bool IsPlaying => _isPlaying;
      
          public SoundFlowAudioPlayer(ILogger<SoundFlowAudioPlayer>? logger = null)
          {
              _logger = logger;
      
              // 創(chuàng)建無界通道用于音頻數(shù)據(jù)緩沖,避免阻塞問題
              var options = new UnboundedChannelOptions
              {
                  SingleReader = true,   // 只有播放任務(wù)讀取
                  SingleWriter = false,  // 多個來源可能寫入音頻數(shù)據(jù)
                  AllowSynchronousContinuations = false // 避免死鎖
              };
      
              InitializeAudioEngine();
              // 以默認參數(shù)預(yù)初始化播放設(shè)備與播放器,便于后續(xù)快速切換/播放
              try
              {
                  InitializePlaybackDevice(_sampleRate, _channels);
              }
              catch (Exception ex)
              {
                  // 預(yù)初始化失敗不致命,延遲到首次播放再初始化
                  _logger?.LogWarning(ex, "SoundFlow預(yù)初始化失敗,將在首次播放時重試");
              }
          }
      
          /// <summary>
          /// 在構(gòu)造函數(shù)中初始化音頻引擎和基礎(chǔ)組件
          /// </summary>
          private void InitializeAudioEngine()
          {
              try
              {
                  // 在構(gòu)造時就初始化引擎
                  _engine = new MiniAudioEngine();
      
                  // 顯示可用的播放設(shè)備(調(diào)試模式)
                  if (_logger != null && _logger.IsEnabled(LogLevel.Debug))
                  {
                      _logger.LogDebug("SoundFlow播放引擎初始化完成");
                      _logger.LogDebug("可用SoundFlow播放設(shè)備:");
                      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音頻播放設(shè)備");
                  }
              }
              catch (Exception ex)
              {
                  _logger?.LogError(ex, "初始化SoundFlow播放引擎失敗");
                  throw;
              }
          }
      
          /// <summary>
          /// 初始化播放設(shè)備(僅在參數(shù)變化時調(diào)用)
          /// </summary>
          private void InitializePlaybackDevice(int sampleRate, int channels)
          {
              if (!ValidateAudioParameters(sampleRate, channels))
              {
                  throw new ArgumentException("Invalid audio parameters");
              }
      
              // 如果參數(shù)相同且設(shè)備已初始化,直接返回
              if (_playbackDevice != null && _sampleRate == sampleRate && _channels == channels)
              {
                  return;
              }
      
              // 清理現(xiàn)有設(shè)備
              if (_playbackDevice != null)
              {
                  try
                  {
                      _playbackDevice.Stop();
                      _playbackDevice = null;
                  }
                  catch (Exception ex)
                  {
                      _logger?.LogDebug(ex, "清理舊播放設(shè)備時出錯");
                  }
              }
      
              _sampleRate = sampleRate;
              _channels = channels;
      
              try
              {
                  // 引擎已在構(gòu)造函數(shù)中初始化
                  if (_engine == null)
                  {
                      throw new InvalidOperationException("SoundFlow引擎未正確初始化");
                  }
      
                  // 修復(fù):統(tǒng)一使用F32格式,與QueueDataProvider保持一致
                  var format = new AudioFormat
                  {
                      SampleRate = sampleRate,
                      Channels = channels,
                      Format = SampleFormat.F32  // 改為F32,與播放器格式一致
                  };
      
                  _playbackDevice = _engine.InitializePlaybackDevice(null, format, DeviceConfig);
      
                  _logger?.LogDebug("已選擇SoundFlow播放設(shè)備: {DeviceName}", _playbackDevice.Info?.Name ?? "默認設(shè)備");
                  _logger?.LogDebug("播放設(shè)備格式: {Format}, {Channels}ch, {SampleRate}Hz",
                      _playbackDevice.Format.Format, _playbackDevice.Format.Channels, _playbackDevice.Format.SampleRate);
      
                  _logger?.LogInformation("SoundFlow音頻播放器設(shè)備初始化成功: {SampleRate}Hz, {Channels}聲道",
                      sampleRate, channels);
              }
              catch (Exception ex)
              {
                  throw new Exception($"初始化SoundFlow音頻播放設(shè)備失敗: {ex.Message}", ex);
              }
          }
      
          /// <summary>
          /// 初始化SoundPlayer與QueueDataProvider(僅在參數(shù)變化時調(diào)用)
          /// </summary>
          private async Task InitializePlayer(int sampleRate, int channels)
          {
              if (_engine == null || _playbackDevice == null)
              {
                  throw new InvalidOperationException("SoundFlow引擎或播放設(shè)備未初始化");
              }
      
              _sampleRate = sampleRate;
              _channels = channels;
      
              try
              {
                  // 創(chuàng)建音頻格式 - 匹配測試項目的要求
                  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();
      
                  // 創(chuàng)建QueueDataProvider - 專為流式數(shù)據(jù)設(shè)計
                  _dataProvider = new QueueDataProvider(format);
      
                  _dataProvider.EndOfStreamReached += (s, e) =>
                  {
                      _logger?.LogDebug("SoundFlow數(shù)據(jù)提供者已到達流末尾");
                      PlaybackStopped?.Invoke(this, EventArgs.Empty);
                  };
      
                  // 創(chuàng)建播放器
                  _soundPlayer = new SoundPlayer(_engine, format, _dataProvider);
      
                  // 添加到播放設(shè)備的混音器
                  _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. 關(guān)鍵詞喚醒

      支持兩種喚醒詞模型:

      • xiaodian(小電):"你好小電"
      • cortana(小娜):"你好小娜"

      基于音頻流實時檢測,CPU占用低,響應(yīng)速度快。

      關(guān)鍵詞喚醒核心邏輯:

          /// <summary>
          /// 初始化語音配置(離線模式,無需訂閱密鑰)
          /// </summary>
          private void InitializeSpeechConfig()
          {
              try
              {
                  // 創(chuàng)建離線語音配置
                  // 對于關(guān)鍵詞檢測,可以使用空的配置,因為我們使用本地.table文件
                  _speechConfig = SpeechConfig.FromSubscription("dummy", "dummy");
      
                  // 設(shè)置為離線模式
                  _speechConfig.SetProperty("SPEECH-UseOfflineRecognition", "true");
      
                  _logger?.LogInformation("語音配置初始化成功(離線模式)");
              }
              catch (Exception ex)
              {
                  _logger?.LogError(ex, "初始化語音配置失敗");
                  _isEnabled = false;
              }
          }
      
          /// <summary>
          /// 啟動關(guān)鍵詞檢測(對應(yīng)py-xiaozhi的start方法)
          /// </summary>
          public async Task<bool> StartAsync(IAudioRecorder? audioRecorder = null)
          {
              if (!_isEnabled)
              {
                  _logger?.LogWarning("關(guān)鍵詞檢測功能未啟用");
                  return false;
              }
      
              if (_isRunning)
              {
                  _logger?.LogWarning("關(guān)鍵詞檢測已在運行");
                  return true;
              }
      
              try
              {
                  await _semaphore.WaitAsync();
      
                  _cancellationTokenSource = new CancellationTokenSource();
      
                  // 設(shè)置音頻源(對應(yīng)py-xiaozhi的多種啟動模式)
                  if (audioRecorder != null)
                  {
                      _audioRecorder = audioRecorder;
                      _useExternalAudioSource = true;
                      _logger?.LogInformation("使用外部音頻源啟動關(guān)鍵詞檢測");
                  }
                  else
                  {
                      _useExternalAudioSource = false;
                      _logger?.LogInformation("使用獨立音頻模式啟動關(guān)鍵詞檢測");
                  }
      
                  // 加載關(guān)鍵詞模型
                  if (!await LoadKeywordModelsAsync())
                  {
                      _logger?.LogError("加載關(guān)鍵詞模型失敗");
                      return false;
                  }
      
                  // 配置音頻輸入 - 使用共享音頻流管理器
                  var audioConfig = await ConfigureSharedAudioInput();
                  if (audioConfig == null)
                  {
                      _logger?.LogError("配置音頻輸入失敗");
                      return false;
                  }
      
                  // 創(chuàng)建關(guān)鍵詞識別器 - 確保每次啟動都是全新實例
                  _keywordRecognizer = new KeywordRecognizer(audioConfig);
      
                  // 訂閱事件
                  SubscribeToRecognizerEvents();
      
                  // 在后臺任務(wù)中啟動關(guān)鍵詞識別,避免阻塞主流程
                  _ = Task.Run(async () =>
                  {
                      try
                      {
                          if (_keywordModel != null && _keywordRecognizer != null)
                          {
                              await _keywordRecognizer.RecognizeOnceAsync(_keywordModel);
                              _logger?.LogInformation("關(guān)鍵詞識別已啟動(后臺任務(wù))");
                          }
                      }
                      catch (Exception ex)
                      {
                          _logger?.LogError(ex, "關(guān)鍵詞識別后臺任務(wù)異常");
                          OnErrorOccurred($"關(guān)鍵詞識別異常: {ex.Message}");
                      }
                  });
      
                  _isRunning = true;
                  _isPaused = false;
      
                  _logger?.LogInformation("關(guān)鍵詞檢測啟動成功");
                  return true;
              }
              catch (Exception ex)
              {
                  _logger?.LogError(ex, "啟動關(guān)鍵詞檢測失敗");
                  return false;
              }
              finally
              {
                  _semaphore.Release();
              }
          }
      

      常見問題排查

      樹莓派運行WebAPI服務(wù)初始化語音設(shè)備失敗

      1. 確認usb聲卡已經(jīng)是默認設(shè)備

      img

      1. 如何禁用默認的聲卡
        通過下面的指令編輯配置文件
      sudo nano /boot/firmware/config.txt
      

      注釋掉圖片上的代碼就可以關(guān)閉
      img

      連接失敗

      1. 確認網(wǎng)絡(luò)連接正常
      2. 查看防火墻是否阻止了連接

      音頻問題

      1. 確認麥克風權(quán)限已授予
      2. 檢查音頻設(shè)備是否正常工作
      3. 查看日志中是否有音頻相關(guān)錯誤

      Android權(quán)限問題

      AndroidManifest.xml中確認權(quán)限聲明:

      <uses-permission android:name="android.permission.RECORD_AUDIO" />
      <uses-permission android:name="android.permission.INTERNET" />
      <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
      

      總結(jié)

      通過這個項目,我想展示.NET在跨平臺開發(fā)方面的強大能力。一套核心代碼,可以運行在Windows、Android、Linux等多個平臺上。

      大多數(shù)的代碼是我指導(dǎo)Github Copilot生成的,要是我個人獨自實現(xiàn)的話,應(yīng)該會花費更多的時間,我個人感覺AI編程目前對于個人開發(fā)者來說確實能夠幫助很多的,如果生成的質(zhì)量不好,我們需要調(diào)整提示詞和更換更厲害的模型,請不要放棄使用。

      項目代碼開源在GitHub上,文檔也比較完善。如果你對.NET跨平臺開發(fā)或AI語音助手感興趣,可以下載下來研究研究。遇到問題歡迎提Issue或者在評論區(qū)討論。

      目前項目的文檔有使用AI進行一些編寫,但是我沒有進行仔細的校驗,請大家以代碼實現(xiàn)為準。

      希望這篇文章能幫助大家快速上手這個項目。后續(xù)我還會繼續(xù)更新相關(guān)的技術(shù)細節(jié)和開發(fā)經(jīng)驗,歡迎持續(xù)關(guān)注!

      參考資源


      本文首發(fā)于個人技術(shù)博客,轉(zhuǎn)載請注明出處。如果對.NET跨平臺開發(fā)和IoT感興趣,歡迎關(guān)注我的博客獲取更多技術(shù)分享!

      posted @ 2025-10-04 17:40  綠蔭阿廣  閱讀(1100)  評論(12)    收藏  舉報
      主站蜘蛛池模板: 国产精品中文字幕综合| 在线 欧美 中文 亚洲 精品| 图片区小说区av区| 亚洲国产成人精品女人久| 久久婷婷五月综合色和啪| 中文字幕在线无码一区二区三区| 日本东京热一区二区三区| 乱女乱妇熟女熟妇综合网| 欧美日本激情| 国产亚洲一级特黄大片在线| 精品午夜福利短视频一区| 亚洲理论在线A中文字幕| 99www久久综合久久爱com| 国产成人午夜福利在线播放| 在线视频中文字幕二区 | 国产亚洲无线码一区二区| 成人无码精品免费视频在线观看| 亚洲av无码精品色午夜蛋壳| 色8久久人人97超碰香蕉987| 中文字幕乱码人妻二区三区| 国产精品日日摸夜夜添夜夜添2021 | 亚洲综合国产成人丁香五| 国产精品久久久久乳精品爆| 国产超碰无码最新上传| 深夜宅男福利免费在线观看| 久久99精品国产99久久6尤物| 女人香蕉久久毛毛片精品| 2020年最新国产精品正在播放| 国产aⅴ夜夜欢一区二区三区| 欧美人与动欧交视频| av一区二区中文字幕| 亚洲高清国产拍精品熟女| 国产女人看国产在线女人| 精品国产成人午夜福利| 精品国产亚洲一区二区三区| 国产成人精品久久性色av| 性欧美VIDEOFREE高清大喷水| 一本久道中文无码字幕av| 久草热大美女黄色片免费看| 人妻在线无码一区二区三区 | 四房播色综合久久婷婷|