Android提高应用篇之模拟信号示波器

来源:网络

点击:2038

A+ A-

所属频道:新闻中心

关键词: Android手机,模拟信号示波器

        本文结合SurfaceView实现一个Android版的手机模拟信号示波器(PS:以前也讲过J2ME版的手机示波器)。最近物联网炒得很火,作为手机软件开发者,如何在不修改手机硬件电路的前提下实现与第三方传感器结合呢?麦克风就是一个很好的ADC接口,通过麦克风与第三方传感器结合,再在软件里对模拟信号做相应的处理,就可以提供更丰富的传感化应用。

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

     

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

    main.xml源码如下:

    view plaincopy to clipboardprint?
    <?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> 
    <?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绘图线程的实现,两个线程同步操作,代码如下:

    view plaincopy to clipboardprint?
    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);// 解锁画布,提交画好的图像  
            }  
        }  

    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,代码如下:

    view plaincopy to clipboardprint?
    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;  
            }  
              
        }  

    (审核编辑: 智汇小新)

    声明:除特别说明之外,新闻内容及图片均来自网络及各大主流媒体。版权归原作者所有。如认为内容侵权,请联系我们删除。