android pcm频谱_Android音频可视化

本文作者:熊鋆洋 (网易云音乐大前端团队)

前言

音频可视化,顾名思义就是将声音以视觉的方式呈现出来。如何将音频信号绘制出来?如何将声音的变化在视觉上清晰的表现出来,让视觉和听觉上的感受一致?这些在 Android 上如何实现?本文将针对这些问题做出解答,尽量对 Android 上的音频可视化实现做一个全面的介绍。

傅里叶变换

Android 音频播放的一般流程是: 1. 播放器从本地音频文件或网络加载编码后的音频数据,解码为 pcm 数据写入 AudioTrack 2. AudioTrack 将 pcm 数据写入 FIFO 3. AudioFlinger 中的 MixerThread 通过 AudioMixer 读取 FIFO 中的数据进行混音后写入 HAL 输出设备进行播放

在这个流程中,直接体现音频特征,可用于可视化绘制的是 pcm 数据。但 pcm 表示各采样时间点上音频信号强度,看起来杂乱无章,难以体现听觉感知到的声音变化。pcm 数据仅可用来绘制体现音频信号平均强度变化的可视化动效,其他大部分动效需要使用对 pcm 数据做傅里叶变换后得到的体现各频率点上信号强度变化的频域数据来绘制。

这里简单回顾下傅里叶变换,它将信号从时域转换为频域,一般用于信号频谱分析,确定其成分。转换结果如下图所示:

pcm 数据是时间离散的,需要使用离散傅里叶变换(DFT),它将包含 N 个复数的序列 $\{x_n\}:=x_0, x_1, ..., x_{N-1}$ 转换为另一个复数序列 $\{X_k\}:=X_0, X_1, ..., X_{N-1}$,计算公式为:

X_k=\sum_{n=0}^{N-1}x_n \cdot e^{-i2 \pi {kn \over N}}=\sum_{n=0}^{N-1}x_n \cdot (cos(2\pi {kn \over N})-i \cdot sin(2\pi {kn \over N}))

直接用上面公式计算长度为 N 的序列的 DFT,时间复杂度为 $O(N^2)$,速度较慢,实际应用中,一般会使用快速傅里叶变换(FFT),将时间复杂度降为 $O(Nlog(N))$。

计算公式看起来很复杂,但不懂也不会影响我们实现音频可视化,FFT 的计算可以使用已有的库,不需要自己来实现。但为了从 FFT 的计算结果得到最终用来绘制的数据,有必要了解以下DFT特性: 输入全部为实数时,输出结果满足共轭对称性:$X_{N-k}=X_k^*$,因此一般实现只返回一半结果 如原始信号采样率为 $f_s$,序列长度为 N,输出频率分辨率为 $f_s/N$

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你可以使用Android的MediaCodec API来获取音频频谱数据。具体步骤如下: 1. 创建一个MediaExtractor对象,用于读取音频文件中的数据。 2. 选择音频轨道并获取音频格式信息。 3. 创建一个MediaCodec对象,并配置输入和输出格式。 4. 将音频数据输入到MediaCodec对象中,获取解码后的数据。 5. 将解码后的数据转换为PCM数据,并使用FFT算法计算频谱数据。 下面是一段示例代码,演示如何获取音频频谱数据: ```java MediaExtractor extractor = new MediaExtractor(); extractor.setDataSource(audioFilePath); int audioTrackIndex = getAudioTrackIndex(extractor); MediaFormat audioFormat = extractor.getTrackFormat(audioTrackIndex); MediaCodec decoder = MediaCodec.createDecoderByType(audioFormat.getString(MediaFormat.KEY_MIME)); decoder.configure(audioFormat, null, null, 0); decoder.start(); ByteBuffer[] inputBuffers = decoder.getInputBuffers(); ByteBuffer[] outputBuffers = decoder.getOutputBuffers(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); while (true) { int inputBufferIndex = decoder.dequeueInputBuffer(10000); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; int sampleSize = extractor.readSampleData(inputBuffer, 0); if (sampleSize < 0) { decoder.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); break; } else { long presentationTimeUs = extractor.getSampleTime(); decoder.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0); extractor.advance(); } } int outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000); if (outputBufferIndex >= 0) { ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; byte[] pcmData = new byte[bufferInfo.size]; outputBuffer.get(pcmData); // 计算频谱数据 int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat.getInteger(MediaFormat.KEY_PCM_ENCODING)); short[] audioData = new short[bufferSize / 2]; ByteBuffer.wrap(pcmData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(audioData); double[] fftData = new double[audioData.length * 2]; for (int i = 0; i < audioData.length; i++) { fftData[i * 2] = audioData[i]; fftData[i * 2 + 1] = 0; } FFT.fft(fftData); double[] magnitude = new double[audioData.length]; for (int i = 0; i < audioData.length; i++) { double real = fftData[i * 2]; double imaginary = fftData[i * 2 + 1]; magnitude[i] = Math.sqrt(real * real + imaginary * imaginary); } // 此时magnitude数组中存储着音频频谱数据 decoder.releaseOutputBuffer(outputBufferIndex, false); } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // 处理输出格式变化 } } decoder.stop(); decoder.release(); extractor.release(); ``` 在上面的代码中,我们使用FFT算法计算了音频频谱数据。FFT算法可以将时域信号转换为频域信号,从而得到音频频谱信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值