|
|
|
@ -54,10 +54,8 @@ public class SimpleAudioReceiver: NSObject { |
|
|
|
private var currentRoute: AudioOutputRoute? |
|
|
|
//public var onAudioData: ((Data) -> Void)? |
|
|
|
public var recordfile: RecordFile? |
|
|
|
private var currentRecognitionMode = "normal" // 当前识别模式:normal, ble_wakeup, phone_call, push_to_talk |
|
|
|
public var isHeadphones = true |
|
|
|
public var isReceiverHFP = false // 是否是接收HFP音频 |
|
|
|
public var isPhoneCallMode = false // AI电话模式:录音+播放强制走电话链路(voiceChat/HFP) |
|
|
|
public var preferBuiltInMic = false // 耳机连接时优先使用手机内置麦克风录音 |
|
|
|
|
|
|
|
/** |
|
|
|
* 初始化 |
|
|
|
@ -125,39 +123,118 @@ public class SimpleAudioReceiver: NSObject { |
|
|
|
|
|
|
|
/** |
|
|
|
* 开始音频输入 |
|
|
|
* 参数:audioSourceType 音频源类型(microphone 或 external);audioDataCallback 音频数据回调 |
|
|
|
* 返回值:无 |
|
|
|
* 异常:无(内部捕获并打印错误,不抛出) |
|
|
|
* - Parameter mod: 识别模式,如 "normal", "phone_call", "push_to_talk", "ble_wakeup"。 |
|
|
|
* - Parameter audioSourceType: 音频源类型(.microphone 或 .external)。 |
|
|
|
* - Parameter audioDataCallback: 音频数据回调。 |
|
|
|
*/ |
|
|
|
public func startAudioRecord(audioSourceType: AudioSourceType = .microphone, audioDataCallback: AudioDataCallback?) { |
|
|
|
_isWriting=true |
|
|
|
self.audioSourceType = audioSourceType; |
|
|
|
self.audioDataCallback = audioDataCallback |
|
|
|
print("音源设制---------- startAudioRecord=audioSourceType:\(audioSourceType)") |
|
|
|
switch audioSourceType { |
|
|
|
case .microphone: |
|
|
|
isReceiverHFP = isPhoneCallMode |
|
|
|
// 动态检测是否存在耳机/蓝牙输出,用于后续路由选项计算 |
|
|
|
isHeadphones = detectHeadphonesOutput() |
|
|
|
public func startAudioRecord(mod: String = "normal", audioSourceType: AudioSourceType = .microphone, audioDataCallback: AudioDataCallback?) { |
|
|
|
self.currentRecognitionMode = mod |
|
|
|
self.audioSourceType = audioSourceType |
|
|
|
self.audioDataCallback = audioDataCallback |
|
|
|
self.isHeadphones = detectHeadphonesOutput() |
|
|
|
|
|
|
|
_isWriting = true |
|
|
|
|
|
|
|
print("音源设置---------- startAudioRecord=mod:\(mod), audioSourceType:\(audioSourceType), isHeadphones:\(isHeadphones)") |
|
|
|
|
|
|
|
do { |
|
|
|
try configureAudioSession() |
|
|
|
|
|
|
|
let isHybridExternalMode = audioSourceType == .external && (currentRecognitionMode == "phone_call" || currentRecognitionMode == "push_to_talk") |
|
|
|
|
|
|
|
if audioSourceType == .microphone || isHybridExternalMode { |
|
|
|
runMicrophoneCapture() |
|
|
|
case .external: |
|
|
|
isReceiverHFP = false |
|
|
|
} else { // True external mode |
|
|
|
runExternalCapture() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public func setPhoneCallMode(_ enabled: Bool) { |
|
|
|
isPhoneCallMode = enabled |
|
|
|
} catch { |
|
|
|
print("配置或启动音频捕获失败: \(error.localizedDescription)") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 设置是否优先使用手机内置麦克风录音 |
|
|
|
* @param enabled 是否启用 |
|
|
|
* @return 无 |
|
|
|
* @throws 无 |
|
|
|
* 根据当前模式和设备状态配置音频会话 |
|
|
|
*/ |
|
|
|
public func setPreferBuiltInMic(_ enabled: Bool) { |
|
|
|
preferBuiltInMic = enabled |
|
|
|
private func configureAudioSession() throws { |
|
|
|
let audioSession = self.audioSession |
|
|
|
|
|
|
|
var desiredCategory: AVAudioSession.Category |
|
|
|
var desiredMode: AVAudioSession.Mode |
|
|
|
var desiredOptions: AVAudioSession.CategoryOptions |
|
|
|
|
|
|
|
let isHybridExternalMode = audioSourceType == .external && (currentRecognitionMode == "phone_call" || currentRecognitionMode == "push_to_talk") |
|
|
|
|
|
|
|
if audioSourceType == .microphone || isHybridExternalMode { |
|
|
|
// --- 麦克风输入逻辑 (包括混合模式) --- |
|
|
|
desiredCategory = .playAndRecord |
|
|
|
|
|
|
|
if currentRecognitionMode == "phone_call" { |
|
|
|
desiredMode = .voiceChat |
|
|
|
desiredOptions = isHeadphones ? [.allowBluetooth] : [.allowBluetooth, .defaultToSpeaker] |
|
|
|
} else { // normal, push_to_talk, ble_wakeup 等 |
|
|
|
desiredMode = .videoChat |
|
|
|
if isHeadphones { |
|
|
|
desiredOptions = [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers] |
|
|
|
} else { |
|
|
|
desiredOptions = [.mixWithOthers, .defaultToSpeaker] |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 如果在真实通话中,强制使用更适合的录音设置 |
|
|
|
if isTelephonyActive() { |
|
|
|
desiredCategory = .record |
|
|
|
desiredMode = .voiceChat |
|
|
|
desiredOptions = [.allowBluetooth] |
|
|
|
} |
|
|
|
|
|
|
|
} else if audioSourceType == .external { |
|
|
|
// --- 纯外部源 (仅播放) 逻辑 --- |
|
|
|
desiredCategory = .playback |
|
|
|
desiredMode = .videoChat |
|
|
|
desiredOptions = isHeadphones ? [.allowBluetoothA2DP, .mixWithOthers] : [.mixWithOthers, .defaultToSpeaker] |
|
|
|
} else { |
|
|
|
// 兜底,理论上不会执行 |
|
|
|
desiredCategory = .playAndRecord |
|
|
|
desiredMode = .default |
|
|
|
desiredOptions = [] |
|
|
|
} |
|
|
|
|
|
|
|
// 仅在需要时更新会话,避免不必要的中断 |
|
|
|
if audioSession.category != desiredCategory || audioSession.mode != desiredMode || audioSession.categoryOptions != desiredOptions { |
|
|
|
try audioSession.setCategory(desiredCategory, mode: desiredMode, options: desiredOptions) |
|
|
|
} |
|
|
|
|
|
|
|
// 激活会话 |
|
|
|
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
|
|
|
|
|
|
|
// --- 路由设置 --- |
|
|
|
if audioSourceType == .microphone || isHybridExternalMode { |
|
|
|
// 为需要麦克风的场景设置输入输出 |
|
|
|
if isHeadphones { |
|
|
|
try audioSession.overrideOutputAudioPort(.none) // 允许耳机/蓝牙输出 |
|
|
|
} else { |
|
|
|
try audioSession.overrideOutputAudioPort(.speaker) // 强制外放 |
|
|
|
} |
|
|
|
|
|
|
|
// 设置首选输入设备 |
|
|
|
if let inputs = audioSession.availableInputs { |
|
|
|
if isHybridExternalMode { |
|
|
|
// 混合模式: 强制使用内置麦克风 |
|
|
|
if let builtInMic = inputs.first(where: { $0.portType == .builtInMic }) { |
|
|
|
try audioSession.setPreferredInput(builtInMic) |
|
|
|
print("混合外部模式: 首选输入已设置为内置麦克风") |
|
|
|
} |
|
|
|
} else if let btInput = inputs.first(where: { $0.portType == .bluetoothHFP }), isHeadphones { |
|
|
|
// 标准麦克风模式: 优先使用蓝牙耳机麦克风 |
|
|
|
try audioSession.setPreferredInput(btInput) |
|
|
|
} else if let builtInMic = inputs.first(where: { $0.portType == .builtInMic }) { |
|
|
|
// 兜底: 使用内置麦克风 |
|
|
|
try audioSession.setPreferredInput(builtInMic) |
|
|
|
} |
|
|
|
} |
|
|
|
} else { // 纯外部源 |
|
|
|
try audioSession.overrideOutputAudioPort(.none) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
@ -255,9 +332,6 @@ public class SimpleAudioReceiver: NSObject { |
|
|
|
// 确保正在写入状态 |
|
|
|
guard self._isWriting else { return } |
|
|
|
|
|
|
|
// 根据通话状态进行安全的音频会话配置 |
|
|
|
try safeConfigureAudioSessionForMic() |
|
|
|
|
|
|
|
// 启动麦克风采集 |
|
|
|
try micCapture.startCapture() |
|
|
|
|
|
|
|
@ -282,154 +356,8 @@ public class SimpleAudioReceiver: NSObject { |
|
|
|
*/ |
|
|
|
private func runExternalCapture() { |
|
|
|
if audioSourceType == .external { |
|
|
|
do { |
|
|
|
// 停止麦克风采集,确保不走通话音道 |
|
|
|
micCapture.stopCapture() |
|
|
|
// 外部模式会话配置:禁止HFP,仅允许A2DP/耳机输出 |
|
|
|
try safeConfigureAudioSessionForExternal() |
|
|
|
} catch { |
|
|
|
print("设置音频会话失败: \(error.localizedDescription)") |
|
|
|
} |
|
|
|
//pushAudioData(data: Data()) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 外部音频源的安全音频会话配置(禁止HFP通话音道) |
|
|
|
* 参数:无 |
|
|
|
* 返回值:无 |
|
|
|
* 异常:可能抛出AVAudioSession设置错误(内部捕获并兜底) |
|
|
|
*/ |
|
|
|
private func safeConfigureAudioSessionForExternal() throws { |
|
|
|
let audioSession = self.audioSession |
|
|
|
do { |
|
|
|
// 外部模式不录麦,仅做播放路由;有耳机时只允许A2DP,禁止HFP |
|
|
|
let outputs = audioSession.currentRoute.outputs |
|
|
|
let hasHeadphones = outputs.contains { out in |
|
|
|
out.portType == .bluetoothA2DP || |
|
|
|
out.portType == .bluetoothLE || |
|
|
|
out.portType == .headphones |
|
|
|
} |
|
|
|
let desiredCategory: AVAudioSession.Category = .playback |
|
|
|
let desiredMode: AVAudioSession.Mode = .videoChat |
|
|
|
let desiredOptions: AVAudioSession.CategoryOptions = hasHeadphones |
|
|
|
? [.allowBluetoothA2DP, .mixWithOthers] |
|
|
|
: [.mixWithOthers, .defaultToSpeaker] |
|
|
|
|
|
|
|
let needsSetCategory = (audioSession.category != desiredCategory) || (audioSession.mode != desiredMode) |
|
|
|
if needsSetCategory { |
|
|
|
try audioSession.setCategory(desiredCategory, mode: desiredMode, options: desiredOptions) |
|
|
|
} |
|
|
|
// 取消扬声器强制,确保耳机/A2DP优先 |
|
|
|
try audioSession.overrideOutputAudioPort(.none) |
|
|
|
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
|
|
|
} catch { |
|
|
|
print("外部模式音频会话配置失败,兜底到playback: \(error)") |
|
|
|
try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers, .defaultToSpeaker]) |
|
|
|
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* 在通话/非通话场景下安全配置音频会话用于麦克风采集 |
|
|
|
* - 通话中:使用 .record + .voiceChat + [.allowBluetooth],避免 A2DP 和强制外放,兼容电话路由 |
|
|
|
* - 非通话:使用 .playAndRecord + .videoChat + [.allowBluetooth, .mixWithOthers, .defaultToSpeaker] |
|
|
|
* - 失败兜底:退回到最简单的 .record + .default,确保录音尽可能可用 |
|
|
|
*/ |
|
|
|
private func safeConfigureAudioSessionForMic() throws { |
|
|
|
let audioSession = self.audioSession |
|
|
|
do { |
|
|
|
// 计算目标配置 |
|
|
|
let desiredCategory: AVAudioSession.Category |
|
|
|
let desiredMode: AVAudioSession.Mode |
|
|
|
let desiredOptions: AVAudioSession.CategoryOptions |
|
|
|
if isPhoneCallMode { |
|
|
|
desiredCategory = .playAndRecord |
|
|
|
desiredMode = .voiceChat |
|
|
|
if isHeadphones { |
|
|
|
desiredOptions = [.allowBluetooth] |
|
|
|
} else { |
|
|
|
desiredOptions = [.allowBluetooth, .defaultToSpeaker] |
|
|
|
} |
|
|
|
} else if isReceiverHFP { |
|
|
|
if isTelephonyActive() { |
|
|
|
// 通话中:录音优先,避免 A2DP(仅播放),改用 HFP(.allowBluetooth) |
|
|
|
desiredCategory = .record |
|
|
|
desiredMode = .voiceChat |
|
|
|
desiredOptions = [.allowBluetooth] |
|
|
|
} else { |
|
|
|
if(isHeadphones){ |
|
|
|
// 非通话:保持原有逻辑,但使用 .allowBluetooth 以支持录音(而不是 A2DP) |
|
|
|
desiredCategory = .playAndRecord |
|
|
|
desiredMode = .videoChat |
|
|
|
desiredOptions = [.allowBluetooth, .mixWithOthers] |
|
|
|
//desiredOptions = [.mixWithOthers, .defaultToSpeaker] |
|
|
|
}else{ |
|
|
|
desiredCategory = .playAndRecord |
|
|
|
desiredMode = .videoChat |
|
|
|
desiredOptions = [.mixWithOthers, .defaultToSpeaker] |
|
|
|
} |
|
|
|
} |
|
|
|
}else{ |
|
|
|
if isTelephonyActive() { |
|
|
|
// 通话中:录音优先,避免 A2DP(仅播放),改用 HFP(.allowBluetooth) |
|
|
|
desiredCategory = .record |
|
|
|
desiredMode = .voiceChat |
|
|
|
desiredOptions = [.allowBluetooth] |
|
|
|
} else { |
|
|
|
if(isHeadphones){ |
|
|
|
// 非通话:保持原有逻辑,但使用 .allowBluetooth 以支持录音(而不是 A2DP) |
|
|
|
desiredCategory = .playAndRecord |
|
|
|
desiredMode = .videoChat |
|
|
|
desiredOptions = [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers] |
|
|
|
//desiredOptions = [.mixWithOthers, .defaultToSpeaker] |
|
|
|
}else{ |
|
|
|
desiredCategory = .playAndRecord |
|
|
|
desiredMode = .videoChat |
|
|
|
desiredOptions = [.mixWithOthers, .defaultToSpeaker] |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 仅在类别或模式变化时才调用 setCategory,避免触发不必要的 categoryChange |
|
|
|
let needsSetCategory = (audioSession.category != desiredCategory) || (audioSession.mode != desiredMode) |
|
|
|
if needsSetCategory { |
|
|
|
try audioSession.setCategory(desiredCategory, |
|
|
|
mode: desiredMode, |
|
|
|
options: desiredOptions) |
|
|
|
} else { |
|
|
|
// 类别与模式一致时,也尽量避免重复 setCategory;如需确保 options,可在确有必要时再设置 |
|
|
|
// 如果你确实需要校正 options,可取消下一行注释,但这可能再次触发 categoryChange |
|
|
|
// try audioSession.setCategory(desiredCategory, mode: desiredMode, options: desiredOptions) |
|
|
|
} |
|
|
|
|
|
|
|
// 保持激活;这里重复 setActive(true) 一般安全,但如果你观察到重复激活也触发路由回调,可在外层控制频率 |
|
|
|
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
|
|
|
|
|
|
|
// 路由校正:有蓝牙输出时不强制外放,并优先选择蓝牙输入(HFP)或内置麦克风 |
|
|
|
let outputs = audioSession.currentRoute.outputs |
|
|
|
let hasBtOutput = outputs.contains { out in |
|
|
|
out.portType == .bluetoothA2DP || out.portType == .bluetoothHFP || out.portType == .bluetoothLE |
|
|
|
} |
|
|
|
if hasBtOutput { |
|
|
|
try audioSession.overrideOutputAudioPort(.none) |
|
|
|
} else if isPhoneCallMode, !isHeadphones { |
|
|
|
try audioSession.overrideOutputAudioPort(.speaker) |
|
|
|
} |
|
|
|
if let inputs = audioSession.availableInputs { |
|
|
|
if preferBuiltInMic, let builtInMic = inputs.first(where: { $0.portType == .builtInMic }) { |
|
|
|
try audioSession.setPreferredInput(builtInMic) |
|
|
|
} else if isReceiverHFP, let btInput = inputs.first(where: { $0.portType == .bluetoothHFP }) { |
|
|
|
try audioSession.setPreferredInput(btInput) |
|
|
|
} else if let builtInMic = inputs.first(where: { $0.portType == .builtInMic }) { |
|
|
|
try audioSession.setPreferredInput(builtInMic) |
|
|
|
} |
|
|
|
} |
|
|
|
} catch { |
|
|
|
// 兜底:尽量保证能开始录音 |
|
|
|
print("音频会话配置失败,尝试兜底配置: \(error)") |
|
|
|
try audioSession.setCategory(.record, mode: .default, options: []) |
|
|
|
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
|
|
|
// 停止麦克风采集,确保不走通话音道 |
|
|
|
micCapture.stopCapture() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -451,12 +379,12 @@ public class SimpleAudioReceiver: NSObject { |
|
|
|
* 继续麦克风捕获 |
|
|
|
*/ |
|
|
|
public func resumeRecord() { |
|
|
|
guard _isWriting==false else { return } |
|
|
|
guard _isWriting==false else { return } |
|
|
|
_isWriting = true |
|
|
|
// 启动录音引擎 |
|
|
|
do { |
|
|
|
print("resumeRecord") |
|
|
|
try safeConfigureAudioSessionForMic() |
|
|
|
try configureAudioSession() |
|
|
|
try micCapture.startCapture() |
|
|
|
} catch { |
|
|
|
if let nsError = error as NSError? { |
|
|
|
|