您的位置:首页 > 移动开发 > Android开发

基于android的实时音频频谱仪

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

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

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

[java] view plaincopy

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三个类,本人也是参考网络达人的代码

[java] view plaincopy

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);

}

}

}

}

}

主程序

[java] view plaincopy

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

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

第一次写技术文章,写的不好,大家不要怪罪,将就着看把
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: