CAVA 音频可视化器 Android 移植版构建指南:FFTW3 依赖获取与 NDK/CMake 集成
CAVA 音频可视化器 Android 移植版构建指南:FFTW3 依赖获取与 NDK/CMake 集成
cava 是一款跨平台音频可视化工具,本仓库中的 cavandroid 子项目将其核心算法(cavacore)移植到了 Android 平台,通过 Kotlin + OpenGL ES 3.0 渲染频谱柱状图。本文以 cavandroid/README.md 为骨架,完整讲解在 Android 上构建 cava 的依赖准备(FFTW3 编译)、目录布局约定、Gradle/CMake 参数配置以及原生库桥接原理,读完你即可独立完成从 FFTW3 交叉编译到 APK 产出的全过程。
一、Android 构建的核心前提:FFTW3 库
cava 的频谱分析底层依赖 FFTW3(Fastest Fourier Transform in the West),cavacore 的头文件直接引用了 fftw3.h(见 cavacore.h)。在 Android 上构建 cava,第一步不是写代码,而是先拿到一份能在 Android 上运行的 FFTW3 预编译库。
官方 README 给出的做法是使用社区维护的 fftw3-android 仓库,它提供了一套现成的 NDK 交叉编译脚本:
git clone https://github.com/Lauszus/fftw3-android
cd fftw3-android
./build.sh
执行 ./build.sh 之前有一个硬性前置条件:确保 ndk-build 已加入 PATH 环境变量。fftw3-android 的构建脚本基于 Android NDK 的 ndk-build 机制,如果找不到该命令,脚本会直接失败。建议先验证:
ndk-build --version
确认 NDK 环境就绪后,build.sh 会为 NDK 支持的各 ABI 架构(如 arm64-v8a、armeabi-v7a、x86 等)产出对应的 libfftw3.so 以及头文件。
二、默认目录布局约定:cava 与 fftw3-android 平级
README 明确约定了默认的查找位置:构建系统会优先在 cava 仓库目录的同级目录下寻找名为 fftw3-android 的文件夹。目录结构如下:
./
../
cava/ # 本仓库(cava 源码根目录)
fftw3-android/ # FFTW3 Android 库目录(与 cava 平级)
这个约定直接体现在 cavandroid/app/build.gradle.kts 的 CMake 参数中:
externalNativeBuild {
cmake {
// Passes optional arguments to CMake.
arguments("-DFFTW_DIR=${projectDir}/../../../fftw3-android")
}
}
其中 projectDir 指 cavandroid/app,向上回溯三级(../../../)即到达仓库根目录的上一级,与 README 描述的 cava/、fftw3-android/ 平级布局完全对应。如果你把 fftw3-android 放在其他位置,只需编辑这一行 CMake 参数,把 ${projectDir}/../../../fftw3-android 改成实际路径即可。
三、根 CMakeLists.txt 的 ANDROID 分支:构建时校验逻辑
构建进入 Native 阶段后,根目录的 CMakeLists.txt 会依据 ANDROID 宏走专用分支,其逻辑对理解目录结构至关重要:
if (ANDROID)
add_library(cavacore SHARED cavacore.c)
if (NOT DEFINED FFTW_DIR)
message(FATAL_ERROR "FFTW_DIR not set, required by android, see cavandroid/README.md")
endif()
if (NOT EXISTS ${FFTW_DIR})
message(FATAL_ERROR "given FFTW_DIR: ${FFTW_DIR} does not exist")
endif()
if (NOT EXISTS "${FFTW_DIR}/jni/fftw3/api/")
message(FATAL_ERROR "given fftw include dir: ${FFTW_DIR}/jni/fftw3/api/ does not exist")
endif()
if (NOT EXISTS "${FFTW_DIR}/obj/local/${CMAKE_ANDROID_ARCH_ABI}/")
message(FATAL_ERROR "given lib dir: ${FFTW_DIR}/obj/local/${CMAKE_ANDROID_ARCH_ABI}/ does not exist, did you build it? See cavandroid/README.md")
endif()
target_include_directories(cavacore PRIVATE "${FFTW_DIR}/jni/fftw3/api/")
target_link_directories(cavacore PRIVATE "${FFTW_DIR}/obj/local/${CMAKE_ANDROID_ARCH_ABI}/")
target_link_libraries(cavacore fftw3)
从中可以提炼出四个构建时必须满足的硬性检查点:
| 检查项 | 失败提示含义 |
|---|---|
FFTW_DIR 已定义 |
未通过 Gradle 传入 -DFFTW_DIR,需要检查 build.gradle.kts 的 cmake arguments |
${FFTW_DIR} 目录存在 |
fftw3-android 目录缺失或路径写错 |
${FFTW_DIR}/jni/fftw3/api/ 存在 |
缺少 FFTW3 头文件(该目录下应有 fftw3.h、fftw3.f 等 API 头文件) |
${FFTW_DIR}/obj/local/${CMAKE_ANDROID_ARCH_ABI}/ 存在 |
对应 ABI 的 libfftw3.so 未编译,即 ./build.sh 尚未执行或失败 |
最后一个检查点说明 fftw3-android 的产物结构必须符合 NDK 输出规范:头文件在 jni/fftw3/api/,共享库在 obj/local/<abi>/。如果你跳过第 1 步直接执行 Gradle 构建,大概率会在这里报 "did you build it?" 的错误,这也是 README 反复强调先跑 build.sh 的原因。
此外,Android 分支会调用 add_library(cavacore SHARED cavacore.c) 将核心算法编译为共享库,其输入源码与桌面版共用同一份 cavacore.c,保证算法行为一致。
四、Gradle 侧的 Native 构建接线
app/build.gradle.kts 是 Android 工程的构建入口,它把上面的 CMake 逻辑完整串联起来:
android {
namespace = "com.karlstav.cava"
compileSdk = 33
defaultConfig {
applicationId = "com.karlstav.cava"
minSdk = 30
targetSdk = 33
versionCode = 14
versionName = "@string/app_ver"
externalNativeBuild {
cmake {
// Passes optional arguments to CMake.
arguments("-DFFTW_DIR=${projectDir}/../../../fftw3-android")
}
}
}
// Encapsulates your external native build configurations.
externalNativeBuild {
cmake {
// Provides a relative path to your CMake build script.
path = file("../../CMakeLists.txt")
}
}
}
关键点:
path = file("../../CMakeLists.txt"):从cavandroid/app向上两级,指向仓库根目录的 CMakeLists.txt,即第三节的 ANDROID 分支所在文件;arguments("-DFFTW_DIR=..."):把 FFTW3 目录注入 CMake,对应第三节的第一个检查点;minSdk = 30(Android 11)、targetSdk = 33、compileSdk = 33,versionCode = 14;- 代码中有一处被注释的 ABI 过滤示例(
ndk { abiFilters += listOf("arm64-v8a") }),默认不限制 ABI,构建时会为当前 NDK 支持的全部 ABI 编译cavacore,因此每个 ABI 都需要对应的libfftw3.so; - 工程通过仓库内的
gradlew/gradlew.bat与 gradle-wrapper.properties 固定 Gradle 版本,直接执行./gradlew assembleDebug即可触发上述 CMake 流程; - debug 与 release 两个签名配置均使用仓库自带的 keystore.jks(密钥库密码/别名见 app/build.gradle.kts)。
五、从 FFTW3 到屏幕像素:cavacore 原生库的调用链
构建产物 libcavacore.so 被加载后,Android 端通过 Kotlin 的 external 声明直接调用 C 层 API(见 MainActivity.kt):
external fun InitCava(bars: Int, sampleRate: Int, lowerCutoff: Int, higherCutoff: Int): FloatArray
external fun ExecCava(audioIn: DoubleArray, numSamples: Int): DoubleArray
external fun DestroyCava()
这三个原生函数对应 cavacore.h 中公开的 cava_init / cava_execute / cava_destroy 生命周期:
InitCava(bars, sampleRate, lowerCutoff, higherCutoff):对应cava_init,传入柱状图数量、采样率、低/高截止频率,返回各柱对应的截止频率数组(用于 X 轴刻度显示);ExecCava(audioIn, numSamples):对应cava_execute,输入 PCM 采样数据,输出归一化的柱状图高度数据;DestroyCava():对应cava_destroy,释放 plan 内存。
加载动作发生在渲染器的 init 块中:System.loadLibrary("cavacore")(见 MainActivity.kt)。
整条数据链路是:
- AndroidManifest.xml 声明
RECORD_AUDIO权限,运行时向用户申请; MainActivity.startRecording()用AudioRecord以 44100 Hz、16 bit、单声道从麦克风采集 PCM(源码中还保留了UNPROCESSED原始音频源作为备选,见 MainActivity.kt);- 后台线程把 PCM 数据按 16 bit 小端方式转换成归一化 double 数组写入环形缓冲(
pcmValue / 32767.0); - 每帧
ExecCava将缓冲数据交给 cavacore 做 FFT 与平滑滤波,输出柱高; GLSurfaceView(EGL Context 版本 3,见 MainActivity.kt)将柱高作为barsuniform 传入着色器渲染。
着色器来自 app/src/main/res/raw 下的 pass_through.vert(顶点)与 bar_spectrum.frag(片段),与桌面版 output/shaders 一脉相承。由于应用要求 OpenGL ES 3.0,Manifest 中声明了 glEsVersion 0x00030000,设备不支持 ES 3.0 时无法安装运行。
六、运行期可调参数与 FAQ
构建安装后,应用内通过 preferences.xml 暴露了与桌面版对齐的实时调参项,配置变更会通过 configReload / changeColor 标志触发 DestroyCava + InitCava 重建或重新上传颜色梯度:
- Number Of Bars:10~256,默认 40;
- Color Preset:预置 7 套配色(Neon Sunrise、Wild Fire、Beige Delight、Amaranth Spirit、Solarized Sunset、Dark Forest、Custom),数据格式为
#背景#前景1#前景2三段十六进制颜色拼接,定义见 array.xml; - Bandwidth:Lower Cutoff(默认 50 Hz,范围 20~10000,步进 10)与 Upper Cutoff(默认 10000 Hz,范围 100~20000),并内置了上下限自动钳制逻辑(上限至少为下限的 2 倍);
- Show Frequency Labels:通过着色器内实现的七段数码管绘制 X 轴频率刻度;
- Custom Color:前景柱顶、柱底与背景色的 RGB 三通道独立调节。
常见构建问题对照:
| 现象 | 原因与对策 |
|---|---|
ndk-build 找不到 |
未安装 NDK 或未加入 PATH,先执行 ndk-build --version 验证 |
FFTW_DIR not set |
build.gradle.kts 的 arguments("-DFFTW_DIR=...") 被改动或拼写错误 |
obj/local/<abi>/ does not exist |
未运行 fftw3-android 的 ./build.sh,或该 ABI 未被其构建脚本支持 |
链接 fftw3 失败 |
fftw3-android 目录结构与 NDK 产物约定不符(应含 jni/fftw3/api/ 与 obj/local/) |
| APK 无法安装/崩溃 | 设备不支持 OpenGL ES 3.0,或未授予 RECORD_AUDIO 权限 |
七、小结
在 Android 上构建 cava 的路径非常清晰:先用 fftw3-android 仓库的 build.sh(前置 ndk-build 在 PATH 中)交叉编译出各 ABI 的 libfftw3.so,把 fftw3-android 目录放到与 cava 仓库平级的位置,然后直接跑 Gradle。根 CMakeLists.txt 的 ANDROID 分支会替你完成四重路径校验,并把 cavacore.c 编译为 libcavacore.so;运行期再由 MainActivity.kt 通过 JNI 调用 InitCava/ExecCava/DestroyCava 完成从麦克风 PCM 到 OpenGL ES 3.0 频谱柱的完整渲染链路。若想更换 FFTW3 的位置,只需修改 cavandroid/app/build.gradle.kts 中的 -DFFTW_DIR 参数。