You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

497 lines
16 KiB

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<AnyObject>.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: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
// 构造简化的SSML文档
let ssml = """
<speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="zh-CN">
<voice name="\(currentVoice)">
<prosody rate="\(currentRate)" pitch="\(currentPitch)" volume="\(currentVolume)">
\(escapedText)
</prosody>
</voice>
</speak>
"""
return ssml
}
}