人们通常用来分析音频的方法是可视化其中存在的频率。通常这些类型的可视化采用均衡器,均衡器允许调整各种频率范围的级别。
将音频信号转换成分量频率(component frequency)的技术采用了一个数学变换,称为离散傅里叶变换(Discrete Fourier Transform,DFT)。DFT通常用来将基于时间的数据转换成基于频率的数据。一种用来执行DFT的算法是快速傅里叶变换(FFT),它非常有效,但是,非常复杂。
幸运的是,存在许多FFT算法的实现,他们位于公共领域中,或者开放源代码,因此可以直接使用他们。其中的一个版本是FFTPACK库的java端口,这个库最初由国家大气研究中心的Paul Swarztrauber开发。其java端口由加拿大亚伯达省Lethbridge大学的Baoshe Zhang实现。www.netlib.org/fftpack/在线提供了各种实现。我们将要使用的文件称为jfftpack.tgz的存档,它由该页面提供链接。可以直接通过www.netlib,org/jffpack.tgz下载该文件。
为了在一个Eclipse Android项目中使用这个程序包或任何其他包含java源代码的程序包,需要将源代码导入到项目中。由于这个存档包含了程序包的正确的目录结构,因此只要将位于javasource目录(ca)的顶层文件夹拖动到项目的src目录中。
下面是一个示例,其绘制了图像均衡器的图形部分。
1 package com.nthm.androidtestActivity; 2 3 import com.nthm.androidtest.R; 4 import android.app.Activity; 5 import android.graphics.Bitmap; 6 import android.graphics.Canvas; 7 import android.graphics.Color; 8 import android.graphics.Paint; 9 import android.media.AudioFormat; 10 import android.media.AudioRecord; 11 import android.media.MediaRecorder; 12 import android.os.AsyncTask; 13 import android.os.Bundle; 14 import android.view.View; 15 import android.view.View.OnClickListener; 16 import android.widget.Button; 17 import android.widget.ImageView;
我们将从fftpack包中导入RealDoubleFFT类。
1 import ca.uol.aig.fftpack.RealDoubleFFT; 2 public class AudioProcessing extends Activity implements OnClickListener {
我们将在AudioRecord对象中使用8kHz的频率,单音频通道和16位的样本。
1 private int frequency=8000; 2 private int channelConfiguration=AudioFormat.CHANNEL_CONFIGURATION_MONO; 3 private int audioEncoding=AudioFormat.ENCODING_PCM_16BIT;
transformer将是我们的FFT对象,通过该FFT对象,可以一次性处理来自AudioRecord对象的256个样本。使用的样本数量将对应于通过FFT对象运行他们之后获得的分量频率的数量。虽然可以自由选择不同的大小,但是确实需要考虑内存和性能的问题,因为需要计算的算术操作时处理器密集型的。
1 private RealDoubleFFT transformer; 2 private int blockSize=256; 3 private Button startStopButton; 4 private boolean started=false;
下面定义的RecordAudio是内部类,其扩展了AsyncTask。
1 private RecordAudio recordTask;
我们将使用ImageView显示一幅位图图像,其表示当前音频流中各种频率的级别。为了绘制这些级别,可以使用通过该位图构造的Canvas和Paint对象。
1 private ImageView imageView; 2 private Bitmap bitmap; 3 private Canvas canvas; 4 private Paint paint; 5 @Override 6 protected void onCreate(Bundle savedInstanceState) { 7 super.onCreate(savedInstanceState); 8 startStopButton=(Button) findViewById(R.id.StartStopButton); 9 startStopButton.setOnClickListener(this);
RealDoubleFFT类的构造函数接受每次处理的样品数量作为参数。这也代表了将要输出的不同频率范围的数量。
1 transformer=new RealDoubleFFT(blockSize);
下面是ImageView的设置和用于绘图的相关对象。
1 imageView=(ImageView) findViewById(R.id.ImageView01); 2 bitmap=Bitmap.createBitmap((int)256, (int)100, Bitmap.Config.ARGB_8888); 3 canvas=new Canvas(bitmap); 4 paint=new Paint(); 5 paint.setColor(Color.GREEN); 6 imageView.setImageBitmap(bitmap); 7 }
这个活动中的大部分工作在下面的RecordAudio类中完成,其扩展了AsyncTask。通过使用AsyncTask,可以在一个单独的线程上运行绑定用户界面的方法。任何放置在doInBackground方法中的代码都将以这种方式运行。
1 private class RecordAudio extends AsyncTask<Void, double[], Void>{ 2 3 @Override 4 protected Void doInBackground(Void... params) { 5 try{
我们将以通常的方式建立和使用AudioRecord。
1 int bufferSize=AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding); 2 AudioRecord audioRecord=new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, audioEncoding, bufferSize);
short类型的数组buffer将接受来自AudioRecord对象的原始PCM样本。Double类型的数组toTransform将以双精度的形式存储相同的数据,因为这是FFT类所需要的类型。
1 short [] buffer=new short[blockSize]; 2 double[] toTransform=new double[blockSize]; 3 audioRecord.startRecording(); 4 while(started){ 5 int bufferReadResult=audioRecord.read(buffer, 0,blockSize);
从AudioRecord对象中读取数据之后进行遍历,并将short值转换成double值。但是,不能直接通过强制类型转换这么做,因为期望值应该在-1.0~1.0之间,而不是整个值范围。将short值除以Short.MAX_VALUE将实现这个目的,因为该值是short类型的最大值。
1 for(int i=0;i<blockSize&&i<bufferReadResult;i++){ 2 toTransform[i]=(double)buffer[i]/Short.MAX_VALUE;//有符号16位 3 }
接下来将双精度值的数组传递给FFT对象。FFT对象重用这个相同的数组来保存输出值。包含的数据将采用频域(frequency domain)而非时域(time domain),这意味着数组的第一个元素不是根据时间表示第一个样本——相反,它表示第一个频率集合的级别。
由于使用256个值(或范围)且采样率是8000,因此可以确定数组中的每个元素将近似覆盖15.625Hz。通过将这个采样率除以2(因为可以捕获的最高频率是采样率的一半),然后除以256,就可以得到这个数字。因此,数组中第一个元素表示的数据将代表在0~15.625Hz之间的音频级别。
1 transformer.ft(toTransform);
调用publishProgress方法将会调用onProgressUpdate。
1 publishProgress(toTransform); 2 } 3 audioRecord.stop(); 4 }catch(Exception e){ 5 e.printStackTrace(); 6 } 7 return null; 8 }
onProgressUpdate子啊活动的主线程上运行,因此可以正常的与用户界面交互。在此实现中,传入了通过FFT对象运行之后的数据。该方法在屏幕上将数据绘制成一系列最大100像素高的线。每一条线表示数组中的一个元素,因此值范围是15.625Hz。第一条线表示频率范围是0~15.625Hz,而最后一条线表示频率范围是3984.375~4000Hz。
1 @Override 2 protected void onProgressUpdate(double[]... toTransform) { 3 super.onProgressUpdate(toTransform); 4 canvas.drawColor(Color.BLACK); 5 for(int i=0;i<toTransform.length;i++){ 6 int x=i; 7 int downy=(int)(100-(toTransform[0][i]*10)); 8 int upy=100; 9 canvas.drawLine(x, downy, x, upy, paint); 10 } 11 imageView.invalidate(); 12 } 13 } 14 @Override 15 public void onClick(View v) { 16 if(started){ 17 started=false; 18 startStopButton.setText("Start"); 19 recordTask.cancel(true); 20 }else{ 21 started=true; 22 startStopButton.setText("Stop"); 23 recordTask=new RecordAudio(); 24 recordTask.execute(); 25 } 26 } 27 }
下面是刚刚定义的AudioProcessing活动所使用的布局XML文件。
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 android:layout_width="match_parent" 3 android:layout_height="match_parent" 4 android:orientation="vertical" 5 > 6 <TextView 7 android:id="@+id/StatusTextView" 8 android:text="hello" 9 android:layout_width="fill_parent" 10 android:layout_height="wrap_content" 11 android:textSize="35dip"></TextView> 12 <ImageView 13 android:id="@+id/ImageView01" 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" 16 /> 17 <Button 18 android:layout_width="wrap_content" 19 android:layout_height="wrap_content" 20 android:id="@+id/StartStopButton" 21 android:text="Start"/> 22 23 </LinearLayout>