import Foundation import AVFoundation import MicrosoftCognitiveServicesSpeech // 自定义语音处理组件,提供音频流处理等功能 import speech /** * Azure TTS Helper * * 基于微软Azure语音服务的TTS实现 * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis */ class AzureTtsHelper: NSObject, ITtsService { private let tag = "AzureTtsHelper" private static let DEFAULT_LANGUAGE = "zh-CN" private static let DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" // Azure语音服务配置 private var speechConfig: SPXSpeechConfiguration? private var synthesizer: SPXSpeechSynthesizer? private var isInitialized = false private var speaking = false // 当前配置 private var currentVoice = DEFAULT_VOICE private var currentLanguage = DEFAULT_LANGUAGE private var currentRate = "+0%" private var currentPitch = "+0%" private var currentVolume = "100%" // 事件监听器列表 private var eventListeners = NSHashTable.weakObjects() // 流式文本处理的缓冲区 private var streamBuffer = "" private var lastSpeakTime: TimeInterval = 0 // 音频会话 private let audioSession = AVAudioSession.sharedInstance() /** * 初始化语音合成服务 * * @param appId 服务应用ID * @param token 服务访问令牌/订阅密钥 * @param resource 服务资源ID/区域(可选) * @param language 语言代码,如"zh-CN" * @return 是否初始化成功 */ func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String) -> Bool { do { // 创建语音配置 if ttsResource.isEmpty { speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: "eastasia") } else { speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: ttsResource) } // 设置语言 speechConfig?.speechSynthesisLanguage = language currentLanguage = language // 设置音频输出格式 - 使用16k、16位的PCM格式 speechConfig?.setPropertyTo("Audio16Khz16BitMonoPcm", byName: "SpeechServiceConnection_SynthOutputFormat") // 创建合成器,使用默认音频输出(扬声器) synthesizer = try SPXSpeechSynthesizer(speechConfig!) // 设置事件监听 setupEventListeners() // 设置默认音色 - 中文默认使用晓晓,英文默认使用Jenny if language.lowercased().starts(with: "zh") { _ = setVoice("zh-CN-XiaoxiaoNeural") } else { _ = setVoice("en-US-JennyNeural") } // 设置初始化完成 isInitialized = true return true } catch { print("\(tag): 语音合成服务初始化失败: \(error.localizedDescription)") return false } } /** * 设置语音角色 * * @param voiceName 语音角色名称(不同服务的语音角色命名可能不同) * @return 是否设置成功 */ func setVoice(_ voiceName: String) -> Bool { if !isInitialized { return false } do { currentVoice = voiceName // 更新语音名称 if let config = speechConfig { config.speechSynthesisVoiceName = voiceName } // 重新创建合成器 recreateSynthesizer() return true } catch { print("\(tag): 设置语音失败: \(error.localizedDescription)") notifyEvent(eventType: .error, params: [ "errorCode": "VOICE_SET_FAILED", "errorMessage": "设置语音失败: \(error.localizedDescription)" ]) return false } } /** * 单次合成并播放 * * @param text 要合成的文本 * @return 是否成功开始合成 */ func speakOnce(_ text: String) -> Bool { if !isInitialized { print("\(tag): 语音合成未初始化") return false } // 已经在播放,先停止 if speaking { _ = stop() } // 重置状态 speaking = true // 生成SSML let ssml = generateSsml(text) do { // 创建合成任务 try synthesizer?.startSpeakingSsml(ssml) return true } catch { print("\(tag): 语音合成失败: \(error.localizedDescription)") notifyEvent(eventType: .error, params: [ "errorCode": "SYNTHESIS_FAILED", "errorMessage": error.localizedDescription ]) speaking = false return false } } /** * 流式合成文本 * * @param text 要合成的文本片段 * @return 是否成功处理 */ func speakStream(_ text: String) -> Bool { if !isInitialized || text.isEmpty { if !isInitialized { notifyEvent(eventType: .error, params: [ "errorCode": "NOT_INITIALIZED", "errorMessage": "TTS引擎未初始化" ]) } return false } do { // 添加新文本到缓冲区 streamBuffer.append(text) // 增加500ms防抖逻辑 let currentTime = Date().timeIntervalSince1970 if currentTime - lastSpeakTime < 0.6 { return true } lastSpeakTime = currentTime let currentText = streamBuffer // 定义标点符号列表 let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":", "\n"] // 查找最后一个标点符号的位置 var lastPunctuationIndex = -1 for (i, char) in currentText.enumerated().reversed() { if punctuationMarks.contains(char) { lastPunctuationIndex = i break } } // 如果找到标点符号,则播放到该标点符号 if lastPunctuationIndex >= 0 { // 提取要播放的文本(包含标点符号) let textToSpeak = String(currentText.prefix(lastPunctuationIndex + 1)) // 剩余的文本保存在缓冲区中 let startIndex = currentText.index(currentText.startIndex, offsetBy: lastPunctuationIndex + 1) streamBuffer = String(currentText[startIndex...]) // 播放提取的文本 return speakOnce(textToSpeak) } // 如果没有找到标点符号,则等待更多文本 return true } catch { print("\(tag): 流式语音合成失败: \(error.localizedDescription)") notifyEvent(eventType: .error, params: [ "errorCode": "STREAM_FAILED", "errorMessage": "流式语音合成失败: \(error.localizedDescription)" ]) return false } } /** * 刷新并播放流式文本缓冲区中的剩余内容 * * @return 是否成功处理 */ func flushStream() -> Bool { if !isInitialized { notifyEvent(eventType: .error, params: [ "errorCode": "NOT_INITIALIZED", "errorMessage": "TTS引擎未初始化" ]) return false } do { // 获取缓冲区中剩余的文本 let remainingText = streamBuffer // 清空缓冲区 streamBuffer = "" // 如果缓冲区为空,直接返回成功 if remainingText.isEmpty { return true } // 播放剩余文本 return speakOnce(remainingText) } catch { print("\(tag): 刷新流式文本失败: \(error.localizedDescription)") notifyEvent(eventType: .error, params: [ "errorCode": "FLUSH_FAILED", "errorMessage": "刷新流式文本失败: \(error.localizedDescription)" ]) return false } } /** * 停止语音合成和播放 * * @return 是否成功停止 */ func stop() -> Bool { if !speaking || synthesizer == nil { return true } do { // 停止合成 try synthesizer?.stopSpeaking() speaking = false return true } catch { print("\(tag): 停止语音合成失败: \(error.localizedDescription)") notifyEvent(eventType: .error, params: [ "errorCode": "STOP_FAILED", "errorMessage": error.localizedDescription ]) return false } } /** * 释放资源 * 在不再需要服务时调用,释放底层资源 */ func dispose() { do { // 停止播放 _ = stop() // 释放合成器 synthesizer = nil // 释放配置 speechConfig = nil // 清空流缓冲区 streamBuffer = "" // 重置状态 isInitialized = false speaking = false } catch { // 即使发生异常,也确保重置状态 isInitialized = false speaking = false } } /** * 添加TTS事件监听器 * * @param listener 事件监听器 */ func addListener(_ listener: TtsEventListener) { eventListeners.add(listener as AnyObject) } /** * 移除TTS事件监听器 * * @param listener 要移除的事件监听器 */ func removeListener(_ listener: TtsEventListener) { eventListeners.remove(listener as AnyObject) } /** * 添加音频数据监听器 * 由于不再支持自定义音频流,此方法实际上不再有效 * * @param listener 音频数据监听器 */ func addAudioDataListener(_ listener: AudioDataListener) { print("\(tag): 警告:不支持音频数据监听器功能") } /** * 移除音频数据监听器 * 由于不再支持自定义音频流,此方法实际上不再有效 * * @param listener 要移除的音频数据监听器 */ func removeAudioDataListener(_ listener: AudioDataListener) { // 不做任何操作 } /** * 当前是否正在播放/合成 */ func isSpeaking() -> Bool { return speaking } // MARK: - 辅助方法 /** * 设置事件监听器 */ private func setupEventListeners() { guard let synthesizer = synthesizer else { return } // 添加合成开始事件处理器 synthesizer.addSynthesisStartedEventHandler { [weak self] _, _ in guard let self = self else { return } self.notifyEvent(eventType: .synthesisStarted) } // 添加合成中事件处理器 synthesizer.addSynthesizingEventHandler { [weak self] _, _ in // 可以在这里处理合成中的事件,目前没有特别操作 } // 添加合成完成事件处理器 synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in guard let self = self else { return } self.speaking = false self.notifyEvent(eventType: .synthesisCompleted) } // 添加合成取消事件处理器 synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in guard let self = self else { return } print("\(self.tag): 语音合成取消") self.speaking = false var params: [String: Any] = [:] do { let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: e.result) if cancellationDetails.reason == SPXCancellationReason.error { params["reason"] = String(describing: cancellationDetails.reason.rawValue) params["errorDetails"] = cancellationDetails.errorDetails ?? "未知错误" } } catch { params["errorDetails"] = "获取取消详情失败: \(error.localizedDescription)" } self.notifyEvent(eventType: .synthesisCanceled, params: params) } } /** * 触发事件通知 */ private func notifyEvent(eventType: TtsEventType, params: [String: Any] = [:]) { let event = TtsEvent(type: eventType, params: params) // 在主线程通知事件 DispatchQueue.main.async { for case let listener as TtsEventListener in self.eventListeners.allObjects { listener.onEvent(event) } } } /** * 重新创建合成器 */ private func recreateSynthesizer() { do { // 使用默认音频输出配置创建合成器 synthesizer = try SPXSpeechSynthesizer(speechConfig!) // 设置事件监听 setupEventListeners() } catch { print("\(tag): 重新创建合成器失败: \(error.localizedDescription)") notifyEvent(eventType: .error, params: [ "errorCode": "RECREATE_FAILED", "errorMessage": "重新创建合成器失败: \(error.localizedDescription)" ]) } } /** * 设置语音参数 */ private func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool { if !isInitialized { return false } do { currentRate = formatPercentage(rate) currentPitch = formatPercentage(pitch) currentVolume = "\(min(max(volume, 0), 100))%" return true } catch { print("\(tag): 设置语音参数失败: \(error.localizedDescription)") notifyEvent(eventType: .error, params: [ "errorCode": "PARAMS_SET_FAILED", "errorMessage": "设置语音参数失败: \(error.localizedDescription)" ]) return false } } /** * 格式化百分比值 */ private func formatPercentage(_ value: Int) -> String { return value >= 0 ? "+\(value)%" : "\(value)%" } /** * 生成SSML */ private func generateSsml(_ rawText: String) -> String { // 转义 XML 保留字符 var escapedText = rawText .replacingOccurrences(of: "&", with: "&") .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") // 构造简化的SSML文档 let ssml = """ \(escapedText) """ return ssml } }