import Foundation import AVFoundation import MicrosoftCognitiveServicesSpeech /// Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 class AzureAsrHelper { private let tag = "AzureAsrHelper" // 核心组件 private var speechConfig: SPXSpeechConfiguration? private var recognizer: SPXSpeechRecognizer? private var audioConfig: SPXAudioConfiguration? // 状态管理 private var isContinuousRecognitionActive = false // 配置参数 private var currentLanguage = "zh-CN" private var supportedLanguages = ["zh-CN"] private var isAutoDetectLanguage = false private var subscriptionKey = "" private var region = "" // 音频源配置 private var audioSourceType = AudioSourceType.microphone // 音频处理 private var microphoneStream: MicrophoneStream? private var externalAudioStream: ExternalAudioPullStream? // 连续识别回调 private var continuousRecognizeCallback: ContinuousRecognizeCallback? /// 应用上下文 private let context: Any /// 音频来源类型 enum AudioSourceType { /// 使用设备麦克风 case microphone /// 使用外部提供的音频数据 case external } /// 一次性识别回调 protocol RecognizeCallback { /// 识别结果回调 /// - Parameters: /// - text: 识别文本 /// - detectedLanguage: 检测到的语言 func onResult(text: String, detectedLanguage: String) /// 识别错误回调 /// - Parameter error: 错误信息 func onError(error: String) } /// 连续识别回调 protocol ContinuousRecognizeCallback { /// 识别中回调 /// - Parameters: /// - recognizing: 识别中的文本 /// - detectedLanguage: 检测到的语言 func onRecognizing(recognizing: String, detectedLanguage: String) /// 识别结果回调 /// - Parameters: /// - text: 识别文本 /// - detectedLanguage: 检测到的语言 func onResult(text: String, detectedLanguage: String) /// 会话开始回调 func onSessionStarted() /// 会话结束回调 func onSessionStopped() /// 识别取消回调 /// - Parameters: /// - reason: 取消原因 /// - errorDetails: 错误详情 func onCanceled(reason: String, errorDetails: String) /// 识别错误回调 /// - Parameter error: 错误信息 func onError(error: String) } /// 初始化Azure语音识别辅助类 /// - Parameter context: 应用上下文 init(_ context: Any) { self.context = context } /// 初始化Azure语音服务 /// - Parameters: /// - subscriptionKey: Azure 订阅密钥 /// - region: Azure 区域 /// - supportedLanguages: 支持的语言数组,默认为["zh-CN"] /// - audioSourceType: 音频源类型,默认为麦克风 /// - Returns: 初始化是否成功 func initialize( subscriptionKey: String, region: String, supportedLanguages: [String] = ["zh-CN"], audioSourceType: AudioSourceType = .microphone ) -> Bool { do { print("\(tag): 初始化 Azure 语音服务, 音频源类型: \(audioSourceType)") // 检查配置是否为空 if subscriptionKey.isEmpty || region.isEmpty { print("\(tag): Azure 配置信息不完整") return false } // 释放之前的资源 dispose() // 保存配置 self.subscriptionKey = subscriptionKey self.region = region self.audioSourceType = audioSourceType // 设置语言 if !supportedLanguages.isEmpty { self.supportedLanguages = supportedLanguages } // 根据支持的语言数量决定是否启用自动语言检测 self.isAutoDetectLanguage = supportedLanguages.count >= 2 // 如果只有一种语言,设置为当前语言 if !isAutoDetectLanguage && !supportedLanguages.isEmpty { self.currentLanguage = supportedLanguages[0] } // 创建语音配置 speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) if isAutoDetectLanguage { // 启用语言检测模式 try speechConfig?.setPropertyTo("Continuous", byId: SPXPropertyId.speechServiceConnection_languageIdMode) } else { // 设置指定的识别语言 speechConfig?.speechRecognitionLanguage = currentLanguage } // 设置音频配置 return setupRecognizer() } catch { print("\(tag): 初始化失败: \(error.localizedDescription)") return false } } /// 设置识别器 private func setupRecognizer() -> Bool { do { // 清理旧的识别器 recognizer = nil // 设置音频配置 switch audioSourceType { case .microphone: // 检查是否连接了耳机 let isHeadsetConnected = isHeadphonesConnected() if !isHeadsetConnected { print("\(tag): 未检测到耳机,使用麦克风输入") // 使用拉流模式进行回音消除 setupMicrophoneStream() } else { print("\(tag): 检测到耳机连接,使用默认麦克风输入") // 使用默认麦克风输入 audioConfig = SPXAudioConfiguration() } case .external: // 创建拉流 setupExternalAudioStream() print("\(tag): 使用外部音频源(拉流模式)") } // 创建识别器 if isAutoDetectLanguage { let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguages: supportedLanguages) recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, audioConfiguration: audioConfig!) } else { recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!) } print("\(tag): Azure 语音服务初始化成功") return true } catch { print("\(tag): 创建识别器失败: \(error.localizedDescription)") stopAudioProcessing() return false } } /// 设置麦克风流 - 使用拉流方式 private func setupMicrophoneStream() { do { // 创建麦克风流 microphoneStream = MicrophoneStream() // 创建音频配置 audioConfig = try SPXAudioConfiguration(streamInput: microphoneStream!) print("\(tag): 已设置麦克风流(拉流模式)") } catch { print("\(tag): 设置麦克风流失败: \(error.localizedDescription)") } } /// 设置外部音频流 - 使用拉流方式 private func setupExternalAudioStream() { do { // 创建外部音频拉流对象 externalAudioStream = ExternalAudioPullStream() // 创建音频配置 audioConfig = try SPXAudioConfiguration(streamInput: externalAudioStream!) print("\(tag): 已设置外部音频流(拉流模式)") } catch { print("\(tag): 设置外部音频流失败: \(error.localizedDescription)") } } /// 向音频流写入音频数据 /// 仅当音频源设置为external时有效 /// - Parameter data: 音频数据字节数组 func pushAudioData(_ data: Data) { if audioSourceType != .external { print("\(tag): 当前未使用外部音频源,忽略推送的音频数据") return } // 使用拉流模式,将数据推入队列 externalAudioStream?.pushAudio(data) } /// 执行一次性语音识别 /// - Parameter callback: 识别结果回调 func recognizeOnce(callback: RecognizeCallback) { if speechConfig == nil { callback.onError(error: "语音服务未初始化") return } // 确保不在连续识别中 if isContinuousRecognitionActive { _ = stopContinuousRecognition() } // 重置识别器 if !setupRecognizer() { callback.onError(error: "重置识别器失败") return } // 启动音频处理 startAudioProcessing() // 执行同步识别 do { let result = try recognizer?.recognizeOnce() // 停止音频处理 stopAudioProcessing() if let result = result, result.reason == .recognizedSpeech { let resultText = result.text ?? "" var detectedLanguage = supportedLanguages[0] // 尝试获取检测到的语言 if isAutoDetectLanguage, let autoDetectResult = result.properties?.getPropertyByName("SourceLanguage") { detectedLanguage = autoDetectResult } callback.onResult(text: resultText, detectedLanguage: detectedLanguage) } else { callback.onError(error: "未能识别语音") } } catch { stopAudioProcessing() callback.onError(error: "识别异常: \(error.localizedDescription)") } } /// 开始连续语音识别 /// - Parameter callback: 连续识别结果回调 /// - Returns: 是否成功开始识别 func startContinuousRecognition(_ callback: ContinuousRecognizeCallback) -> Bool { if speechConfig == nil { callback.onError(error: "语音服务未初始化") return false } if isContinuousRecognitionActive { return true } // 重置识别器 if !setupRecognizer() { callback.onError(error: "重置识别器失败") return false } // 保存回调 continuousRecognizeCallback = callback do { // 设置各种事件监听 setupEventListeners(callback) // 启动音频处理 startAudioProcessing() // 开始连续识别 try recognizer?.startContinuousRecognition() isContinuousRecognitionActive = true return true } catch { isContinuousRecognitionActive = false stopAudioProcessing() callback.onError(error: "启动连续识别失败: \(error.localizedDescription)") return false } } /// 设置事件监听器 private func setupEventListeners(_ callback: ContinuousRecognizeCallback) { guard let recognizer = recognizer else { return } // 识别中事件 recognizer.addRecognizing { [weak self] _, event in guard let self = self else { return } var detectedLanguage = "" if self.isAutoDetectLanguage, let lang = event.result.properties?.getPropertyByName("SourceLanguage") { detectedLanguage = lang } print("\(self.tag): 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)") callback.onRecognizing(recognizing: event.result.text ?? "", detectedLanguage: detectedLanguage) } // 识别完成事件 recognizer.addRecognized { [weak self] _, event in guard let self = self else { return } if event.result.reason == .recognizedSpeech { var detectedLanguage = "" if self.isAutoDetectLanguage, let lang = event.result.properties?.getPropertyByName("SourceLanguage") { detectedLanguage = lang } print("\(self.tag): 识别完成: \(event.result.text ?? ""), 语言: \(detectedLanguage)") callback.onResult(text: event.result.text ?? "", detectedLanguage: detectedLanguage) } } // 会话开始事件 recognizer.addSessionStarted { [weak self] _, _ in guard let self = self else { return } print("\(self.tag): 识别会话已开始") callback.onSessionStarted() } // 会话结束事件 recognizer.addSessionStopped { [weak self] _, _ in guard let self = self else { return } print("\(self.tag): 识别会话已结束") self.isContinuousRecognitionActive = false self.stopAudioProcessing() callback.onSessionStopped() } // 取消事件 recognizer.addCanceled { [weak self] _, event in guard let self = self else { return } let errorDetails = event.errorDetails ?? "未知错误" let reason = String(describing: event.reason) print("\(self.tag): 识别取消: \(errorDetails)") self.isContinuousRecognitionActive = false self.stopAudioProcessing() callback.onCanceled(reason: reason, errorDetails: errorDetails) callback.onError(error: "识别取消: \(errorDetails)") // 兼容旧接口 } } /// 停止连续语音识别 /// - Returns: 是否成功停止 func stopContinuousRecognition() -> Bool { if speechConfig == nil { print("\(tag): 语音服务未初始化") return false } if !isContinuousRecognitionActive { print("\(tag): 未进行连续识别,忽略停止请求") return true } do { print("\(tag): 停止连续语音识别") if recognizer == nil { print("\(tag): 识别器为空,重置状态") isContinuousRecognitionActive = false return true } // 停止连续识别 try recognizer?.stopContinuousRecognition() // 停止音频处理 stopAudioProcessing() // 等待一段时间以确保资源正确释放 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.isContinuousRecognitionActive = false } print("\(tag): 连续识别停止指令已发送") return true } catch { // 强制重置状态 isContinuousRecognitionActive = false print("\(tag): 停止连续识别失败: \(error.localizedDescription)") // 停止音频处理 stopAudioProcessing() // 尝试强制关闭识别器 recognizer = nil return false } } /// 检查连续识别是否活跃 /// - Returns: 是否活跃 func isContinuousRecognitionActive() -> Bool { return isContinuousRecognitionActive } /// 释放所有资源 func dispose() { // 如果正在进行连续识别,先停止 if isContinuousRecognitionActive { _ = stopContinuousRecognition() } // 停止音频处理 stopAudioProcessing() // 清理所有资源 recognizer = nil speechConfig = nil audioConfig = nil microphoneStream = nil externalAudioStream = nil // 重置状态 isContinuousRecognitionActive = false continuousRecognizeCallback = nil } /// 启动音频处理 private func startAudioProcessing() { switch audioSourceType { case .microphone: // 拉流模式不需要额外启动,SDK会自动拉取数据 break case .external: // 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData break } } /// 停止音频处理 private func stopAudioProcessing() { microphoneStream?.close() microphoneStream = nil externalAudioStream?.close() externalAudioStream = nil } /// 检查是否连接了耳机 private func isHeadphonesConnected() -> Bool { let audioSession = AVAudioSession.sharedInstance() let outputs = audioSession.currentRoute.outputs // 检查是否存在耳机类型的输出 let headphoneTypes: [AVAudioSession.Port] = [ .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE ] for output in outputs { if headphoneTypes.contains(output.portType) { return true } } return false } /// 麦克风流 - 拉流模式 /// 实现SPXPullAudioInputStreamDelegate,为Azure SDK提供音频数据 class MicrophoneStream: NSObject, SPXPullAudioInputStreamDelegate { private let tag = "MicrophoneStream" private var audioEngine: AVAudioEngine? private var audioBuffer = Data() private let bufferLock = NSLock() override init() { super.init() setupAudioEngine() } private func setupAudioEngine() { do { print("\(tag): 初始化音频引擎") // 创建音频引擎 audioEngine = AVAudioEngine() // 设置音频会话 let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.record, mode: .default) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) // 获取输入节点 guard let inputNode = audioEngine?.inputNode else { print("\(tag): 无法获取输入节点") return } // 创建16kHz, 16bit, 单声道格式 let format = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 16000, channels: 1, interleaved: true) // 设置音频处理回调 let bufferSize = 4096 inputNode.installTap(onBus: 0, bufferSize: UInt32(bufferSize), format: format) { [weak self] (buffer, time) in guard let self = self else { return } // 将音频数据转换为Data let audioBuffer = buffer.int16ChannelData?[0] let audioBufferSize = Int(buffer.frameLength * buffer.format.streamDescription.pointee.mBytesPerFrame) if let audioBuffer = audioBuffer { let data = Data(bytes: audioBuffer, count: audioBufferSize) self.bufferLock.lock() self.audioBuffer.append(data) self.bufferLock.unlock() } } // 启动音频引擎 audioEngine?.prepare() try audioEngine?.start() print("\(tag): 音频引擎启动成功") } catch { print("\(tag): 设置音频引擎失败: \(error.localizedDescription)") } } // SPXPullAudioInputStreamDelegate协议方法 func read(_ buffer: UnsafeMutablePointer, size: UInt32) -> Int32 { bufferLock.lock() defer { bufferLock.unlock() } let maxSize = Int(size) if audioBuffer.count == 0 { // 没有数据可读 return 0 } let readSize = min(audioBuffer.count, maxSize) audioBuffer.copyBytes(to: buffer, from: 0.., size: UInt32) -> Int32 { bufferLock.lock() defer { bufferLock.unlock() } if isClosed { return 0 } let maxSize = Int(size) if audioBuffer.count == 0 { // 没有数据可读,返回0表示需要等待 return 0 } let readSize = min(audioBuffer.count, maxSize) audioBuffer.copyBytes(to: buffer, from: 0..