随音舞动:Visualizer实现音频律动效果
🎬 前言
在许多音乐类 App 中,播放音频时的**律动柱状图(Spectrum)**或波形可视化是一个令人愉悦的视觉效果。
本文将介绍如何在 Android 中实现音频播放时的“跳动效果”,从最基础的 MediaPlayer + Visualizer 到不需要录音权限的 ExoPlayer + FFTAudioProcessor。
通过本文你将学到:
- 如何正确使用
Visualizer实现波形/频谱捕获 - 为什么需要录音权限
- 如何使用 ExoPlayer 自定义 FFT 处理
- 如何绘制一个平滑的柱状律动视图
⚙️ Visualizer 与权限问题
🎤 录音权限
Visualizer 是 Android 音频系统的一个特效类,用于实时获取播放流的波形或频谱数据。
如果未申请录音权限,Visualizer 无法初始化,会直接抛出异常:
java.lang.RuntimeException: Cannot initialize Visualizer engine, error: -3at android.media.audiofx.Visualizer.<init>(Visualizer.java:238)
解决方法:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
🧩 初始化 Visualizer
推荐在 MediaPlayer.setOnPreparedListener 中初始化 Visualizer:
if (visualizer == null) {val audioSessionId = mediaPlayer.audioSessionIdvisualizer = Visualizer(audioSessionId).apply {captureSize = Visualizer.getCaptureSizeRange()[1]setDataCaptureListener(object : Visualizer.OnDataCaptureListener {override fun onWaveFormDataCapture(visualizer: Visualizer?, waveform: ByteArray?, samplingRate: Int) {// 可用于波形绘制}override fun onFftDataCapture(visualizer: Visualizer?, fft: ByteArray?, samplingRate: Int) {// FFT 频谱数据,可驱动可视化视图}}, Visualizer.getMaxCaptureRate() / 2, false, true)enabled = true}
}
🌈 绘制柱状律动 SpectrumView
以下是一个高性能、平滑的自定义频谱柱状图实现:
class SpectrumView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {var barWidth = 20f // 每根柱子宽度var barSpace = 8f // 柱子间距private var barCount = 0private lateinit var magnitudes: FloatArray // 当前柱子高度private lateinit var targetMagnitudes: FloatArray // FFT 目标高度private val riseFactor = 0.4f // 上升速度private val decayFactor = 0.05f // 下降速度private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color.GREENstyle = Paint.Style.FILL}// 计算柱子数量override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)barCount = ((w + barSpace) / (barWidth + barSpace)).toInt().coerceAtLeast(1)magnitudes = FloatArray(barCount)targetMagnitudes = FloatArray(barCount)}/** 更新 FFT 数据 */fun updateFFT(fft: ByteArray) {if (barCount == 0) returnval n = fft.size / 2val binPerBar = max(1, n / barCount)for (i in 0 until barCount) {var sum = 0ffor (j in 0 until binPerBar) {val index = (i * binPerBar + j) * 2if (index + 1 < fft.size) {val re = fft[index].toInt().toFloat()val im = fft[index + 1].toInt().toFloat()sum += sqrt(re * re + im * im)}}val avg = sum / binPerBartargetMagnitudes[i] = (avg / 128f).coerceAtMost(1f) // 归一化}invalidate()}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)if (barCount == 0) returnval heightF = height.toFloat()for (i in 0 until barCount) {val target = targetMagnitudes[i]// 上升快,下降慢magnitudes[i] = if (target > magnitudes[i]) {magnitudes[i] + (target - magnitudes[i]) * riseFactor} else {magnitudes[i] * (1 - decayFactor)}val left = i * (barWidth + barSpace)val top = heightF - magnitudes[i] * heightFval right = left + barWidthval bottom = heightFcanvas.drawRoundRect(RectF(left, top, right, bottom), 8f, 8f, paint)}postInvalidateOnAnimation()}
}
✨ 特点:
- 动态计算柱子数量,适应不同屏幕宽度
- 上升快、下降慢,模拟物理惯性
- 使用 postInvalidateOnAnimation() 提升动画流畅度
🧠 Visualizer 原理解析
- 工作机制
Visualizer 通过底层的 AudioFlinger(Android 音频混音服务)直接读取播放流的音频缓冲区。
它并不是“录音”,而是监听当前音频输出设备的数据,因此录音权限只是访问接口所需的安全授权。
整个过程如下:
MediaPlayer / AudioTrack↓
AudioFlinger(系统音频混音器)↓
Visualizer(数据捕获层)↓
应用层回调 OnDataCaptureListener
Visualizer 内部通过音频会话 ID(audioSessionId)将自身绑定到音频输出流,实现数据监听。
- 数据捕获类型
