Android
音频可视化,指的是将音频的频率绘制到屏幕上,达到一种视觉效果,使播放或录制过程更加生动形象。
在Android
进行视频可视化涉及的三个主要知识点,其中比较难以理解的傅里叶变换公式。
Android
原生的Visualizer
使用(获取频率数据)- 傅里叶变换(音频从时域到频域变换理论)
- 自定义
View
(展示频率数据)
一、开发难点
-
Android
原生的Visualizer
限制- 需要录音权限(播放音乐需要录音权限?)
- 音量为0时,获取不到数据(有可能被误认为Bug)
- 仅支持AudioTrack,MediaPlayer
解决方案,自定
Visualizer
,可以参考末尾文章。 -
傅里叶变换的理解
如果从数学角度去推导和验证傅里叶变换,需要学习三角函数及其正交性、微积分、欧拉定理等等。感兴趣可看文章末尾B站视频。在这里,我们暂且知道傅里叶变换可以将函数分解成正余弦函数之和。在工程上应用,可以从时域变换到频域,从而可以观察一些特性。例如在音频上,在频率可以分析大多数男生为低频,女生为高频,可以进行变音处理和声纹模仿等应用。
通过
Visualizer
可以拿到傅里叶变换后的数据,或者通过第三方库区计算。 -
自定义
View
拿到频率数据,如何处理这些数据,并在View进行绘制。
二、Visualizer
比较庆幸的,Android
原生为我们提供了Visualizer
类,让我们可以快速得从音频获取原始的波形数据或快速傅里叶变换后数据。下面简单介绍其使用。
我们在创建AudioTrack
或者AudioRecord
实例后,可以获取对应的audioSessionId
,用于创建Visualizer
实例。
kotlin
val visualizer = Visualizer(audioTrack.audioSessionId)
通过setCaptureSize
函数设置采样率大小,其大小我们一般通过getCaptureSizeRange
函数来获取。getCaptureSizeRange
函数返回两个int类型数组,第一个表示最小值,第二个表示最大值,用来表示采样值的范围。
ini
visualizer.captureSize = Visualizer.getCaptureSizeRange()[1]
接着通过setDataCaptureListener获取采样数据回调。
arduino
setDataCaptureListener(OnDataCaptureListener listener,int rate, boolean waveform, boolean fft)
OnDataCaptureListener
采样数据回调类,拥有onWaveFormDataCapture
和onFftDataCapture
两个函数,前者回调波形数据,后者回调傅里叶变换后数据。rate
采样的频率,设置范围在0~Visualizer.getMaxCaptureRate()
。waveform
是否返回波形数据,false
的话,OnDataCaptureListener
的onWaveFormDataCapture
函数不会有回调。fft
是否返回傅里叶变换后数据,false
的话,OnDataCaptureListener
的onFftDataCapture
函数不会有回调。
kotlin
visualizer.setDataCaptureListener(object : OnDataCaptureListener {
override fun onWaveFormDataCapture(visualizer: Visualizer?, waveform: ByteArray?, samplingRate: Int) {
}
override fun onFftDataCapture(visualizer: Visualizer?, fft: ByteArray?, samplingRate: Int) {
}
}, Visualizer.getMaxCaptureRate() / 2, false, true)
开始采样:
ini
visualizer.enabled = true
退出界面或者停止,记得设置:
kotlin
visualizer.enabled = false
三、自定义View
通过给Visualizer设置OnDataCaptureListener之后,可以onFftDataCapture函数中获取快速傅里叶变换后的数据,但如何处理返回后的fft数据呢?
通过FFT的数组格式,获取到每个频率点的实部和虚部。
scss
val n = fft!!.size
val magnitudes = FloatArray(n / 2 + 1)
val phases = FloatArray(n / 2 + 1)
magnitudes[0] = Math.abs(fft[0].toInt()) as Float // DC
magnitudes[n / 2] = Math.abs(fft[1].toInt()) as Float // Nyquist
phases[0] = 0.also { phases[n / 2] = it.toFloat() }.toFloat()
for (k in 1 until n / 2) {
val i = k * 2
//取频率点实部与虚部的模
magnitudes[k] = Math.hypot(fft!![i].toDouble(), fft!![i + 1].toDouble()).toFloat()
}
按照官方代码示例,我们去实数与虚数的模作为数据绘制点,模代表幅值的大小。
拿到数据magnitudes
之后在View
中进行绘制。
将每个点以条形状的形式画出:
ini
mStrokeWidth = (mRect.width() - (mSpectrumCount - 1) * mItemMargin) / mSpectrumCount * 1.0f;
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setStyle(Paint.Style.FILL);
for (int i = 0; i < mSpectrumCount; i++) {
canvas.drawLine(mRect.width() * i / mSpectrumCount, mRect.height() / 2, mRect.width() * i / mSpectrumCount, 2 + mRect.height() / 2 - mRawAudioBytes[i], mPaint);
}
参考文章: