4 changed files with 452 additions and 276 deletions
@ -0,0 +1,317 @@ |
|||
import AVFoundation |
|||
|
|||
class MicrophoneCapture: NSObject { |
|||
// MARK: - 单例实例 |
|||
/** |
|||
* 单例实例,确保整个应用中只有一个音频捕获对象 |
|||
* 避免多个实例同时访问麦克风资源导致冲突 |
|||
*/ |
|||
static let shared = MicrophoneCapture() |
|||
|
|||
private var audioEngine: AVAudioEngine! |
|||
private var audioSession: AVAudioSession! |
|||
private var audioFormat: AVAudioFormat? |
|||
private var audioInputNode: AVAudioInputNode! |
|||
private var sampleRate: Double = 16000 // 修改为16000采样率,符合Microsoft要求 |
|||
private var isCapturing = false |
|||
|
|||
// 音频数据回调 |
|||
var audioDataHandler: ((Data) -> Void)? |
|||
|
|||
/** |
|||
* 私有初始化方法,防止外部直接创建实例 |
|||
* 只能通过shared单例访问 |
|||
*/ |
|||
private override init() { |
|||
super.init() |
|||
setupAudioSession() |
|||
} |
|||
|
|||
/// 获取最佳音频格式 (iOS 通常支持标准采样率) |
|||
public func getOptimalAudioFormat() -> AVAudioFormat? { |
|||
let sampleRate: Double = 16000 // Microsoft要求最低16kHz |
|||
return AVAudioFormat( |
|||
commonFormat: .pcmFormatInt16, // 16位little endian有符号整数 |
|||
sampleRate: sampleRate, |
|||
channels: 1, |
|||
interleaved: true |
|||
) |
|||
} |
|||
|
|||
// MARK: - 音频会话配置 |
|||
/** |
|||
* 配置音频会话参数 |
|||
* 设置音频类别、采样率、缓冲区大小等 |
|||
*/ |
|||
public func setupAudioSession() { |
|||
audioFormat = getOptimalAudioFormat() |
|||
audioSession = AVAudioSession.sharedInstance() |
|||
|
|||
do { |
|||
// 设置音频会话参数 |
|||
try audioSession.setCategory(.playAndRecord, |
|||
mode: .videoChat, |
|||
options: [.allowBluetooth, |
|||
// .allowAirPlay, |
|||
// .allowBluetoothA2DP, |
|||
//.mixWithOthers, |
|||
.defaultToSpeaker |
|||
]) |
|||
|
|||
try audioSession.setPreferredSampleRate(sampleRate) |
|||
try audioSession.setPreferredIOBufferDuration(0.005) // 5ms缓冲 |
|||
|
|||
// 设置蓝牙设备优先级 |
|||
if let bluetoothInput = audioSession.availableInputs?.first(where: { $0.portType == .bluetoothHFP }) { |
|||
try audioSession.setPreferredInput(bluetoothInput) |
|||
} |
|||
|
|||
try audioSession.setActive(true) |
|||
print("音频会话配置成功") |
|||
} catch { |
|||
print("音频会话配置失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
|
|||
// MARK: - 开始捕获 |
|||
/** |
|||
* 开始音频捕获 |
|||
* 检查麦克风权限并启动音频引擎 |
|||
*/ |
|||
public func startCapture() throws { |
|||
guard !isCapturing else { return } |
|||
print("开始捕获") |
|||
// 检查麦克风权限 |
|||
switch audioSession.recordPermission { |
|||
case .granted: |
|||
try setupAudioEngine() |
|||
case .denied: |
|||
throw NSError(domain: "麦克风权限被拒绝", code: 0) |
|||
case .undetermined: |
|||
audioSession.requestRecordPermission { granted in |
|||
if granted { |
|||
do { |
|||
try self.setupAudioEngine() |
|||
} catch { |
|||
print("音频引擎设置失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
@unknown default: |
|||
throw NSError(domain: "未知权限状态", code: 1) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 检查音频格式是否符合Microsoft音频堆栈要求 |
|||
* 如果符合则无需转换,直接使用原始格式 |
|||
*/ |
|||
private func isMicrosoftCompatibleFormat(_ format: AVAudioFormat) -> Bool { |
|||
// Microsoft支持的采样率:16kHz的整数倍,最低16kHz |
|||
let sampleRate = format.sampleRate |
|||
guard sampleRate >= 16000 && sampleRate.truncatingRemainder(dividingBy: 16000) == 0 else { |
|||
return false |
|||
} |
|||
|
|||
// Microsoft支持的格式 |
|||
switch format.commonFormat { |
|||
case .pcmFormatFloat32: // 32位IEEE little endian浮点 |
|||
return true |
|||
case .pcmFormatInt32: // 32位little endian有符号整数 |
|||
return true |
|||
case .pcmFormatInt16: // 16位little endian有符号整数 |
|||
return true |
|||
default: |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置音频引擎和音频处理管道 |
|||
* 根据Microsoft音频堆栈要求优化格式转换 |
|||
*/ |
|||
private func setupAudioEngine() throws { |
|||
audioEngine = AVAudioEngine() |
|||
|
|||
audioInputNode = audioEngine.inputNode |
|||
|
|||
// 获取实际硬件采样率 |
|||
sampleRate = audioSession.sampleRate |
|||
|
|||
// 移除之前的音频处理块,避免重复添加 |
|||
audioInputNode.removeTap(onBus: 0) |
|||
|
|||
// 设置音频格式 |
|||
let inputFormat = audioInputNode.inputFormat(forBus: 0) |
|||
|
|||
// iOS 13+ 启用语音处理 |
|||
if #available(iOS 13.0, *) { |
|||
try audioInputNode.setVoiceProcessingEnabled(true) |
|||
} |
|||
|
|||
// 检查输入格式是否符合Microsoft要求 |
|||
let needsConversion = !isMicrosoftCompatibleFormat(inputFormat) |
|||
|
|||
// if needsConversion { |
|||
// 需要格式转换 |
|||
guard let targetFormat = audioFormat, |
|||
let converter = AVAudioConverter(from: inputFormat, to: targetFormat) else { |
|||
throw NSError(domain: "AudioSetup", code: 2) |
|||
} |
|||
|
|||
print("输入格式不符合Microsoft要求,进行格式转换") |
|||
print("输入格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)") |
|||
print("目标格式: \(targetFormat.sampleRate)Hz, \(targetFormat.commonFormat.rawValue)") |
|||
|
|||
// 添加tap进行格式转换 |
|||
audioInputNode.installTap(onBus: 0, |
|||
bufferSize: 1024, |
|||
format: inputFormat) { [weak self] buffer, when in |
|||
|
|||
guard let strongSelf = self 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 = strongSelf.audioBufferToData(convertedBuffer) |
|||
print("转换后数据长度: \(data.count)") |
|||
strongSelf.audioDataHandler?(data) |
|||
} |
|||
} |
|||
// } else { |
|||
// // 格式已符合Microsoft要求,直接使用 |
|||
// print("输入格式符合Microsoft要求,无需转换") |
|||
// print("格式: \(inputFormat.sampleRate)Hz, \(inputFormat.commonFormat.rawValue)") |
|||
|
|||
// audioInputNode.installTap(onBus: 0, |
|||
// bufferSize: 1024, |
|||
// format: inputFormat) { [weak self] buffer, when in |
|||
|
|||
// guard let strongSelf = self else { return } |
|||
//strongSelf.processAudioBuffer(buffer) |
|||
// // 直接处理原始音频数据 |
|||
// let data = strongSelf.audioBufferToData(convertedBuffer) |
|||
// print("转换后数据长度: \(data.count)") |
|||
// strongSelf.audioDataHandler?(data) |
|||
|
|||
// } |
|||
// } |
|||
|
|||
// 启动引擎 |
|||
do { |
|||
try audioEngine.start() |
|||
isCapturing = true |
|||
} catch { |
|||
print("音频引擎启动失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将音频缓冲区转换为Data格式 |
|||
* 支持16位整数和32位浮点格式 |
|||
*/ |
|||
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 |
|||
} |
|||
|
|||
// MARK: - 音频数据处理 |
|||
/** |
|||
* 处理音频缓冲区数据 |
|||
* 将浮点音频数据转换为16位PCM格式 |
|||
*/ |
|||
private func processAudioBuffer(_ buffer: AVAudioPCMBuffer) { |
|||
guard let floatData = buffer.floatChannelData?[0] else { return } |
|||
|
|||
let channelCount = Int(buffer.format.channelCount) |
|||
let frameLength = Int(buffer.frameLength) |
|||
let sampleCount = frameLength * channelCount |
|||
|
|||
// 转换为16位PCM数据(兼容大多数音频处理场景) |
|||
var pcmData = Data() |
|||
for i in 0..<sampleCount { |
|||
let sample = floatData[i] * Float32(Int16.max) |
|||
var pcmSample = Int16(sample) |
|||
pcmData.append(Data(bytes: &pcmSample, count: MemoryLayout<Int16>.size)) |
|||
} |
|||
|
|||
// 通过回调返回数据 |
|||
DispatchQueue.main.async { |
|||
print("音频数据回调\(pcmData.count)") |
|||
self.audioDataHandler?(pcmData) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 停止捕获 |
|||
/** |
|||
* 停止音频捕获 |
|||
* 清理音频引擎和会话资源 |
|||
*/ |
|||
func stopCapture() { |
|||
guard isCapturing else { return } |
|||
|
|||
audioEngine.stop() |
|||
audioInputNode.removeTap(onBus: 0) |
|||
// try? audioSession.setActive(false) |
|||
isCapturing = false |
|||
} |
|||
|
|||
/** |
|||
* 重置音频捕获状态 |
|||
* 用于单例模式下的状态清理 |
|||
*/ |
|||
func reset() { |
|||
stopCapture() |
|||
audioDataHandler = nil |
|||
} |
|||
|
|||
deinit { |
|||
stopCapture() |
|||
} |
|||
} |
|||
Loading…
Reference in new issue