音频频谱动画的原理与实现(一)

您所在的位置:网站首页 音乐频谱分析 音频频谱动画的原理与实现(一)

音频频谱动画的原理与实现(一)

2024-06-18 15:12| 来源: 网络整理| 查看: 265

背景

微信的语音消息的按住说话,通过动画反馈出用户输入声音的大小,得到了比较好的效果,增强了用户体验。 在这里插入图片描述 微信的录音反馈做的很不错,但并没有表现出音频的频域信息,那么如何表示出声音的频率信息呢?

基础知识

音频基础知识可以参考:音频基本概念,下面只列举和本文有关的几个概念。

采样

在信号处理中,采样就是将连续时间的信号减少成离散时间的信号。 声音是一种连续压力波,通过对声音定时采样可以得到计算机能表示离散的数据,采样的频率被称采样率(Sample Rate)。 在这里插入图片描述 为了不失真地恢复模拟信号,采样频率应该不小于模拟信号频谱中最高频率的2倍,这个定理被称为采样定理又被称为奈奎斯特采样定理(Nyquist–Shannon sampling theorem)。

量化

将采样的结果表示成数据的过程被称为量化,这个数值范围被称为位深(bit depth)表示数值位数,常见的位数有8bit和16bit,位数越大对信号表达就越精确。

响度

声强亦称声强或声强度,反映的是声音的客观物理强弱,决定于发音体振动的振幅,振幅越大,音强越强。 响度(loudness),又称音量,是量度声音大小的知觉量,与声强不同,响度是受主观知觉影响的物理量。在同等声强下,不同频率的声音会造成不同的听觉感知。

等响曲线

人类的可听频率范围(20Hz 到 20000Hz)中,由于听觉对 3 000 Hz 左右的声音较为敏感,该段频率也能造成较大的听觉感知。 在这里插入图片描述等响曲线的横坐标为频率,纵坐标为声压级。在同一条曲线之上,所有频率和声压的组合,都有着一样的响度。 最下方的曲线表示人类能听到的最小的声音响度,即听阈。等响曲线反映了响度听觉的许多特点:

声压级愈高,响度一般也愈高。响度频率有关,相同声压级的纯音,频率不同,响度也不同。对于不同频率的纯音,提高声压级带来的响度增长,也有所不同。 A计权

在相同声强下,人耳对不同频率音频有不同的音量感受,因此需要对不同频率的音频进行加权,得到对应的音量,从而模拟耳朵的听觉效果。在声音测量中,我们以分贝(dB) 为单位测量声音的响度。 在这里插入图片描述 上图横坐标是频率,纵坐标是响度增益。在几种常用的加权曲线中,A权重曲线对低频部分相比其他计权有着最多的衰减,是最常用加权策略。其函数实现为如下,其中f代表频率

傅里叶变换

傅里叶在1807年提出,任何连续周期信号可以由一组适当的正弦曲线组合而成。任何周期函数,都可以看作是不同振幅,不同相位正弦波的叠加。 以其名称命名的傅里叶变换(Fourier transform),是用于信号在时域(或空域)和频域之间的变换。 在这里插入图片描述 录音获得的数据都是时域的,横轴是时间,纵轴是信号强度。 频谱动画要求横轴是频域数据,傅里叶变换可以实现时域信息到频域信息的转变。 计算机处理的是离散傅里叶变换(DFT),快速傅里叶变换(FFT)是快速计算离散傅里叶变换(DFT)或其逆变换的方法,它将DFT的复杂度从O(n²)降低到O(nlogn)。 苹果的Accelerate框架中vDSP部分提供了数字信号处理的函数实现,包含FFT,其使用可参考《Real FFT/IFFT with the Accelerate Framework》。 另外关于FFT比较有趣的文章可以看看 《让你永远忘不了的傅里叶变换解析》 《An Interactive Introduction to Fourier Transforms》 《如果看了这篇文章你还不懂傅里叶变换,那就过来掐死我吧》

整体流程

在这里插入图片描述

要实现对录制声音的频谱动画,将功能分解为以上几个部分,Recorder实现对音频PCM数据的采集,RealtimeAnalyser对PCM进行频域变换和数据处理,最后由RecordFeedbackView产生反馈动画。

iOS音频采集

iOS中实现音频采集的接口有很多,例如AVAudioRecorder、AudioQueue、AVAudioEngine、AudioUnit。 本文使用AudioQueue实现录音功能,AudioQueue的录音的过程如下:AudioQueue将麦克风获取的数据填充到AudioQueueBuffer中 ,通过callback函数回调给app,app将AudioQueueBuffer代表的数据消耗后,将AudioQueueBuffer重新入队到AudioQueue中。 使用AudioQueue实现录音的AudioRecorder在github地址:AudioRecorder,简要介绍其中比较关键的部分。

初始化Recoder - (void)start { [[AVAudioSession sharedInstance] setActive:YES error:nil]; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; mAqState.mDataFormat.mSampleRate = self.sampleRate; //采样率, 1s采集的次数 mAqState.mDataFormat.mFormatID = kAudioFormatLinearPCM; //数据格式 PCM mAqState.mDataFormat.mBitsPerChannel = AUDIO_BIT_LEN; //在一个数据帧中,每个通道的样本数据的位数。 mAqState.mDataFormat.mChannelsPerFrame = 1; //每帧数据通道数(左右声道) mAqState.mDataFormat.mFramesPerPacket = 1; //每包数据帧数 mAqState.mDataFormat.mBytesPerFrame = (mAqState.mDataFormat.mBitsPerChannel / 8) * mAqState.mDataFormat.mChannelsPerFrame; mAqState.mDataFormat.mBytesPerPacket = mAqState.mDataFormat.mBytesPerFrame * mAqState.mDataFormat.mFramesPerPacket; mAqState.mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked; // 上面使用的格式是signed integer类型,后面在做fft计算的时候需要转换成float类型,如果直接使用Float就少了这个过程。 // mAqState.mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsFloat | kLinearPCMFormatFlagIsPacked ; UInt32 frameCount = self.fftSize; mAqState.bufferByteSize = frameCount * (mAqState.mDataFormat.mBitsPerChannel / 8); AudioQueueNewInput(&mAqState.mDataFormat, HandleInputBuffer, (__bridge void *)(self), NULL, kCFRunLoopCommonModes, 0, &mAqState.mQueue); UInt32 channels = 0; UInt32 channelsSize = 0; AudioChannelLayout channelLayout; UInt32 layoutSize = sizeof(channelLayout); AudioQueueGetProperty(mAqState.mQueue, kAudioQueueDeviceProperty_NumberChannels, &channels, &channelsSize); AudioQueueGetProperty(mAqState.mQueue, kAudioQueueProperty_ChannelLayout, &channelLayout, &layoutSize); for (int i = 0; i mSampleTime / recorder->mAqState.mDataFormat.mSampleRate; if (inNumPackets > 0) { [recorder outputPcmBuffer:inBuffer recordTime:recordTime]; } if (recorder->mAqState.mIsRunning) { AudioQueueEnqueueBuffer(recorder->mAqState.mQueue, inBuffer, 0, NULL); } } - (void)outputPcmBuffer:(AudioQueueBufferRef)buffer recordTime:(NSTimeInterval)recordTime { int length = buffer->mAudioDataByteSize / mAqState.mDataFormat.mBytesPerFrame; NSData *data = [NSData dataWithBytes:buffer->mAudioData length:buffer->mAudioDataByteSize]; // 将录制的PCM数据写文件 if (self.outputSteam) { [self.outputSteam write:(uint8_t *)buffer->mAudioData maxLength:buffer->mAudioDataByteSize]; } // 交给分析模块 [self.analyzer onRecievePcmData:data frameCount:length]; }

屏幕绘制的频率是16ms一帧,动画需要16ms更新一下动画数据,我们使用的SampleRate使用的是16000,所以我们将AudioQueueBuffer对应的FrameCount设置为1024; 当录制的数据填满一个AudioQueueBuffer时,会通过AudioQueueNewInput函数传入的HandleInputBuffer函数将PCM数据回调给RealtimeAnalyser,所以基本每次刷新屏幕都可以获取到最新的频域数据。

以上是Recorder部分的全部介绍,这部分总体上比较简单,这里的难点部分主要在RealtimeAnalyser这部分。

RealtimeAnalyser

RealtimeAnalyser的实现我放到了github上,可以点击查看。

数值转换 - (void)onRecievePcmData:(NSData *)rawData frameCount:(UInt32)frameCount { __weak typeof(self) weakSelf = self; dispatch_async(_processQueue, ^{ __strong typeof(weakSelf) strongSelf = weakSelf; float fft_data[frameCount]; short *pcmbuffer = (short *)rawData.bytes; vDSP_vflt16(pcmbuffer, 1, fft_data, 1, frameCount); float scalar = 1.0 / (1


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3