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.

696 lines
24 KiB

import Foundation
import AVFoundation
import MicrosoftCognitiveServicesSpeech
/// Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别
class AzureAsrHelper {
private let tag = "AzureAsrHelper"
// 核心组件
private var speechConfig: SPXSpeechConfiguration?
private var recognizer: SPXSpeechRecognizer?
private var audioConfig: SPXAudioConfiguration?
// 状态管理
private var isContinuousRecognitionActive = false
// 配置参数
private var currentLanguage = "zh-CN"
private var supportedLanguages = ["zh-CN"]
private var isAutoDetectLanguage = false
private var subscriptionKey = ""
private var region = ""
// 音频源配置
private var audioSourceType = AudioSourceType.microphone
// 音频处理
private var microphoneStream: MicrophoneStream?
private var externalAudioStream: ExternalAudioPullStream?
// 连续识别回调
private var continuousRecognizeCallback: ContinuousRecognizeCallback?
/// 应用上下文
private let context: Any
/// 音频来源类型
enum AudioSourceType {
/// 使用设备麦克风
case microphone
/// 使用外部提供的音频数据
case external
}
/// 一次性识别回调
protocol RecognizeCallback {
/// 识别结果回调
/// - Parameters:
/// - text: 识别文本
/// - detectedLanguage: 检测到的语言
func onResult(text: String, detectedLanguage: String)
/// 识别错误回调
/// - Parameter error: 错误信息
func onError(error: String)
}
/// 连续识别回调
protocol ContinuousRecognizeCallback {
/// 识别中回调
/// - Parameters:
/// - recognizing: 识别中的文本
/// - detectedLanguage: 检测到的语言
func onRecognizing(recognizing: String, detectedLanguage: String)
/// 识别结果回调
/// - Parameters:
/// - text: 识别文本
/// - detectedLanguage: 检测到的语言
func onResult(text: String, detectedLanguage: String)
/// 会话开始回调
func onSessionStarted()
/// 会话结束回调
func onSessionStopped()
/// 识别取消回调
/// - Parameters:
/// - reason: 取消原因
/// - errorDetails: 错误详情
func onCanceled(reason: String, errorDetails: String)
/// 识别错误回调
/// - Parameter error: 错误信息
func onError(error: String)
}
/// 初始化Azure语音识别辅助类
/// - Parameter context: 应用上下文
init(_ context: Any) {
self.context = context
}
/// 初始化Azure语音服务
/// - Parameters:
/// - subscriptionKey: Azure 订阅密钥
/// - region: Azure 区域
/// - supportedLanguages: 支持的语言数组,默认为["zh-CN"]
/// - audioSourceType: 音频源类型,默认为麦克风
/// - Returns: 初始化是否成功
func initialize(
subscriptionKey: String,
region: String,
supportedLanguages: [String] = ["zh-CN"],
audioSourceType: AudioSourceType = .microphone
) -> Bool {
do {
print("\(tag): 初始化 Azure 语音服务, 音频源类型: \(audioSourceType)")
// 检查配置是否为空
if subscriptionKey.isEmpty || region.isEmpty {
print("\(tag): Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
// 保存配置
self.subscriptionKey = subscriptionKey
self.region = region
self.audioSourceType = audioSourceType
// 设置语言
if !supportedLanguages.isEmpty {
self.supportedLanguages = supportedLanguages
}
// 根据支持的语言数量决定是否启用自动语言检测
self.isAutoDetectLanguage = supportedLanguages.count >= 2
// 如果只有一种语言,设置为当前语言
if !isAutoDetectLanguage && !supportedLanguages.isEmpty {
self.currentLanguage = supportedLanguages[0]
}
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
if isAutoDetectLanguage {
// 启用语言检测模式
try speechConfig?.setPropertyTo("Continuous", byId: SPXPropertyId.speechServiceConnection_languageIdMode)
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
}
// 设置音频配置
return setupRecognizer()
} catch {
print("\(tag): 初始化失败: \(error.localizedDescription)")
return false
}
}
/// 设置识别器
private func setupRecognizer() -> Bool {
do {
// 清理旧的识别器
recognizer = nil
// 设置音频配置
switch audioSourceType {
case .microphone:
// 检查是否连接了耳机
let isHeadsetConnected = isHeadphonesConnected()
if !isHeadsetConnected {
print("\(tag): 未检测到耳机,使用麦克风输入")
// 使用拉流模式进行回音消除
setupMicrophoneStream()
} else {
print("\(tag): 检测到耳机连接,使用默认麦克风输入")
// 使用默认麦克风输入
audioConfig = SPXAudioConfiguration()
}
case .external:
// 创建拉流
setupExternalAudioStream()
print("\(tag): 使用外部音频源(拉流模式)")
}
// 创建识别器
if isAutoDetectLanguage {
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguages: supportedLanguages)
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, audioConfiguration: audioConfig!)
} else {
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
}
print("\(tag): Azure 语音服务初始化成功")
return true
} catch {
print("\(tag): 创建识别器失败: \(error.localizedDescription)")
stopAudioProcessing()
return false
}
}
/// 设置麦克风流 - 使用拉流方式
private func setupMicrophoneStream() {
do {
// 创建麦克风流
microphoneStream = MicrophoneStream()
// 创建音频配置
audioConfig = try SPXAudioConfiguration(streamInput: microphoneStream!)
print("\(tag): 已设置麦克风流(拉流模式)")
} catch {
print("\(tag): 设置麦克风流失败: \(error.localizedDescription)")
}
}
/// 设置外部音频流 - 使用拉流方式
private func setupExternalAudioStream() {
do {
// 创建外部音频拉流对象
externalAudioStream = ExternalAudioPullStream()
// 创建音频配置
audioConfig = try SPXAudioConfiguration(streamInput: externalAudioStream!)
print("\(tag): 已设置外部音频流(拉流模式)")
} catch {
print("\(tag): 设置外部音频流失败: \(error.localizedDescription)")
}
}
/// 向音频流写入音频数据
/// 仅当音频源设置为external时有效
/// - Parameter data: 音频数据字节数组
func pushAudioData(_ data: Data) {
if audioSourceType != .external {
print("\(tag): 当前未使用外部音频源,忽略推送的音频数据")
return
}
// 使用拉流模式,将数据推入队列
externalAudioStream?.pushAudio(data)
}
/// 执行一次性语音识别
/// - Parameter callback: 识别结果回调
func recognizeOnce(callback: RecognizeCallback) {
if speechConfig == nil {
callback.onError(error: "语音服务未初始化")
return
}
// 确保不在连续识别中
if isContinuousRecognitionActive {
_ = stopContinuousRecognition()
}
// 重置识别器
if !setupRecognizer() {
callback.onError(error: "重置识别器失败")
return
}
// 启动音频处理
startAudioProcessing()
// 执行同步识别
do {
let result = try recognizer?.recognizeOnce()
// 停止音频处理
stopAudioProcessing()
if let result = result, result.reason == .recognizedSpeech {
let resultText = result.text ?? ""
var detectedLanguage = supportedLanguages[0]
// 尝试获取检测到的语言
if isAutoDetectLanguage, let autoDetectResult = result.properties?.getPropertyByName("SourceLanguage") {
detectedLanguage = autoDetectResult
}
callback.onResult(text: resultText, detectedLanguage: detectedLanguage)
} else {
callback.onError(error: "未能识别语音")
}
} catch {
stopAudioProcessing()
callback.onError(error: "识别异常: \(error.localizedDescription)")
}
}
/// 开始连续语音识别
/// - Parameter callback: 连续识别结果回调
/// - Returns: 是否成功开始识别
func startContinuousRecognition(_ callback: ContinuousRecognizeCallback) -> Bool {
if speechConfig == nil {
callback.onError(error: "语音服务未初始化")
return false
}
if isContinuousRecognitionActive {
return true
}
// 重置识别器
if !setupRecognizer() {
callback.onError(error: "重置识别器失败")
return false
}
// 保存回调
continuousRecognizeCallback = callback
do {
// 设置各种事件监听
setupEventListeners(callback)
// 启动音频处理
startAudioProcessing()
// 开始连续识别
try recognizer?.startContinuousRecognition()
isContinuousRecognitionActive = true
return true
} catch {
isContinuousRecognitionActive = false
stopAudioProcessing()
callback.onError(error: "启动连续识别失败: \(error.localizedDescription)")
return false
}
}
/// 设置事件监听器
private func setupEventListeners(_ callback: ContinuousRecognizeCallback) {
guard let recognizer = recognizer else { return }
// 识别中事件
recognizer.addRecognizing { [weak self] _, event in
guard let self = self else { return }
var detectedLanguage = ""
if self.isAutoDetectLanguage, let lang = event.result.properties?.getPropertyByName("SourceLanguage") {
detectedLanguage = lang
}
print("\(self.tag): 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
callback.onRecognizing(recognizing: event.result.text ?? "", detectedLanguage: detectedLanguage)
}
// 识别完成事件
recognizer.addRecognized { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == .recognizedSpeech {
var detectedLanguage = ""
if self.isAutoDetectLanguage, let lang = event.result.properties?.getPropertyByName("SourceLanguage") {
detectedLanguage = lang
}
print("\(self.tag): 识别完成: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
callback.onResult(text: event.result.text ?? "", detectedLanguage: detectedLanguage)
}
}
// 会话开始事件
recognizer.addSessionStarted { [weak self] _, _ in
guard let self = self else { return }
print("\(self.tag): 识别会话已开始")
callback.onSessionStarted()
}
// 会话结束事件
recognizer.addSessionStopped { [weak self] _, _ in
guard let self = self else { return }
print("\(self.tag): 识别会话已结束")
self.isContinuousRecognitionActive = false
self.stopAudioProcessing()
callback.onSessionStopped()
}
// 取消事件
recognizer.addCanceled { [weak self] _, event in
guard let self = self else { return }
let errorDetails = event.errorDetails ?? "未知错误"
let reason = String(describing: event.reason)
print("\(self.tag): 识别取消: \(errorDetails)")
self.isContinuousRecognitionActive = false
self.stopAudioProcessing()
callback.onCanceled(reason: reason, errorDetails: errorDetails)
callback.onError(error: "识别取消: \(errorDetails)") // 兼容旧接口
}
}
/// 停止连续语音识别
/// - Returns: 是否成功停止
func stopContinuousRecognition() -> Bool {
if speechConfig == nil {
print("\(tag): 语音服务未初始化")
return false
}
if !isContinuousRecognitionActive {
print("\(tag): 未进行连续识别,忽略停止请求")
return true
}
do {
print("\(tag): 停止连续语音识别")
if recognizer == nil {
print("\(tag): 识别器为空,重置状态")
isContinuousRecognitionActive = false
return true
}
// 停止连续识别
try recognizer?.stopContinuousRecognition()
// 停止音频处理
stopAudioProcessing()
// 等待一段时间以确保资源正确释放
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.isContinuousRecognitionActive = false
}
print("\(tag): 连续识别停止指令已发送")
return true
} catch {
// 强制重置状态
isContinuousRecognitionActive = false
print("\(tag): 停止连续识别失败: \(error.localizedDescription)")
// 停止音频处理
stopAudioProcessing()
// 尝试强制关闭识别器
recognizer = nil
return false
}
}
/// 检查连续识别是否活跃
/// - Returns: 是否活跃
func isContinuousRecognitionActive() -> Bool {
return isContinuousRecognitionActive
}
/// 释放所有资源
func dispose() {
// 如果正在进行连续识别,先停止
if isContinuousRecognitionActive {
_ = stopContinuousRecognition()
}
// 停止音频处理
stopAudioProcessing()
// 清理所有资源
recognizer = nil
speechConfig = nil
audioConfig = nil
microphoneStream = nil
externalAudioStream = nil
// 重置状态
isContinuousRecognitionActive = false
continuousRecognizeCallback = nil
}
/// 启动音频处理
private func startAudioProcessing() {
switch audioSourceType {
case .microphone:
// 拉流模式不需要额外启动,SDK会自动拉取数据
break
case .external:
// 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData
break
}
}
/// 停止音频处理
private func stopAudioProcessing() {
microphoneStream?.close()
microphoneStream = nil
externalAudioStream?.close()
externalAudioStream = nil
}
/// 检查是否连接了耳机
private func isHeadphonesConnected() -> Bool {
let audioSession = AVAudioSession.sharedInstance()
let outputs = audioSession.currentRoute.outputs
// 检查是否存在耳机类型的输出
let headphoneTypes: [AVAudioSession.Port] = [
.headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE
]
for output in outputs {
if headphoneTypes.contains(output.portType) {
return true
}
}
return false
}
/// 麦克风流 - 拉流模式
/// 实现SPXPullAudioInputStreamDelegate,为Azure SDK提供音频数据
class MicrophoneStream: NSObject, SPXPullAudioInputStreamDelegate {
private let tag = "MicrophoneStream"
private var audioEngine: AVAudioEngine?
private var audioBuffer = Data()
private let bufferLock = NSLock()
override init() {
super.init()
setupAudioEngine()
}
private func setupAudioEngine() {
do {
print("\(tag): 初始化音频引擎")
// 创建音频引擎
audioEngine = AVAudioEngine()
// 设置音频会话
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.record, mode: .default)
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
// 获取输入节点
guard let inputNode = audioEngine?.inputNode else {
print("\(tag): 无法获取输入节点")
return
}
// 创建16kHz, 16bit, 单声道格式
let format = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 16000, channels: 1, interleaved: true)
// 设置音频处理回调
let bufferSize = 4096
inputNode.installTap(onBus: 0, bufferSize: UInt32(bufferSize), format: format) { [weak self] (buffer, time) in
guard let self = self else { return }
// 将音频数据转换为Data
let audioBuffer = buffer.int16ChannelData?[0]
let audioBufferSize = Int(buffer.frameLength * buffer.format.streamDescription.pointee.mBytesPerFrame)
if let audioBuffer = audioBuffer {
let data = Data(bytes: audioBuffer, count: audioBufferSize)
self.bufferLock.lock()
self.audioBuffer.append(data)
self.bufferLock.unlock()
}
}
// 启动音频引擎
audioEngine?.prepare()
try audioEngine?.start()
print("\(tag): 音频引擎启动成功")
} catch {
print("\(tag): 设置音频引擎失败: \(error.localizedDescription)")
}
}
// SPXPullAudioInputStreamDelegate协议方法
func read(_ buffer: UnsafeMutablePointer<UInt8>, size: UInt32) -> Int32 {
bufferLock.lock()
defer { bufferLock.unlock() }
let maxSize = Int(size)
if audioBuffer.count == 0 {
// 没有数据可读
return 0
}
let readSize = min(audioBuffer.count, maxSize)
audioBuffer.copyBytes(to: buffer, from: 0..<readSize)
if readSize < audioBuffer.count {
// 保留未读的数据
audioBuffer = audioBuffer.subdata(in: readSize..<audioBuffer.count)
} else {
// 清空缓冲区
audioBuffer = Data()
}
return Int32(readSize)
}
// 关闭音频资源
func close() {
print("\(tag): 关闭音频引擎")
// 停止音频引擎
if let inputNode = audioEngine?.inputNode {
inputNode.removeTap(onBus: 0)
}
audioEngine?.stop()
audioEngine = nil
// 关闭音频会话
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch {
print("\(tag): 关闭音频会话失败: \(error.localizedDescription)")
}
// 清空缓冲区
bufferLock.lock()
audioBuffer = Data()
bufferLock.unlock()
print("\(tag): 音频资源已释放")
}
}
/// 外部音频拉流
/// 实现SPXPullAudioInputStreamDelegate,将外部推送的音频数据转换为SDK可拉取的形式
class ExternalAudioPullStream: NSObject, SPXPullAudioInputStreamDelegate {
private let tag = "ExternalAudioPullStream"
private let audioQueue = DispatchQueue(label: "com.yunqiinnovation.azure_speech.externalAudio")
private var audioBuffer = Data()
private let bufferLock = NSLock()
private var isClosed = false
// 外部调用:推送音频数据
func pushAudio(_ data: Data) {
if isClosed { return }
bufferLock.lock()
audioBuffer.append(data)
bufferLock.unlock()
}
// SPXPullAudioInputStreamDelegate协议方法
func read(_ buffer: UnsafeMutablePointer<UInt8>, size: UInt32) -> Int32 {
bufferLock.lock()
defer { bufferLock.unlock() }
if isClosed {
return 0
}
let maxSize = Int(size)
if audioBuffer.count == 0 {
// 没有数据可读,返回0表示需要等待
return 0
}
let readSize = min(audioBuffer.count, maxSize)
audioBuffer.copyBytes(to: buffer, from: 0..<readSize)
if readSize < audioBuffer.count {
// 保留未读的数据
audioBuffer = audioBuffer.subdata(in: readSize..<audioBuffer.count)
} else {
// 清空缓冲区
audioBuffer = Data()
}
return Int32(readSize)
}
// 关闭音频资源
func close() {
print("\(tag): 关闭外部音频流")
bufferLock.lock()
isClosed = true
audioBuffer = Data()
bufferLock.unlock()
print("\(tag): 外部音频流已关闭")
}
}
}