|
|
@ -37,6 +37,11 @@ public class AzureAsrHelper: NSObject { |
|
|
private var lastNetworkLostTime: Int64 = 0 |
|
|
private var lastNetworkLostTime: Int64 = 0 |
|
|
private let networkLostDebounceInterval: Int64 = 3000 // 3秒防抖间隔 |
|
|
private let networkLostDebounceInterval: Int64 = 3000 // 3秒防抖间隔 |
|
|
|
|
|
|
|
|
|
|
|
// 防抖机制相关变量 - 只保留开始操作的防抖 |
|
|
|
|
|
private var lastAudioStartTime: Int64 = 0 |
|
|
|
|
|
private let audioOperationDebounceInterval: Int64 = 500 // 500毫秒防抖间隔 |
|
|
|
|
|
private var audioStartWorkItem: DispatchWorkItem? |
|
|
|
|
|
|
|
|
// 状态管理 |
|
|
// 状态管理 |
|
|
private var _isContinuousRecognitionActive = false |
|
|
private var _isContinuousRecognitionActive = false |
|
|
|
|
|
|
|
|
@ -216,11 +221,137 @@ public class AzureAsrHelper: NSObject { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 开始连续语音识别 |
|
|
* 异步启动音频录制(带防抖与状态二次校验) |
|
|
* |
|
|
* |
|
|
* @param callback 连续识别结果回调 |
|
|
|
|
|
* @param audioSourceType 音频源类型 |
|
|
* @param audioSourceType 音频源类型 |
|
|
* @return 是否成功开始识别 |
|
|
* @param audioDataCallback 音频数据回调 |
|
|
|
|
|
* @param completion 完成回调(true/false) |
|
|
|
|
|
*/ |
|
|
|
|
|
/// 异步启动音频录制(带防抖与状态二次校验) |
|
|
|
|
|
/// - 参数: |
|
|
|
|
|
/// - audioSourceType: 音频源类型 |
|
|
|
|
|
/// - audioDataCallback: 音频数据回调 |
|
|
|
|
|
/// - completion: 完成回调(true/false) |
|
|
|
|
|
/// |
|
|
|
|
|
/// 修复点: |
|
|
|
|
|
/// 1) 新任务创建前,取消旧的未执行任务,确保只有“最后一次启动请求”会生效 |
|
|
|
|
|
/// 2) 任务执行前二次校验:必须是当前挂起任务且 _isContinuousRecognitionActive 仍为 true 才执行启动 |
|
|
|
|
|
private func startAudioRecordAsync( |
|
|
|
|
|
audioSourceType: AudioSourceType, |
|
|
|
|
|
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, |
|
|
|
|
|
completion: @escaping (Bool) -> Void |
|
|
|
|
|
) { |
|
|
|
|
|
let currentTime = Int64(Date().timeIntervalSince1970 * 1000) |
|
|
|
|
|
|
|
|
|
|
|
// 防抖:距离上次启动过短,取消旧任务 |
|
|
|
|
|
if currentTime - lastAudioStartTime < audioOperationDebounceInterval { |
|
|
|
|
|
audioStartWorkItem?.cancel() |
|
|
|
|
|
os_log("音频启动操作被防抖过滤,距离上次启动仅%{public}dms", log: log, type: .debug, currentTime - lastAudioStartTime) |
|
|
|
|
|
} else { |
|
|
|
|
|
// 即使未触发防抖,也保证旧任务不会残留 |
|
|
|
|
|
audioStartWorkItem?.cancel() |
|
|
|
|
|
} |
|
|
|
|
|
lastAudioStartTime = currentTime |
|
|
|
|
|
|
|
|
|
|
|
// 创建新的启动任务 |
|
|
|
|
|
let workItem = DispatchWorkItem { [weak self] in |
|
|
|
|
|
guard let self = self else { |
|
|
|
|
|
completion(false) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
print("startAudioRecordAsync\(_isContinuousRecognitionActive)") |
|
|
|
|
|
// 二次校验:stop 可能已发生,确认仍需启动 |
|
|
|
|
|
guard self._isContinuousRecognitionActive else { |
|
|
|
|
|
os_log("已收到停止请求,取消启动音频录制", log: self.log, type: .info) |
|
|
|
|
|
completion(false) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
os_log("开始异步启动音频录制", log: self.log, type: .info) |
|
|
|
|
|
|
|
|
|
|
|
// 在后台队列执行耗时的音频启动操作 |
|
|
|
|
|
DispatchQueue.global(qos: .userInitiated).async { |
|
|
|
|
|
let success = self.performAudioStart(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) |
|
|
|
|
|
|
|
|
|
|
|
// 回到主队列执行完成回调 |
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
|
completion(success) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 保存"当前挂起任务" |
|
|
|
|
|
audioStartWorkItem = workItem |
|
|
|
|
|
|
|
|
|
|
|
// 延迟执行以实现防抖 |
|
|
|
|
|
DispatchQueue.main.asyncAfter( |
|
|
|
|
|
deadline: .now() + .milliseconds(Int(audioOperationDebounceInterval)), |
|
|
|
|
|
execute: workItem |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 执行音频启动操作 |
|
|
|
|
|
*/ |
|
|
|
|
|
private func performAudioStart( |
|
|
|
|
|
audioSourceType: AudioSourceType, |
|
|
|
|
|
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? |
|
|
|
|
|
) -> Bool { |
|
|
|
|
|
do { |
|
|
|
|
|
audioStream?.startAudioRecord( |
|
|
|
|
|
audioSourceType: audioSourceType == .microphone ? .microphone : .external, |
|
|
|
|
|
audioDataCallback: audioDataCallback |
|
|
|
|
|
) |
|
|
|
|
|
os_log("音频录制启动成功", log: log, type: .info) |
|
|
|
|
|
return true |
|
|
|
|
|
} catch { |
|
|
|
|
|
os_log("音频录制启动失败: %{public}@", log: log, type: .error, error.localizedDescription) |
|
|
|
|
|
return false |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 异步停止音频录制(移除防抖机制,立即执行) |
|
|
|
|
|
* |
|
|
|
|
|
* @param completion 完成回调 |
|
|
|
|
|
*/ |
|
|
|
|
|
private func stopAudioRecordAsync(completion: @escaping (Bool) -> Void) { |
|
|
|
|
|
os_log("开始立即停止音频录制", log: log, type: .info) |
|
|
|
|
|
|
|
|
|
|
|
// 在后台队列执行耗时的音频停止操作 |
|
|
|
|
|
DispatchQueue.global(qos: .userInitiated).async { |
|
|
|
|
|
let success = self.performAudioStop() |
|
|
|
|
|
|
|
|
|
|
|
// 回到主队列执行完成回调 |
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
|
completion(success) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 执行音频停止操作 |
|
|
|
|
|
*/ |
|
|
|
|
|
private func performAudioStop() -> Bool { |
|
|
|
|
|
do { |
|
|
|
|
|
audioStream?.stopMicrophoneCapture() |
|
|
|
|
|
os_log("音频录制停止成功", log: log, type: .info) |
|
|
|
|
|
return true |
|
|
|
|
|
} catch { |
|
|
|
|
|
os_log("音频录制停止失败: %{public}@", log: log, type: .error, error.localizedDescription) |
|
|
|
|
|
return false |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 开始连续语音识别(优化:在音频启动成功后再次校验活跃状态) |
|
|
|
|
|
* |
|
|
|
|
|
* @param audioSourceType 音频源类型 |
|
|
|
|
|
* @param audioDataCallback 音频数据回调 |
|
|
|
|
|
* @return 是否成功开始识别(调度成功即返回 true) |
|
|
|
|
|
* |
|
|
|
|
|
* 修复点:在音频启动成功后的闭包中,先判断 _isContinuousRecognitionActive 是否仍然为 true, |
|
|
|
|
|
* 若 stop 已发生则直接跳过 recognizer 的启动,避免“已停止但仍启动识别”的情况。 |
|
|
*/ |
|
|
*/ |
|
|
public func startContinuousRecognition( |
|
|
public func startContinuousRecognition( |
|
|
audioSourceType: AudioSourceType = .microphone, |
|
|
audioSourceType: AudioSourceType = .microphone, |
|
|
@ -241,11 +372,15 @@ public class AzureAsrHelper: NSObject { |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
self.audioSourceType = audioSourceType |
|
|
// 先取消任何挂起的启动任务 |
|
|
|
|
|
cancelPendingAudioStart() |
|
|
|
|
|
|
|
|
|
|
|
// 延迟设置状态,直到音频启动任务真正开始执行 |
|
|
|
|
|
self.audioSourceType = audioSourceType |
|
|
|
|
|
print("startContinuousRecognition:准备启动") |
|
|
|
|
|
|
|
|
do { |
|
|
do { |
|
|
if recognizer == nil { |
|
|
if recognizer == nil { |
|
|
// 检查continuousCallback是否为空,只有在不为空时才设置事件监听器 |
|
|
// 检查continuousCallback是否为空,只有在不为空时才设置事件监听器 |
|
|
if let callback = self.continuousCallback { |
|
|
if let callback = self.continuousCallback { |
|
|
setupEventListeners(callback: callback) |
|
|
setupEventListeners(callback: callback) |
|
|
@ -253,33 +388,45 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
print("startContinuousRecognition:\(audioSourceType)") |
|
|
print("startContinuousRecognition:\(audioSourceType)") |
|
|
// 启动音频处理 |
|
|
// 只有在音频启动成功后才设置状态为 true |
|
|
audioStream?.startAudioRecord(audioSourceType: audioSourceType == .microphone ? .microphone : .external, audioDataCallback: audioDataCallback) |
|
|
self._isContinuousRecognitionActive = true |
|
|
|
|
|
// 异步启动音频处理 |
|
|
// 开始连续识别 |
|
|
startAudioRecordAsync(audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in |
|
|
try recognizer?.startContinuousRecognition() |
|
|
guard let self = self else { return } |
|
|
_isContinuousRecognitionActive = true |
|
|
|
|
|
|
|
|
if success { |
|
|
os_log("连续识别已启动,音频源: %{public}@", log: log, type: .info, audioSourceType == .microphone ? "麦克风" : "外部") |
|
|
// 只有在音频启动成功后才设置状态为 true |
|
|
|
|
|
self._isContinuousRecognitionActive = true |
|
|
|
|
|
print("startContinuousRecognition:\(self._isContinuousRecognitionActive)") |
|
|
|
|
|
|
|
|
|
|
|
// 启动语音识别 |
|
|
|
|
|
do { |
|
|
|
|
|
try self.recognizer?.startContinuousRecognition() |
|
|
|
|
|
os_log("连续识别已启动,音频源: %{public}@", log: self.log, type: .info, audioSourceType == .microphone ? "麦克风" : "外部") |
|
|
|
|
|
} catch { |
|
|
|
|
|
os_log("启动连续识别失败: %{public}@", log: self.log, type: .error, error.localizedDescription) |
|
|
|
|
|
self.stopContinuousRecognition() |
|
|
|
|
|
self._isContinuousRecognitionActive = false |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
os_log("音频启动失败,无法开始识别", log: self.log, type: .error) |
|
|
|
|
|
self._isContinuousRecognitionActive = false |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
return true |
|
|
return true |
|
|
} catch { |
|
|
} catch { |
|
|
stopContinuousRecognition() |
|
|
stopContinuousRecognition() |
|
|
_isContinuousRecognitionActive = false |
|
|
_isContinuousRecognitionActive = false |
|
|
//callback.onError("启动连续识别失败: \(error.localizedDescription)") |
|
|
|
|
|
return false |
|
|
return false |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 停止连续语音识别 |
|
|
* 停止连续语音识别(优化:确保取消未执行的启动任务) |
|
|
* |
|
|
* - 返回: 停止操作是否成功 |
|
|
* @return 停止操作是否成功 |
|
|
|
|
|
*/ |
|
|
|
|
|
/** |
|
|
|
|
|
* 停止连续语音识别 |
|
|
|
|
|
* @return 停止操作是否成功 |
|
|
|
|
|
*/ |
|
|
*/ |
|
|
public func stopContinuousRecognition() -> Bool { |
|
|
public func stopContinuousRecognition() -> Bool { |
|
|
guard speechConfig != nil else { |
|
|
guard speechConfig != nil else { |
|
|
@ -288,17 +435,29 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
guard let recognizer = recognizer else { |
|
|
guard let recognizer = recognizer else { |
|
|
os_log("识别器为空,重置状态", log: log, type: .info) |
|
|
os_log("识别器为空,重置状态", log: log, type: .info) |
|
|
return true |
|
|
// 仍需取消潜在的挂起启动任务,避免稍后被触发 |
|
|
} |
|
|
cancelPendingAudioStart() |
|
|
|
|
|
|
|
|
if !_isContinuousRecognitionActive { |
|
|
|
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
do { |
|
|
do { |
|
|
_isContinuousRecognitionActive = false |
|
|
|
|
|
|
|
|
|
|
|
audioStream?.stopMicrophoneCapture() |
|
|
|
|
|
|
|
|
// 关键:取消尚未执行的启动任务,防止 stop 后音频又被启动 |
|
|
|
|
|
cancelPendingAudioStart() |
|
|
|
|
|
|
|
|
|
|
|
// 异步停止音频处理 |
|
|
|
|
|
stopAudioRecordAsync { [weak self] (success: Bool) in |
|
|
|
|
|
guard let self = self else { return } |
|
|
|
|
|
if success { |
|
|
|
|
|
// 标记不再活跃,供启动任务二次校验 |
|
|
|
|
|
self._isContinuousRecognitionActive = false |
|
|
|
|
|
os_log("音频停止成功", log: self.log, type: .info) |
|
|
|
|
|
} else { |
|
|
|
|
|
os_log("音频停止失败", log: self.log, type: .error) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 停止连续识别 |
|
|
// 停止连续识别 |
|
|
try recognizer.stopContinuousRecognition() |
|
|
try recognizer.stopContinuousRecognition() |
|
|
self.recognizer = nil |
|
|
self.recognizer = nil |
|
|
@ -307,17 +466,23 @@ public class AzureAsrHelper: NSObject { |
|
|
} catch { |
|
|
} catch { |
|
|
// 强制重置状态 |
|
|
// 强制重置状态 |
|
|
_isContinuousRecognitionActive = false |
|
|
_isContinuousRecognitionActive = false |
|
|
|
|
|
|
|
|
|
|
|
// 关键:即使异常,也取消未执行的启动任务 |
|
|
|
|
|
cancelPendingAudioStart() |
|
|
|
|
|
|
|
|
os_log("强制停止识别失败", log: log, type: .error) |
|
|
os_log("强制停止识别失败", log: log, type: .error) |
|
|
|
|
|
|
|
|
// 停止音频处理 |
|
|
// 异步停止音频处理 |
|
|
audioStream?.stopMicrophoneCapture() |
|
|
stopAudioRecordAsync { [weak self] (_: Bool) in |
|
|
|
|
|
guard let self = self else { return } |
|
|
|
|
|
os_log("强制停止音频处理完成", log: self.log, type: .info) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 【修复】在 catch 块中安全地停止识别器,避免再次抛出异常 |
|
|
// 尝试安全地停止识别器 |
|
|
do { |
|
|
do { |
|
|
try recognizer.stopContinuousRecognition() |
|
|
try recognizer.stopContinuousRecognition() |
|
|
self.recognizer = nil |
|
|
self.recognizer = nil |
|
|
} catch { |
|
|
} catch { |
|
|
// 如果强制停止也失败,记录错误但不再抛出异常 |
|
|
|
|
|
os_log("强制停止识别器失败: %@", log: log, type: .error, error.localizedDescription) |
|
|
os_log("强制停止识别器失败: %@", log: log, type: .error, error.localizedDescription) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -333,7 +498,7 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 释放所有资源 |
|
|
* 释放资源 |
|
|
*/ |
|
|
*/ |
|
|
public func dispose() { |
|
|
public func dispose() { |
|
|
do { |
|
|
do { |
|
|
@ -344,7 +509,8 @@ public class AzureAsrHelper: NSObject { |
|
|
try? recognizer?.stopContinuousRecognition() |
|
|
try? recognizer?.stopContinuousRecognition() |
|
|
_isContinuousRecognitionActive = false |
|
|
_isContinuousRecognitionActive = false |
|
|
} |
|
|
} |
|
|
|
|
|
// 取消防抖任务 - 只取消开始操作的防抖 |
|
|
|
|
|
audioStartWorkItem?.cancel() |
|
|
// 停止音频处理 |
|
|
// 停止音频处理 |
|
|
audioStream?.releaseAudioResources() |
|
|
audioStream?.releaseAudioResources() |
|
|
// 释放网络监听资源 |
|
|
// 释放网络监听资源 |
|
|
@ -353,7 +519,7 @@ public class AzureAsrHelper: NSObject { |
|
|
recognizer = nil |
|
|
recognizer = nil |
|
|
speechConfig = nil |
|
|
speechConfig = nil |
|
|
audioConfig = nil |
|
|
audioConfig = nil |
|
|
audioStream = nil |
|
|
audioStream = nil |
|
|
// 确保状态被重置 |
|
|
// 确保状态被重置 |
|
|
_isContinuousRecognitionActive = false |
|
|
_isContinuousRecognitionActive = false |
|
|
//externalAudioStream = nil |
|
|
//externalAudioStream = nil |
|
|
@ -362,8 +528,8 @@ public class AzureAsrHelper: NSObject { |
|
|
_isContinuousRecognitionActive = false |
|
|
_isContinuousRecognitionActive = false |
|
|
//externalAudioStream = nil |
|
|
//externalAudioStream = nil |
|
|
audioConfig = nil |
|
|
audioConfig = nil |
|
|
recognizer = nil |
|
|
recognizer = nil |
|
|
speechConfig = nil |
|
|
speechConfig = nil |
|
|
audioStream = nil |
|
|
audioStream = nil |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -853,6 +1019,15 @@ extension AzureAsrHelper: NetworkStateMonitor.NetworkStateListener { |
|
|
|
|
|
|
|
|
self.handleNetworkLost() |
|
|
self.handleNetworkLost() |
|
|
} |
|
|
} |
|
|
|
|
|
// .MARK: - 启动任务管理 |
|
|
|
|
|
/// 取消尚未执行的音频启动任务,防止 stop 后仍然触发启动 |
|
|
|
|
|
private func cancelPendingAudioStart() { |
|
|
|
|
|
audioStartWorkItem?.cancel() |
|
|
|
|
|
audioStartWorkItem = nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|