CAVA 音频可视化器 Android 移植版构建指南:FFTW3 依赖获取与 NDK/CMake 集成

原创2026-10-01 10:46:56343 阅读
文章标签:音视频桌面应用

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)。

整条数据链路是:

  1. AndroidManifest.xml 声明 RECORD_AUDIO 权限,运行时向用户申请;
  2. MainActivity.startRecording() 用 AudioRecord 以 44100 Hz、16 bit、单声道从麦克风采集 PCM(源码中还保留了 UNPROCESSED 原始音频源作为备选,见 MainActivity.kt);
  3. 后台线程把 PCM 数据按 16 bit 小端方式转换成归一化 double 数组写入环形缓冲(pcmValue / 32767.0);
  4. 每帧 ExecCava 将缓冲数据交给 cavacore 做 FFT 与平滑滤波,输出柱高;
  5. GLSurfaceView(EGL Context 版本 3,见 MainActivity.kt)将柱高作为 bars uniform 传入着色器渲染。

着色器来自 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 参数。

登录后查看全文
cava