design.md 7.2 KB

Context

当前 ijkplayer 的音频管线流程为:FFmpeg 解码 → AudioRendererOnWriteData 回调 → PCM 填充 → 均衡器处理 → OH_AudioRenderer 输出。PCM 数据在原生层产生和消费,ArkTS 层完全无法访问。播放界面 (LocalMusic.ets) 使用 Swiper 容器承载封面和歌词两个页面,已有 Canvas 使用经验(ScanFilePage.ets 中用于 Lottie 动画渲染)。

关键约束:

  • PCM 数据仅存在于 C 层 AudioRendererOnWriteData 回调中,生命周期极短
  • 音频回调运行在独立线程,与 JS 线程不同
  • ArkTS 禁止使用 unknown/any 类型,禁止解构赋值
  • 需控制跨线程数据拷贝和 FFT 计算的性能开销

Goals / Non-Goals

Goals:

  • 在原生层零拷贝方式捕获 PCM 数据到环形缓冲区
  • 通过 NAPI 接口将 PCM 数据高效传递给 ArkTS 层
  • 在 ArkTS 侧实现轻量 FFT,生成频谱数据
  • 提供 4 种可切换的 Canvas 频谱动画特效
  • 所有动效参数(颜色、亮度、大小、速度)完全由频谱数据实时驱动
  • 集成到播放界面 Swiper 中,用户可自由滑动切换

Non-Goals:

  • 不实现录音或音频采集功能(仅读取播放中的 PCM)
  • 不在 C 层做 FFT(避免增加原生层复杂度,ArkTS 侧 FFT 性能已足够)
  • 不支持自定义特效插件系统(初版提供内置 4 种)
  • 不修改音频播放逻辑或影响播放质量

Decisions

决策 1:PCM 数据捕获方案 — 原生环形缓冲区

选择:在 SDL_Aout_Opaque 结构体中新增环形缓冲区,在 AudioRendererOnWriteData 均衡器处理之后直接 memcpy 到缓冲区。

备选方案

  • A) 使用 OpenHarmony AudioCapturer API 从系统层采集 — 需要额外权限,延迟高,且无法获取 app 内部播放的精确 PCM
  • B) 在 FFmpeg 解码层拦截 — 太早,未经均衡器处理,且与解码线程耦合
  • C) 通过 MessageCallback 推送数据 — libuv 回调开销大,不适合高频音频数据

理由:直接在 AudioRendererOnWriteData 末尾拷贝,代码侵入最小(仅加几行),数据是均衡器处理后的最终 PCM,与用户听到的一致。

环形缓冲区设计

// 新增到 SDL_Aout_Opaque
#define SPECTRUM_BUFFER_SIZE 4096  // 2048 samples * 2 channels * sizeof(int16_t)
typedef struct SpectrumBuffer {
    int16_t data[SPECTRUM_BUFFER_SIZE / sizeof(int16_t)];
    volatile int write_pos;
    volatile bool has_data;
    int sample_rate;
    int channels;
} SpectrumBuffer;

决策 2:NAPI 桥接方案 — 同步拉取模式

选择:新增 _getSpectrumData(id) NAPI 函数,ArkTS 侧按帧主动调用获取当前缓冲区快照,返回 number[] 数组。

备选方案

  • A) 推送模式(原生层定时通过 libuv 回调推送) — 每秒 30-60 次 uv_queue_work 调用会产生大量 GC 压力
  • B) SharedArrayBuffer 共享内存 — OpenHarmony NAPI 对 SharedArrayBuffer 支持有限且复杂

理由:拉取模式让 ArkTS 侧控制采样频率,与 Canvas 绑帧渲染天然同步,实现简单。每次调用仅拷贝一个窗口大小的数据(2048 个样本),开销可控。

决策 3:FFT 实现方案 — ArkTS 纯计算

选择:在 ArkTS 侧实现 Cooley-Tukey 基 2 FFT 算法(1024 点),输出 512 个频率 bin 的幅值。

备选方案

  • A) 在 C 层用 FFmpeg 的 libavutil/tx.h 做 FFT — 减少一次数据传递,但增加原生层复杂度,FFT 结果传到 JS 层也需要拷贝
  • B) 引入第三方 FFT 库 — 增加外部依赖,与项目"无新依赖"原则不符

理由:1024 点 FFT 计算量极小(约 5000 次乘加),现代处理器在 ArkTS 中也能在 1ms 内完成。放在 ArkTS 侧便于调试和迭代,避免频繁修改原生代码重新编译。

处理流程

PCM int16[] → 取单声道 → 加窗(Hann) → FFT → 取幅值 → 对数映射 → 分频段 → 平滑

频段划分(对数分布,映射到 32-64 个可视化柱):

  • 低频 (32-250Hz):约 8 柱,覆盖鼓点/贝斯
  • 中频 (250-4000Hz):约 16 柱,覆盖人声/乐器
  • 高频 (4000-16000Hz):约 8 柱,覆盖镲片/泛音

决策 4:频谱可视化架构 — 策略模式 + Canvas 统一渲染

选择:定义 SpectrumRenderer 接口,每种特效实现该接口的 render(ctx, spectrumData, canvasWidth, canvasHeight) 方法。主组件持有 Canvas 和定时器,按当前选择的渲染策略绘制。

理由:策略模式使新增特效只需实现一个类,不影响其他特效和主逻辑。Canvas 是 ArkUI 内置能力,无需额外依赖。

四种特效实现要点

特效 渲染方式 数据驱动参数
柱状频谱 fillRect 绘制等间距矩形条 高度=幅值,颜色=频段映射渐变
波形频谱 bezierCurveTo 绘制平滑曲线 Y轴=幅值,线宽=能量强度
圆形频谱 极坐标变换后 lineTo 绘制放射线 半径=幅值,颜色=角度映射
粒子频谱 粒子池 + arc 绘制圆点 速度/大小/透明度=幅值,颜色=频段

决策 5:UI 集成方案 — Swiper 第二页

选择:在 LocalMusic.ets 的 Swiper 中,在封面和歌词之间插入频谱页面,顺序变为:封面 → 频谱 → 歌词。频谱页面上方显示小型特效切换按钮。

备选方案

  • A) 作为封面页面的背景叠加显示 — 与旋转唱片视觉冲突,遮挡封面信息
  • B) 放在底部播放栏 — 空间太小,无法展现频谱细节

理由:独立 Swiper 页面提供最大渲染空间,不干扰现有封面和歌词体验。用户可以根据需要滑动查看。

决策 6:性能控制策略

  • 渲染帧率:Canvas 绑帧 30fps(使用 setInterval 33ms),不追求 60fps 以省电
  • FFT 窗口:1024 点,每次渲染前计算一次,复用上一帧数据避免重复计算
  • 平滑算法:指数移动平均 current = current * 0.7 + previous * 0.3,避免跳变
  • 生命周期:仅在频谱页面可见且播放中时才启动定时器和 NAPI 调用,页面切走或暂停时立即停止
  • 内存:粒子池预分配固定数量(200 个),复用而不是创建/销毁

Risks / Trade-offs

  • [PCM 数据延迟] → 环形缓冲区引入约 1 帧(~23ms@44100Hz)延迟,对视觉频谱影响可忽略
  • [线程安全] → 环形缓冲区使用 volatile 标记 + 单写单读模式,避免加锁开销。写端为音频线程,读端为 NAPI 调用线程,天然无竞争
  • [FFT 精度] → 1024 点 FFT 频率分辨率约 43Hz(@44100Hz),低频区分辨率较粗,通过对数频率映射缓解
  • [不同采样率] → 缓冲区记录 sample_rate,FFT 分析器动态适配。主流音频均为 44100/48000Hz
  • [性能下降] → 粒子特效较重,在低端设备上可能卡顿。兜底方案:检测帧率下降时自动降级到柱状频谱
  • [原生层修改风险] → 仅在 AudioRendererOnWriteData 末尾加 memcpy,不影响播放逻辑。环形缓冲区独立于播放状态,写满自动覆盖旧数据

Open Questions

  • 是否需要支持频谱特效偏好的持久化存储(记住用户上次选择的特效)?
  • 粒子特效的粒子数量是否需要根据设备性能动态调整?