import Foundation import MicrosoftCognitiveServicesSpeech import AVFoundation import AudioToolbox /// 自定义麦克风流实现,优化ASR语音输入捕获 class AzureMicrophoneStream: NSObject { var ioUnit: AudioUnit? var audioFormat: AudioStreamBasicDescription var audioBufferList: AudioBufferList var audioList: [Float] = [] let audioListQueue = DispatchQueue(label: "azureAudioListQueue") private var isActive = false override init() { // 音频会话配置 let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .defaultToSpeaker, .mixWithOthers]) try audioSession.setActive(true) print("[AzureMicrophoneStream] 音频会话配置成功") } catch { print("[AzureMicrophoneStream] 配置音频会话失败: \(error.localizedDescription)") } // 设置音频格式为16KHz 16位单声道PCM audioFormat = AudioStreamBasicDescription( mSampleRate: 16000.0, mFormatID: kAudioFormatLinearPCM, mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked, mBytesPerPacket: 2, mFramesPerPacket: 1, mBytesPerFrame: 2, mChannelsPerFrame: 1, mBitsPerChannel: 16, mReserved: 0 ) audioBufferList = AudioBufferList( mNumberBuffers: 1, mBuffers: AudioBuffer( mNumberChannels: audioFormat.mChannelsPerFrame, mDataByteSize: 4096, mData: malloc(4096) ) ) super.init() } func start() -> Bool { if isActive { return true // 已经在运行 } if setupAudioUnit() { isActive = startAudioUnit() return isActive } return false } private func setupAudioUnit() -> Bool { print("[AzureMicrophoneStream] 设置音频单元") var ioUnitDescription = AudioComponentDescription( componentType: kAudioUnitType_Output, componentSubType: kAudioUnitSubType_VoiceProcessingIO, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0 ) guard let ioUnitRef = AudioComponentFindNext(nil, &ioUnitDescription) else { print("[AzureMicrophoneStream] 无法找到音频组件") return false } if CheckHasError(AudioComponentInstanceNew(ioUnitRef, &ioUnit), "创建IO单元") { ioUnit = nil return false } var enableInput: UInt32 = 1 let kInputBus: AudioUnitElement = 1 let kOutputBus: AudioUnitElement = 0 if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, kInputBus, &enableInput, UInt32(MemoryLayout.size)), "设置输入总线的EnableIO属性") { return false } var enableOutput: UInt32 = 0 if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &enableOutput, UInt32(MemoryLayout.size)), "设置输出总线的EnableIO属性") { return false } var flag: UInt32 = 0 if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Output, kInputBus, &flag, UInt32(MemoryLayout.size)), "设置ShouldAllocateBuffer属性") { return false } let size = UInt32(MemoryLayout.size) if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kInputBus, &audioFormat, size), "设置输入总线的StreamFormat属性") { return false } if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &audioFormat, size), "设置输出总线的StreamFormat属性") { return false } var inputCallback = AURenderCallbackStruct( inputProc: AzureMicrophoneStream.OnRecordedDataIsAvailable, inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) ) if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, kInputBus, &inputCallback, UInt32(MemoryLayout.size)), "设置输入回调") { return false } var hasError = CheckHasError(AudioUnitInitialize(ioUnit!), "初始化IO单元") if hasError { // 如果初始化失败,重试一次 Thread.sleep(forTimeInterval: 0.5) hasError = CheckHasError(AudioUnitInitialize(ioUnit!), "重试初始化IO单元") } print("[AzureMicrophoneStream] 音频单元设置\(hasError ? "失败" : "成功")") return !hasError } private func startAudioUnit() -> Bool { print("[AzureMicrophoneStream] 启动音频单元") guard let ioUnit = ioUnit else { print("[AzureMicrophoneStream] IO单元未初始化") return false } return !CheckHasError(AudioOutputUnitStart(ioUnit), "启动IO单元") } func stop() { print("[AzureMicrophoneStream] 停止音频单元") guard isActive, let ioUnit = ioUnit else { return } _ = CheckHasError(AudioOutputUnitStop(ioUnit), "停止IO单元") isActive = false } func dispose() { stop() if let ioUnit = ioUnit { _ = CheckHasError(AudioUnitUninitialize(ioUnit), "反初始化IO单元") _ = CheckHasError(AudioComponentInstanceDispose(ioUnit), "释放IO单元") } // 释放缓冲区 if let buffer = audioBufferList.mBuffers.mData { free(buffer) } self.ioUnit = nil // 清空音频数据 audioListQueue.sync { audioList.removeAll() } } static let OnRecordedDataIsAvailable: AURenderCallback = { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData in let wrapper = Unmanaged.fromOpaque(inRefCon).takeUnretainedValue() let expectedDataByteSize = inNumberFrames * wrapper.audioFormat.mBytesPerFrame if wrapper.audioBufferList.mBuffers.mDataByteSize < expectedDataByteSize { wrapper.audioBufferList.mBuffers.mData = realloc(wrapper.audioBufferList.mBuffers.mData, Int(expectedDataByteSize)) wrapper.audioBufferList.mBuffers.mDataByteSize = expectedDataByteSize } let status = wrapper.CheckErrorStatus(AudioUnitRender(wrapper.ioUnit!, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, &wrapper.audioBufferList), "AudioUnitRender调用") var audioDataFloat = [Float](repeating: 0.0, count: Int(inNumberFrames)) let buffer = wrapper.audioBufferList.mBuffers let bufferData = buffer.mData!.assumingMemoryBound(to: Int16.self) for j in 0...size)) { audioDataFloat[j] = Float(bufferData[j]) / 32768.0 // 归一化到[-1.0, 1.0]范围 } if status == noErr { wrapper.audioListQueue.async { wrapper.audioList.append(contentsOf: audioDataFloat) } } return status } private func CheckHasError(_ status: OSStatus, _ operation: String) -> Bool { if status != noErr { print("[AzureMicrophoneStream] \(operation)失败: \(status)") return true } return false } private func CheckErrorStatus(_ status: OSStatus, _ operation: String) -> OSStatus { if status != noErr { print("[AzureMicrophoneStream] \(operation)失败: \(status)") } return status } // 读取音频数据,适配Azure SDK func read(bytes: inout [UInt8]) -> Int { return audioListQueue.sync { if audioList.isEmpty { return 0 } // 确保有足够的数据 let minFrames = 1280 if audioList.count < minFrames { return 0 } let frameLength = minFrames let buffer = Array(audioList.prefix(frameLength)) audioList.removeFirst(frameLength) // 转换为Int16数据 var int16Data = buffer.map { Int16($0 * 32767) } let data = Data(buffer: UnsafeBufferPointer(start: &int16Data, count: int16Data.count)) bytes = [UInt8](data) return frameLength * 2 // 每个样本2字节(16位PCM) } } } /// Azure ASR工具类,负责实现语音识别服务接口 @available(iOS 13.0, *) class AzureAsrHelper: NSObject { // MARK: - 属性 /// 事件处理回调 private var eventHandler: (String, [String: Any]) -> Void /// 语音配置信息 private var speechSubscriptionKey: String = "" private var serviceRegion: String = "" /// 语音识别相关 private var speechConfig: SPXSpeechConfiguration? private var recognizer: SPXSpeechRecognizer? private var audioConfig: SPXAudioConfiguration? /// 麦克风流 private var microphoneStream: AzureMicrophoneStream? private var pushStreamConfig: SPXPushAudioInputStream? /// 状态标志 private var isInitialized = false private var _isContinuousRecognitionActive = false /// 当前语言和支持的语言 private var currentLanguage = "zh-CN" private var supportedLanguages: [String] = ["zh-CN", "en-US"] private var isAutoDetectLanguage = false // MARK: - 初始化 init(eventHandler: @escaping (String, [String: Any]) -> Void) { self.eventHandler = eventHandler super.init() } deinit { dispose() } // MARK: - ASR Service 接口实现 /// 初始化语音识别服务 /// - Parameters: /// - speechSubscriptionKey: Azure 语音服务订阅密钥 /// - serviceRegion: Azure 服务区域 (如 eastasia) /// - supportedLanguages: 支持的语言代码数组 (可选) /// - Returns: 初始化是否成功 func initialize(speechSubscriptionKey: String, serviceRegion: String, supportedLanguages: [String]? = nil) -> Bool { print("[AzureAsrHelper] 初始化 Azure 语音服务") // 检查配置是否为空 if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { print("[AzureAsrHelper] 错误: Azure 配置信息不完整") eventHandler("error", ["message": "Azure 配置信息不完整"]) return false } // 释放之前的资源 dispose() // 记录配置信息 self.speechSubscriptionKey = speechSubscriptionKey self.serviceRegion = serviceRegion // 设置语言 if let languages = supportedLanguages, !languages.isEmpty { self.supportedLanguages = languages } // 根据支持的语言数量决定是否启用自动语言检测 isAutoDetectLanguage = self.supportedLanguages.count >= 2 // 如果只有一种语言,设置为当前语言 if !isAutoDetectLanguage && !self.supportedLanguages.isEmpty { currentLanguage = self.supportedLanguages[0] } // 创建麦克风流 microphoneStream = AzureMicrophoneStream() print("[AzureAsrHelper] Azure 语音服务初始化成功") isInitialized = true return true } /// 重置 recognizer private func resetRecognizer() -> Bool { // 释放之前的 recognizer recognizer = nil audioConfig = nil do { // 创建语音配置 speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion) // 创建推送流 pushStreamConfig = try SPXPushAudioInputStream() // 创建音频配置,使用推送流 audioConfig = try SPXAudioConfiguration(streamInput: pushStreamConfig!) // 设置语言配置 if isAutoDetectLanguage { // 设置自动语言检测 speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode) // 创建自动语言检测配置 let autoDetectSourceLanguageConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) // 创建识别器 recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig, audioConfiguration: audioConfig! ) } else { // 设置指定的识别语言 speechConfig?.speechRecognitionLanguage = currentLanguage // 创建识别器 recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!) } return true } catch { print("[AzureAsrHelper] 错误: 重置识别器失败: \(error.localizedDescription)") eventHandler("error", ["message": "重置识别器失败: \(error.localizedDescription)"]) return false } } /// 启动音频捕获和数据推送 private func startAudioStream() -> Bool { guard let micStream = microphoneStream else { print("[AzureAsrHelper] 错误: 麦克风流未初始化") return false } // 启动麦克风 if !micStream.start() { print("[AzureAsrHelper] 错误: 启动麦克风流失败") return false } // 创建并启动音频推送线程 DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self, let pushStream = self.pushStreamConfig else { return } var isRunning = true var audioBuffer = [UInt8](repeating: 0, count: 16000) while isRunning { autoreleasepool { // 读取麦克风数据 let bytesRead = micStream.read(bytes: &audioBuffer) if bytesRead > 0 { do { // 推送音频数据到Azure识别流 let data = Data(bytes: audioBuffer, count: bytesRead) try pushStream.write(data) } catch { print("[AzureAsrHelper] 推送音频数据失败: \(error.localizedDescription)") isRunning = false } } // 检查是否应该继续捕获 if !self._isContinuousRecognitionActive { isRunning = false } // 添加适当的休眠以避免过度消耗CPU if bytesRead == 0 { Thread.sleep(forTimeInterval: 0.01) } } } print("[AzureAsrHelper] 音频推送线程已停止") } return true } /// 执行一次性语音识别 /// - Returns: 是否成功启动识别 func recognizeOnce() -> Bool { if !isInitialized { print("[AzureAsrHelper] 错误: 语音服务未初始化") eventHandler("error", ["message": "语音服务未初始化"]) return false } // 如果正在连续识别,先停止 if _isContinuousRecognitionActive { stopContinuousRecognition() } // 重置 recognizer if !resetRecognizer() { return false } // 启动音频流 if !startAudioStream() { return false } do { // 设置回调 recognizer?.addRecognizedEventHandler { [weak self] _, event in guard let self = self else { return } if event.result.reason == SPXResultReason.recognizedSpeech { let detectedLanguage = self.getDetectedLanguage(from: event.result) self.eventHandler("result", [ "text": event.result.text ?? "", "detectedLanguage": detectedLanguage ]) } } recognizer?.addCanceledEventHandler { [weak self] _, event in guard let self = self else { return } let errorDetails = event.errorDetails ?? "未知错误" self.eventHandler("error", ["message": "识别异常: \(errorDetails)"]) } // 通知会话开始 eventHandler("sessionStarted", [:]) // 执行识别 try recognizer?.recognizeOnceAsync { [weak self] result in guard let self = self else { return } // 停止麦克风流 self.microphoneStream?.stop() if result.reason == SPXResultReason.recognizedSpeech { let detectedLanguage = self.getDetectedLanguage(from: result) self.eventHandler("result", [ "text": result.text ?? "", "detectedLanguage": detectedLanguage ]) } else if result.reason == SPXResultReason.canceled { do { let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result) let errorDetails = details.errorDetails ?? "未知错误" self.eventHandler("error", ["message": "识别取消: \(errorDetails)"]) } catch { print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)") self.eventHandler("error", ["message": "识别取消,无法获取详细原因"]) } } } return true } catch { print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)") eventHandler("error", ["message": "识别异常: \(error.localizedDescription)"]) microphoneStream?.stop() return false } } /// 开始连续语音识别 /// - Returns: 是否成功启动识别 func startContinuousRecognition() -> Bool { if !isInitialized { print("[AzureAsrHelper] 错误: 语音服务未初始化") eventHandler("error", ["message": "语音服务未初始化"]) return false } // 如果已经在进行连续识别,先停止 if _isContinuousRecognitionActive { stopContinuousRecognition() } // 重置 recognizer if !resetRecognizer() { return false } do { // 设置识别事件处理 setupContinuousRecognitionCallbacks() // 启动连续识别 try recognizer?.startContinuousRecognition() _isContinuousRecognitionActive = true // 启动音频流 if !startAudioStream() { try recognizer?.stopContinuousRecognition() _isContinuousRecognitionActive = false return false } // 通知会话开始 eventHandler("sessionStarted", [:]) print("[AzureAsrHelper] 连续识别开始") return true } catch { print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)") eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"]) _isContinuousRecognitionActive = false microphoneStream?.stop() return false } } /// 停止连续语音识别 /// - Returns: 是否成功停止识别 func stopContinuousRecognition() -> Bool { if !_isContinuousRecognitionActive || recognizer == nil { return true } // 停止麦克风流 microphoneStream?.stop() do { try recognizer?.stopContinuousRecognition() _isContinuousRecognitionActive = false eventHandler("sessionStopped", [:]) print("[AzureAsrHelper] 连续识别已停止") return true } catch { print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)") eventHandler("error", ["message": "停止连续识别失败: \(error.localizedDescription)"]) _isContinuousRecognitionActive = false return false } } /// 检查连续识别是否活跃 /// - Returns: 连续识别是否处于活跃状态 func isContinuousRecognitionActive() -> Bool { return _isContinuousRecognitionActive } /// 释放资源 func dispose() { print("[AzureAsrHelper] 释放资源") // 停止连续识别 if _isContinuousRecognitionActive { stopContinuousRecognition() } // 关闭麦克风流 microphoneStream?.dispose() microphoneStream = nil // 关闭推送流 if let pushStream = pushStreamConfig { do { try pushStream.close() } catch { print("[AzureAsrHelper] 关闭推送流失败: \(error.localizedDescription)") } } // 释放资源 recognizer = nil speechConfig = nil audioConfig = nil pushStreamConfig = nil // 重置状态 _isContinuousRecognitionActive = false isInitialized = false } // MARK: - 私有辅助方法 /// 设置连续识别回调 private func setupContinuousRecognitionCallbacks() { // 最终识别结果 recognizer?.addRecognizedEventHandler { [weak self] _, event in guard let self = self else { return } if event.result.reason == SPXResultReason.recognizedSpeech { let detectedLanguage = self.getDetectedLanguage(from: event.result) print("[AzureAsrHelper] 识别结果: \(event.result.text ?? ""), 语言: \(detectedLanguage)") self.eventHandler("result", [ "text": event.result.text ?? "", "detectedLanguage": detectedLanguage ]) } } // 识别中事件 recognizer?.addRecognizingEventHandler { [weak self] _, event in guard let self = self else { return } if event.result.reason == SPXResultReason.recognizingSpeech { let detectedLanguage = self.getDetectedLanguage(from: event.result) print("[AzureAsrHelper] 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)") self.eventHandler("recognizing", [ "text": event.result.text ?? "", "detectedLanguage": detectedLanguage ]) } } // 会话事件 recognizer?.addSessionStartedEventHandler { [weak self] _, _ in guard let self = self else { return } self._isContinuousRecognitionActive = true self.eventHandler("sessionStarted", [:]) } recognizer?.addSessionStoppedEventHandler { [weak self] _, _ in guard let self = self else { return } self._isContinuousRecognitionActive = false self.eventHandler("sessionStopped", [:]) } // 取消事件 recognizer?.addCanceledEventHandler { [weak self] _, event in guard let self = self else { return } let reason = event.reason.rawValue let errorDetails = event.errorDetails ?? "" self.eventHandler("canceled", [ "reason": reason, "errorDetails": errorDetails ]) self._isContinuousRecognitionActive = false self.microphoneStream?.stop() } } /// 从结果中获取检测到的语言 private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String { if isAutoDetectLanguage { do { let langResult = try SPXAutoDetectSourceLanguageResult(result) return langResult.language ?? currentLanguage } catch { print("[AzureAsrHelper] 错误: 获取检测到的语言失败: \(error.localizedDescription)") return currentLanguage } } else { return currentLanguage } } }