import Foundation import AVFoundation import MicrosoftCognitiveServicesSpeech // 自定义语音处理组件,提供音频流处理等功能 import speech import os.log /** * Azure ASR Helper * * 基于微软Azure语音服务的ASR实现 * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-recognize-speech */ public class AzureAsrHelper: NSObject { private let tag = "AzureAsrHelper" // 日志对象 private let log = OSLog(subsystem: "com.azure.speech", category: "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 = "" // 音频源配置 public enum AudioSourceType { /** 使用设备麦克风 */ case microphone /** 使用外部提供的音频数据 */ case external } private var audioSourceType = AudioSourceType.microphone // 音频处理 private var externalAudioStream: ExternalAudioPullStream? /** * 初始化Azure语音服务 * * @param subscriptionKey Azure 订阅密钥 * @param region Azure 区域 * @param supportedLanguages 支持的语言数组 * @param audioSourceType 音频源类型 * @return 初始化是否成功 */ public func initialize( subscriptionKey: String, region: String, supportedLanguages: [String] = ["zh-CN"], audioSourceType: AudioSourceType = .microphone ) -> Bool { do { // 检查配置是否为空 if subscriptionKey.isEmpty || region.isEmpty { os_log("Azure 配置信息不完整", log: log, type: .error) 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 { // 直接启用语言检测模式 speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") os_log("启用语言检测模式", log: log, type: .info) } else { // 设置指定的识别语言 speechConfig?.speechRecognitionLanguage = currentLanguage } // 创建识别器 return setupRecognizer() } catch { os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription) return false } } /** * 向音频流写入音频数据 * 仅当音频源设置为external时有效 * * @param data 音频数据字节数组 */ public func pushAudioData(data: Data) { if audioSourceType != .external { return } // 使用拉流模式,将数据推入队列 externalAudioStream?.pushAudio(data) } /** * 开始连续语音识别 * * @param callback 连续识别结果回调 * @param audioSourceType 音频源类型 * @return 是否成功开始识别 */ public func startContinuousRecognition( callback: ContinuousRecognizeCallback, audioSourceType: AudioSourceType = .microphone ) -> Bool { guard speechConfig != nil else { callback.onError("语音服务未初始化") return false } if _isContinuousRecognitionActive { return true } self.audioSourceType = audioSourceType // 重置识别器 if !setupRecognizer() { callback.onError("重置识别器失败") return false } do { // 设置各种事件监听 setupEventListeners(callback: callback) // 启动音频处理 startAudioProcessing() // 开始连续识别 try recognizer?.startContinuousRecognition() _isContinuousRecognitionActive = true os_log("连续识别已启动,音频源: %{public}@", log: log, type: .info, audioSourceType == .microphone ? "麦克风" : "外部") return true } catch { stopAudioProcessing() _isContinuousRecognitionActive = false callback.onError("启动连续识别失败: \(error.localizedDescription)") return false } } /** * 停止连续语音识别 * * @return 是否成功停止 */ public func stopContinuousRecognition() -> Bool { guard speechConfig != nil else { os_log("语音服务未初始化", log: log, type: .error) return false } if !_isContinuousRecognitionActive { return true } do { guard let recognizer = recognizer else { os_log("识别器为空,重置状态", log: log, type: .info) _isContinuousRecognitionActive = false return true } if audioSourceType == .external { pushAudioData(data: Data()) } // 停止连续识别 try recognizer.stopContinuousRecognition() // 停止音频处理 stopAudioProcessing() // 会话结束事件会设置_isContinuousRecognitionActive = false return true } catch { // 强制重置状态 _isContinuousRecognitionActive = false os_log("停止连续识别失败: %{public}@", log: log, type: .error, error.localizedDescription) // 停止音频处理 stopAudioProcessing() // 尝试强制关闭识别器 recognizer = nil return false } } /** * 检查连续识别是否活跃 */ public func isContinuousRecognitionActive() -> Bool { return self._isContinuousRecognitionActive } /** * 释放所有资源 */ public func dispose() { do { // 如果正在进行连续识别,先停止 if _isContinuousRecognitionActive { // 直接停止,不等待结果 try? recognizer?.stopContinuousRecognition() _isContinuousRecognitionActive = false } // 停止音频处理 stopAudioProcessing() // 释放资源 recognizer = nil speechConfig = nil audioConfig = nil // 确保状态被重置 _isContinuousRecognitionActive = false externalAudioStream = nil } catch { // 确保状态被重置 _isContinuousRecognitionActive = false externalAudioStream = nil audioConfig = nil recognizer = nil speechConfig = nil } } // MARK: - 私有方法 /** * 设置识别器 */ private func setupRecognizer() -> Bool { do { // 清理旧的识别器 recognizer = nil // 设置音频配置 switch audioSourceType { case .microphone: // 使用默认麦克风输入配置 audioConfig = try SPXAudioConfiguration() case .external: // 使用拉流方式处理外部音频 setupExternalAudioStream() } // 创建识别器 if isAutoDetectLanguage { let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, audioConfiguration: audioConfig! ) } else { recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig!, audioConfiguration: audioConfig! ) } return true } catch { os_log("创建识别器失败: %{public}@", log: log, type: .error, error.localizedDescription) stopAudioProcessing() return false } } /** * 设置外部音频流 - 使用拉流方式 */ private func setupExternalAudioStream() { do { // 创建外部音频拉流对象 externalAudioStream = ExternalAudioPullStream() // 创建音频配置 audioConfig = SPXAudioConfiguration(streamInput: externalAudioStream!.pullStream) } catch { os_log("设置外部音频流失败: %{public}@", log: log, type: .error, error.localizedDescription) externalAudioStream = nil } } /** * 设置事件监听器 */ private func setupEventListeners(callback: ContinuousRecognizeCallback) { guard let recognizer = recognizer else { return } // 识别中事件 recognizer.addRecognizingEventHandler { [weak self] (sender, event) in guard let self = self else { return } let result = event.result let detectedLanguage = self.getDetectedLanguage(from: result) // os_log("识别中: %{public}@", log: self.log, type: .info, result.text ?? "") // 直接在当前线程调用回调 callback.onRecognizing(result.text ?? "", detectedLanguage) } // 识别完成事件 recognizer.addRecognizedEventHandler { [weak self] (sender, event) in guard let self = self else { return } let result = event.result if result.reason == SPXResultReason.recognizedSpeech { let detectedLanguage = self.getDetectedLanguage(from: result) // 直接在当前线程调用回调 callback.onResult(result.text ?? "", detectedLanguage) } } // 会话开始事件 recognizer.addSessionStartedEventHandler { (sender, event) in // 直接在当前线程调用回调 callback.onSessionStarted() } // 会话结束事件 recognizer.addSessionStoppedEventHandler { [weak self] (sender, event) in guard let self = self else { return } // 直接在当前线程调用回调 callback.onSessionStopped() self._isContinuousRecognitionActive = false self.stopAudioProcessing() } // 取消事件 recognizer.addCanceledEventHandler { [weak self] (sender, event) in guard let self = self else { return } let errorDetails = event.errorDetails ?? "未知错误" let reason = String(describing: event.reason.rawValue) os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails) callback.onCanceled(reason, errorDetails) } } /** * 获取检测到的语言 */ private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String { if !isAutoDetectLanguage { return "" } // 尝试从属性中获取语言 if let properties = result.properties, let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") { return language } // 尝试另一种方式获取语言 do { let langResult = try SPXAutoDetectSourceLanguageResult(result) return langResult.language ?? "" } catch { return "" } } /** * 启动音频处理 */ private func startAudioProcessing() { switch audioSourceType { case .microphone: // 拉流模式不需要额外启动,SDK会自动拉取数据 break case .external: // 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData break } } /** * 停止音频处理 */ private func stopAudioProcessing() { if let stream = externalAudioStream { stream.close() externalAudioStream = nil // os_log("外部音频流已关闭", log: log, type: .info) } } /** * 外部音频拉流 * 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式 */ private class ExternalAudioPullStream: NSObject { private(set) var pullStream: SPXPullAudioInputStream! private let queue = LinkedBlockingQueue() private var closed = false override init() { super.init() pullStream = SPXPullAudioInputStream( readHandler: { [weak self] (data: NSMutableData, size: UInt) -> Int in guard let self = self else { return 0 } return self.read(buffer: data, size: Int(size)) }, closeHandler: { [weak self] in self?.close() } ) } /** * 外部调用:推送音频数据到队列 * @param data 音频数据 */ func pushAudio(_ data: Data) { if !closed { queue.put(data) } } /** * SDK调用:从队列中拉取数据 * @param buffer SDK提供的缓冲区 * @param size 缓冲区大小 * @return 读取的字节数,0表示流结束 */ private func read(buffer: NSMutableData, size: Int) -> Int { // 阻塞等待下一块数据 guard let chunk = queue.take() else { return 0 // 队列已关闭 } // 如果是空数据,表示流结束 if chunk.isEmpty { return 0 } let toCopy = min(chunk.count, size) buffer.append(chunk.prefix(toCopy)) return toCopy } /** * SDK调用:关闭流 */ func close() { closed = true queue.close() } } /** * iOS版LinkedBlockingQueue实现 * 与Android LinkedBlockingQueue保持一致的API和行为 */ private class LinkedBlockingQueue { private var queue: [T] = [] private var isClosed = false // 使用DispatchSemaphore实现阻塞行为 private let availableItems: DispatchSemaphore private let queueLock = NSLock() init() { self.availableItems = DispatchSemaphore(value: 0) } /** * 阻塞式获取元素(等价于Android的take()) * @return 队列中的元素,如果队列已关闭则返回nil */ func take() -> T? { // 等待可用元素(阻塞直到有元素或队列关闭) availableItems.wait() queueLock.lock() defer { queueLock.unlock() } // 检查队列是否已关闭 if isClosed && queue.isEmpty { return nil } // 获取第一个元素 guard !queue.isEmpty else { return nil } return queue.removeFirst() } /** * 添加元素(等价于Android的put()) * @param item 要添加的元素 */ func put(_ item: T) { queueLock.lock() defer { queueLock.unlock() } // 检查队列是否已关闭 if isClosed { return } // 添加元素(无容量限制,与Android一致) queue.append(item) // 通知有新元素可用 availableItems.signal() } /** * 关闭队列 * 关闭后不能再添加新元素,但可以继续取出已有元素 */ func close() { queueLock.lock() defer { queueLock.unlock() } isClosed = true // 唤醒所有等待的take()操作 for _ in 0..<100 { // 假设最多100个等待者 availableItems.signal() } } } /** * 连续识别回调接口 */ public protocol ContinuousRecognizeCallback { /** * 返回识别结果 * * @param text 识别的文本 * @param detectedLanguage 检测到的语言 */ func onResult(_ text: String, _ detectedLanguage: String) /** * 识别进行中调用 * * @param recognizing 正在识别的文本 * @param detectedLanguage 检测到的语言 */ func onRecognizing(_ recognizing: String, _ detectedLanguage: String) /** * 会话开始时调用 */ func onSessionStarted() /** * 会话结束时调用 */ func onSessionStopped() /** * 识别取消时调用 * * @param reason 取消原因 * @param errorDetails 错误详情 */ func onCanceled(_ reason: String, _ errorDetails: String) /** * 识别出错时调用 * * @param error 错误信息 */ func onError(_ error: String) } }