import Foundation import MicrosoftCognitiveServicesSpeech import AVFoundation /// Azure TTS工具类,负责实现TTS服务接口 @available(iOS 13.0, *) class AzureTtsHelper: NSObject { // MARK: - 属性 /// 事件处理回调 private var eventHandler: (([String: Any]) -> Void)? /// 语音配置信息 private var speechSubscriptionKey: String = "" private var serviceRegion: String = "" /// 语音合成配置 private var speechConfig: SPXSpeechConfiguration? /// 语音合成器 private var synthesizer: SPXSpeechSynthesizer? /// 是否初始化成功 private var isInitialized = false /// 当前是否正在播放 private var _isSpeaking = false /// 音频会话配置 private var isAudioSessionConfigured = false /// 音频会话管理器 private let audioSessionManager = AudioSessionManager.shared // MARK: - 语音设置 /// 当前语音 private var currentVoice = "zh-CN-XiaoxiaoNeural" /// 支持的语音映射 private var voiceMap: [String: String] = [ "zh-CN": "zh-CN-XiaoxiaoNeural", "en-US": "en-US-JennyNeural", "ja-JP": "ja-JP-NanamiNeural", "ko-KR": "ko-KR-SunHiNeural", "zh-TW": "zh-TW-HsiaoChenNeural", "zh-HK": "zh-HK-HiuMaanNeural" ] /// 当前语音合成参数 private var currentSpeechRate = "0%" private var currentPitch = "0%" private var currentVolume = "100%" // MARK: - 初始化 override init() { super.init() } deinit { dispose() } /// 设置事件处理器 @objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) { self.eventHandler = handler } // MARK: - TTS 接口实现 /// 初始化语音合成服务 /// - Parameters: /// - speechSubscriptionKey: Azure 语音服务订阅密钥 /// - serviceRegion: Azure 服务区域 (如 eastasia) /// - language: 语言代码 (默认 zh-CN) /// - Returns: 初始化是否成功 func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool { print("[AzureTtsHelper] 初始化语音合成服务") // 检查配置是否为空 if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { print("[AzureTtsHelper] 错误: Azure 配置信息不完整") eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) return false } // 释放之前的资源 dispose() // 记录配置信息 self.speechSubscriptionKey = speechSubscriptionKey self.serviceRegion = serviceRegion // 配置音频会话 if !configureAudioSession() { print("[AzureTtsHelper] 警告: 音频会话配置失败,将尝试继续初始化") } do { // 创建语音配置 speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion) // 设置默认语音 let defaultVoice = getDefaultVoiceForLanguage(language) currentVoice = defaultVoice speechConfig?.speechSynthesisVoiceName = defaultVoice // 创建语音合成器 synthesizer = try SPXSpeechSynthesizer(speechConfig!) // 设置事件处理器 setupSynthesizerEvents() isInitialized = true print("[AzureTtsHelper] TTS 引擎初始化成功") return true } catch { print("[AzureTtsHelper] 错误: 初始化语音合成服务失败: \(error.localizedDescription)") eventHandler?(["type": "error", "message": "初始化语音合成服务失败: \(error.localizedDescription)"]) return false } } /// 配置音频会话 private func configureAudioSession() -> Bool { do { // 使用音频会话管理器配置媒体播放模式 try audioSessionManager.configureForVoiceInteraction() isAudioSessionConfigured = true print("[AzureTtsHelper] 音频会话配置成功") return true } catch { print("[AzureTtsHelper] 警告: 音频会话配置失败: \(error.localizedDescription)") isAudioSessionConfigured = false return false } } /// 设置语音 /// - Parameter voiceName: 语音名称 (如 "zh-CN-XiaoxiaoNeural") /// - Returns: 设置是否成功 func setVoice(voiceName: String) -> Bool { // 检查是否初始化 if !isInitialized { print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化") eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"]) return false } // 检查参数有效性 if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { print("[AzureTtsHelper] 错误: Azure 配置信息不完整") eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) return false } if voiceName.isEmpty { print("[AzureTtsHelper] 错误: 声音名称为空") eventHandler?(["type": "error", "message": "声音名称不能为空"]) return false } if voiceName == currentVoice { print("[AzureTtsHelper] 已设置语音: \(voiceName)") return true } print("[AzureTtsHelper] 设置声音: \(voiceName)") currentVoice = voiceName // 更新语音配置 if let speechConfig = speechConfig { speechConfig.speechSynthesisVoiceName = voiceName return true } return false } /// 设置语音合成参数 /// - Parameters: /// - rate: 语速,范围 -100 到 100,默认为 0 /// - pitch: 音调,范围 -100 到 100,默认为 0 /// - volume: 音量,范围 0 到 100,默认为 100 /// - Returns: 是否设置成功 func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool { // 检查是否初始化 if !isInitialized { print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化") eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"]) return false } // 检查参数有效性 if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { print("[AzureTtsHelper] 错误: Azure 配置信息不完整") eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) return false } // 转换参数格式 currentSpeechRate = formatRateParam(rate) currentPitch = formatPitchParam(pitch) currentVolume = formatVolumeParam(volume) print("[AzureTtsHelper] 已设置语音参数: 语速=\(currentSpeechRate), 音调=\(currentPitch), 音量=\(currentVolume)") return true } /// 合成文本为语音并播放 /// - Parameter text: 要合成的文本 /// - Returns: 操作是否成功启动 func speakText(text: String) -> Bool { // 检查是否初始化 if !isInitialized { print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化") eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"]) return false } // 检查参数有效性 if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty { print("[AzureTtsHelper] 错误: Azure 配置信息不完整") eventHandler?(["type": "error", "message": "Azure 配置信息不完整"]) return false } if text.isEmpty { print("[AzureTtsHelper] 警告: 要播放的文本为空") return true } // 确保音频会话已配置 if !isAudioSessionConfigured { _ = configureAudioSession() } print("[AzureTtsHelper] 开始语音合成: \(text.prefix(50))...") // 生成SSML let ssml = generateSsml(text: text) // 直接进行SSML合成 return speakSsmlInternal(text: ssml) } /// 内部SSML合成和播放 private func speakSsmlInternal(text: String) -> Bool { guard let synthesizer = synthesizer else { print("[AzureTtsHelper] 错误: 合成器未初始化") eventHandler?(["type": "error", "message": "合成器未初始化"]) return false } _isSpeaking = true eventHandler?(["type": "started"]) Task { do { // 使用异步方法进行合成并直接播放 _ = try synthesizer.startSpeakingSsml(text) } catch { print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)") DispatchQueue.main.async { self._isSpeaking = false self.eventHandler?(["type": "error", "message": "语音合成失败: \(error.localizedDescription)"]) } } } return true } /// 停止当前语音合成 /// - Returns: 操作是否成功 func stopSpeaking() -> Bool { // 检查是否初始化 if !isInitialized { print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化") eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"]) return false } if !_isSpeaking { print("[AzureTtsHelper] 未进行语音播放,忽略停止请求") return true } // 停止合成 do { try synthesizer?.stopSpeaking() _isSpeaking = false eventHandler?(["type": "canceled"]) print("[AzureTtsHelper] 已停止语音合成") return true } catch { print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)") eventHandler?(["type": "error", "message": "停止语音合成失败: \(error.localizedDescription)"]) return false } } /// 检查是否正在播放 /// - Returns: 当前是否正在播放语音 func isSpeaking() -> Bool { return _isSpeaking } /// 释放资源 func dispose() { // 尝试停止所有语音合成 if _isSpeaking { do { if let synthesizer = synthesizer { try synthesizer.stopSpeaking() print("[AzureTtsHelper] 语音合成已停止") } } catch { print("[AzureTtsHelper] 警告: 停止语音合成失败: \(error.localizedDescription)") } _isSpeaking = false } // 释放合成器 synthesizer = nil speechConfig = nil // 重置状态 isInitialized = false isAudioSessionConfigured = false print("[AzureTtsHelper] 资源已释放") } // MARK: - 私有辅助方法 /// 设置合成器事件处理 private func setupSynthesizerEvents() { guard let synthesizer = synthesizer else { return } // 添加书签到达事件处理 synthesizer.addBookmarkReachedEventHandler { _, e in print("[AzureTtsHelper] 书签事件: 音频偏移: \((e.audioOffset + 5000) / 10000)ms, 文本: \"\(e.text)\"") } // 合成完成事件 synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in guard let self = self else { return } print("[AzureTtsHelper] 语音合成完成: 音频持续时间: \(e.result.audioDuration)") DispatchQueue.main.async { self._isSpeaking = false self.eventHandler?(["type": "completed"]) } } // 合成取消事件 synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in guard let self = self else { return } let result = e.result do { let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result) print("[AzureTtsHelper] 语音合成取消: 原因: \(cancellationDetails.reason)") if cancellationDetails.reason == SPXCancellationReason.error { print("[AzureTtsHelper] 错误代码: \(cancellationDetails.errorCode)") print("[AzureTtsHelper] 错误详情: \(cancellationDetails.errorDetails ?? "未知")") } DispatchQueue.main.async { self._isSpeaking = false self.eventHandler?(["type": "error", "message": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"]) } } catch { print("[AzureTtsHelper] 获取取消详情时出错: \(error)") DispatchQueue.main.async { self._isSpeaking = false self.eventHandler?(["type": "error", "message": "语音合成被取消"]) } } } // 合成开始事件 synthesizer.addSynthesisStartedEventHandler { _, _ in // print("[AzureTtsHelper] 语音合成开始") } // 合成中事件 synthesizer.addSynthesizingEventHandler { _, _ in // print("[AzureTtsHelper] 语音合成中") } } /// 生成 SSML 文本 private func generateSsml(text: String) -> String { return """ \(text) """ } /// 格式化语速参数 private func formatRateParam(_ rate: Int) -> String { let clampedRate = rate.clamp(min: -100, max: 100) if clampedRate == 0 { return "0%" } else if clampedRate < 0 { return "\(Int(Double(clampedRate) * 0.9))%" } else { return "+\(clampedRate)%" } } /// 格式化音调参数 private func formatPitchParam(_ pitch: Int) -> String { let clampedPitch = pitch.clamp(min: -100, max: 100) if clampedPitch == 0 { return "0%" } else { return "\(Int(Double(clampedPitch) * 0.5))%" } } /// 格式化音量参数 private func formatVolumeParam(_ volume: Int) -> String { let clampedVolume = volume.clamp(min: 0, max: 100) return "\(clampedVolume)%" } /// 获取指定语言的默认语音 private func getDefaultVoiceForLanguage(_ language: String) -> String { return voiceMap[language] ?? "zh-CN-XiaoxiaoNeural" } } // MARK: - 扩展 extension Int { func clamp(min: Int, max: Int) -> Int { if self < min { return min } if self > max { return max } return self } }