音频应用对延迟极其敏感,尤其实时监听、乐器效果器、语音通话场景。iOS默认音频会话为了兼容性和省电,通常会配置较大的IO缓冲区和较保守的采样率,这会让输入到输出的往返时间明显增加。对需要低延迟的应用,必须主动干预Core Audio的配置,并量化实际往返延迟。本文以Remote IO Audio Unit和AVAudioSession为核心,介绍测量与配置方法。

往返延迟的组成与Core Audio延迟属性
往返延迟通常指声音从输入端进入设备,经过ADC转换、缓冲处理、应用逻辑,再经过DAC转换从输出端播放出来的总时间。这个时间可以拆成几部分:输入硬件转换延迟、输出硬件转换延迟、安全缓冲延迟、驱动缓冲延迟以及应用程序持有的音频单元缓冲延迟。Core Audio把可查询的部分暴露在Audio Unit的kAudioUnitProperty_Latency属性中,而AVAudioSession则提供了更容易访问的inputLatency、outputLatency和ioBufferDuration接口。
估算往返延迟时可以采用下面的公式:
estimatedRTL = inputLatency + outputLatency + ioBufferDuration * 2
其中ioBufferDuration需要乘以2,是因为输入缓冲和输出缓冲都会占用一个缓冲区周期。实际物理往返路径还可能包含额外延迟,例如蓝牙设备、USB音频接口或者系统安全机制,因此这个公式更适合作为性能标定参考,而不能完全替代真实测量。
import AVFoundation
let session = AVAudioSession.sharedInstance()
let inputLatency = session.inputLatency
let outputLatency = session.outputLatency
let ioBuffer = session.ioBufferDuration
let estimatedRTL = inputLatency + outputLatency + ioBuffer * 2
print("Input latency: \(inputLatency * 1000) ms")
print("Output latency: \(outputLatency * 1000) ms")
print("IO buffer: \(ioBuffer * 1000) ms")
print("Estimated RTL: \(estimatedRTL * 1000) ms")
如果应用直接持有Audio Unit,也可以通过AudioUnitGetProperty读取延迟值。下面的代码展示了从Remote IO单元读取全局延迟的方式:
Float64 latency = 0;
UInt32 size = sizeof(latency);
AudioUnitGetProperty(audioUnit,
kAudioUnitProperty_Latency,
kAudioUnitScope_Global,
0,
&latency,
&size);
NSLog(@"Audio unit latency: %f ms", latency * 1000.0);
这个值通常只代表音频单元自身引入的缓冲延迟,小于会话层给出的整体延迟。做延迟优化时应当综合两个层面的数据,而不是只看单一接口。
通过AVAudioSession调整缓冲区大小并激活低延迟配置
要获得更低的往返延迟,核心手段是缩小ioBufferDuration。iOS允许通过setPreferredIOBufferDuration设置首选缓冲区时长,但最终生效值由设备硬件、当前采样率和会话类别共同决定。为了尽量接近目标,建议先将音频会话类别设置为playAndRecord,并保持采样率为48000Hz,因为多数iOS设备的音频硬件在48000Hz下支持更小的缓冲区。
低延迟配置应当在激活会话之前完成,否则系统可能延用旧的参数。下面的Swift示例展示了完整的配置流程:
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord, options: [.defaultToSpeaker])
try session.setPreferredSampleRate(48000)
try session.setPreferredIOBufferDuration(0.005)
try session.setActive(true)
print("Sample rate: \(session.sampleRate)")
print("IO buffer duration: \(session.ioBufferDuration)")
} catch {
print("Failed to configure session: \(error)")
}
这里没有加入蓝牙相关选项,原因是蓝牙A2DP和HFP设备通常强制使用较大缓冲区,会破坏低延迟目标。如果应用必须支持蓝牙,应当根据当前路由动态调整配置,并在测量前排除蓝牙设备。
设置完成后必须读取实际的ioBufferDuration进行验证。如果设备不支持5毫秒,系统可能返回11.6毫秒甚至23.2毫秒。还需要注意,已经初始化的Audio Unit会持有自己的最大帧数设置,此时仅调用setPreferredIOBufferDuration可能不会改变渲染回调的帧数。可以在Audio Unit上同步设置kAudioUnitProperty_MaximumFramesPerSlice,将其调整为与目标缓冲时长对应的帧数,例如48000Hz下5毫秒对应240帧。
用Remote IO回调时间戳实现实际往返延迟测量
属性计算只能给出估算值,真实物理往返延迟还需要实际测量。Remote IO Audio Unit的输入和输出渲染回调都会提供AudioTimeStamp,其中包含mSampleTime和mHostTime。在输入回调中记录样本时间戳,在输出回调中记录输出样本时间戳,两者的差值除以采样率就能得到近似的往返延迟。
下面的Objective-C代码展示了回调中记录时间戳并计算延迟的简单实现:
static OSStatus inputCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
MeasurementContext *ctx = (MeasurementContext *)inRefCon;
ctx->inputSampleTime = inTimeStamp->mSampleTime;
ctx->inputHostTime = inTimeStamp->mHostTime;
return noErr;
}
static OSStatus renderCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
MeasurementContext *ctx = (MeasurementContext *)inRefCon;
Float64 deltaSamples = inTimeStamp->mSampleTime - ctx->inputSampleTime;
Float64 sampleRate = ctx->sampleRate;
ctx->lastMeasuredRTL = deltaSamples / sampleRate;
return noErr;
}
这种基于回调时间戳的方法要求声音确实从输出端回到输入端。如果使用内置扬声器和麦克风,测量结果会包含房间声学传播时间,不完全是硬件往返延迟。更精确的方案是使用带物理回环线缆的音频接口,或使用音频设备上的直接监听回路,这样就可以排除空气传播的干扰。
对于无法做物理回环的环境,可以采用脉冲信号配合互相关检测的方法。应用先播放一小段线性调频信号或脉冲,同时在输入端持续采集,然后使用Accelerate框架中的vDSP相关函数在采集缓冲区中寻找测试信号的位置。找到的相关峰值对应的样本偏移除以采样率,就是端到端延迟。这种方法不依赖回调时间戳的不同时钟源,适合自动化测试和不同设备间的横向比较。
完成延迟测量后,建议将结果持久化或上报,以便对不同iOS设备、不同系统版本进行回归监控。低延迟配置不是一次性的,系统权限变化、蓝牙连接、来电中断和采样率切换都可能改变当前缓冲区大小,应用应该监听AVAudioSession.routeChangeNotification和媒体服务重置通知,在路由变化后重新检查和调整配置。
Core Audio音频延迟往返延迟修改时间:2026-08-29 03:36:13