基于android的实时音频频谱仪

前一段实习,本来打算做c++,到了公司发现没啥项目,于是乎转行做了android,写的第一个程序竟然要我处理信号,咱可是一心搞计算机的,没接触过信号的东西,什么都没接触过,于是乎, 找各种朋友,各种熟人,现在想想,专注语言是不对的,语言就是一工具,关键还是业务,算法。好了,废话不多说,上程序,注释都很详细,应该能看懂。

        分析声音,其实很简单,就是运用傅里叶变换,将声音信号由时域转化到频域(程序用的是快速傅里叶变换,比较简单),为啥要这样,好处多多,不细讲,公司里的用处是为了检测手机发出声音的信号所在的频率集中范围。

第一个类,复数的计算,用到加减乘,很简单。

 

[java] view plaincopy
 
  1. package com.mobao360.sunshine;  
  2. //复数的加减乘运算  
  3. public class Complex {  
  4.     public double real;  
  5.     public double image;  
  6.       
  7.     //三个构造函数  
  8.     public Complex() {  
  9.         // TODO Auto-generated constructor stub  
  10.         this.real = 0;  
  11.         this.image = 0;  
  12.     }  
  13.   
  14.     public Complex(double real, double image){  
  15.         this.real = real;  
  16.         this.image = image;  
  17.     }  
  18.       
  19.     public Complex(int real, int image) {  
  20.         Integer integer = real;  
  21.         this.real = integer.floatValue();  
  22.         integer = image;  
  23.         this.image = integer.floatValue();  
  24.     }  
  25.       
  26.     public Complex(double real) {  
  27.         this.real = real;  
  28.         this.image = 0;  
  29.     }  
  30.     //乘法  
  31.     public Complex cc(Complex complex) {  
  32.         Complex tmpComplex = new Complex();  
  33.         tmpComplex.real = this.real * complex.real - this.image * complex.image;  
  34.         tmpComplex.image = this.real * complex.image + this.image * complex.real;  
  35.         return tmpComplex;  
  36.     }  
  37.     //加法  
  38.     public Complex sum(Complex complex) {  
  39.         Complex tmpComplex = new Complex();  
  40.         tmpComplex.real = this.real + complex.real;  
  41.         tmpComplex.image = this.image + complex.image;  
  42.         return tmpComplex;  
  43.     }  
  44.     //减法  
  45.     public Complex cut(Complex complex) {  
  46.         Complex tmpComplex = new Complex();  
  47.         tmpComplex.real = this.real - complex.real;  
  48.         tmpComplex.image = this.image - complex.image;  
  49.         return tmpComplex;  
  50.     }  
  51.     //获得一个复数的值  
  52.     public int getIntValue(){  
  53.         int ret = 0;  
  54.         ret = (int) Math.round(Math.sqrt(this.real*this.real - this.image*this.image));  
  55.         return ret;  
  56.     }  
  57. }  

 

        这个类是有三个功能,第一,采集数据;第二,进行快速傅里叶计算;第三,绘图。

        采集数据用AudioRecord类,网上讲解这个类的蛮多的,搞清楚构造类的各个参数就可以。

        绘图用的是SurfaceView Paint Canvas三个类,本人也是参考网络达人的代码

 

[java] view plaincopy
 
  1. package com.mobao360.sunshine;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.lang.Short;  
  5.   
  6.   
  7. import android.content.Context;  
  8. import android.graphics.Canvas;  
  9. import android.graphics.Color;  
  10. import android.graphics.DashPathEffect;  
  11. import android.graphics.Paint;  
  12. import android.graphics.Path;  
  13. import android.graphics.PathEffect;  
  14. import android.graphics.Rect;  
  15. import android.media.AudioRecord;  
  16. import android.util.Log;  
  17. import android.view.SurfaceView;  
  18.   
  19. public class AudioProcess {  
  20.     public static final float pi= (float) 3.1415926;  
  21.     //应该把处理前后处理后的普线都显示出来  
  22.     private ArrayList<short[]> inBuf = new ArrayList<short[]>();//原始录入数据  
  23.     private ArrayList<int[]> outBuf = new ArrayList<int[]>();//处理后的数据  
  24.     private boolean isRecording = false;  
  25.   
  26.     Context mContext;  
  27.     private int shift = 30;  
  28.     public int frequence = 0;  
  29.       
  30.     private int length = 256;  
  31.     //y轴缩小的比例  
  32.     public int rateY = 21;  
  33.     //y轴基线  
  34.     public int baseLine = 0;  
  35.     //初始化画图的一些参数  
  36.     public void initDraw(int rateY, int baseLine,Context mContext, int frequence){  
  37.         this.mContext = mContext;  
  38.         this.rateY = rateY;  
  39.         this.baseLine = baseLine;  
  40.         this.frequence = frequence;  
  41.     }  
  42.     //启动程序  
  43.     public void start(AudioRecord audioRecord, int minBufferSize, SurfaceView sfvSurfaceView) {  
  44.         isRecording = true;  
  45.         new RecordThread(audioRecord, minBufferSize).start();  
  46.         new DrawThread(sfvSurfaceView).start();  
  47.     }  
  48.     //停止程序  
  49.     public void stop(SurfaceView sfvSurfaceView){  
  50.         isRecording = false;  
  51.         inBuf.clear();  
  52.     }  
  53.       
  54.     //录音线程  
  55.     class RecordThread extends Thread{  
  56.         private AudioRecord audioRecord;  
  57.         private int minBufferSize;  
  58.           
  59.         public RecordThread(AudioRecord audioRecord,int minBufferSize){  
  60.             this.audioRecord = audioRecord;  
  61.             this.minBufferSize = minBufferSize;  
  62.         }  
  63.           
  64.         public void run(){  
  65.             try{  
  66.                 short[] buffer = new short[minBufferSize];  
  67.                 audioRecord.startRecording();  
  68.                 while(isRecording){  
  69.                     int res = audioRecord.read(buffer, 0, minBufferSize);  
  70.                     synchronized (inBuf){  
  71.                         inBuf.add(buffer);  
  72.                     }  
  73.                     //保证长度为2的幂次数  
  74.                     length=up2int(res);  
  75.                     short[]tmpBuf = new short[length];  
  76.                     System.arraycopy(buffer, 0, tmpBuf, 0, length);  
  77.                       
  78.                     Complex[]complexs = new Complex[length];  
  79.                     int[]outInt = new int[length];  
  80.                     for(int i=0;i < length; i++){  
  81.                         Short short1 = tmpBuf[i];  
  82.                         complexs[i] = new Complex(short1.doubleValue());  
  83.                     }  
  84.                     fft(complexs,length);  
  85.                     for (int i = 0; i < length; i++) {  
  86.                         outInt[i] = complexs[i].getIntValue();  
  87.                     }  
  88.                     synchronized (outBuf) {  
  89.                         outBuf.add(outInt);  
  90.                     }  
  91.                 }  
  92.                 audioRecord.stop();  
  93.             }catch (Exception e) {  
  94.                 // TODO: handle exception  
  95.                 Log.i("Rec E",e.toString());  
  96.             }  
  97.               
  98.         }  
  99.     }  
  100.   
  101.     //绘图线程  
  102.     class DrawThread extends Thread{  
  103.         //画板  
  104.         private SurfaceView sfvSurfaceView;  
  105.         //当前画图所在屏幕x轴的坐标  
  106.         //画笔  
  107.         private Paint mPaint;  
  108.         private Paint tPaint;  
  109.         private Paint dashPaint;  
  110.         public DrawThread(SurfaceView sfvSurfaceView) {  
  111.             this.sfvSurfaceView = sfvSurfaceView;  
  112.             //设置画笔属性  
  113.             mPaint = new Paint();  
  114.             mPaint.setColor(Color.BLUE);  
  115.             mPaint.setStrokeWidth(2);  
  116.             mPaint.setAntiAlias(true);  
  117.               
  118.             tPaint = new Paint();  
  119.             tPaint.setColor(Color.YELLOW);  
  120.             tPaint.setStrokeWidth(1);  
  121.             tPaint.setAntiAlias(true);  
  122.               
  123.             //画虚线  
  124.             dashPaint = new Paint();  
  125.             dashPaint.setStyle(Paint.Style.STROKE);  
  126.             dashPaint.setColor(Color.GRAY);  
  127.             Path path = new Path();  
  128.             path.moveTo(0, 10);  
  129.             path.lineTo(480,10);   
  130.             PathEffect effects = new DashPathEffect(new float[]{5,5,5,5},1);  
  131.             dashPaint.setPathEffect(effects);  
  132.         }  
  133.           
  134.         @SuppressWarnings("unchecked")  
  135.         public void run() {  
  136.             while (isRecording) {  
  137.                 ArrayList<int[]>buf = new ArrayList<int[]>();  
  138.                 synchronized (outBuf) {  
  139.                     if (outBuf.size() == 0) {  
  140.                         continue;  
  141.                     }  
  142.                     buf = (ArrayList<int[]>)outBuf.clone();  
  143.                     outBuf.clear();  
  144.                 }  
  145.                 //根据ArrayList中的short数组开始绘图  
  146.                 for(int i = 0; i < buf.size(); i++){  
  147.                     int[]tmpBuf = buf.get(i);  
  148.                     SimpleDraw(tmpBuf, rateY, baseLine);  
  149.                 }  
  150.                   
  151.             }  
  152.         }  
  153.           
  154.         /**  
  155.          * 绘制指定区域  
  156.          *   
  157.          * @param start  
  158.          *            X 轴开始的位置(全屏)  
  159.          * @param buffer  
  160.          *             缓冲区  
  161.          * @param rate  
  162.          *            Y 轴数据缩小的比例  
  163.          * @param baseLine  
  164.          *            Y 轴基线  
  165.          */   
  166.   
  167.         private void SimpleDraw(int[] buffer, int rate, int baseLine){  
  168.             Canvas canvas = sfvSurfaceView.getHolder().lockCanvas(  
  169.                     new Rect(0, 0, buffer.length,sfvSurfaceView.getHeight()));  
  170.             canvas.drawColor(Color.BLACK);  
  171.             canvas.drawText("幅度值", 0, 3, 2, 15, tPaint);  
  172.             canvas.drawText("原点(0,0)", 0, 7, 5, baseLine + 15, tPaint);  
  173.             canvas.drawText("频率(HZ)", 0, 6, sfvSurfaceView.getWidth() - 50, baseLine + 30, tPaint);  
  174.             canvas.drawLine(shift, 20, shift, baseLine, tPaint);  
  175.             canvas.drawLine(shift, baseLine, sfvSurfaceView.getWidth(), baseLine, tPaint);  
  176.             canvas.save();  
  177.             canvas.rotate(30, shift, 20);  
  178.             canvas.drawLine(shift, 20, shift, 30, tPaint);  
  179.             canvas.rotate(-60, shift, 20);  
  180.             canvas.drawLine(shift, 20, shift, 30, tPaint);  
  181.             canvas.rotate(30, shift, 20);  
  182.             canvas.rotate(30, sfvSurfaceView.getWidth()-1, baseLine);  
  183.             canvas.drawLine(sfvSurfaceView.getWidth() - 1, baseLine, sfvSurfaceView.getWidth() - 11, baseLine, tPaint);  
  184.             canvas.rotate(-60, sfvSurfaceView.getWidth()-1, baseLine);  
  185.             canvas.drawLine(sfvSurfaceView.getWidth() - 1, baseLine, sfvSurfaceView.getWidth() - 11, baseLine, tPaint);  
  186.             canvas.restore();  
  187.             //tPaint.setStyle(Style.STROKE);  
  188.             for(int index = 64; index <= 512; index = index + 64){  
  189.                 canvas.drawLine(shift + index, baseLine, shift + index, 40, dashPaint);  
  190.                 String str = String.valueOf(frequence / 1024 * index);  
  191.                 canvas.drawText( str, 0, str.length(), shift + index - 15, baseLine + 15, tPaint);  
  192.             }  
  193.             int y;  
  194.             for(int i = 0; i < buffer.length; i = i + 1){  
  195.                 y = baseLine - buffer[i] / rateY ;  
  196.                 canvas.drawLine(2*i + shift, baseLine, 2*i +shift, y, mPaint);  
  197.             }  
  198.             sfvSurfaceView.getHolder().unlockCanvasAndPost(canvas);  
  199.         }  
  200.     }  
  201.       
  202.     /** 
  203.      * 向上取最接近iint的2的幂次数.比如iint=320时,返回256 
  204.      * @param iint 
  205.      * @return 
  206.      */  
  207.     private int up2int(int iint) {  
  208.         int ret = 1;  
  209.         while (ret<=iint) {  
  210.             ret = ret << 1;  
  211.         }  
  212.         return ret>>1;  
  213.     }  
  214.       
  215.     //快速傅里叶变换  
  216.     public void fft(Complex[] xin,int N)  
  217.     {  
  218.         int f,m,N2,nm,i,k,j,L;//L:运算级数  
  219.         float p;  
  220.         int e2,le,B,ip;  
  221.         Complex w = new Complex();  
  222.         Complex t = new Complex();  
  223.         N2 = N / 2;//每一级中蝶形的个数,同时也代表m位二进制数最高位的十进制权值  
  224.         f = N;//f是为了求流程的级数而设立的  
  225.         for(m = 1; (f = f / 2) != 1; m++);                             //得到流程图的共几级  
  226.         nm = N - 2;  
  227.         j = N2;  
  228.         /******倒序运算——雷德算法******/  
  229.         for(i = 1; i <= nm; i++)  
  230.         {  
  231.             if(i < j)//防止重复交换  
  232.             {  
  233.                 t = xin[j];  
  234.                 xin[j] = xin[i];  
  235.                 xin[i] = t;  
  236.             }  
  237.             k = N2;  
  238.             while(j >= k)  
  239.             {  
  240.                 j = j - k;  
  241.                 k = k / 2;  
  242.             }  
  243.             j = j + k;  
  244.         }  
  245.         /******蝶形图计算部分******/  
  246.         for(L=1; L<=m; L++)                                    //从第1级到第m级  
  247.         {  
  248.             e2 = (int) Math.pow(2, L);  
  249.             //e2=(int)2.pow(L);  
  250.             le=e2+1;  
  251.             B=e2/2;  
  252.             for(j=0;j<B;j++)                                    //j从0到2^(L-1)-1  
  253.             {  
  254.                 p=2*pi/e2;  
  255.                 w.real = Math.cos(p * j);  
  256.                 //w.real=Math.cos((double)p*j);                                   //系数W  
  257.                 w.image = Math.sin(p*j) * -1;  
  258.                 //w.imag = -sin(p*j);  
  259.                 for(i=j;i<N;i=i+e2)                                //计算具有相同系数的数据  
  260.                 {  
  261.                     ip=i+B;                                           //对应蝶形的数据间隔为2^(L-1)  
  262.                     t=xin[ip].cc(w);  
  263.                     xin[ip] = xin[i].cut(t);  
  264.                     xin[i] = xin[i].sum(t);  
  265.                 }  
  266.             }  
  267.         }  
  268.     }  
  269. }  


        主程序

 

 

[java] view plaincopy
 
  1. package com.mobao360.sunshine;  
  2.   
  3. import java.util.ArrayList;  
  4.   
  5. import android.app.Activity;  
  6. import android.app.AlertDialog;  
  7. import android.content.Context;  
  8. import android.content.DialogInterface;  
  9. import android.os.Bundle;  
  10. import android.util.Log;  
  11. import android.view.SurfaceView;  
  12. import android.view.View;  
  13. import android.widget.AdapterView;  
  14. import android.widget.ArrayAdapter;  
  15. import android.widget.Button;  
  16. import android.widget.Spinner;  
  17. import android.widget.TextView;  
  18. import android.widget.Toast;  
  19. import android.widget.ZoomControls;  
  20. import android.media.AudioFormat;  
  21. import android.media.AudioRecord;  
  22. import android.media.MediaRecorder;  
  23.   
  24. public class AudioMaker extends Activity {  
  25.     /** Called when the activity is first created. */  
  26.     static  int frequency = 8000;//分辨率    
  27.     static final int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO;    
  28.     static final int audioEncodeing = AudioFormat.ENCODING_PCM_16BIT;   
  29.     static final int yMax = 50;//Y轴缩小比例最大值    
  30.     static final int yMin = 1;//Y轴缩小比例最小值    
  31.       
  32.     int minBufferSize;//采集数据需要的缓冲区大小  
  33.     AudioRecord audioRecord;//录音  
  34.     AudioProcess audioProcess = new AudioProcess();//处理  
  35.       
  36.     Button btnStart,btnExit;  //开始停止按钮  
  37.     SurfaceView sfv;  //绘图所用  
  38.     ZoomControls zctlX,zctlY;//频谱图缩放  
  39.     Spinner spinner;//下拉菜单  
  40.     ArrayList<String> list=new ArrayList<String>();  
  41.     ArrayAdapter<String>adapter;//下拉菜单适配器  
  42.     TextView tView;  
  43.       
  44.       
  45.     @Override  
  46.     public void onCreate(Bundle savedInstanceState) {  
  47.         super.onCreate(savedInstanceState);  
  48.         setContentView(R.layout.main);  
  49.           
  50.         initControl();  
  51.         }  
  52.     @Override  
  53.     protected void onDestroy(){  
  54.         super.onDestroy();  
  55.         android.os.Process.killProcess(android.os.Process.myPid());  
  56.     }  
  57.       
  58.   //初始化控件信息  
  59.     private void initControl() {  
  60.         //获取采样率  
  61.         tView = (TextView)this.findViewById(R.id.tvSpinner);  
  62.         spinner = (Spinner)this.findViewById(R.id.spinnerFre);  
  63.         String []ls =getResources().getStringArray(R.array.action);  
  64.         for(int i=0;i<ls.length;i++){  
  65.             list.add(ls[i]);  
  66.         }  
  67.         adapter=new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item,list);  
  68.         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);  
  69.         spinner.setAdapter(adapter);  
  70.         spinner.setPrompt("请选择采样率");  
  71.         spinner.setOnItemSelectedListener(new Spinner.OnItemSelectedListener(){  
  72.             @SuppressWarnings("unchecked")  
  73.             public void onItemSelected(AdapterView arg0,View agr1,int arg2,long arg3){  
  74.                 frequency = Integer.parseInt(adapter.getItem(arg2));  
  75.                 tView.setText("您选择的是:"+adapter.getItem(arg2)+"HZ");  
  76.                 Log.i("sunshine",String.valueOf(minBufferSize));  
  77.                 arg0.setVisibility(View.VISIBLE);  
  78.             }  
  79.             @SuppressWarnings("unchecked")  
  80.             public void onNothingSelected(AdapterView arg0){  
  81.                 arg0.setVisibility(View.VISIBLE);  
  82.             }  
  83.         });  
  84.           
  85.           
  86.         Context mContext = getApplicationContext();  
  87.         //按键  
  88.         btnStart = (Button)this.findViewById(R.id.btnStart);  
  89.         btnExit = (Button)this.findViewById(R.id.btnExit);  
  90.         //按键事件处理  
  91.         btnStart.setOnClickListener(new ClickEvent());  
  92.         btnExit.setOnClickListener(new ClickEvent());  
  93.         //画笔和画板  
  94.         sfv = (SurfaceView)this.findViewById(R.id.SurfaceView01);  
  95.         //初始化显示  
  96.         audioProcess.initDraw(yMax/2, sfv.getHeight(),mContext,frequency);  
  97.         //画板缩放  
  98.         zctlY = (ZoomControls)this.findViewById(R.id.zctlY);  
  99.         zctlY.setOnZoomInClickListener(new View.OnClickListener() {    
  100.             @Override    
  101.             public void onClick(View v) {    
  102.                 if(audioProcess.rateY - 5>yMin){  
  103.                     audioProcess.rateY = audioProcess.rateY - 5;    
  104.                     setTitle("Y轴缩小"+String.valueOf(audioProcess.rateY)+"倍");  
  105.                 }else{  
  106.                     audioProcess.rateY = 1;  
  107.                     setTitle("原始尺寸");  
  108.                 }  
  109.             }    
  110.         });    
  111.             
  112.         zctlY.setOnZoomOutClickListener(new View.OnClickListener() {    
  113.             @Override    
  114.             public void onClick(View v) {    
  115.                 if(audioProcess.rateY<yMax){  
  116.                     audioProcess.rateY = audioProcess.rateY + 5;        
  117.                     setTitle("Y轴缩小"+String.valueOf(audioProcess.rateY)+"倍");    
  118.                 }else {  
  119.                     setTitle("Y轴已经不能再缩小");  
  120.                 }  
  121.             }    
  122.         });  
  123.     }  
  124.       
  125.     /** 
  126.      * 按键事件处理 
  127.      */  
  128.     class ClickEvent implements View.OnClickListener{  
  129.         @Override  
  130.         public void onClick(View v){  
  131.             Button button = (Button)v;  
  132.             if(button == btnStart){  
  133.                 if(button.getText().toString().equals("Start")){  
  134.                     try {  
  135.                         //录音  
  136.                         minBufferSize = AudioRecord.getMinBufferSize(frequency,   
  137.                                 channelConfiguration,   
  138.                                 audioEncodeing);  
  139.                         //minBufferSize = 2 * minBufferSize;   
  140.                         audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,frequency,  
  141.                                 channelConfiguration,  
  142.                                 audioEncodeing,  
  143.                                 minBufferSize);  
  144.                         audioProcess.baseLine = sfv.getHeight()-100;  
  145.                         audioProcess.frequence = frequency;  
  146.                         audioProcess.start(audioRecord, minBufferSize, sfv);  
  147.                         Toast.makeText(AudioMaker.this,   
  148.                                 "当前设备支持您所选择的采样率:"+String.valueOf(frequency),   
  149.                                 Toast.LENGTH_SHORT).show();  
  150.                         btnStart.setText(R.string.btn_exit);  
  151.                         spinner.setEnabled(false);  
  152.                     } catch (Exception e) {  
  153.                         // TODO: handle exception  
  154.                         Toast.makeText(AudioMaker.this,   
  155.                                 "当前设备不支持你所选择的采样率"+String.valueOf(frequency)+",请重新选择",   
  156.                                 Toast.LENGTH_SHORT).show();  
  157.                     }  
  158.                 }else if (button.getText().equals("Stop")) {  
  159.                     spinner.setEnabled(true);  
  160.                     btnStart.setText(R.string.btn_start);  
  161.                     audioProcess.stop(sfv);  
  162.                 }  
  163.             }  
  164.             else {  
  165.                 new AlertDialog.Builder(AudioMaker.this)   
  166.                  .setTitle("提示")   
  167.                  .setMessage("确定退出?")   
  168.                  .setPositiveButton("确定", new DialogInterface.OnClickListener() {   
  169.                 public void onClick(DialogInterface dialog, int whichButton) {   
  170.                 setResult(RESULT_OK);//确定按钮事件   
  171.                 AudioMaker.this.finish();  
  172.                  finish();   
  173.                  }   
  174.                  })   
  175.                  .setNegativeButton("取消", new DialogInterface.OnClickListener() {   
  176.                 public void onClick(DialogInterface dialog, int whichButton) {   
  177.                  //取消按钮事件   
  178.                  }   
  179.                  })   
  180.                  .show();  
  181.             }  
  182.               
  183.         }  
  184.     }  
  185. }  

 


 程序源码下载地址:http://download.csdn.net/detail/sunshine_okey/3790484

 

详细的看代码吧,有什么写的详细的可以留言

第一次写技术文章,写的不好,大家不要怪罪,将就着看把

原文地址:https://www.cnblogs.com/Free-Thinker/p/4759949.html