26、K歌与实时耳返场景:耳返通路设计、混音与监听、低延时耳返实现(<20ms)
K歌和实时耳返,这俩场景其实是一回事的两面。你想想看,用户唱歌时如果听不到自己的声音,或者听到的声音比嘴巴慢了半拍,那基本就唱不下去了。我最早做K歌App时,产品经理丢给我一句话:「延迟超过30ms,用户就骂娘。」嗯,后来我们硬是压到了15ms以内,用户才说「还行」。
今天我们就聊聊,怎么把耳返延迟做到20ms以下。说白了,就是跟时间赛跑。
耳返通路设计:信号怎么走?
先搞清楚一条基本链路。麦克风采集到声音,经过处理,再送到耳机。这个路径上每一个环节都在消耗时间。
核心通路:
Mic → ADC → 音频回调 → 混音处理 → 播放回调 → DAC → 耳机
我在项目中遇到过一种情况:明明代码写得没问题,延迟就是降不下来。后来发现是音频回调的buffer size设得太大了。你想想看,一个buffer 1024帧,采样率44100,光这一个buffer就占了23ms。再加上其他环节,妥妥超30ms。
所以耳返通路设计的第一原则:路径要短,buffer要小。
混音与监听:怎么把声音「叠」在一起?
K歌场景下,用户听到的不能只是自己的干声。得有伴奏、有效果、还得有自己加了混响后的声音。这就是混音要做的事。
我建议把混音拆成两步:
- 本地混音:把麦克风采集的干声和伴奏混在一起,直接送耳机。这一步要快,不能等网络。
- 效果混音:把干声加上混响、回声等效果,再混入监听通路。这一步可以稍微慢一点,但也不能超过20ms。
我的经验:本地混音用浮点运算,效果混音用定点运算。浮点精度高,但定点速度快。两者结合,既保证音质又控制延迟。
混音时有个坑:音量平衡。伴奏太响盖过人声,用户会觉得自己唱得不好。人声太响盖过伴奏,又像清唱。我一般建议人声比伴奏高3-6dB,具体看用户喜好。
低延时耳返实现:20ms怎么啃下来?
好,到了最硬核的部分。20ms以内,怎么做到?
我直接给结论:用AAudio,别用AudioTrack。AAudio是Android 8.0引入的,专门为低延迟场景设计的。它绕过了AudioFlinger的混音线程,直接跟HAL层通信。
代码示例:
// 创建AAudio流
AAudioStreamBuilder *builder;
AAudio_createStreamBuilder(&builder);
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
AAudioStreamBuilder_setSampleRate(builder, 48000);
AAudioStreamBuilder_setFramesPerDataCallback(builder, 192); // 4ms @48kHz
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStream *stream;
AAudioStreamBuilder_openStream(builder, &stream);
AAudioStream_requestStart(stream);
这里有个关键参数:framesPerDataCallback。我设了192帧,采样率48000,算下来就是4ms一个回调。为什么是192?因为再小的话CPU扛不住,再大的话延迟就上去了。192是我试出来的平衡点。
注意:AAudio的独占模式(EXCLUSIVE)不是所有设备都支持。如果设备不支持,会回退到共享模式,延迟会高一些。建议在代码里做fallback处理。
另外,麦克风采集也要用AAudio。采集和播放用同一个回调线程,这样能省掉一次线程切换的开销。
// 采集+播放双工模式
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_DUPLEX);
AAudioStreamBuilder_setDataCallback(builder, myCallback, nullptr);
// 回调函数里直接做混音
aaudio_data_callback_result_t myCallback(
AAudioStream *stream,
void *userData,
void *audioData,
int32_t numFrames) {
float *output = (float *)audioData;
// 从麦克风读取数据
// 混入伴奏
// 加上效果
// 直接写入output
return AAUDIO_CALLBACK_RESULT_CONTINUE;
}
我曾经在一个项目里,用这套方案把延迟压到了12ms。用户反馈说「跟有线耳机差不多」。嗯,那是我最开心的一次上线。
避坑指南
做低延时耳返,坑不少。我列几个常见的:
- Buffer太小导致Xrun:回调频率太高,CPU来不及处理,就会出现断音。解决办法是适当增大buffer,或者优化处理逻辑。
- 采样率不匹配:采集和播放的采样率不一致,会导致音调变化。强制统一到48000Hz。
- 蓝牙耳机延迟高:蓝牙耳机的延迟天然高,40-100ms很正常。如果用户用蓝牙耳机,20ms基本不可能。建议提示用户换有线耳机。
- 系统音频策略干扰:有些手机会在通话模式下强制走电话音频通路,延迟暴增。需要检测并处理。
总结一下:
耳返延迟要压到20ms以下,核心就三点:
- 用AAudio,设小buffer(192帧左右)
- 采集和播放走同一个回调线程
- 混音逻辑要轻量,别做重处理
做到这三点,15ms不是梦。
好了,K歌耳返这块就聊到这儿。下一节我们聊聊更复杂的场景——多人实时合唱,那才是真正的挑战。
公众号:蓝海资料掘金营,微信deep3321