4 changed files with 454 additions and 305 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