前一段实习,本来打算做c++,到了公司发现没啥项目,于是乎转行做了android,写的第一个程序竟然要我处理信号,咱可是一心搞计算机的,没接触过信号的东西,什么都没接触过,于是乎, 找各种朋友,各种熟人,现在想想,专注语言是不对的,语言就是一工具,关键还是业务,算法。好了,废话不多说,上程序,注释都很详细,应该能看懂。
分析声音,其实很简单,就是运用傅里叶变换,将声音信号由时域转化到频域(程序用的是快速傅里叶变换,比较简单),为啥要这样,好处多多,不细讲,公司里的用处是为了检测手机发出声音的信号所在的频率集中范围。
第一个类,复数的计算,用到加减乘,很简单。
package com.mobao360.sunshine; //复数的加减乘运算 public class Complex { public double real; public double image; //三个构造函数 public Complex() { // TODO Auto-generated constructor stub this.real = 0; this.image = 0; } public Complex(double real, double image){ this.real = real; this.image = image; } public Complex(int real, int image) { Integer integer = real; this.real = integer.floatValue(); integer = image; this.image = integer.floatValue(); } public Complex(double real) { this.real = real; this.image = 0; } //乘法 public Complex cc(Complex complex) { Complex tmpComplex = new Complex(); tmpComplex.real = this.real * complex.real - this.image * complex.image; tmpComplex.image = this.real * complex.image + this.image * complex.real; return tmpComplex; } //加法 public Complex sum(Complex complex) { Complex tmpComplex = new Complex(); tmpComplex.real = this.real + complex.real; tmpComplex.image = this.image + complex.image; return tmpComplex; } //减法 public Complex cut(Complex complex) { Complex tmpComplex = new Complex(); tmpComplex.real = this.real - complex.real; tmpComplex.image = this.image - complex.image; return tmpComplex; } //获得一个复数的值 public int getIntValue(){ int ret = 0; ret = (int) Math.round(Math.sqrt(this.real*this.real - this.image*this.image)); return ret; } }
这个类是有三个功能,第一,采集数据;第二,进行快速傅里叶计算;第三,绘图。
采集数据用AudioRecord类,网上讲解这个类的蛮多的,搞清楚构造类的各个参数就可以。
绘图用的是SurfaceView Paint Canvas三个类,本人也是参考网络达人的代码
package com.mobao360.sunshine; import java.util.ArrayList; import java.lang.Short; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathEffect; import android.graphics.Rect; import android.media.AudioRecord; import android.util.Log; import android.view.SurfaceView; public class AudioProcess { public static final float pi= (float) 3.1415926; //应该把处理前后处理后的普线都显示出来 private ArrayList<short[]> inBuf = new ArrayList<short[]>();//原始录入数据 private ArrayList<int[]> outBuf = new ArrayList<int[]>();//处理后的数据 private boolean isRecording = false; Context mContext; private int shift = 30; public int frequence = 0; private int length = 256; //y轴缩小的比例 public int rateY = 21; //y轴基线 public int baseLine = 0; //初始化画图的一些参数 public void initDraw(int rateY, int baseLine,Context mContext, int frequence){ this.mContext = mContext; this.rateY = rateY; this.baseLine = baseLine; this.frequence = frequence; } //启动程序 public void start(AudioRecord audioRecord, int minBufferSize, SurfaceView sfvSurfaceView) { isRecording = true; new RecordThread(audioRecord, minBufferSize).start(); new DrawThread(sfvSurfaceView).start(); } //停止程序 public void stop(SurfaceView sfvSurfaceView){ isRecording = false; inBuf.clear(); } //录音线程 class RecordThread extends Thread{ private AudioRecord audioRecord; private int minBufferSize; public RecordThread(AudioRecord audioRecord,int minBufferSize){ this.audioRecord = audioRecord; this.minBufferSize = minBufferSize; } public void run(){ try{ short[] buffer = new short[minBufferSize]; audioRecord.startRecording(); while(isRecording){ int res = audioRecord.read(buffer, 0, minBufferSize); synchronized (inBuf){ inBuf.add(buffer); } //保证长度为2的幂次数 length=up2int(res); short[]tmpBuf = new short[length]; System.arraycopy(buffer, 0, tmpBuf, 0, length); Complex[]complexs = new Complex[length]; int[]outInt = new int[length]; for(int i=0;i < length; i++){ Short short1 = tmpBuf[i]; complexs[i] = new Complex(short1.doubleValue()); } fft(complexs,length); for (int i = 0; i < length; i++) { outInt[i] = complexs[i].getIntValue(); } synchronized (outBuf) { outBuf.add(outInt); } } audioRecord.stop(); }catch (Exception e) { // TODO: handle exception Log.i("Rec E",e.toString()); } } } //绘图线程 class DrawThread extends Thread{ //画板 private SurfaceView sfvSurfaceView; //当前画图所在屏幕x轴的坐标 //画笔 private Paint mPaint; private Paint tPaint; private Paint dashPaint; public DrawThread(SurfaceView sfvSurfaceView) { this.sfvSurfaceView = sfvSurfaceView; //设置画笔属性 mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStrokeWidth(2); mPaint.setAntiAlias(true); tPaint = new Paint(); tPaint.setColor(Color.YELLOW); tPaint.setStrokeWidth(1); tPaint.setAntiAlias(true); //画虚线 dashPaint = new Paint(); dashPaint.setStyle(Paint.Style.STROKE); dashPaint.setColor(Color.GRAY); Path path = new Path(); path.moveTo(0, 10); path.lineTo(480,10); PathEffect effects = new DashPathEffect(new float[]{5,5,5,5},1); dashPaint.setPathEffect(effects); } @SuppressWarnings("unchecked") public void run() { while (isRecording) { ArrayList<int[]>buf = new ArrayList<int[]>(); synchronized (outBuf) { if (outBuf.size() == 0) { continue; } buf = (ArrayList<int[]>)outBuf.clone(); outBuf.clear(); } //根据ArrayList中的short数组开始绘图 for(int i = 0; i < buf.size(); i++){ int[]tmpBuf = buf.get(i); SimpleDraw(tmpBuf, rateY, baseLine); } } } /** * 绘制指定区域 * * @param start * X 轴开始的位置(全屏) * @param buffer * 缓冲区 * @param rate * Y 轴数据缩小的比例 * @param baseLine * Y 轴基线 */ private void SimpleDraw(int[] buffer, int rate, int baseLine){ Canvas canvas = sfvSurfaceView.getHolder().lockCanvas( new Rect(0, 0, buffer.length,sfvSurfaceView.getHeight())); canvas.drawColor(Color.BLACK); canvas.drawText("幅度值", 0, 3, 2, 15, tPaint); canvas.drawText("原点(0,0)", 0, 7, 5, baseLine + 15, tPaint); canvas.drawText("频率(HZ)", 0, 6, sfvSurfaceView.getWidth() - 50, baseLine + 30, tPaint); canvas.drawLine(shift, 20, shift, baseLine, tPaint); canvas.drawLine(shift, baseLine, sfvSurfaceView.getWidth(), baseLine, tPaint); canvas.save(); canvas.rotate(30, shift, 20); canvas.drawLine(shift, 20, shift, 30, tPaint); canvas.rotate(-60, shift, 20); canvas.drawLine(shift, 20, shift, 30, tPaint); canvas.rotate(30, shift, 20); canvas.rotate(30, sfvSurfaceView.getWidth()-1, baseLine); canvas.drawLine(sfvSurfaceView.getWidth() - 1, baseLine, sfvSurfaceView.getWidth() - 11, baseLine, tPaint); canvas.rotate(-60, sfvSurfaceView.getWidth()-1, baseLine); canvas.drawLine(sfvSurfaceView.getWidth() - 1, baseLine, sfvSurfaceView.getWidth() - 11, baseLine, tPaint); canvas.restore(); //tPaint.setStyle(Style.STROKE); for(int index = 64; index <= 512; index = index + 64){ canvas.drawLine(shift + index, baseLine, shift + index, 40, dashPaint); String str = String.valueOf(frequence / 1024 * index); canvas.drawText( str, 0, str.length(), shift + index - 15, baseLine + 15, tPaint); } int y; for(int i = 0; i < buffer.length; i = i + 1){ y = baseLine - buffer[i] / rateY ; canvas.drawLine(2*i + shift, baseLine, 2*i +shift, y, mPaint); } sfvSurfaceView.getHolder().unlockCanvasAndPost(canvas); } } /** * 向上取最接近iint的2的幂次数.比如iint=320时,返回256 * @param iint * @return */ private int up2int(int iint) { int ret = 1; while (ret<=iint) { ret = ret << 1; } return ret>>1; } //快速傅里叶变换 public void fft(Complex[] xin,int N) { int f,m,N2,nm,i,k,j,L;//L:运算级数 float p; int e2,le,B,ip; Complex w = new Complex(); Complex t = new Complex(); N2 = N / 2;//每一级中蝶形的个数,同时也代表m位二进制数最高位的十进制权值 f = N;//f是为了求流程的级数而设立的 for(m = 1; (f = f / 2) != 1; m++); //得到流程图的共几级 nm = N - 2; j = N2; /******倒序运算——雷德算法******/ for(i = 1; i <= nm; i++) { if(i < j)//防止重复交换 { t = xin[j]; xin[j] = xin[i]; xin[i] = t; } k = N2; while(j >= k) { j = j - k; k = k / 2; } j = j + k; } /******蝶形图计算部分******/ for(L=1; L<=m; L++) //从第1级到第m级 { e2 = (int) Math.pow(2, L); //e2=(int)2.pow(L); le=e2+1; B=e2/2; for(j=0;j<B;j++) //j从0到2^(L-1)-1 { p=2*pi/e2; w.real = Math.cos(p * j); //w.real=Math.cos((double)p*j); //系数W w.image = Math.sin(p*j) * -1; //w.imag = -sin(p*j); for(i=j;i<N;i=i+e2) //计算具有相同系数的数据 { ip=i+B; //对应蝶形的数据间隔为2^(L-1) t=xin[ip].cc(w); xin[ip] = xin[i].cut(t); xin[i] = xin[i].sum(t); } } } } }
package com.mobao360.sunshine; import java.util.ArrayList; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; import android.view.SurfaceView; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import android.widget.ZoomControls; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; public class AudioMaker extends Activity { /** Called when the activity is first created. */ static int frequency = 8000;//分辨率 static final int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO; static final int audioEncodeing = AudioFormat.ENCODING_PCM_16BIT; static final int yMax = 50;//Y轴缩小比例最大值 static final int yMin = 1;//Y轴缩小比例最小值 int minBufferSize;//采集数据需要的缓冲区大小 AudioRecord audioRecord;//录音 AudioProcess audioProcess = new AudioProcess();//处理 Button btnStart,btnExit; //开始停止按钮 SurfaceView sfv; //绘图所用 ZoomControls zctlX,zctlY;//频谱图缩放 Spinner spinner;//下拉菜单 ArrayList<String> list=new ArrayList<String>(); ArrayAdapter<String>adapter;//下拉菜单适配器 TextView tView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); initControl(); } @Override protected void onDestroy(){ super.onDestroy(); android.os.Process.killProcess(android.os.Process.myPid()); } //初始化控件信息 private void initControl() { //获取采样率 tView = (TextView)this.findViewById(R.id.tvSpinner); spinner = (Spinner)this.findViewById(R.id.spinnerFre); String []ls =getResources().getStringArray(R.array.action); for(int i=0;i<ls.length;i++){ list.add(ls[i]); } adapter=new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item,list); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinner.setPrompt("请选择采样率"); spinner.setOnItemSelectedListener(new Spinner.OnItemSelectedListener(){ @SuppressWarnings("unchecked") public void onItemSelected(AdapterView arg0,View agr1,int arg2,long arg3){ frequency = Integer.parseInt(adapter.getItem(arg2)); tView.setText("您选择的是:"+adapter.getItem(arg2)+"HZ"); Log.i("sunshine",String.valueOf(minBufferSize)); arg0.setVisibility(View.VISIBLE); } @SuppressWarnings("unchecked") public void onNothingSelected(AdapterView arg0){ arg0.setVisibility(View.VISIBLE); } }); Context mContext = getApplicationContext(); //按键 btnStart = (Button)this.findViewById(R.id.btnStart); btnExit = (Button)this.findViewById(R.id.btnExit); //按键事件处理 btnStart.setOnClickListener(new ClickEvent()); btnExit.setOnClickListener(new ClickEvent()); //画笔和画板 sfv = (SurfaceView)this.findViewById(R.id.SurfaceView01); //初始化显示 audioProcess.initDraw(yMax/2, sfv.getHeight(),mContext,frequency); //画板缩放 zctlY = (ZoomControls)this.findViewById(R.id.zctlY); zctlY.setOnZoomInClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(audioProcess.rateY - 5>yMin){ audioProcess.rateY = audioProcess.rateY - 5; setTitle("Y轴缩小"+String.valueOf(audioProcess.rateY)+"倍"); }else{ audioProcess.rateY = 1; setTitle("原始尺寸"); } } }); zctlY.setOnZoomOutClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(audioProcess.rateY<yMax){ audioProcess.rateY = audioProcess.rateY + 5; setTitle("Y轴缩小"+String.valueOf(audioProcess.rateY)+"倍"); }else { setTitle("Y轴已经不能再缩小"); } } }); } /** * 按键事件处理 */ class ClickEvent implements View.OnClickListener{ @Override public void onClick(View v){ Button button = (Button)v; if(button == btnStart){ if(button.getText().toString().equals("Start")){ try { //录音 minBufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncodeing); //minBufferSize = 2 * minBufferSize; audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,frequency, channelConfiguration, audioEncodeing, minBufferSize); audioProcess.baseLine = sfv.getHeight()-100; audioProcess.frequence = frequency; audioProcess.start(audioRecord, minBufferSize, sfv); Toast.makeText(AudioMaker.this, "当前设备支持您所选择的采样率:"+String.valueOf(frequency), Toast.LENGTH_SHORT).show(); btnStart.setText(R.string.btn_exit); spinner.setEnabled(false); } catch (Exception e) { // TODO: handle exception Toast.makeText(AudioMaker.this, "当前设备不支持你所选择的采样率"+String.valueOf(frequency)+",请重新选择", Toast.LENGTH_SHORT).show(); } }else if (button.getText().equals("Stop")) { spinner.setEnabled(true); btnStart.setText(R.string.btn_start); audioProcess.stop(sfv); } } else { new AlertDialog.Builder(AudioMaker.this) .setTitle("提示") .setMessage("确定退出?") .setPositiveButton("确定", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { setResult(RESULT_OK);//确定按钮事件 AudioMaker.this.finish(); finish(); } }) .setNegativeButton("取消", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { //取消按钮事件 } }) .show(); } } } }
详细的看代码吧,有什么写的详细的可以留言
第一次写技术文章,写的不好,大家不要怪罪,将就着看把