文章

微信小程序音频开发复盘:为什么我最终放弃了后台多音轨混音?

AI 摘要

看了很多白噪音相关的小程序,好奇为什么没有人做混音的,想着要不自己做一个,比如主干道放着“雨声”,用户还能自己叠加上“雷声”和“虫鸣”,并且能分别调音量。

这在原生开发里是常规操作,但到了微信小程序里,这就是个彻头彻尾的深坑。折腾了几天,经过各种机型测试和架构推翻后,我得出的结论是:不要在微信小程序里尝试后台多音轨混音,直接降级做单音轨才是唯一解。

坑是怎么踩出来的?

小程序的音频 API 主要是两个:

  • BackgroundAudioManager (BAM):系统级,切后台能活,但只能播一个。
  • InnerAudioContext (IAC):页面级,能多开,用来做混音。

一开始的设想用 BAM 播主音(比如雨声)保住后台,用几个 IAC 播辅音。但在真机上跑起来,只要一退到后台或锁屏,问题全出来了。

1. iOS 的音频焦点(Audio Focus)直接“杀后台”

苹果对后台音频的管理是不讲道理的。当你退到后台,微信底层用 BAM 向系统要了 Playback 的权限,iOS 就会为了保证音质和省电,把不属于这个会话的其他音频流全干掉。这就导致,在 iPhone 上只要一切后台,IAC 播的雷声和虫鸣瞬间静音,只剩 BAM 的雨声。

2. JS 运行环境被冻结

就算你想办法绕,也绕不开小程序的机制。退后台或者熄屏一分钟后,微信会直接把你的 JavaScript 线程挂起。 IAC 是页面级的 API,它的播放、暂停、循环全都依赖 JS 驱动。JS 一停,IAC 彻底失去动力,直接死掉。只有底层的 BAM 还在独自运作。

3. Android 端的底层资源抢占

在安卓上,有些机型切后台不会立刻杀 IAC,但这反而引出了更恶劣的 Bug。 在测试华为和小米的几款机器时,如果强行让几个解码器同时跑,底层的 AudioFlinger 会发生严重的资源抢占。结果就是各种杂音、高频破音,甚至直接触发 onError 把整个小程序的音频模块搞崩溃。

挣扎:试过服务端混音,算不过账

既然前端混不了,我试过一条“邪路”:把计算扔给服务器。 前端把用户配好的参数(雨声 80%,雷声 30%)发给后端,后端用 FFmpeg 实时合成一条单音轨的流,再推给前端的 BAM 播。

跑通了,但也直接毙了。

  1. 延迟没法忍: 用户拖一下音量条,声音要等 1.5 秒以上才变化,体验极差。
  2. 服务器烧钱: 白噪音一听就是半小时起步,实时合成加推流,带宽高得离谱。我一个独立开发者,这成本根本扛不住。

认清现实:强制降级为单音轨

最后我选择向现实妥协,彻底放弃后台混音,实施架构降级:

核心策略:后台只单音轨,前台靠生命周期补救。

  1. 用户切到后台时,不管开了多少音轨,直接放弃 IAC,只让 BAM 接着播主音轨。
  2. 在 onAppHide 时,把副音轨当前的进度(currentTime)和时间戳存下来。
  3. 当用户切回前台触发 onAppShow 时,算一下中间隔了多少秒,用 IAC.seek() 把副音轨的进度硬拉平,重新开始播放。

为了防止重播那一下太突兀,我加了个两三百毫秒的线性音量淡入(Fade-in)做掩护。但在有些低端安卓机上,一调用 seek 就卡,这种时候就只能连时间对齐也放弃,直接从头重播。

总结

在小程序的规则下开发,有时候真的不能头铁。原生 App 能做的事,在这里就是行不通。后台单音轨保活虽然是个妥协的产物,但它不报错、不破音、不耗多余的电,这是目前在微信生态里能做到的最稳的方案了。