|
|
@ -6,15 +6,20 @@ import os.log |
|
|
/** |
|
|
/** |
|
|
* 简单音频接收器类,用于处理音频录制和流传输 |
|
|
* 简单音频接收器类,用于处理音频录制和流传输 |
|
|
* 对应Android的SimpleAudioReceiver功能 |
|
|
* 对应Android的SimpleAudioReceiver功能 |
|
|
|
|
|
* 使用单例模式确保全局唯一实例 |
|
|
*/ |
|
|
*/ |
|
|
public class SimpleAudioReceiver: NSObject { |
|
|
public class SimpleAudioReceiver: NSObject { |
|
|
|
|
|
|
|
|
/// 单例实例(线程安全) |
|
|
// MARK: - 单例实现 |
|
|
/// 通过 SimpleAudioReceiver.shared 获取全局唯一实例 |
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 单例实例 |
|
|
|
|
|
*/ |
|
|
public static let shared = SimpleAudioReceiver() |
|
|
public static let shared = SimpleAudioReceiver() |
|
|
|
|
|
|
|
|
/// 私有化构造函数,防止外部直接实例化 |
|
|
/** |
|
|
/// 使用方式:通过 SimpleAudioReceiver.shared 访问实例 |
|
|
* 私有初始化方法,防止外部创建实例 |
|
|
|
|
|
*/ |
|
|
private override init() { |
|
|
private override init() { |
|
|
super.init() |
|
|
super.init() |
|
|
initAudioRecord() |
|
|
initAudioRecord() |
|
|
@ -22,19 +27,7 @@ public class SimpleAudioReceiver: NSObject { |
|
|
|
|
|
|
|
|
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)! |
|
|
|
|
|
}() |
|
|
|
|
|
/** |
|
|
/** |
|
|
* 音频来源类型 |
|
|
* 音频来源类型 |
|
|
*/ |
|
|
*/ |
|
|
@ -48,7 +41,7 @@ public class SimpleAudioReceiver: NSObject { |
|
|
// MARK: - 音频流类 |
|
|
// MARK: - 音频流类 |
|
|
|
|
|
|
|
|
public private(set) var pushAudioStream: SPXPushAudioInputStream? |
|
|
public private(set) var pushAudioStream: SPXPushAudioInputStream? |
|
|
private var writeQueue = LinkedBlockingQueue<Data>() |
|
|
private let 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() |
|
|
@ -69,10 +62,14 @@ public class SimpleAudioReceiver: NSObject { |
|
|
/** |
|
|
/** |
|
|
* 初始化音频录制组件 |
|
|
* 初始化音频录制组件 |
|
|
* 包括音频格式、推流、音频引擎等核心组件的初始化 |
|
|
* 包括音频格式、推流、音频引擎等核心组件的初始化 |
|
|
|
|
|
* 注意:由于是单例模式,此方法可以被多次调用但只会初始化一次 |
|
|
*/ |
|
|
*/ |
|
|
public func initAudioRecord() { |
|
|
public func initAudioRecord() { |
|
|
// 每次初始化前,先释放上一次的资源,避免重复引擎/线程/tap |
|
|
// 防止重复初始化 |
|
|
releaseAudioResources() |
|
|
guard audioEngine == nil else { |
|
|
|
|
|
print("音频录制组件已初始化,跳过重复初始化") |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
print("初始化了") |
|
|
print("初始化了") |
|
|
audioFormat = getOptimalAudioFormat() |
|
|
audioFormat = getOptimalAudioFormat() |
|
|
@ -85,20 +82,6 @@ public class SimpleAudioReceiver: NSObject { |
|
|
// 创建新的写线程 |
|
|
// 创建新的写线程 |
|
|
writeThread = DispatchQueue(label: "audio.stream.writer") |
|
|
writeThread = DispatchQueue(label: "audio.stream.writer") |
|
|
startWriteThread() |
|
|
startWriteThread() |
|
|
// 配置音频引擎 |
|
|
|
|
|
do { |
|
|
|
|
|
try startCapture { buffer in |
|
|
|
|
|
// 处理实时音频数据 |
|
|
|
|
|
let channelData = buffer.floatChannelData?[0] |
|
|
|
|
|
let frameCount = buffer.frameLength |
|
|
|
|
|
// print("麦克风启动: \(frameCount)") |
|
|
|
|
|
// print("🎵 音频数据: 帧数=\(frameCount), 声道数据=\(channelData != nil ? "有效" : "无效")") |
|
|
|
|
|
self.writeQueue.put(self.audioBufferToDataOptimized(buffer)) |
|
|
|
|
|
// 分析或传输数据... |
|
|
|
|
|
} |
|
|
|
|
|
} catch { |
|
|
|
|
|
print("麦克风启动失败: \(error)") |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 获取最佳音频格式 (iOS 通常支持标准采样率) |
|
|
/// 获取最佳音频格式 (iOS 通常支持标准采样率) |
|
|
@ -185,7 +168,6 @@ public class SimpleAudioReceiver: NSObject { |
|
|
print("写入数据长度: \(dataToWrite.count)") |
|
|
print("写入数据长度: \(dataToWrite.count)") |
|
|
self.audioDataCallback!.onAudio(dataToWrite) |
|
|
self.audioDataCallback!.onAudio(dataToWrite) |
|
|
} |
|
|
} |
|
|
print("写入数据长度: \(dataToWrite.count)") |
|
|
|
|
|
try self.pushAudioStream?.write(dataToWrite) |
|
|
try self.pushAudioStream?.write(dataToWrite) |
|
|
if self.recordfile != nil { |
|
|
if self.recordfile != nil { |
|
|
// print("写入recordfile数据长度: \(dataToWrite.count)") |
|
|
// print("写入recordfile数据长度: \(dataToWrite.count)") |
|
|
@ -214,381 +196,262 @@ public class SimpleAudioReceiver: NSObject { |
|
|
// 放入队列,由写线程写入 |
|
|
// 放入队列,由写线程写入 |
|
|
writeQueue.put(data) |
|
|
writeQueue.put(data) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 开始音频捕获 |
|
|
|
|
|
* 1) 重置音频引擎并配置音频会话 |
|
|
|
|
|
* 2) 安装 tap 时不指定格式(format: nil),由系统使用硬件实际格式 |
|
|
|
|
|
* 3) 在回调内将硬件格式转换为统一的 16kHz 单声道后回调上层 |
|
|
|
|
|
* @param bufferHandler 音频缓冲区处理回调(已转换为目标格式) |
|
|
|
|
|
* @throws 音频引擎启动失败时抛出错误 |
|
|
|
|
|
*/ |
|
|
|
|
|
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) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 先安全地移除已存在的 tap |
|
|
|
|
|
removeTapSafely() |
|
|
|
|
|
|
|
|
|
|
|
// 重置音频引擎(在获取格式之前) |
|
|
|
|
|
audioEngine.reset() |
|
|
|
|
|
|
|
|
|
|
|
// 配置音频会话 |
|
|
|
|
|
do { |
|
|
|
|
|
try setupAudioSession() |
|
|
|
|
|
} catch { |
|
|
|
|
|
print("音频会话设置失败: \(error.localizedDescription)") |
|
|
|
|
|
throw error |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 重新获取输入节点(此处获取的输出格式在未启动时可能为 0Hz,仅用于日志) |
|
|
|
|
|
let inputNode = audioEngine.inputNode |
|
|
|
|
|
let preFormat = inputNode.outputFormat(forBus: 0) |
|
|
|
|
|
print("当前节点(启动前)输出格式: 采样率=\(preFormat.sampleRate)Hz, 声道数=\(preFormat.channelCount), 格式=\(preFormat.commonFormat.rawValue)") |
|
|
|
|
|
|
|
|
|
|
|
// 目标格式 - 统一使用16000Hz单声道(Float32),后续再转 Int16 |
|
|
|
|
|
let targetFormat = self.targetFormat16000Mono |
|
|
|
|
|
print("使用目标音频格式: 采样率=\(targetFormat.sampleRate)Hz, 声道数=\(targetFormat.channelCount)") |
|
|
|
|
|
|
|
|
|
|
|
// 使用硬件原生格式安装 tap(format: nil),回调中异步转换 |
|
|
|
|
|
inputNode.installTap(onBus: 0, |
|
|
|
|
|
bufferSize: 1024, |
|
|
|
|
|
format: nil) { [weak self] (buffer, time) in |
|
|
|
|
|
guard let self = self else { return } |
|
|
|
|
|
let srcFormat = buffer.format |
|
|
|
|
|
|
|
|
|
|
|
// 将重采样与格式转换放到专用队列,避免阻塞音频渲染线程 |
|
|
|
|
|
self.conversionQueue.async { |
|
|
|
|
|
// 如果源格式与目标格式不同,复用/重建转换器 |
|
|
|
|
|
var outputBuffer: AVAudioPCMBuffer? = buffer |
|
|
|
|
|
if srcFormat.sampleRate != targetFormat.sampleRate || |
|
|
|
|
|
srcFormat.channelCount != targetFormat.channelCount || |
|
|
|
|
|
srcFormat.commonFormat != targetFormat.commonFormat { |
|
|
|
|
|
|
|
|
|
|
|
if self.cachedConverter == nil || |
|
|
|
|
|
self.cachedConverter?.inputFormat != srcFormat || |
|
|
|
|
|
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 { |
|
|
|
|
|
print("无法创建转换后的音频缓冲区") |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
print("无法创建音频转换器: 从\(srcFormat.sampleRate)Hz到\(targetFormat.sampleRate)Hz") |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 回调上层(注意:此时不在实时渲染线程) |
|
|
|
|
|
if let out = outputBuffer { |
|
|
|
|
|
bufferHandler(out) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 启用语音处理(如果支持) |
|
|
// 私有方法:启动麦克风捕获 |
|
|
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时出现崩溃 |
|
|
* 优化:复用已初始化的音频引擎,减少启动延迟 |
|
|
*/ |
|
|
*/ |
|
|
private func removeTapSafely() { |
|
|
|
|
|
guard let audioEngine = audioEngine else { return } |
|
|
|
|
|
|
|
|
|
|
|
let inputNode = audioEngine.inputNode |
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有tap需要移除 |
|
|
|
|
|
if inputNode.numberOfInputs > 0 { |
|
|
|
|
|
do { |
|
|
|
|
|
inputNode.removeTap(onBus: 0) |
|
|
|
|
|
print("成功移除音频tap") |
|
|
|
|
|
} catch { |
|
|
|
|
|
print("移除音频tap时出错: \(error.localizedDescription)") |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
// 修改 runMicrophoneCapture 方法 |
|
|
|
|
|
private func runMicrophoneCapture() { |
|
|
private func runMicrophoneCapture() { |
|
|
do { |
|
|
do { |
|
|
print("🎤 开始配置麦克风捕获") |
|
|
// 【新增】首先配置音频会话 |
|
|
|
|
|
try audioSession.setCategory( |
|
|
|
|
|
.playAndRecord, |
|
|
|
|
|
mode: .videoChat, |
|
|
|
|
|
options: [.defaultToSpeaker,.allowBluetooth,.mixWithOthers] |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
// 【新增】请求麦克风权限(如果尚未授权) |
|
|
|
|
|
if audioSession.recordPermission != .granted { |
|
|
|
|
|
audioSession.requestRecordPermission { granted in |
|
|
|
|
|
if !granted { |
|
|
|
|
|
print("麦克风权限被拒绝") |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 【新增】激活音频会话(在配置音频引擎之前) |
|
|
|
|
|
try audioSession.setActive(true) |
|
|
|
|
|
|
|
|
|
|
|
// 检查音频引擎是否已初始化,避免重复创建 |
|
|
if audioEngine == nil { |
|
|
if audioEngine == nil { |
|
|
audioEngine = AVAudioEngine() |
|
|
audioEngine = AVAudioEngine() |
|
|
print("🔧 创建新的音频引擎") |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 如果音频引擎正在运行,先停止 |
|
|
if audioEngine?.isRunning == true { |
|
|
if audioEngine?.isRunning == true { |
|
|
audioEngine?.stop() |
|
|
audioEngine?.stop() |
|
|
print("⏹️ 停止现有音频引擎") |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
try startCapture { buffer in |
|
|
// 获取音频输入节点(麦克风) |
|
|
// 将音频处理移到专用队列,避免阻塞音频线程 |
|
|
guard let inputNode = audioEngine?.inputNode else { |
|
|
self.conversionQueue.async { |
|
|
throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法获取音频输入节点"]) |
|
|
let audioData = self.audioBufferToDataOptimized(buffer) |
|
|
} |
|
|
self.writeQueue.put(audioData) |
|
|
|
|
|
|
|
|
// 移除之前的音频处理块,避免重复添加 |
|
|
|
|
|
inputNode.removeTap(onBus: 0) |
|
|
|
|
|
|
|
|
|
|
|
// 获取硬件支持的原始音频格式 |
|
|
|
|
|
let hardwareFormat = inputNode.inputFormat(forBus: 0) |
|
|
|
|
|
|
|
|
|
|
|
// iOS 13+ 启用语音处理 |
|
|
|
|
|
if #available(iOS 13.0, *) { |
|
|
|
|
|
try inputNode.setVoiceProcessingEnabled(true) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 检查目标音频格式和转换器是否可用 |
|
|
|
|
|
guard let targetFormat = audioFormat, // 外部定义的期望音频格式 |
|
|
|
|
|
let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else { |
|
|
|
|
|
throw NSError(domain: "AudioSetup", code: 2) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 在输入节点上安装录音回调 |
|
|
|
|
|
inputNode.installTap(onBus: 0, |
|
|
|
|
|
bufferSize: UInt32(bufferSize), // 每次回调的缓冲区大小 |
|
|
|
|
|
format: hardwareFormat) { // 使用原始硬件格式 |
|
|
|
|
|
[weak self] buffer, time in // 弱引用避免循环引用 |
|
|
|
|
|
|
|
|
|
|
|
// 确保实例存在且正在写入状态 |
|
|
|
|
|
guard let self = self, self._isWriting else { return } |
|
|
|
|
|
|
|
|
|
|
|
// 创建目标格式的音频缓冲区 |
|
|
|
|
|
let convertedBuffer = AVAudioPCMBuffer( |
|
|
|
|
|
pcmFormat: targetFormat, |
|
|
|
|
|
// 计算转换后的帧容量(考虑采样率差异) |
|
|
|
|
|
frameCapacity: AVAudioFrameCount( |
|
|
|
|
|
targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate |
|
|
|
|
|
) |
|
|
|
|
|
)! |
|
|
|
|
|
|
|
|
|
|
|
var error: NSError? |
|
|
|
|
|
// 执行音频格式转换 |
|
|
|
|
|
let status = converter.convert( |
|
|
|
|
|
to: convertedBuffer, |
|
|
|
|
|
error: &error, |
|
|
|
|
|
withInputFrom: { inNumPackets, outStatus in |
|
|
|
|
|
outStatus.pointee = .haveData // 标记有数据可用 |
|
|
|
|
|
return buffer // 返回原始音频数据 |
|
|
|
|
|
} |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
// 转换成功且无错误 |
|
|
|
|
|
if status == .haveData, error == nil { |
|
|
|
|
|
// 将音频缓冲区转换为二进制数据 |
|
|
|
|
|
let data = self.audioBufferToData(convertedBuffer) |
|
|
|
|
|
// 将数据放入写入队列(后续处理) |
|
|
|
|
|
self.writeQueue.put(data) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
try audioEngine?.start() |
|
|
|
|
|
print("✅ 麦克风捕获配置完成") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try audioEngine?.start() |
|
|
} catch { |
|
|
} catch { |
|
|
print("麦克风启动失败: \(error)") |
|
|
print("麦克风启动失败: \(error)") |
|
|
|
|
|
// 【新增】添加详细错误处理 |
|
|
if let nsError = error as NSError? { |
|
|
if let nsError = error as NSError? { |
|
|
print("错误域: \(nsError.domain), 错误代码: \(nsError.code)") |
|
|
print("错误域: \(nsError.domain), 错误代码: \(nsError.code)") |
|
|
print("错误描述: \(nsError.localizedDescription)") |
|
|
print("错误描述: \(nsError.localizedDescription)") |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data { |
|
|
|
|
|
let frameLength = Int(buffer.frameLength) |
|
|
|
|
|
let channelCount = 1 |
|
|
|
|
|
let dataLength = frameLength * channelCount * MemoryLayout<Int16>.size |
|
|
|
|
|
|
|
|
|
|
|
// Handle 16-bit integer format |
|
|
|
|
|
if let int16Data = buffer.int16ChannelData { |
|
|
|
|
|
return Data( |
|
|
|
|
|
bytes: int16Data.pointee, |
|
|
|
|
|
count: dataLength |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
// Handle float format |
|
|
|
|
|
else if let floatData = buffer.floatChannelData { |
|
|
|
|
|
var int16Array = [Int16](repeating: 0, count: frameLength) |
|
|
|
|
|
let floatBuffer = floatData.pointee |
|
|
|
|
|
|
|
|
|
|
|
for i in 0..<frameLength { |
|
|
|
|
|
let sample = floatBuffer[i] |
|
|
|
|
|
let clamped = max(-1.0, min(sample, 1.0)) |
|
|
|
|
|
let scaled = clamped * Float(Int16.max) |
|
|
|
|
|
int16Array[i] = Int16(scaled) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return Data( |
|
|
|
|
|
bytes: int16Array, |
|
|
|
|
|
count: dataLength |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return Data() // Fallback for unsupported formats |
|
|
|
|
|
} |
|
|
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) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 停止麦克风捕获 |
|
|
* 继续麦克风捕获 |
|
|
* 安全地停止音频会话和音频引擎 |
|
|
|
|
|
*/ |
|
|
*/ |
|
|
public func stopMicrophoneCapture() { |
|
|
|
|
|
guard _isWriting == true else { return } |
|
|
|
|
|
|
|
|
|
|
|
_isWriting = false |
|
|
|
|
|
|
|
|
|
|
|
// 先停止音频引擎 |
|
|
|
|
|
audioEngine?.stop() |
|
|
|
|
|
|
|
|
|
|
|
// 然后安全地移除 tap |
|
|
|
|
|
removeTapSafely() |
|
|
|
|
|
|
|
|
|
|
|
print("stopMicrophoneCapture") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 释放音频资源 |
|
|
* 继续麦克风捕获 |
|
|
* 完全清理所有音频相关资源 |
|
|
|
|
|
*/ |
|
|
*/ |
|
|
public func releaseAudioResources() { |
|
|
public func resumeRecord() { |
|
|
print("释放了") |
|
|
guard _isWriting==false else { return } |
|
|
|
|
|
_isWriting = true |
|
|
// 停止麦克风捕获(含 _isWriting=false 与移除 tap) |
|
|
// 启动录音引擎 |
|
|
stopMicrophoneCapture() |
|
|
do { |
|
|
|
|
|
print("resumeRecord") |
|
|
// 停止音频引擎 |
|
|
try audioEngine?.start() |
|
|
audioEngine?.stop() |
|
|
} catch { |
|
|
|
|
|
os_log("Failed to start audio engine: %@", log: log, type: .error, error.localizedDescription) |
|
|
// 安全关闭旧的写队列以唤醒阻塞的 take(),然后重建一个新的队列 |
|
|
|
|
|
let oldQueue = self.writeQueue |
|
|
|
|
|
isRunning = false |
|
|
|
|
|
writeThread?.async { |
|
|
|
|
|
oldQueue.close() |
|
|
|
|
|
} |
|
|
} |
|
|
writeThread = nil |
|
|
|
|
|
// 重建队列,避免旧数据残留以及被 close 影响 |
|
|
|
|
|
writeQueue = LinkedBlockingQueue<Data>() |
|
|
|
|
|
|
|
|
|
|
|
// 再次安全地移除 tap(幂等) |
|
|
|
|
|
removeTapSafely() |
|
|
|
|
|
|
|
|
|
|
|
// 重置状态 |
|
|
|
|
|
audioEngine = nil |
|
|
|
|
|
} |
|
|
|
|
|
// MARK: - 协议定义 |
|
|
|
|
|
/** |
|
|
|
|
|
* 将音频缓冲区转换为Data格式 |
|
|
|
|
|
* 统一处理采样率转换和格式转换 |
|
|
|
|
|
* @param buffer 音频缓冲区 |
|
|
|
|
|
* @return 转换后的音频数据 |
|
|
|
|
|
*/ |
|
|
|
|
|
/** |
|
|
|
|
|
* 优化的音频缓冲区转换方法 |
|
|
|
|
|
* 减少内存分配,提高转换效率 |
|
|
|
|
|
*/ |
|
|
|
|
|
// 在类属性中添加 |
|
|
|
|
|
private var reusableDataBuffer: Data? |
|
|
|
|
|
private let bufferReuseQueue = DispatchQueue(label: "buffer.reuse", qos: .userInitiated) |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 带缓冲区复用的音频转换方法 |
|
|
|
|
|
*/ |
|
|
|
|
|
private func audioBufferToDataOptimized(_ buffer: AVAudioPCMBuffer) -> Data { |
|
|
|
|
|
guard let floatChannelData = buffer.floatChannelData else { |
|
|
|
|
|
return Data() |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let frameLength = Int(buffer.frameLength) |
|
|
|
|
|
let requiredSize = frameLength * 2 |
|
|
|
|
|
|
|
|
|
|
|
// 复用或创建缓冲区 |
|
|
|
|
|
if reusableDataBuffer == nil || reusableDataBuffer!.count < requiredSize { |
|
|
|
|
|
reusableDataBuffer = Data(count: requiredSize) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var outputData = reusableDataBuffer! |
|
|
|
|
|
let firstChannelData = floatChannelData[0] |
|
|
|
|
|
|
|
|
|
|
|
outputData.withUnsafeMutableBytes { rawBytes in |
|
|
|
|
|
let int16Ptr = rawBytes.bindMemory(to: Int16.self) |
|
|
|
|
|
|
|
|
|
|
|
for frame in 0..<frameLength { |
|
|
|
|
|
let floatSample = firstChannelData[frame] |
|
|
|
|
|
let clampedSample = max(-1.0, min(1.0, floatSample)) |
|
|
|
|
|
let int16Sample = Int16(clampedSample * 32767.0) |
|
|
|
|
|
int16Ptr[frame] = int16Sample.littleEndian |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 返回实际使用的数据部分 |
|
|
public func stopMicrophoneCapture() { |
|
|
return outputData.prefix(requiredSize) |
|
|
guard _isWriting==true else { return } |
|
|
} |
|
|
|
|
|
|
|
|
_isWriting=false |
|
|
/** |
|
|
audioEngine?.pause() |
|
|
* 转换音频缓冲区格式 |
|
|
print("stopMicrophoneCapture") |
|
|
* @param buffer 原始音频缓冲区 |
|
|
|
|
|
* @param targetFormat 目标音频格式 |
|
|
|
|
|
* @return 转换后的音频缓冲区,转换失败时返回nil |
|
|
|
|
|
*/ |
|
|
|
|
|
private func convertAudioBuffer(_ buffer: AVAudioPCMBuffer, to targetFormat: AVAudioFormat) -> AVAudioPCMBuffer? { |
|
|
|
|
|
guard let converter = AVAudioConverter(from: buffer.format, to: targetFormat) else { |
|
|
|
|
|
print("无法创建音频转换器: 从\(buffer.format.sampleRate)Hz到\(targetFormat.sampleRate)Hz") |
|
|
|
|
|
return nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 计算转换后的帧数 |
|
|
|
|
|
let capacity = AVAudioFrameCount(Double(buffer.frameLength) * targetFormat.sampleRate / buffer.format.sampleRate) |
|
|
|
|
|
guard let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: capacity) else { |
|
|
|
|
|
print("无法创建转换后的音频缓冲区") |
|
|
|
|
|
return nil |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
public func releaseAudioResources() { |
|
|
var error: NSError? |
|
|
print("释放了") |
|
|
let status = converter.convert(to: convertedBuffer, error: &error) { _, outStatus in |
|
|
stopMicrophoneCapture() |
|
|
outStatus.pointee = .haveData |
|
|
audioEngine?.stop() |
|
|
return buffer |
|
|
writeThread?.async { |
|
|
|
|
|
self.writeQueue.close() |
|
|
|
|
|
} |
|
|
|
|
|
writeThread = nil |
|
|
|
|
|
audioEngine?.inputNode.removeTap(onBus: 0) |
|
|
|
|
|
isRunning=false |
|
|
|
|
|
audioEngine = nil |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if status == .error { |
|
|
// 音频路由管理 |
|
|
print("音频转换失败: \(error?.localizedDescription ?? "未知错误")") |
|
|
public enum AudioOutputRoute { |
|
|
return nil |
|
|
case speaker |
|
|
|
|
|
case receiver |
|
|
|
|
|
case bluetooth |
|
|
} |
|
|
} |
|
|
|
|
|
public func setAudioOutputRoute(_ route: AudioOutputRoute) { |
|
|
return convertedBuffer |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 配置音频会话 |
|
|
|
|
|
* 设置录音和播放所需的音频会话参数 |
|
|
|
|
|
* @throws 音频会话配置失败时抛出错误 |
|
|
|
|
|
*/ |
|
|
|
|
|
private func setupAudioSession() throws { |
|
|
|
|
|
// 添加 .mixWithOthers 选项以支持同时播放和录音 |
|
|
|
|
|
try audioSession.setCategory(.playAndRecord, |
|
|
|
|
|
mode: .default, |
|
|
|
|
|
options: [.defaultToSpeaker, .allowBluetooth, .mixWithOthers]) |
|
|
|
|
|
|
|
|
|
|
|
// 设置较低的音频延迟以减少卡顿 |
|
|
|
|
|
try audioSession.setPreferredIOBufferDuration(0.005) // 5ms |
|
|
|
|
|
|
|
|
|
|
|
try audioSession.setActive(true) |
|
|
|
|
|
|
|
|
|
|
|
print("音频会话配置成功: 采样率=\(audioSession.sampleRate)Hz, 输入声道=\(audioSession.inputNumberOfChannels)") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 继续麦克风捕获 |
|
|
|
|
|
*/ |
|
|
|
|
|
public func resumeRecord() { |
|
|
|
|
|
guard _isWriting == false else { return } |
|
|
|
|
|
_isWriting = true |
|
|
|
|
|
|
|
|
|
|
|
// 启动录音引擎 |
|
|
|
|
|
do { |
|
|
do { |
|
|
print("resumeRecord") |
|
|
// |
|
|
|
|
|
if self._isWriting { |
|
|
// 确保音频引擎存在且已准备好 |
|
|
print("当前正在识别") |
|
|
guard let audioEngine = audioEngine else { |
|
|
try audioEngine?.pause() |
|
|
print("Audio engine not initialized") |
|
|
// 先停用以避免冲突 |
|
|
return |
|
|
try audioSession.setActive(false) |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
print("setAudioOutputRoutecurrent,route=\(route)") |
|
|
if !audioEngine.isRunning { |
|
|
switch route { |
|
|
try audioEngine.start() |
|
|
case .speaker: |
|
|
|
|
|
print("setAudioOutputRoutecurrent,speaker") |
|
|
|
|
|
// 使用扬声器时必须用videoChat模式 |
|
|
|
|
|
try audioSession.setCategory( |
|
|
|
|
|
.playAndRecord, |
|
|
|
|
|
mode: .videoChat, |
|
|
|
|
|
options: [.defaultToSpeaker,.allowBluetooth,.mixWithOthers] |
|
|
|
|
|
) |
|
|
|
|
|
try audioSession.overrideOutputAudioPort(.speaker) |
|
|
|
|
|
|
|
|
|
|
|
case .receiver: |
|
|
|
|
|
// 听筒模式使用voiceChat节省资源 |
|
|
|
|
|
try audioSession.setCategory( |
|
|
|
|
|
.playAndRecord, |
|
|
|
|
|
mode: .voiceChat, |
|
|
|
|
|
options: [.allowBluetooth] |
|
|
|
|
|
) |
|
|
|
|
|
try audioSession.overrideOutputAudioPort(.none) |
|
|
|
|
|
|
|
|
|
|
|
case .bluetooth: |
|
|
|
|
|
// 完整蓝牙设备支持 |
|
|
|
|
|
try audioSession.setCategory( |
|
|
|
|
|
.playAndRecord, |
|
|
|
|
|
mode: .voiceChat, |
|
|
|
|
|
options: [.allowBluetooth, .allowBluetoothA2DP] |
|
|
|
|
|
) |
|
|
|
|
|
try audioSession.overrideOutputAudioPort(.none) |
|
|
|
|
|
// 不需要override,系统自动路由 |
|
|
|
|
|
} |
|
|
|
|
|
self.currentRoute = route |
|
|
|
|
|
if self._isWriting { |
|
|
|
|
|
|
|
|
|
|
|
// 重新激活 |
|
|
|
|
|
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) |
|
|
|
|
|
|
|
|
|
|
|
try audioEngine?.start() |
|
|
|
|
|
// writeThread?.async { |
|
|
|
|
|
// self.writeQueue.close() |
|
|
|
|
|
// } |
|
|
} |
|
|
} |
|
|
} catch { |
|
|
} catch { |
|
|
os_log("Failed to start audio engine: %@", log: log, type: .error, error.localizedDescription) |
|
|
print("路由切换失败: \(error)") |
|
|
_isWriting = false |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 音频路由管理 |
|
|
|
|
|
public enum AudioOutputRoute { |
|
|
|
|
|
case speaker |
|
|
|
|
|
case receiver |
|
|
|
|
|
case bluetooth |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 音频数据回调接口 |
|
|
* 音频数据回调接口 |
|
|
@ -609,6 +472,25 @@ private func setupAudioSession() throws { |
|
|
return _isWriting |
|
|
return _isWriting |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 重置单例实例(用于测试或特殊情况下的重新初始化) |
|
|
|
|
|
* 注意:调用此方法会释放所有音频资源并重置状态 |
|
|
|
|
|
*/ |
|
|
|
|
|
public func resetInstance() { |
|
|
|
|
|
releaseAudioResources() |
|
|
|
|
|
|
|
|
|
|
|
// 重置所有状态 |
|
|
|
|
|
pushAudioStream = nil |
|
|
|
|
|
audioEngine = nil |
|
|
|
|
|
audioFormat = nil |
|
|
|
|
|
audioSourceType = .microphone |
|
|
|
|
|
audioDataCallback = nil |
|
|
|
|
|
isRunning = false |
|
|
|
|
|
_isWriting = false |
|
|
|
|
|
writeThread = nil |
|
|
|
|
|
currentRoute = nil |
|
|
|
|
|
recordfile = nil |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
// MARK: - iOS版LinkedBlockingQueue实现 |
|
|
// MARK: - iOS版LinkedBlockingQueue实现 |
|
|
private class LinkedBlockingQueue<T> { |
|
|
private class LinkedBlockingQueue<T> { |
|
|
@ -683,3 +565,6 @@ private class LinkedBlockingQueue<T> { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// MARK: - 协议定义 |
|
|
|
|
|
|
|
|
|