15 changed files with 1342 additions and 209 deletions
@ -0,0 +1,408 @@ |
|||||
|
import Foundation |
||||
|
import MicrosoftCognitiveServicesSpeech |
||||
|
import AVFoundation |
||||
|
|
||||
|
/// 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 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] |
||||
|
} |
||||
|
|
||||
|
// 创建识别器和设置回调 |
||||
|
if !createRecognizerAndSetupCallbacks() { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
print("[AzureAsrHelper] Azure 语音服务初始化成功") |
||||
|
isInitialized = true |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 创建识别器并设置回调 |
||||
|
private func createRecognizerAndSetupCallbacks() -> Bool { |
||||
|
// 释放之前的 recognizer |
||||
|
recognizer = nil |
||||
|
audioConfig = nil |
||||
|
|
||||
|
do { |
||||
|
// 创建语音配置 |
||||
|
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion) |
||||
|
|
||||
|
// 设置音频输入参数 |
||||
|
try setupAudioSession() |
||||
|
|
||||
|
// 直接使用麦克风音频配置 |
||||
|
audioConfig = try SPXAudioConfiguration() |
||||
|
|
||||
|
// 设置语言配置 |
||||
|
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!) |
||||
|
} |
||||
|
|
||||
|
// 设置所有回调 |
||||
|
setupAllCallbacks() |
||||
|
|
||||
|
return true |
||||
|
} catch { |
||||
|
print("[AzureAsrHelper] 错误: 创建识别器失败: \(error.localizedDescription)") |
||||
|
eventHandler("error", ["message": "创建识别器失败: \(error.localizedDescription)"]) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 设置音频会话 |
||||
|
private func setupAudioSession() throws { |
||||
|
let audioSession = AVAudioSession.sharedInstance() |
||||
|
|
||||
|
// 使用playAndRecord类别允许同时录音和播放 |
||||
|
try audioSession.setCategory(.playAndRecord, |
||||
|
mode: .voiceChat, // 使用voiceChat模式能够更好地支持回音消除 |
||||
|
options: [.allowBluetooth, .defaultToSpeaker, .allowAirPlay]) |
||||
|
|
||||
|
// 设置首选的输入和输出 |
||||
|
let currentRoute = audioSession.currentRoute |
||||
|
|
||||
|
// 获取当前是否连接了耳机或外部麦克风 |
||||
|
let hasHeadphones = currentRoute.outputs.contains { |
||||
|
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP |
||||
|
} |
||||
|
|
||||
|
// 如果没有耳机,明确启用内置麦克风和扬声器的回音消除 |
||||
|
if !hasHeadphones { |
||||
|
try audioSession.setMode(.voiceChat) // 语音聊天模式有更强的回音消除 |
||||
|
|
||||
|
// 启用回音消除和噪声抑制 |
||||
|
try audioSession.setInputGain(0.8) // 适当降低输入增益以减少扬声器音频被麦克风捕获的可能性 |
||||
|
} else { |
||||
|
// 耳机模式,可以使用不同的设置 |
||||
|
try audioSession.setMode(.voiceChat) |
||||
|
try audioSession.setInputGain(1.0) |
||||
|
} |
||||
|
|
||||
|
// 设置合适的采样率 |
||||
|
try audioSession.setPreferredSampleRate(16000.0) // Azure语音识别推荐的采样率 |
||||
|
try audioSession.setPreferredIOBufferDuration(0.01) // 较小的缓冲区大小以减少延迟 |
||||
|
|
||||
|
// 激活音频会话 |
||||
|
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
||||
|
|
||||
|
print("[AzureAsrHelper] 音频会话配置成功,已启用回音消除") |
||||
|
} |
||||
|
|
||||
|
/// 设置所有回调 |
||||
|
private func setupAllCallbacks() { |
||||
|
guard let recognizer = recognizer else { return } |
||||
|
|
||||
|
// 最终识别结果 |
||||
|
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 } |
||||
|
|
||||
|
print("[AzureAsrHelper] 识别会话已开始") |
||||
|
self._isContinuousRecognitionActive = true |
||||
|
self.eventHandler("sessionStarted", [:]) |
||||
|
} |
||||
|
|
||||
|
recognizer.addSessionStoppedEventHandler { [weak self] _, _ in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
print("[AzureAsrHelper] 识别会话已结束") |
||||
|
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 ?? "未知错误" |
||||
|
|
||||
|
print("[AzureAsrHelper] 识别取消: \(errorDetails)") |
||||
|
|
||||
|
self.eventHandler("canceled", [ |
||||
|
"reason": reason, |
||||
|
"errorDetails": errorDetails |
||||
|
]) |
||||
|
|
||||
|
self._isContinuousRecognitionActive = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 执行一次性语音识别 |
||||
|
/// - Returns: 是否成功启动识别 |
||||
|
func recognizeOnce() -> Bool { |
||||
|
if !isInitialized { |
||||
|
print("[AzureAsrHelper] 错误: 语音服务未初始化") |
||||
|
eventHandler("error", ["message": "语音服务未初始化"]) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 如果正在连续识别,先停止 |
||||
|
if _isContinuousRecognitionActive { |
||||
|
stopContinuousRecognition() |
||||
|
} |
||||
|
|
||||
|
// 确保识别器已创建 |
||||
|
if recognizer == nil && !createRecognizerAndSetupCallbacks() { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
do { |
||||
|
// 通知会话开始 |
||||
|
eventHandler("sessionStarted", [:]) |
||||
|
|
||||
|
// 执行识别 |
||||
|
try recognizer?.recognizeOnceAsync { [weak self] result in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
if result.reason == SPXResultReason.recognizedSpeech { |
||||
|
let detectedLanguage = self.getDetectedLanguage(from: result) |
||||
|
self.eventHandler("result", [ |
||||
|
"text": result.text ?? "", |
||||
|
"detectedLanguage": detectedLanguage |
||||
|
]) |
||||
|
} else if result.reason == SPXResultReason.noMatch { |
||||
|
print("[AzureAsrHelper] 无匹配结果") |
||||
|
self.eventHandler("noMatch", [:]) |
||||
|
} 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)"]) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 开始连续语音识别 |
||||
|
/// - Returns: 是否成功启动识别 |
||||
|
func startContinuousRecognition() -> Bool { |
||||
|
if !isInitialized { |
||||
|
print("[AzureAsrHelper] 错误: 语音服务未初始化") |
||||
|
eventHandler("error", ["message": "语音服务未初始化"]) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 如果已经在进行连续识别,先停止 |
||||
|
if _isContinuousRecognitionActive { |
||||
|
stopContinuousRecognition() |
||||
|
} |
||||
|
|
||||
|
// 确保识别器已创建 |
||||
|
if recognizer == nil && !createRecognizerAndSetupCallbacks() { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 重新确保音频设置正确 |
||||
|
do { |
||||
|
try setupAudioSession() |
||||
|
} catch { |
||||
|
print("[AzureAsrHelper] 警告: 设置音频会话失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
|
||||
|
do { |
||||
|
// 启动连续识别 |
||||
|
try recognizer?.startContinuousRecognition() |
||||
|
_isContinuousRecognitionActive = true |
||||
|
|
||||
|
print("[AzureAsrHelper] 连续识别开始") |
||||
|
return true |
||||
|
} catch { |
||||
|
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)") |
||||
|
eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"]) |
||||
|
_isContinuousRecognitionActive = false |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 停止连续语音识别 |
||||
|
/// - Returns: 是否成功停止识别 |
||||
|
func stopContinuousRecognition() -> Bool { |
||||
|
if !_isContinuousRecognitionActive || recognizer == nil { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
do { |
||||
|
try recognizer?.stopContinuousRecognition() |
||||
|
_isContinuousRecognitionActive = false |
||||
|
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() |
||||
|
} |
||||
|
|
||||
|
// 释放音频会话 |
||||
|
do { |
||||
|
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) |
||||
|
} catch { |
||||
|
print("[AzureAsrHelper] 警告: 释放音频会话失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
|
||||
|
// 释放资源 |
||||
|
recognizer = nil |
||||
|
speechConfig = nil |
||||
|
audioConfig = nil |
||||
|
|
||||
|
// 重置状态 |
||||
|
_isContinuousRecognitionActive = false |
||||
|
isInitialized = false |
||||
|
} |
||||
|
|
||||
|
/// 从结果中获取检测到的语言 |
||||
|
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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,427 @@ |
|||||
|
import Foundation |
||||
|
import MicrosoftCognitiveServicesSpeech |
||||
|
import AVFoundation |
||||
|
|
||||
|
/// Azure TTS工具类,负责实现TTS服务接口 |
||||
|
@available(iOS 13.0, *) |
||||
|
class AzureTtsHelper: NSObject { |
||||
|
// MARK: - 属性 |
||||
|
|
||||
|
/// 事件处理回调 |
||||
|
private var eventHandler: (String, [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 |
||||
|
|
||||
|
// 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: - 初始化 |
||||
|
|
||||
|
init(eventHandler: @escaping (String, [String: Any]) -> Void) { |
||||
|
self.eventHandler = eventHandler |
||||
|
super.init() |
||||
|
} |
||||
|
|
||||
|
deinit { |
||||
|
dispose() |
||||
|
} |
||||
|
|
||||
|
// 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("error", ["error": "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("error", ["error": "初始化语音合成服务失败: \(error.localizedDescription)"]) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 配置音频会话 |
||||
|
private func configureAudioSession() -> Bool { |
||||
|
let audioSession = AVAudioSession.sharedInstance() |
||||
|
do { |
||||
|
// 使用playback类别,但支持混合和空中播放 |
||||
|
try audioSession.setCategory(.playback, |
||||
|
mode: .spokenAudio, |
||||
|
options: [.mixWithOthers, .allowAirPlay, .duckOthers]) |
||||
|
|
||||
|
// 根据设备类型选择最佳配置 |
||||
|
let currentRoute = audioSession.currentRoute |
||||
|
let hasHeadphones = currentRoute.outputs.contains { |
||||
|
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP |
||||
|
} |
||||
|
|
||||
|
// 优化音频路由 |
||||
|
if hasHeadphones { |
||||
|
// 耳机模式,使用默认设置 |
||||
|
try audioSession.setPreferredIOBufferDuration(0.005) // 较小的缓冲区大小以减少延迟 |
||||
|
} else { |
||||
|
// 扬声器模式 |
||||
|
try audioSession.setPreferredIOBufferDuration(0.005) |
||||
|
} |
||||
|
|
||||
|
// 避免完全激活音频会话,因为ASR可能已经激活 |
||||
|
// 这里使用setActive(false)是为了不与ASR冲突 |
||||
|
if !audioSession.isOtherAudioPlaying { |
||||
|
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
||||
|
} |
||||
|
|
||||
|
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("error", ["error": "TTS 引擎尚未初始化"]) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if voiceName.isEmpty { |
||||
|
print("[AzureTtsHelper] 错误: 声音名称为空") |
||||
|
eventHandler("error", ["error": "声音名称不能为空"]) |
||||
|
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("error", ["error": "TTS 引擎尚未初始化"]) |
||||
|
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("error", ["error": "TTS 引擎尚未初始化"]) |
||||
|
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("error", ["error": "合成器未初始化"]) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
_isSpeaking = true |
||||
|
eventHandler("started", [:]) |
||||
|
|
||||
|
Task { |
||||
|
do { |
||||
|
// 使用异步方法进行合成并直接播放 |
||||
|
_ = try await synthesizer.startSpeakingSsml(text) |
||||
|
|
||||
|
} catch { |
||||
|
print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)") |
||||
|
DispatchQueue.main.async { |
||||
|
self._isSpeaking = false |
||||
|
self.eventHandler("error", ["error": "语音合成失败: \(error.localizedDescription)"]) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 停止当前语音合成 |
||||
|
/// - Returns: 操作是否成功 |
||||
|
func stopSpeaking() -> Bool { |
||||
|
if !isInitialized || !_isSpeaking { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// 停止合成 |
||||
|
do { |
||||
|
try synthesizer?.stopSpeaking() |
||||
|
_isSpeaking = false |
||||
|
eventHandler("canceled", [:]) |
||||
|
print("[AzureTtsHelper] 已停止语音合成") |
||||
|
return true |
||||
|
} catch { |
||||
|
print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)") |
||||
|
eventHandler("error", ["error": "停止语音合成失败: \(error.localizedDescription)"]) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 检查是否正在播放 |
||||
|
/// - Returns: 当前是否正在播放语音 |
||||
|
func isSpeaking() -> Bool { |
||||
|
return _isSpeaking |
||||
|
} |
||||
|
|
||||
|
/// 释放资源 |
||||
|
func dispose() { |
||||
|
try? stopSpeaking() |
||||
|
|
||||
|
// 释放合成器和配置 |
||||
|
synthesizer = nil |
||||
|
speechConfig = nil |
||||
|
|
||||
|
isInitialized = false |
||||
|
_isSpeaking = false |
||||
|
isAudioSessionConfigured = false |
||||
|
print("[AzureTtsHelper] TTS 引擎已释放") |
||||
|
} |
||||
|
|
||||
|
// 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("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("error", ["error": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"]) |
||||
|
} |
||||
|
} catch { |
||||
|
print("[AzureTtsHelper] 获取取消详情时出错: \(error)") |
||||
|
|
||||
|
DispatchQueue.main.async { |
||||
|
self._isSpeaking = false |
||||
|
self.eventHandler("error", ["error": "语音合成被取消"]) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 合成开始事件 |
||||
|
synthesizer.addSynthesisStartedEventHandler { _, _ in |
||||
|
// print("[AzureTtsHelper] 语音合成开始") |
||||
|
} |
||||
|
|
||||
|
// 合成中事件 |
||||
|
synthesizer.addSynthesizingEventHandler { _, _ in |
||||
|
// print("[AzureTtsHelper] 语音合成中") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 生成 SSML 文本 |
||||
|
private func generateSsml(text: String) -> String { |
||||
|
return """ |
||||
|
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='zh-CN'> |
||||
|
<voice name='\(currentVoice)'> |
||||
|
<prosody rate='\(currentSpeechRate)' pitch='\(currentPitch)' volume='\(currentVolume)'> |
||||
|
\(text) |
||||
|
</prosody> |
||||
|
</voice> |
||||
|
</speak> |
||||
|
""" |
||||
|
} |
||||
|
|
||||
|
/// 格式化语速参数 |
||||
|
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 |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue