天天看点

11.模拟信号示波器

上次简单地介绍了AudioRecord和AudioTrack的使用,这次就结合SurfaceView实现一个Android版的手机模拟信号示波器。最近物联网炒得很火,作为手机软件开发者,如何在不修改手机硬件电路的前提下实现与第三方传感器结合呢?麦克风就是一个很好的ADC接口,通过麦克风与第三方传感器结合,再在软件里对模拟信号做相应的处理,就可以提供更丰富的传感化应用。

先来看看本文程序运行的效果图(屏幕录像速度较慢,真机实际运行起来会更加流畅):

11.模拟信号示波器

本文程序使用8000hz的采样率,对X轴方向绘图的实时性要求较高,如果不降低X轴的分辨率,程序的实时性较差,因此程序对X轴数据缩小区间为8倍~16倍。由于采用16位采样,因此Y轴数据的高度相对于手机屏幕来说也偏大,程序也对Y轴数据做缩小,区间为1倍~10倍。在SurfaceView的OnTouchListener方法里加入了波形基线的位置调节,直接在SurfaceView控件上触摸即可控制整体波形偏上或偏下显示。

main.xml源码如下:

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

<?

xml

version

=

"1.0"

encoding

=

"utf-8"

?>

<

LinearLayout

xmlns:android

=

"http://schemas.android.com/apk/res/android"

android:orientation

=

"vertical"

android:layout_width

=

"fill_parent"

android:layout_height

=

"fill_parent"

>

<

LinearLayout

android:id

=

"@+id/LinearLayout01"

android:layout_width

=

"fill_parent"

android:layout_height

=

"wrap_content"

android:orientation

=

"horizontal"

>

<

Button

android:id

=

"@+id/btnStart"

android:layout_width

=

"80dip"

android:layout_height

=

"wrap_content"

android:text

=

"开始"

/>

<

Button

android:id

=

"@+id/btnExit"

android:layout_width

=

"80dip"

android:layout_height

=

"wrap_content"

android:text

=

"停止"

/>

<

ZoomControls

android:id

=

"@+id/zctlX"

android:layout_width

=

"wrap_content"

android:layout_height

=

"wrap_content"

/>

<

ZoomControls

android:id

=

"@+id/zctlY"

android:layout_width

=

"wrap_content"

android:layout_height

=

"wrap_content"

/>

</

LinearLayout

>

<

SurfaceView

android:id

=

"@+id/SurfaceView01"

android:layout_height

=

"fill_parent"

android:layout_width

=

"fill_parent"

>

</

SurfaceView

>

</

LinearLayout

>

ClsOscilloscope.java是实现示波器的类库,包含AudioRecord操作线程和SurfaceView绘图线程的实现,两个线程同步操作,代码如下:

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162

package

com.testOscilloscope;

import

java.util.ArrayList;

import

android.graphics.Canvas;

import

android.graphics.Color;

import

android.graphics.Paint;

import

android.graphics.Rect;

import

android.media.AudioRecord;

import

android.view.SurfaceView;

public

class

ClsOscilloscope {

private

ArrayList<

short

[]> inBuf = 

new

ArrayList<

short

[]>();

private

boolean

isRecording = 

false

;

// 线程控制标记

public

int

rateX = 

4

;

public

int

rateY = 

4

;

public

int

baseLine = 

;

public

void

initOscilloscope(

int

rateX, 

int

rateY, 

int

baseLine) {

this

.rateX = rateX;

this

.rateY = rateY;

this

.baseLine = baseLine;

}

public

void

Start(AudioRecord audioRecord, 

int

recBufSize, SurfaceView sfv,

Paint mPaint) {

isRecording = 

true

;

new

RecordThread(audioRecord, recBufSize).start();

// 开始录制线程

new

DrawThread(sfv, mPaint).start();

// 开始绘制线程

}

public

void

Stop() {

isRecording = 

false

;

inBuf.clear();

// 清除

}

class

RecordThread 

extends

Thread {

private

int

recBufSize;

private

AudioRecord audioRecord;

public

RecordThread(AudioRecord audioRecord, 

int

recBufSize) {

this

.audioRecord = audioRecord;

this

.recBufSize = recBufSize;

}

public

void

run() {

try

{

short

[] buffer = 

new

short

[recBufSize];

audioRecord.startRecording();

// 开始录制

while

(isRecording) {

// 从MIC保存数据到缓冲区

int

bufferReadResult = audioRecord.read(buffer, 

,

recBufSize);

short

[] tmpBuf = 

new

short

[bufferReadResult / rateX];

for

(

int

i = 

, ii = 

; i < tmpBuf.length; i++, ii = i

* rateX) {

tmpBuf[i] = buffer[ii];

}

synchronized

(inBuf) {

//

inBuf.add(tmpBuf);

// 添加数据

}

}

audioRecord.stop();

catch

(Throwable t) {

}

}

};

class

DrawThread 

extends

Thread {

private

int

oldX = 

;

// 上次绘制的X坐标

private

int

oldY = 

;

// 上次绘制的Y坐标

private

SurfaceView sfv;

// 画板

private

int

X_index = 

;

// 当前画图所在屏幕X轴的坐标

private

Paint mPaint;

// 画笔

public

DrawThread(SurfaceView sfv, Paint mPaint) {

this

.sfv = sfv;

this

.mPaint = mPaint;

}

public

void

run() {

while

(isRecording) {

ArrayList<

short

[]> buf = 

new

ArrayList<

short

[]>();

synchronized

(inBuf) {

if

(inBuf.size() == 

)

continue

;

buf = (ArrayList<

short

[]>) inBuf.clone();

// 保存

inBuf.clear();

// 清除

}

for

(

int

i = 

; i < buf.size(); i++) {

short

[] tmpBuf = buf.get(i);

SimpleDraw(X_index, tmpBuf, rateY, baseLine);

// 把缓冲区数据画出来

X_index = X_index + tmpBuf.length;

if

(X_index > sfv.getWidth()) {

X_index = 

;

}

}

}

}

void

SimpleDraw(

int

start, 

short

[] buffer, 

int

rate, 

int

baseLine) {

if

(start == 

)

oldX = 

;

Canvas canvas = sfv.getHolder().lockCanvas(

new

Rect(start, 

, start + buffer.length, sfv.getHeight()));

// 关键:获取画布

canvas.drawColor(Color.BLACK);

// 清除背景

int

y;

for

(

int

i = 

; i < buffer.length; i++) {

// 有多少画多少

int

x = i + start;

y = buffer[i] / rate + baseLine;

// 调节缩小比例,调节基准线

canvas.drawLine(oldX, oldY, x, y, mPaint);

oldX = x;

oldY = y;

}

sfv.getHolder().unlockCanvasAndPost(canvas);

// 解锁画布,提交画好的图像

}

}

}

testOscilloscope.java是主程序,控制UI和ClsOscilloscope,代码如下:

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145

package

com.testOscilloscope;

import

android.app.Activity;

import

android.graphics.Color;

import

android.graphics.Paint;

import

android.media.AudioFormat;

import

android.media.AudioRecord;

import

android.media.MediaRecorder;

import

android.os.Bundle;

import

android.view.MotionEvent;

import

android.view.SurfaceView;

import

android.view.View;

import

android.view.View.OnTouchListener;

import

android.widget.Button;

import

android.widget.ZoomControls;

public

class

testOscilloscope 

extends

Activity {

Button btnStart, btnExit;

SurfaceView sfv;

ZoomControls zctlX, zctlY;

ClsOscilloscope clsOscilloscope = 

new

ClsOscilloscope();

static

final

int

frequency = 

8000

;

// 分辨率

static

final

int

channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO;

static

final

int

audioEncoding = AudioFormat.ENCODING_PCM_16BIT;

static

final

int

xMax = 

16

;

// X轴缩小比例最大值,X轴数据量巨大,容易产生刷新延时

static

final

int

xMin = 

8

;

// X轴缩小比例最小值

static

final

int

yMax = 

10

;

// Y轴缩小比例最大值

static

final

int

yMin = 

1

;

// Y轴缩小比例最小值

int

recBufSize;

// 录音最小buffer大小

AudioRecord audioRecord;

Paint mPaint;

@Override

public

void

onCreate(Bundle savedInstanceState) {

super

.onCreate(savedInstanceState);

setContentView(R.layout.main);

// 录音组件

recBufSize = AudioRecord.getMinBufferSize(frequency,

channelConfiguration, audioEncoding);

audioRecord = 

new

AudioRecord(MediaRecorder.AudioSource.MIC, frequency,

channelConfiguration, audioEncoding, recBufSize);

// 按键

btnStart = (Button) 

this

.findViewById(R.id.btnStart);

btnStart.setOnClickListener(

new

ClickEvent());

btnExit = (Button) 

this

.findViewById(R.id.btnExit);

btnExit.setOnClickListener(

new

ClickEvent());

// 画板和画笔

sfv = (SurfaceView) 

this

.findViewById(R.id.SurfaceView01);

sfv.setOnTouchListener(

new

TouchEvent());

mPaint = 

new

Paint();

mPaint.setColor(Color.GREEN);

// 画笔为绿色

mPaint.setStrokeWidth(

1

);

// 设置画笔粗细

// 示波 器类库

clsOscilloscope.initOscilloscope(xMax / 

2

, yMax / 

2

,

sfv.getHeight() / 

2

);

// 缩放控件,X轴的数据缩小的比率高些

zctlX = (ZoomControls) 

this

.findViewById(R.id.zctlX);

zctlX.setOnZoomInClickListener(

new

View.OnClickListener() {

@Override

public

void

onClick(View v) {

if

(clsOscilloscope.rateX > xMin)

clsOscilloscope.rateX--;

setTitle(

"X轴缩小"

+ String.valueOf(clsOscilloscope.rateX) +

"倍"

","

"Y轴缩小"

+ String.valueOf(clsOscilloscope.rateY)

"倍"

);

}

});

zctlX.setOnZoomOutClickListener(

new

View.OnClickListener() {

@Override

public

void

onClick(View v) {

if

(clsOscilloscope.rateX < xMax)

clsOscilloscope.rateX++;

setTitle(

"X轴缩小"

+ String.valueOf(clsOscilloscope.rateX) +

"倍"

","

"Y轴缩小"

+ String.valueOf(clsOscilloscope.rateY)

"倍"

);

}

});

zctlY = (ZoomControls) 

this

.findViewById(R.id.zctlY);

zctlY.setOnZoomInClickListener(

new

View.OnClickListener() {

@Override

public

void

onClick(View v) {

if

(clsOscilloscope.rateY > yMin)

clsOscilloscope.rateY--;

setTitle(

"X轴缩小"

+ String.valueOf(clsOscilloscope.rateX) +

"倍"

","

"Y轴缩小"

+ String.valueOf(clsOscilloscope.rateY)

"倍"

);

}

});

zctlY.setOnZoomOutClickListener(

new

View.OnClickListener() {

@Override

public

void

onClick(View v) {

if

(clsOscilloscope.rateY < yMax)

clsOscilloscope.rateY++;

setTitle(

"X轴缩小"

+ String.valueOf(clsOscilloscope.rateX) +

"倍"

","

"Y轴缩小"

+ String.valueOf(clsOscilloscope.rateY)

"倍"

);

}

});

}

@Override

protected

void

onDestroy() {

super

.onDestroy();

android.os.Process.killProcess(android.os.Process.myPid());

}

class

ClickEvent 

implements

View.OnClickListener {

@Override

public

void

onClick(View v) {

if

(v == btnStart) {

clsOscilloscope.baseLine = sfv.getHeight() / 

2

;

clsOscilloscope.Start(audioRecord, recBufSize, sfv, mPaint);

else

if

(v == btnExit) {

clsOscilloscope.Stop();

}

}

}

class

TouchEvent 

implements

OnTouchListener {

@Override

public

boolean

onTouch(View v, MotionEvent event) {

clsOscilloscope.baseLine = (

int

) event.getY();

return

true

;

}

}

}