基于android的实时音频频谱仪
16lz
2021-01-26
前一段实习,本来打算做c++,到了公司发现没啥项目,于是乎转行做了android,写的第一个程序竟然要我处理信号,咱可是一心搞计算机的,没接触过信号的东西,什么都没接触过,于是乎, 找各种朋友,各种熟人,现在想想,专注语言是不对的,语言就是一工具,关键还是业务,算法。好了,废话不多说,上程序,注释都很详细,应该能看懂。
分析声音,其实很简单,就是运用傅里叶变换,将声音信号由时域转化到频域(程序用的是快速傅里叶变换,比较简单),为啥要这样,好处多多,不细讲,公司里的用处是为了检测手机发出声音的信号所在的频率集中范围。
第一个类,复数的计算,用到加减乘,很简单。
package com.mobao360.sunshine;//复数的加减乘运算public class Complex {public double real;public double image;//三个构造函数public Complex() {// TODO Auto-generated constructor stubthis.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类,网上讲解这个类的蛮多的,搞清楚构造类的各个参数就可以。
绘图用的是SurfaceViewPaintCanvas三个类,本人也是参考网络达人的代码
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 exceptionLog.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();} } }}
程序源码下载地址: http://download.csdn.net/detail/sunshine_okey/3790484
详细的看代码吧,有什么写的详细的可以留言
第一次写技术文章,写的不好,大家不要怪罪,将就着看把
更多相关文章
- 一句话锁定MySQL数据占用元凶
- Android通过ksoap向webserice传递复杂类型数据
- android黑科技之读取用户短信+插入短信到系统短信数据库
- 在 SQL 数据库中保存数据
- Android之路——第一个上线 APP项目总结
- Android基于IIS的APK下载(三)用JSON传输更新数据
- 友盟2013年上半年数据报告:与开发者相关的各种干货数据
- 为什么Android应用用Java开发,为什么Android大型游戏要用数据包?这
- android显示RGB565数据图像