前面簡單地介紹了android程序開發中audiorecord和audiotrack的使用,這次再結合surfaceview實現一個android版的手機模擬信號示波器。最近物聯網炒得很火,作為手機軟件開發者,如何在不修改手機硬件電路的前提下實現與第三方傳感器結合呢?麥克風就是一個很好的adc接口,通過麥克風與第三方傳感器結合,再在軟件里對模擬信號做相應的處理,就可以提供更豐富的傳感化應用。
先來看看本文程序運行的效果圖(屏幕錄像截圖的速度較慢,真機實際運行起來會很流暢):
本文程序使用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
|
<?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_height= "wrap_content" android:layout_width= "fill_parent" android:orientation= "horizontal" > <button android:layout_height= "wrap_content" android:id= "@+id/btnstart" android:text= "開始" android:layout_width= "80dip" ></button> <button android:layout_height= "wrap_content" android:text= "停止" android:id= "@+id/btnexit" android:layout_width= "80dip" ></button> <zoomcontrols android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:id= "@+id/zctlx" ></zoomcontrols> <zoomcontrols android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:id= "@+id/zctly" ></zoomcontrols> </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
|
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 ; // 線程控制標記 /** * x軸縮小的比例 */ public int ratex = 4 ; /** * y軸縮小的比例 */ public int ratey = 4 ; /** * y軸基線 */ public int baseline = 0 ; /** * 初始化 */ public void initoscilloscope( int ratex, int ratey, int baseline) { this .ratex = ratex; this .ratey = ratey; this .baseline = baseline; } /** * 開始 * * @param recbufsize * audiorecord的minbuffersize */ 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(); // 清除 } /** * 負責從mic保存數據到inbuf * * @author gv * */ 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, 0 , recbufsize); short [] tmpbuf = new short [bufferreadresult / ratex]; for ( int i = 0 , ii = 0 ; i < tmpbuf.length; i++, ii = i * ratex) { tmpbuf[i] = buffer[ii]; } synchronized (inbuf) { // inbuf.add(tmpbuf); // 添加數據 } } audiorecord.stop(); } catch (throwable t) { } } }; /** * 負責繪制inbuf中的數據 * * @author gv * */ class drawthread extends thread { private int oldx = 0 ; // 上次繪制的x坐標 private int oldy = 0 ; // 上次繪制的y坐標 private surfaceview sfv; // 畫板 private int x_index = 0 ; // 當前畫圖所在屏幕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() == 0 ) continue ; buf = (arraylist< short []>) inbuf.clone(); // 保存 inbuf.clear(); // 清除 } for ( int i = 0 ; 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 = 0 ; } } } } /** * 繪制指定區域 * * @param start * x軸開始的位置(全屏) * @param buffer * 緩沖區 * @param rate * y軸數據縮小的比例 * @param baseline * y軸基線 */ void simpledraw( int start, short [] buffer, int rate, int baseline) { if (start == 0 ) oldx = 0 ; canvas canvas = sfv.getholder().lockcanvas( new rect(start, 0 , start + buffer.length, sfv.getheight())); // 關鍵:獲取畫布 canvas.drawcolor(color.black); // 清除背景 int y; for ( int i = 0 ; 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
|
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 { /** called when the activity is first created. */ 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()); } /** * 按鍵事件處理 * @author gv * */ 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(); } } } /** * 觸摸屏動態設置波形圖基線 * @author gv * */ class touchevent implements ontouchlistener{ @override public boolean ontouch(view v, motionevent event) { clsoscilloscope.baseline=( int )event.gety(); return true ; } } } |
希望本文實例對于讀者進行android項目開發能起到一定的借鑒與幫助作用。