Browse Source

修复音频格式设置异常导致崩溃

weicu
fdp 1 year ago
parent
commit
4775bfc6cb
  1. 18
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  2. 8
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift
  3. 251
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

18
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -203,8 +203,8 @@ public class AzureAsrHelper: NSObject {
*/ */
private func preInitializeAudioComponents() { private func preInitializeAudioComponents() {
if audioStream == nil { if audioStream == nil {
audioStream = SimpleAudioReceiver() audioStream = SimpleAudioReceiver.shared
audioStream?.initAudioRecord() //audioStream?.initAudioRecord()
} }
// 预创建音频配置 // 预创建音频配置
if let pushStream = audioStream?.pushAudioStream { if let pushStream = audioStream?.pushAudioStream {
@ -346,14 +346,14 @@ public class AzureAsrHelper: NSObject {
} }
// 停止音频处理 // 停止音频处理
audioStream?.releaseAudioResources() //audioStream?.releaseAudioResources()
// 释放网络监听资源 // 释放网络监听资源
networkMonitor.dispose() networkMonitor.dispose()
// 释放资源 // 释放资源
recognizer = nil recognizer = nil
speechConfig = nil speechConfig = nil
audioConfig = nil audioConfig = nil
audioStream = nil //audioStream = nil
// 确保状态被重置 // 确保状态被重置
_isContinuousRecognitionActive = false _isContinuousRecognitionActive = false
//externalAudioStream = nil //externalAudioStream = nil
@ -466,8 +466,8 @@ public class AzureAsrHelper: NSObject {
private func setupMicrophoneStream() { private func setupMicrophoneStream() {
// 只有在 audioStream 为 nil 时才创建新实例 // 只有在 audioStream 为 nil 时才创建新实例
if audioStream == nil { if audioStream == nil {
audioStream = SimpleAudioReceiver() audioStream = SimpleAudioReceiver.shared
audioStream?.initAudioRecord() // 确保调用初始化 //audioStream?.initAudioRecord() // 确保调用初始化
} }
// 检查音频配置是否已存在 // 检查音频配置是否已存在
@ -669,14 +669,14 @@ public class AzureAsrHelper: NSObject {
if audioStream == nil { if audioStream == nil {
// 创建外部音频拉流对象 // 创建外部音频拉流对象
audioStream = SimpleAudioReceiver() audioStream = SimpleAudioReceiver.shared
audioStream?.initAudioRecord() //audioStream?.initAudioRecord()
} }
if audioStream?.recordfile == nil { if audioStream?.recordfile == nil {
audioStream?.recordfile = RecordFile() audioStream?.recordfile = RecordFile()
} }
// 修复:移除多余的 audioDataCallback 参数 // 修复:移除多余的 audioDataCallback 参数
audioStream?.startAudioRecord( audioStream?.startAudioRecord(

8
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift

@ -329,8 +329,8 @@ import os.log
* 初始化音频处理器 * 初始化音频处理器
*/ */
private func initializeAudioProcessor() { private func initializeAudioProcessor() {
audioProcessor = SimpleAudioReceiver() audioProcessor = SimpleAudioReceiver.shared
audioProcessor?.initAudioRecord() // audioProcessor?.initAudioRecord()
// 检查音频配置是否已存在 // 检查音频配置是否已存在
if audioConfig == nil, let pushStream = audioProcessor?.pushAudioStream { if audioConfig == nil, let pushStream = audioProcessor?.pushAudioStream {
audioConfig = SPXAudioConfiguration(streamInput: pushStream) audioConfig = SPXAudioConfiguration(streamInput: pushStream)
@ -913,8 +913,8 @@ import os.log
audioConfig = nil audioConfig = nil
// 释放其他组件 // 释放其他组件
audioProcessor?.releaseAudioResources() //audioProcessor?.releaseAudioResources()
audioProcessor = nil //audioProcessor = nil
translationService?.dispose() translationService?.dispose()
translationService = nil translationService = nil

251
local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

@ -9,9 +9,32 @@ import os.log
*/ */
public class SimpleAudioReceiver: NSObject { public class SimpleAudioReceiver: NSObject {
/// 单例实例(线程安全)
/// 通过 SimpleAudioReceiver.shared 获取全局唯一实例
public static let shared = SimpleAudioReceiver()
/// 私有化构造函数,防止外部直接实例化
/// 使用方式:通过 SimpleAudioReceiver.shared 访问实例
private override init() {
super.init()
initAudioRecord()
}
private let tag = "SimpleAudioReceiver" private let tag = "SimpleAudioReceiver"
private let log = OSLog(subsystem: "com.azure.speech", category: "SimpleAudioReceiver") private let log = OSLog(subsystem: "com.azure.speech", category: "SimpleAudioReceiver")
/// 音频格式转换专用队列(避免在音频渲染线程上做重操作)
private let conversionQueue = DispatchQueue(label: "audio.stream.convert", qos: .userInitiated)
/// 缓存的音频转换器,避免每个缓冲区重复创建
private var cachedConverter: AVAudioConverter?
/// 统一的目标格式(16kHz/单声道/Float32,用于后续再转 Int16)
private lazy var targetFormat16000Mono: AVAudioFormat = {
return AVAudioFormat(commonFormat: .pcmFormatFloat32,
sampleRate: 16000,
channels: 1,
interleaved: false)!
}()
/** /**
* 音频来源类型 * 音频来源类型
*/ */
@ -25,7 +48,7 @@ public class SimpleAudioReceiver: NSObject {
// MARK: - 音频流类 // MARK: - 音频流类
public private(set) var pushAudioStream: SPXPushAudioInputStream? public private(set) var pushAudioStream: SPXPushAudioInputStream?
private let writeQueue = LinkedBlockingQueue<Data>() private var writeQueue = LinkedBlockingQueue<Data>()
private var audioEngine: AVAudioEngine? private var audioEngine: AVAudioEngine?
private var audioFormat: AVAudioFormat? private var audioFormat: AVAudioFormat?
private let audioSession = AVAudioSession.sharedInstance() private let audioSession = AVAudioSession.sharedInstance()
@ -48,13 +71,15 @@ public class SimpleAudioReceiver: NSObject {
* 包括音频格式、推流、音频引擎等核心组件的初始化 * 包括音频格式、推流、音频引擎等核心组件的初始化
*/ */
public func initAudioRecord() { public func initAudioRecord() {
// 每次初始化前,先释放上一次的资源,避免重复引擎/线程/tap
releaseAudioResources()
print("初始化了") print("初始化了")
audioFormat = getOptimalAudioFormat() audioFormat = getOptimalAudioFormat()
pushAudioStream = SPXPushAudioInputStream() pushAudioStream = SPXPushAudioInputStream()
// 初始化音频引擎 // 初始化音频引擎
audioEngine = AVAudioEngine() audioEngine = AVAudioEngine()
isRunning = true isRunning = true
// 创建新的写线程 // 创建新的写线程
@ -66,15 +91,13 @@ public class SimpleAudioReceiver: NSObject {
// 处理实时音频数据 // 处理实时音频数据
let channelData = buffer.floatChannelData?[0] let channelData = buffer.floatChannelData?[0]
let frameCount = buffer.frameLength let frameCount = buffer.frameLength
print("麦克风启动: \(frameCount)") // print("麦克风启动: \(frameCount)")
print("🎵 音频数据: 帧数=\(frameCount), 声道数据=\(channelData != nil ? "有效" : "无效")") // print("🎵 音频数据: 帧数=\(frameCount), 声道数据=\(channelData != nil ? "有效" : "无效")")
self.writeQueue.put(self.audioBufferToData(buffer)) self.writeQueue.put(self.audioBufferToData(buffer))
// 分析或传输数据... // 分析或传输数据...
} }
} catch { } catch {
print("麦克风启动失败: \(error)") print("麦克风启动失败: \(error)")
} }
} }
@ -192,93 +215,128 @@ public class SimpleAudioReceiver: NSObject {
writeQueue.put(data) writeQueue.put(data)
} }
/** /**
* 开始采集音频数据 * 开始音频捕获
* @param bufferHandler 音频缓冲区处理回调 * 1) 重置音频引擎并配置音频会话
* @throws 音频引擎启动失败时抛出错误 * 2) 安装 tap 时不指定格式(format: nil),由系统使用硬件实际格式
*/ * 3) 在回调内将硬件格式转换为统一的 16kHz 单声道后回调上层
func startCapture(bufferHandler: @escaping (AVAudioPCMBuffer) -> Void) throws { * @param bufferHandler 音频缓冲区处理回调(已转换为目标格式)
guard let audioEngine = audioEngine else { * @throws 音频引擎启动失败时抛出错误
throw NSError(domain: "SimpleAudioReceiver", code: -1, userInfo: [NSLocalizedDescriptionKey: "Audio engine not initialized"]) */
} func startCapture(bufferHandler: @escaping (AVAudioPCMBuffer) -> Void) throws {
guard let audioEngine = audioEngine else {
// 确保音频引擎完全停止 throw NSError(domain: "SimpleAudioReceiver", code: -1, userInfo: [NSLocalizedDescriptionKey: "Audio engine not initialized"])
if audioEngine.isRunning { }
audioEngine.stop()
// 等待一小段时间确保完全停止 // 确保音频引擎完全停止
Thread.sleep(forTimeInterval: 0.1) if audioEngine.isRunning {
} audioEngine.stop()
// 等待一小段时间确保完全停止
// 先安全地移除已存在的 tap Thread.sleep(forTimeInterval: 0.1)
removeTapSafely() }
// 重置音频引擎(在获取格式之前) // 先安全地移除已存在的 tap
audioEngine.reset() removeTapSafely()
// 重置音频引擎(在获取格式之前)
audioEngine.reset()
// 配置音频会话
do {
try setupAudioSession() try setupAudioSession()
} catch {
// 重新获取输入节点和格式(reset后需要重新获取) print("音频会话设置失败: \(error.localizedDescription)")
let inputNode = audioEngine.inputNode throw error
let inputFormat = inputNode.outputFormat(forBus: 0) }
print("硬件输入格式: 采样率=\(inputFormat.sampleRate)Hz, 声道数=\(inputFormat.channelCount), 格式=\(inputFormat.commonFormat.rawValue)") // 重新获取输入节点(此处获取的输出格式在未启动时可能为 0Hz,仅用于日志)
let inputNode = audioEngine.inputNode
// 创建目标格式 - 统一使用16000Hz单声道 let preFormat = inputNode.outputFormat(forBus: 0)
guard let targetFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, print("当前节点(启动前)输出格式: 采样率=\(preFormat.sampleRate)Hz, 声道数=\(preFormat.channelCount), 格式=\(preFormat.commonFormat.rawValue)")
sampleRate: 16000,
channels: 1, // 目标格式 - 统一使用16000Hz单声道(Float32),后续再转 Int16
interleaved: false) else { let targetFormat = self.targetFormat16000Mono
throw NSError(domain: "SimpleAudioReceiver", code: -2, userInfo: [NSLocalizedDescriptionKey: "无法创建目标音频格式"]) print("使用目标音频格式: 采样率=\(targetFormat.sampleRate)Hz, 声道数=\(targetFormat.channelCount)")
}
// 使用硬件原生格式安装 tap(format: nil),回调中异步转换
print("使用目标音频格式: 采样率=\(targetFormat.sampleRate)Hz, 声道数=\(targetFormat.channelCount)") inputNode.installTap(onBus: 0,
bufferSize: 1024,
// 检查硬件格式是否有效 format: nil) { [weak self] (buffer, time) in
let isHardwareFormatValid = inputFormat.sampleRate > 0 && inputFormat.channelCount > 0 guard let self = self else { return }
let srcFormat = buffer.format
if isHardwareFormatValid {
print("使用硬件原生格式安装tap") // 将重采样与格式转换放到专用队列,避免阻塞音频渲染线程
// 使用硬件原生格式安装tap,然后在回调中进行格式转换 self.conversionQueue.async {
inputNode.installTap(onBus: 0, // 如果源格式与目标格式不同,复用/重建转换器
bufferSize: 1024, var outputBuffer: AVAudioPCMBuffer? = buffer
format: inputFormat) { (buffer, time) in if srcFormat.sampleRate != targetFormat.sampleRate ||
// 如果硬件格式与目标格式不匹配,进行转换 srcFormat.channelCount != targetFormat.channelCount ||
if inputFormat.sampleRate != targetFormat.sampleRate || srcFormat.commonFormat != targetFormat.commonFormat {
inputFormat.channelCount != targetFormat.channelCount {
// 执行格式转换 if self.cachedConverter == nil ||
if let convertedBuffer = self.convertAudioBuffer(buffer, to: targetFormat) { self.cachedConverter?.inputFormat != srcFormat ||
bufferHandler(convertedBuffer) self.cachedConverter?.outputFormat != targetFormat {
self.cachedConverter = AVAudioConverter(from: srcFormat, to: targetFormat)
}
if let converter = self.cachedConverter {
let capacity = AVAudioFrameCount(Double(buffer.frameLength) * targetFormat.sampleRate / srcFormat.sampleRate)
if let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: capacity) {
var error: NSError?
let status = converter.convert(to: convertedBuffer, error: &error) { _, outStatus in
outStatus.pointee = .haveData
return buffer
}
if status == .error {
// 限制日志:仅在错误时打印,避免频繁输出
print("音频转换失败: \(error?.localizedDescription ?? "未知错误")")
return
}
outputBuffer = convertedBuffer
} else { } else {
print("音频格式转换失败,跳过此缓冲区") print("无法创建转换后的音频缓冲区")
return
} }
} else { } else {
bufferHandler(buffer) print("无法创建音频转换器: 从\(srcFormat.sampleRate)Hz到\(targetFormat.sampleRate)Hz")
return
} }
} }
} else {
print("硬件音频格式无效,使用目标格式直接安装tap") // 回调上层(注意:此时不在实时渲染线程)
// 硬件格式无效时,直接使用目标格式安装tap if let out = outputBuffer {
inputNode.installTap(onBus: 0, bufferHandler(out)
bufferSize: 1024,
format: targetFormat) { (buffer, time) in
// 直接使用目标格式的缓冲区
bufferHandler(buffer)
}
}
if #available(iOS 13.0, *) {
do {
try inputNode.setVoiceProcessingEnabled(true)
} catch {
print("启用语音处理失败: \(error.localizedDescription)")
} }
} }
audioEngine.prepare()
//try audioEngine.start()
print("音频引擎启动成功")
} }
// 启用语音处理(如果支持)
enableVoiceProcessingIfAvailable(inputNode: inputNode)
// 准备并启动音频引擎
audioEngine.prepare()
// print("音频引擎启动成功。Tap使用硬件格式: 采样率=\(postFormat.sampleRate)Hz, 声道数=\(postFormat.channelCount)")
//print("音频引擎启动成功,使用格式: 采样率=\(tapFormat.sampleRate)Hz, 声道数=\(tapFormat.channelCount)")
}
/**
* 启用语音处理(如果设备支持)
* @param inputNode 输入节点
*/
private func enableVoiceProcessingIfAvailable(inputNode: AVAudioInputNode) {
if #available(iOS 13.0, *) {
do {
try inputNode.setVoiceProcessingEnabled(true)
print("语音处理已启用")
} catch {
print("启用语音处理失败: \(error.localizedDescription)")
// 不抛出错误,因为这不是关键功能
}
} else {
print("当前iOS版本不支持语音处理")
}
}
/** /**
* 安全地移除音频tap * 安全地移除音频tap
* 避免在移除tap时出现崩溃 * 避免在移除tap时出现崩溃
@ -321,8 +379,8 @@ public class SimpleAudioReceiver: NSObject {
// 处理实时音频数据 // 处理实时音频数据
let channelData = buffer.floatChannelData?[0] let channelData = buffer.floatChannelData?[0]
let frameCount = buffer.frameLength let frameCount = buffer.frameLength
print("麦克风启动: \(frameCount)") // print("麦克风启动: \(frameCount)")
print("🎵 音频数据: 帧数=\(frameCount), 声道数据=\(channelData != nil ? "有效" : "无效")") // print("🎵 音频数据: 帧数=\(frameCount), 声道数据=\(channelData != nil ? "有效" : "无效")")
self.writeQueue.put(self.audioBufferToData(buffer)) self.writeQueue.put(self.audioBufferToData(buffer))
// 分析或传输数据... // 分析或传输数据...
} }
@ -342,7 +400,7 @@ public class SimpleAudioReceiver: NSObject {
private func runExternalCapture() { private func runExternalCapture() {
if audioSourceType == .external { if audioSourceType == .external {
//pushAudioData(data: Data()) //pushAudioData(data: Data())
//stopMicrophoneCapture()
audioEngine?.pause() audioEngine?.pause()
//audioEngine?.inputNode.removeTap(onBus: 0) //audioEngine?.inputNode.removeTap(onBus: 0)
} }
@ -375,23 +433,26 @@ public class SimpleAudioReceiver: NSObject {
public func releaseAudioResources() { public func releaseAudioResources() {
print("释放了") print("释放了")
// 停止麦克风捕获 // 停止麦克风捕获(含 _isWriting=false 与移除 tap)
stopMicrophoneCapture() stopMicrophoneCapture()
// 停止音频引擎 // 停止音频引擎
audioEngine?.stop() audioEngine?.stop()
// 关闭写入队列 // 安全关闭旧的写队列以唤醒阻塞的 take(),然后重建一个新的队列
let oldQueue = self.writeQueue
isRunning = false
writeThread?.async { writeThread?.async {
self.writeQueue.close() oldQueue.close()
} }
writeThread = nil writeThread = nil
// 重建队列,避免旧数据残留以及被 close 影响
writeQueue = LinkedBlockingQueue<Data>()
// 安全地移除 tap // 再次安全地移除 tap(幂等)
removeTapSafely() removeTapSafely()
// 重置状态 // 重置状态
isRunning = false
audioEngine = nil audioEngine = nil
} }
// MARK: - 协议定义 // MARK: - 协议定义
@ -503,10 +564,10 @@ private func setupAudioSession() throws {
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth])
try audioSession.setActive(true) try audioSession.setActive(true)
// 设置首选的音频参数 - 修改为16000Hz以匹配Azure Speech要求 // // 设置首选的音频参数 - 修改为16000Hz以匹配Azure Speech要求
try audioSession.setPreferredSampleRate(16000) // try audioSession.setPreferredSampleRate(16000)
// 设置输入声道数 // // 设置输入声道数
try audioSession.setPreferredInputNumberOfChannels(1) // try audioSession.setPreferredInputNumberOfChannels(1)
print("音频会话配置成功: 采样率=\(audioSession.sampleRate)Hz, 输入声道=\(audioSession.inputNumberOfChannels)") print("音频会话配置成功: 采样率=\(audioSession.sampleRate)Hz, 输入声道=\(audioSession.inputNumberOfChannels)")
} }

Loading…
Cancel
Save