北科AI vad RaisoundVad
authorliangzhenying <867174124@qq.com>
Thu, 29 Jan 2026 01:47:04 +0000 (09:47 +0800)
committerliangzhenying <867174124@qq.com>
Thu, 29 Jan 2026 01:47:04 +0000 (09:47 +0800)
ShenYin/ShenYin.xcodeproj/project.pbxproj
ShenYin/ShenYin/Launch/SceneDelegate.swift
ShenYin/ShenYin/Sdk/CCAiManager.swift
ShenYin/ShenYin/Sdk/LogManager.swift
ShenYin/ShenYin/UI/Base/BaseTC.swift
ShenYin/ShenYin/UI/Home/Device/HomeHeadInfoView.swift
ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift
ShenYin/ShenYin/UI/Raisound/RSChatVC.swift
ShenYin/ShenYin/UI/Raisound/RSManager.swift
ShenYin/ShenYin/UI/Raisound/RSRecordBar.swift [new file with mode: 0644]

index 3c2cf08..7604f7b 100644 (file)
                                CODE_SIGN_ENTITLEMENTS = ShenYin/ShenYin.entitlements;
                                CODE_SIGN_IDENTITY = "Apple Development";
                                CODE_SIGN_STYLE = Automatic;
-                               CURRENT_PROJECT_VERSION = 100040;
+                               CURRENT_PROJECT_VERSION = 100041;
                                DEVELOPMENT_TEAM = G23X75CJ67;
                                FRAMEWORK_SEARCH_PATHS = (
                                        "$(inherited)",
                                CODE_SIGN_ENTITLEMENTS = ShenYin/ShenYin.entitlements;
                                CODE_SIGN_IDENTITY = "Apple Development";
                                CODE_SIGN_STYLE = Automatic;
-                               CURRENT_PROJECT_VERSION = 100040;
+                               CURRENT_PROJECT_VERSION = 100041;
                                DEVELOPMENT_TEAM = G23X75CJ67;
                                FRAMEWORK_SEARCH_PATHS = (
                                        "$(inherited)",
index 1f9d25a..0e753dc 100644 (file)
@@ -19,7 +19,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         App.shared.window = window
         AppLaunch.start()
 //        MapManager.shared.setupSdk()
-//        window?.rootViewController = UINavigationController(rootViewController: SportSetVC())
+//        window?.rootViewController = UINavigationController(rootViewController: RSChatVC())
     }
 
     func sceneDidDisconnect(_ scene: UIScene) {
index 133c0d7..54d64d3 100644 (file)
@@ -11,6 +11,7 @@ class CCAiManager {
 }
 extension CCAiManager{
     static func preparePlayBlueTooth(){
+        Logger.debug("准备播放")
         PcmSound.sharedInstance().stopAudioUnit()
         PcmSound.sharedInstance().cleanup()
         PcmSound.sharedInstance().setup(16000, withchannels: 1)
@@ -37,6 +38,7 @@ extension CCAiManager{
 //    }
     
     static func stopPlay(){
+        Logger.debug("停止播放")
         PcmSound.sharedInstance().stopAudioUnit()
         PcmSound.sharedInstance().cleanup()
     }
index 8735333..f61e054 100644 (file)
@@ -9,7 +9,7 @@ import Foundation
 
 /// 日记管理
 class Logger {
-    static var isDebug: Bool = true
+    static var isDebug: Bool = false
     
     fileprivate static let shared = Logger()
     private var fileHandle:FileHandle?
index 3e4cd97..f2105fd 100644 (file)
@@ -100,6 +100,6 @@ class BaseTC:UITabBarController{
         
 
 
-//        self.selectedIndex = 2
+//        self.selectedIndex = 3
     }
 }
index 42bda3d..b559388 100644 (file)
@@ -15,6 +15,11 @@ class HomeHeadInfoView: BaseColletionSetionHeader {
     override func setupViews() {
         addSubviews([icon,titleLabel,deviceLabel,nameLabel])
         
+        icon.do { v in
+            if CCDevice.isJoy{
+                v.yc.setCornerRadius(25)
+            }
+        }
         titleLabel.do { (v) in
             v.textColor = .color(.first)
             v.font = .regular(24)
index cf9afb3..f071a96 100644 (file)
@@ -99,13 +99,13 @@ public class MSVoiceChat {
                     ]
                 ],
                 "inbound_attributes": [ // ASR输入音频配置
-                    "disable_vad": true, // 开启vad 自动断句 (开启后可以有客户端处理回音消除后持续发送音频由服务器判断是否转写和发送LLM)
+                    "disable_vad": false, // 开启vad 自动断句 (开启后可以有客户端处理回音消除后持续发送音频由服务器判断是否转写和发送LLM)
                     "vad_silence": 400, // 设置vad 静音阈值 (毫秒) 连续静音超过 阈值自动切换状态
                     "interrupt_options": [
                         "concatenate_results": true, // 是否在打断后拼接前后的ASR结果
-                        "thinking_time_only": true // 是否只在思考时间允许打断 思考时间(ASR结束--->第一帧音频合成)
-                        // "keyword_interrupt": false, // 启用打断词功能 (打断词只有在思考以及之后才能打断)
-                        // "interrupt_keywords": ["停止", "打断","打断一下", "等一下", "别说了", "打住"] // 配置打断词列表
+                        "thinking_time_only": true, // 是否只在思考时间允许打断 思考时间(ASR结束--->第一帧音频合成)
+//                         "keyword_interrupt": false, // 启用打断词功能 (打断词只有在思考以及之后才能打断)
+//                         "interrupt_keywords": ["停止", "打断","打断一下", "等一下", "别说了", "打住"] // 配置打断词列表
                     ],
                     "cfg": [
                         "format": "pcm", // 音频输入格式
@@ -167,7 +167,7 @@ public class MSVoiceChat {
             Logger.debug("还未鉴权")
             return
         }
-        Logger.debug("send \(pcmData)")
+//        Logger.debug("send \(pcmData)")
         socket?.send(data: pcmData)
     }
 }
@@ -215,7 +215,7 @@ extension MSVoiceChat:WebSocketDelegate{
     }
     
     func webSocketMessageData(_ data: Data){
-                Logger.debug("MSVoiceChat receive data ",data)
+//                Logger.debug("MSVoiceChat receive data ",data)
         ttsCallBack?(data)
     }
     
index be16014..57ad74f 100644 (file)
@@ -9,21 +9,26 @@
 import UIKit
 import AVFAudio
 class RSChatVC:BaseVC{
-    let toolBar = ChatRecordBar()
+    let toolBar = RSRecordBar()
     let tableView = UITableView(frame: CGRectZero, style: .plain)
     let tipsLabel = UILabel()
     let recordMamager =  AiMouseRecordManager()
     
     var dataList:[RSChatCellModel] = []
+    
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        RSManager.shared.start()
+    }
 
     override func viewDidAppear(_ animated: Bool) {
         super.viewDidAppear(animated)
         PcmSound.sharedInstance().delegate = self
-        RSManager.shared.start()
     }
     override func viewDidDisappear(_ animated: Bool) {
         super.viewDidDisappear(animated)
         CCAiManager.stopPlay()
+        toolBar.endBtnAction()
     }
     override func setupViews() {
         navBar.leftView.isHidden = true
@@ -76,10 +81,24 @@ class RSChatVC:BaseVC{
         RSManager.shared.chat.eventCallBack = {[weak self] event in
             guard let self = self else {return}
             if event.event == "started" {
-                print("开始已任务,可以开始录音")
-                //2、开启录音
                 recordMamager.start()
             }
+            else if event.event == "failed"{
+                toolBar.recordBtn.state = .active
+            }
+            else if event.event == "vad.active"{
+                toolBar.recordBtn.state = .active
+            }
+            else if event.event == "asr.started"{
+                toolBar.recordBtn.state = .asr
+                CCAiManager.stopPlay()
+            }
+            else  if event.event == "asr.completed"{
+                
+            }
+            else if event.event == "llm.created"{
+                toolBar.recordBtn.state = .respose
+            }
             else if event.event == "asr.result",let text = event.payload?.content,let id = event.payload?.dialog_id{
                 if let model = dataList.last,model.id == id,model.isSend == true  {
                     model.content = text
@@ -100,8 +119,8 @@ class RSChatVC:BaseVC{
                 }
                 refreshData()
             }
-            else if event.event == "llm.completed",let text = event.payload?.content,let id = event.payload?.dialog_id{
-
+            else if event.event == "tts.started"{
+                CCAiManager.preparePlayBlueTooth()
             }
         }
         RSManager.shared.chat.ttsCallBack = { data in
@@ -113,13 +132,13 @@ class RSChatVC:BaseVC{
     func refreshData(){
         tableView.reloadData()
         if dataList.count > 0 {
-            tableView.scrollToRow(at: IndexPath(row: dataList.count - 1, section: 0), at: .none, animated: true)
+            tableView.scrollToRow(at: IndexPath(row: dataList.count - 1, section: 0), at: .none, animated: false)
         }
     }
 }
 
-extension RSChatVC:ChatRecordBarDelegate{
-    func recordBarCancel(_ toolBar: ChatRecordBar) {
+extension RSChatVC:RSRecordBarDelegate{
+    func recordBarCancel(_ toolBar: RSRecordBar) {
         recordMamager.stop()
         RSManager.shared.chat.cancelSpeedch()
         if let item = dataList.last,item.content.isEmpty{
@@ -128,15 +147,14 @@ extension RSChatVC:ChatRecordBarDelegate{
         }
     }
     
-    func recordBarStart(_ toolBar: ChatRecordBar) {
-        CCAiManager.stopPlay()
+    func recordBarStart(_ toolBar: RSRecordBar) {
         RSManager.shared.chat.start()
     }
     
-    func recordBarStop(_ toolBar: ChatRecordBar) {
+    func recordBarStop(_ toolBar: RSRecordBar) {
         recordMamager.stop()
-        RSManager.shared.chat.endSpeedch()
-        CCAiManager.preparePlayBlueTooth()
+        RSManager.shared.chat.cancelSpeedch()
+        CCAiManager.stopPlay()
     }
 }
 
index 9c73899..13b046f 100644 (file)
@@ -8,9 +8,9 @@
 class RSManager{
     static let shared = RSManager()
 //    8FFDCB2185D5、testflihgt
-//    8FFDCB2185D7、目前
+//    8FFDCB2185D7、testflihgt
 //    8FFDCB2185D8、8FFDCB2185D9 还未使用
-    let deviceId = "8FFDCB2185D7"
+    let deviceId = "8FFDCB2185D8"
     var chat:MSVoiceChat = MSVoiceChat()
     
     func start(){
@@ -38,7 +38,7 @@ class RSManager{
     func startLogin(password:String){
         RSAuth.shared.login(deviceId: deviceId, password: password){[weak self]  token,error in
             if let err = error{
-                if case  .reponseError(let code,let message) = err,code == 50021{
+                if case  .reponseError(let code,_) = err,code == 50021{
                     //50021是账号或密码错误.可以在这里调用注册,重新注册后,之前的密码会被覆盖,APP更新保存的新密码
                     self?.registerAction()
                 }
diff --git a/ShenYin/ShenYin/UI/Raisound/RSRecordBar.swift b/ShenYin/ShenYin/UI/Raisound/RSRecordBar.swift
new file mode 100644 (file)
index 0000000..9f96597
--- /dev/null
@@ -0,0 +1,197 @@
+//
+//  RSRecordBar.swift
+//  ShenYin
+//
+//  Created by liangshi on 2026/1/28.
+//
+
+@objc protocol RSRecordBarDelegate {
+    func recordBarStart(_ toolBar:RSRecordBar)
+    func recordBarCancel(_ toolBar:RSRecordBar)
+    func recordBarStop(_ toolBar:RSRecordBar)
+}
+
+class RSRecordBar:BaseView{
+    let startBtn = CustomButton()
+    let endBtn = CustomButton()
+    
+    let recordBtn = RSRecordStateView()
+    weak var delegate:RSRecordBarDelegate?
+    
+    override func setupViews() {
+        startBtn.do { v in
+            v.yc.setCornerRadius(16)
+            v.backgroundColor = .color(.cellBg)
+            v.setTitleColor(.color(.first), for: .normal)
+            v.titleLabel?.font = .bold(16)
+            v.setTitle("开始对话".l10n, for: .normal)
+            v.addTarget(self, action: #selector(startBtnAction), for: .touchUpInside)
+            v.isHidden = false
+        }
+        endBtn.do { v in
+            v.yc.setCornerRadius(16)
+            v.backgroundColor = .color(.cellBg)
+            v.setTitleColor(.color(.first), for: .normal)
+            v.titleLabel?.font = .bold(16)
+            v.setTitle("结束对话".l10n, for: .normal)
+            v.addTarget(self, action: #selector(endBtnAction), for: .touchUpInside)
+            v.isHidden = true
+        }
+        recordBtn.do { v in
+            v.isHidden = true
+        }
+
+        addSubviews([startBtn,endBtn,recordBtn])
+        
+        startBtn.snp.makeConstraints { make in
+            make.left.right.equalToSuperview().inset(18)
+            make.top.height.equalToSuperview()
+        }
+        endBtn.snp.makeConstraints { make in
+            make.right.equalToSuperview().inset(18)
+            make.width.equalTo(100)
+            make.top.height.equalToSuperview()
+        }
+        recordBtn.snp.makeConstraints { make in
+            make.left.equalToSuperview().inset(18)
+            make.right.equalTo(endBtn.snp.left).offset(-10)
+            make.top.height.equalToSuperview()
+        }
+        NotificationManager.add(self, selector: #selector(deviceChange(_:)), forName: .deviceValidChange)
+        updateState()
+    }
+    @objc func startBtnAction(){
+        if !CCBleManager.shared.isConnetd {
+            return
+        }
+        startBtn.isHidden = true
+        endBtn.isHidden = false
+        recordBtn.isHidden = false
+        recordBtn.state = .active
+        delegate?.recordBarStart(self)
+    }
+    @objc func endBtnAction(){
+        startBtn.isHidden = false
+        endBtn.isHidden = true
+        recordBtn.isHidden = true
+        delegate?.recordBarStop(self)
+    }
+    @objc func deviceChange(_ noti:Notification){
+        updateState()
+    }
+    func updateState(){
+        self.alpha = CCBleManager.shared.isConnetd ? 1:0.3
+    }
+}
+
+
+enum RSRecordState {
+    case active // 正在录音
+    case asr // 识别
+    case respose//响应
+}
+class RSRecordStateView: UIView {
+    
+    private var animationBg = UIView()
+    private var animationView = UIImageView()
+
+    
+    // 配置属性
+    private let normalColor = UIColor.white
+    private let recordingColor = UIColor.color(.theme)
+    private let titleLabel = UILabel()
+    
+    // 状态标识
+    private var isRecording = false
+    private var isCanceling = false
+    
+    private var startTime: Date?
+    private let minimumDuration: TimeInterval = 0.5
+    
+    // 取消阈值(可根据需求调整)
+    private let cancelThreshold: CGFloat = 30
+    
+    var state:RSRecordState = .active{
+        didSet{
+            if state == .active {
+                animationBg.backgroundColor = .color(.theme)
+                animationView.isHidden = false
+                titleLabel.isHidden = true
+                alpha = 1
+            }
+            else{
+                alpha = 0.5
+                animationBg.backgroundColor = .color(.theme)
+                animationView.isHidden = true
+                animationView.isHidden = true
+                titleLabel.isHidden = false
+                titleLabel.text =  state == .asr ? "识别中".l10n : "回复中".l10n
+            }
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupUI()
+    }
+    
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setupUI()
+    }
+    
+    private func setupUI() {
+        
+        titleLabel.do { v in
+            v.textColor = .white
+            v.font = .bold(16)
+            v.textAlignment = .center
+        }
+        
+        animationBg.do { v in
+            v.yc.setCornerRadius(16)
+            animationBg.backgroundColor = .color(.bgColor)
+        }
+        
+        animationView.kf.setImage(with: Bundle.main.url(forResource: "dialog_record", withExtension: "gif")!)
+        self.updateUI()
+        
+        addSubviews([animationBg])
+        animationBg.addSubviews([titleLabel,animationView])
+        titleLabel.snp.makeConstraints { make in
+            make.edges.equalToSuperview()
+        }
+
+        animationBg.snp.makeConstraints { make in
+            make.left.right.equalToSuperview()
+            make.top.height.equalToSuperview()
+        }
+        animationView.snp.makeConstraints { make in
+            make.width.equalTo(218)
+            make.height.equalTo(24)
+            make.center.equalToSuperview()
+        }
+    }
+
+
+    private func updateUI() {
+        if isRecording {
+            animationView.isHidden = false
+            if isCanceling {
+                animationBg.backgroundColor = .hexString("#FF4E4E")
+                titleLabel.isHidden = true
+            }
+            else{
+                titleLabel.isHidden = true
+                animationBg.backgroundColor = .color(.theme)
+            }
+        } else {
+            titleLabel.isHidden = false
+            
+            animationBg.backgroundColor = .color(.cellBg)
+            animationView.isHidden = true
+            animationView.isHidden = true
+        }
+    }
+    
+}