## 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,与用户听到的一致。 **环形缓冲区设计**: ```c // 新增到 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 - 是否需要支持频谱特效偏好的持久化存储(记住用户上次选择的特效)? - 粒子特效的粒子数量是否需要根据设备性能动态调整?