macOS AudioToolbox kAudioUnitSubType_HALOutput 为何输入静音?
结论
先检查构建产物中的麦克风隐私声明和录音权限配置,尤其是 NSMicrophoneUsageDescription。目前的现象不足以证明 kAudioUnitSubType_HALOutput 在 macOS 11 后失效,也没有必要因此重写整个音频程序。
从 macOS 10.14 起,访问麦克风需要用户授权。尚未授权或授权被拒绝时,采集可能得到静音。即使 Audio Unit 初始化、启动成功,也仍需检查权限。Apple 授权状态说明
AudioOutputUnitStop() 不会撤销用户授予的麦克风权限。停止后,麦克风使用指示消失,表示当前采集活动已停止。再次启动时没有弹出授权对话框,通常是因为系统已经记住了用户的选择。
题目没有给出 RenderRMTone 的实现,还需要确认回调是否调用了 AudioUnitRender(),从 bus 1 取得实际输入数据。
先补齐麦克风权限配置
在实际运行的程序中加入用途说明
在应用的 Info.plist 中加入:
<key>NSMicrophoneUsageDescription</key>
<string>用于采集音频输入并进行实时信号处理。</string>
Xcode 中对应的显示名称是 Privacy - Microphone Usage Description。用途说明必须是非空字符串,并能说明访问麦克风的目的。Apple 媒体采集授权说明
检查时要以最终生成的程序为准。对于 .app,应确认成品的 Contents/Info.plist 中存在该键;只检查工程目录里的 plist 还不够。
如果目标是 Command Line Tool,需要将用途说明嵌入可执行文件。Xcode 提供了以下设置:
INFOPLIST_FILE = path/to/Info.plist
CREATE_INFOPLIST_SECTION_IN_BINARY = YES
后一个设置会将处理后的 plist 嵌入 Mach-O 的 __TEXT,__info_plist section。把 plist 文件放在可执行文件旁边,不能完成嵌入。Apple 构建设置说明
按目标配置启用 Audio Input
是否需要 entitlement,取决于程序启用了哪些安全机制。“本地运行”或“离线使用”都不足以作为判断依据。
| 目标配置 | 应检查的配置 |
|---|---|
| 启用 Hardened Runtime | Hardened Runtime > Resource Access > Audio Input,对应 com.apple.security.device.audio-input |
| 启用 App Sandbox | App Sandbox > Hardware > Audio Input,对应 com.apple.security.device.microphone |
这两个 entitlement 分别用于不同配置。如果目标同时启用了两种机制,就要分别检查。配置 entitlement 后,仍然需要 NSMicrophoneUsageDescription 和用户授权。Audio Input entitlement、App Sandbox 麦克风 entitlement
如果旧构建可以运行,新构建却只有静音,应比较两者的 SDK、签名、entitlements 和最终 plist。仅凭新旧构建表现不同,无法认定问题来自 CPU 架构或 Rosetta。
显式检查授权,再启动 Audio Unit
不要依靠 AudioOutputUnitStart() 顺带触发授权提示。可以用 AVCaptureDevice 查询并申请麦克风权限,继续保留原有的 AUHAL 采集代码,无需创建 AVCaptureSession。
下面的辅助函数适用于 macOS 10.14 及以上,需从主线程调用,结果也会在主线程返回:
#import <AVFoundation/AVFoundation.h>
#import <dispatch/dispatch.h>
static void EnsureMicrophoneAccess(void (^completion)(BOOL granted))
{
AVAuthorizationStatus status =
[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
switch (status) {
case AVAuthorizationStatusAuthorized:
completion(YES);
return;
case AVAuthorizationStatusNotDetermined:
[AVCaptureDevice
requestAccessForMediaType:AVMediaTypeAudio
completionHandler:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(granted);
});
}];
return;
case AVAuthorizationStatusDenied:
case AVAuthorizationStatusRestricted:
completion(NO);
return;
}
completion(NO);
}
首次创建、初始化和启动时,可以这样调用:
EnsureMicrophoneAccess(^(BOOL granted) {
if (!granted) {
// Show an explanation in the application's UI.
return;
}
[self createToneUnit];
CheckError(AudioUnitInitialize(toneUnit),
"Error initializing unit:");
CheckError(AudioOutputUnitStart(toneUnit),
"Error starting unit:");
});
工程需要链接 AVFoundation.framework。如果还支持 macOS 10.14 之前的系统,应将权限 API 放在 @available(macOS 10.14, *) 分支内。
授权申请是异步操作,必须等结果返回后再启动采集。系统会保存用户的授权决定。用户已经拒绝后,再次请求通常不会反复弹窗,应引导用户到系统设置的麦克风隐私页面修改权限。Apple 媒体采集授权说明
检查现有 AUHAL 的设备和回调用法
修正权限配置后,可以保留原有 DSP 结构,再检查设备选择、回调和流格式。
明确选择默认输入设备
给出的代码启用了输入,但没有显式设置 kAudioOutputUnitProperty_CurrentDevice。如果要从默认输入设备采集,应在启用所需 I/O 后、初始化前,取得默认输入设备并绑定到 AUHAL:
#import <CoreAudio/CoreAudio.h>
AudioObjectPropertyAddress address = {
kAudioHardwarePropertyDefaultInputDevice,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain
};
AudioDeviceID device = kAudioObjectUnknown;
UInt32 size = sizeof(device);
CheckError(AudioObjectGetPropertyData(kAudioObjectSystemObject,
&address,
0,
NULL,
&size,
&device),
"Couldn't get default input device");
if (device == kAudioObjectUnknown) {
// No default input device is available. Abort setup.
return;
}
CheckError(AudioUnitSetProperty(toneUnit,
kAudioOutputUnitProperty_CurrentDevice,
kAudioUnitScope_Global,
0,
&device,
sizeof(device)),
"Couldn't set input device");
一个 AUHAL 实例对应一个设备。如果程序还需要播放,而默认输入和默认输出属于不同设备,就需要处理双设备的情况,不能假定一个实例会自动连接两者。Apple TN2091
分清播放回调和输入数据
原代码设置的是:
kAudioUnitProperty_SetRenderCallback
kAudioUnitScope_Input
0
这组参数注册的是播放方向的回调。系统调用它,获取将要送往输出设备的数据,因此不能直接把回调参数中的 ioData 当作麦克风输入。
如果程序保留输入、输出双工结构,可以继续使用这个回调,但必须在回调中从 bus 1 拉取输入,例如:
OSStatus status = AudioUnitRender(toneUnit,
ioActionFlags,
inTimeStamp,
1,
inNumberFrames,
inputBufferList);
if (status != noErr) {
return status;
}
// Process inputBufferList with the DSP code.
// Fill the playback ioData buffers as required.
inputBufferList 必须与输入客户端格式匹配,并有足够的容量。缓冲区应提前准备好,避免在音频实时线程中分配内存。
如果程序只做采集,可以关闭输出,并使用 kAudioOutputUnitProperty_SetInputCallback 注册输入通知回调。该回调只通知“输入已就绪”,仍需调用 AudioUnitRender() 取得样本。只替换属性名后继续把 ioData 当成输入,仍然不正确。Apple 输入回调说明
从输入设备读取采样率
原代码从 output scope、bus 0 读取 R,随后却用另一个变量 sampleRate 配置流格式,无法确定两者是否一致。
对于采集路径,应在选定设备后,从 input scope、bus 1 读取设备格式,再据此设置 output scope、bus 1 的客户端格式:
AudioStreamBasicDescription deviceFormat = {0};
UInt32 size = sizeof(deviceFormat);
CheckError(AudioUnitGetProperty(toneUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
1,
&deviceFormat,
&size),
"Couldn't get input device format");
streamFormat.mSampleRate = deviceFormat.mSampleRate;
客户端格式结构体也应先清零,再填写其余字段。单声道不一定对应所需的物理输入通道,多通道设备还可能需要明确的 channel map。AUHAL 的设备格式、客户端格式及采样率要求可参见 TN2091 格式说明。
原代码中还有一处错误:
NSAssert(RenderRMTone, @"Couldn't find unit of subType HAL output");
这里检查的是函数地址,应改为检查组件查找结果:
NSAssert(rmOutput != NULL, @"Couldn't find HAL output unit");
正式的错误处理也不能只依赖断言,因为断言可能在 Release 构建中被禁用。
停止后如何重新启动
如果只是暂停采集,设备、格式和缓冲区都没有改变,可以让 unit 保持已初始化状态:
CheckError(AudioOutputUnitStop(toneUnit),
"Error stopping unit:");
// Later, after confirming microphone authorization:
CheckError(AudioOutputUnitStart(toneUnit),
"Error restarting unit:");
每次停止时都无需取消初始化或销毁实例,也无需重复申请已经授予的权限。
如果调用过 AudioUnitUninitialize(),重新启动前要再次初始化;如果实例已经释放,则需要重新创建、配置和初始化。释放后应将 toneUnit 置为 NULL,并在控制线程上串行处理这些操作,避免 GUI 操作与音频回调访问已失效的状态。
如果授权状态已经是 AVAuthorizationStatusAuthorized,重新启动后仍然静音,应按顺序检查:
- 当前设备是否仍然有效,是否为预期的输入设备。
RenderRMTone是否继续执行,其中AudioUnitRender()的实际返回值是什么。- DSP 处理前的原始输入是否已经为零,缓冲区字节数是否正确重设。
- 系统输入音量、静音状态和物理通道选择是否正确。
设备拔插、默认输入设备切换或采样率变化后,需要重新核对设备与格式。启动时读取一次默认设备,不会让程序持续跟随默认设备的变化。
先修正最终产物中的 NSMicrophoneUsageDescription、适用的 entitlement 和授权时序,再检查设备选择、bus 1 输入拉取及缓冲区处理。静音可能出现在不同环节,需要逐项定位。
备注:内容仅供参考。
