Home | History | Annotate | Download | only in loopback
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package org.drrickorang.loopback;
     18 
     19 import java.util.Arrays;
     20 
     21 import android.content.Context;
     22 import android.graphics.Canvas;
     23 import android.graphics.Paint;
     24 import android.graphics.Path;
     25 import android.graphics.Paint.Style;
     26 import android.os.Vibrator;
     27 import android.util.AttributeSet;
     28 import android.util.Log;
     29 import android.view.GestureDetector;
     30 import android.view.MotionEvent;
     31 import android.view.ScaleGestureDetector;
     32 import android.view.View;
     33 import android.view.animation.LinearInterpolator;
     34 import android.widget.Scroller;
     35 
     36 
     37 /**
     38  * This view is the wave plot shows on the main activity.
     39  */
     40 
     41 public class WavePlotView extends View  {
     42     private static final String TAG = "WavePlotView";
     43 
     44     private double [] mBigDataArray;
     45     private double [] mValuesArray;  //top points to plot
     46     private double [] mValuesArray2; //bottom
     47 
     48     private double [] mInsetArray;
     49     private double [] mInsetArray2;
     50     private int       mInsetSize = 20;
     51 
     52     private double mZoomFactorX = 1.0; //1:1  1 sample / point .  Note: Point != pixel.
     53     private int    mCurrentOffset = 0;
     54     private int    mArraySize = 100; //default size
     55     private int    mSamplingRate;
     56 
     57     private GestureDetector        mDetector;
     58     private ScaleGestureDetector   mSGDetector;
     59     private MyScaleGestureListener mSGDListener;
     60     private Scroller mScroller;
     61 
     62     private int mWidth;
     63     private int mHeight;
     64     private boolean mHasDimensions;
     65 
     66     private Paint mMyPaint;
     67     private Paint mPaintZoomBox;
     68     private Paint mPaintInsetBackground;
     69     private Paint mPaintInsetBorder;
     70     private Paint mPaintInset;
     71     private Paint mPaintGrid;
     72     private Paint mPaintGridText;
     73 
     74     // Default values used when we don't have a valid waveform to display.
     75     // This saves us having to add multiple special cases to handle null waveforms.
     76     private int mDefaultSampleRate = 48000; // chosen because it is common in real world devices
     77     private double[] mDefaultDataVector = new double[mDefaultSampleRate]; // 1 second of fake audio
     78 
     79     public WavePlotView(Context context, AttributeSet attrs) {
     80         super(context, attrs);
     81         mSGDListener = new MyScaleGestureListener();
     82         mDetector = new GestureDetector(context, new MyGestureListener());
     83         mSGDetector = new ScaleGestureDetector(context, mSGDListener);
     84         mScroller = new Scroller(context, new LinearInterpolator(), true);
     85         initPaints();
     86 
     87         // Initialize the value array to 1s silence
     88         mSamplingRate = mDefaultSampleRate;
     89         mBigDataArray = new double[mSamplingRate];
     90         Arrays.fill(mDefaultDataVector, 0);
     91     }
     92 
     93 
     94     /** Initiate all the Paint objects. */
     95     private void initPaints() {
     96         final int COLOR_WAVE = 0xFF1E4A99;
     97         final int COLOR_ZOOM_BOX = 0X50E0E619;
     98         final int COLOR_INSET_BACKGROUND = 0xFFFFFFFF;
     99         final int COLOR_INSET_BORDER = 0xFF002260;
    100         final int COLOR_INSET_WAVE = 0xFF910000;
    101         final int COLOR_GRID = 0x7F002260;
    102         final int COLOR_GRID_TEXT = 0xFF002260;
    103 
    104         mMyPaint = new Paint();
    105         mMyPaint.setColor(COLOR_WAVE);
    106         mMyPaint.setAntiAlias(true);
    107         mMyPaint.setStyle(Style.FILL_AND_STROKE);
    108         mMyPaint.setStrokeWidth(1);
    109 
    110         mPaintZoomBox = new Paint();
    111         mPaintZoomBox.setColor(COLOR_ZOOM_BOX);
    112         mPaintZoomBox.setAntiAlias(true);
    113         mPaintZoomBox.setStyle(Style.FILL);
    114 
    115         mPaintInsetBackground = new Paint();
    116         mPaintInsetBackground.setColor(COLOR_INSET_BACKGROUND);
    117         mPaintInsetBackground.setAntiAlias(true);
    118         mPaintInsetBackground.setStyle(Style.FILL);
    119 
    120         mPaintInsetBorder = new Paint();
    121         mPaintInsetBorder.setColor(COLOR_INSET_BORDER);
    122         mPaintInsetBorder.setAntiAlias(true);
    123         mPaintInsetBorder.setStyle(Style.STROKE);
    124         mPaintInsetBorder.setStrokeWidth(1);
    125 
    126         mPaintInset = new Paint();
    127         mPaintInset.setColor(COLOR_INSET_WAVE);
    128         mPaintInset.setAntiAlias(true);
    129         mPaintInset.setStyle(Style.FILL_AND_STROKE);
    130         mPaintInset.setStrokeWidth(1);
    131 
    132         final int textSize = 25;
    133         mPaintGrid = new Paint(Paint.ANTI_ALIAS_FLAG);
    134         mPaintGrid.setColor(COLOR_GRID); //gray
    135         mPaintGrid.setTextSize(textSize);
    136 
    137         mPaintGridText = new Paint(Paint.ANTI_ALIAS_FLAG);
    138         mPaintGridText.setColor(COLOR_GRID_TEXT); //BLACKgray
    139         mPaintGridText.setTextSize(textSize);
    140     }
    141 
    142     public double getZoom() {
    143         return mZoomFactorX;
    144     }
    145 
    146 
    147     /** Return max zoom out value (> 1.0)/ */
    148     public double getMaxZoomOut() {
    149         double maxZoom = 1.0;
    150 
    151         if (mBigDataArray != null) {
    152             int n = mBigDataArray.length;
    153             maxZoom = ((double) n) / mArraySize;
    154         }
    155 
    156         return maxZoom;
    157     }
    158 
    159 
    160     public double getMinZoomOut() {
    161         double minZoom = 1.0;
    162         return minZoom;
    163     }
    164 
    165 
    166     public int getOffset() {
    167         return mCurrentOffset;
    168     }
    169 
    170 
    171     public void setZoom(double zoom) {
    172         double newZoom = zoom;
    173         double maxZoom = getMaxZoomOut();
    174         double minZoom = getMinZoomOut();
    175 
    176         //foolproof:
    177         if (newZoom < minZoom)
    178             newZoom = minZoom;
    179 
    180         if (newZoom > maxZoom)
    181             newZoom = maxZoom;
    182 
    183         mZoomFactorX = newZoom;
    184         //fix offset if this is the case
    185         setOffset(0, true); //just touch offset in case it needs to be fixed.
    186     }
    187 
    188 
    189     public void setOffset(int sampleOffset, boolean relative) {
    190         int newOffset = sampleOffset;
    191 
    192         if (relative) {
    193             newOffset = mCurrentOffset + sampleOffset;
    194         }
    195 
    196         if (mBigDataArray != null) {
    197             int n = mBigDataArray.length;
    198             //update offset if last sample is more than expected
    199             int lastSample = newOffset + (int)getWindowSamples();
    200             if (lastSample >= n) {
    201                 int delta = lastSample - n;
    202                 newOffset -= delta;
    203             }
    204 
    205             if (newOffset < 0)
    206                 newOffset = 0;
    207 
    208             if (newOffset >= n)
    209                 newOffset = n - 1;
    210 
    211             mCurrentOffset = newOffset;
    212         }
    213     }
    214 
    215 
    216     public double getWindowSamples() {
    217         //samples in current window
    218         double samples = 0;
    219         if (mBigDataArray != null) {
    220             double zoomFactor = getZoom();
    221             samples = mArraySize * zoomFactor;
    222         }
    223 
    224         return samples;
    225     }
    226 
    227 
    228     public void refreshGraph() {
    229         computeViewArray(mZoomFactorX, mCurrentOffset);
    230     }
    231 
    232 
    233     @Override
    234     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    235         mWidth = w;
    236         mHeight = h;
    237         log("New w: " + mWidth + " h: " + mHeight);
    238         mHasDimensions = true;
    239         initView();
    240         refreshView();
    241     }
    242 
    243 
    244     private void initView() {
    245         //re init graphical elements
    246         mArraySize = mWidth;
    247         mInsetSize = mWidth / 5;
    248         mValuesArray = new double[mArraySize];
    249         mValuesArray2 = new double[mArraySize];
    250         Arrays.fill(mValuesArray, 0);
    251         Arrays.fill(mValuesArray2, 0);
    252 
    253         //inset
    254         mInsetArray = new double[mInsetSize];
    255         mInsetArray2 = new double[mInsetSize];
    256         Arrays.fill(mInsetArray, (double) 0);
    257         Arrays.fill(mInsetArray2, (double) 0);
    258     }
    259 
    260 
    261     @Override
    262     protected void onDraw(Canvas canvas) {
    263         super.onDraw(canvas);
    264         boolean showGrid = true;
    265         boolean showInset = true;
    266 
    267         int i;
    268         int w = getWidth();
    269         int h = getHeight();
    270 
    271         double valueMax = 1.0;
    272         double valueMin = -1.0;
    273         double valueRange = valueMax - valueMin;
    274 
    275         //print gridline time in ms/seconds, etc.
    276         if (showGrid) {
    277             //current number of samples in display
    278             double samples = getWindowSamples();
    279             if (samples > 0.0 && mSamplingRate > 0) {
    280                 double windowMs = (1000.0 * samples) / mSamplingRate;
    281 
    282                 //decide the best units: ms, 10ms, 100ms, 1 sec, 2 sec
    283                 double msPerDivision = windowMs / 10;
    284                 log(" windowMS: " + windowMs + " msPerdivision: " + msPerDivision);
    285 
    286                 int divisionInMS = 1;
    287                 //find the best level for markings:
    288                 if (msPerDivision <= 5) {
    289                     divisionInMS = 1;
    290                 } else if (msPerDivision < 15) {
    291                     divisionInMS = 10;
    292                 } else if (msPerDivision < 30) {
    293                     divisionInMS = 20;
    294                 } else if (msPerDivision < 60) {
    295                     divisionInMS = 40;
    296                 } else if (msPerDivision < 150) {
    297                     divisionInMS = 100;
    298                 } else if (msPerDivision < 400) {
    299                     divisionInMS = 200;
    300                 } else if (msPerDivision < 750) {
    301                     divisionInMS = 500;
    302                 } else {
    303                     divisionInMS = 1000;
    304                 }
    305                 log(" chosen Division in MS: " + divisionInMS);
    306 
    307                 //current offset in samples
    308                 int currentOffsetSamples = getOffset();
    309                 double currentOffsetMs = (1000.0 * currentOffsetSamples) / mSamplingRate;
    310                 int gridCount = (int) ((currentOffsetMs + divisionInMS) / divisionInMS);
    311                 double startGridCountFrac = ((currentOffsetMs) % divisionInMS);
    312                 log(" gridCount:" + gridCount + " fraction: " + startGridCountFrac +
    313                     "  firstDivision: " + gridCount * divisionInMS);
    314 
    315                 double currentGridMs = divisionInMS - startGridCountFrac; //in mS
    316                 while (currentGridMs <= windowMs) {
    317                     float newX = (float) (w * currentGridMs / windowMs);
    318                     canvas.drawLine(newX, 0, newX, h, mPaintGrid);
    319 
    320                     double currentGridValueMS = gridCount * divisionInMS;
    321                     String label = String.format("%.0f ms", (float) currentGridValueMS);
    322 
    323                     //path
    324                     Path myPath = new Path();
    325                     myPath.moveTo(newX, h);
    326                     myPath.lineTo(newX, h / 2);
    327 
    328                     canvas.drawTextOnPath(label, myPath, 10, -3, mPaintGridText);
    329 
    330                     //advance
    331                     currentGridMs += divisionInMS;
    332                     gridCount++;
    333                 }
    334 
    335                 //horizontal line
    336                 canvas.drawLine(0, h / 2, w, h / 2, mPaintGrid);
    337             }
    338         }
    339 
    340         float deltaX = (float) w / mArraySize;
    341 
    342         //top
    343         Path myPath = new Path();
    344         myPath.moveTo(0, h / 2); //start
    345 
    346         if (mBigDataArray != null) {
    347             if (getZoom() >= 2) {
    348                 for (i = 0; i < mArraySize; ++i) {
    349                     float top = (float) ((valueMax - mValuesArray[i]) / valueRange) * h;
    350                     float bottom = (float) ((valueMax - mValuesArray2[i]) / valueRange) * h + 1;
    351                     float left = i * deltaX;
    352                     canvas.drawRect(left, top, left + deltaX, bottom, mMyPaint);
    353                 }
    354             } else {
    355                 for (i = 0; i < (mArraySize - 1); ++i) {
    356                     float first = (float) ((valueMax - mValuesArray[i]) / valueRange) * h;
    357                     float second = (float) ((valueMax - mValuesArray[i + 1]) / valueRange) * h;
    358                     float left = i * deltaX;
    359                     canvas.drawLine(left, first, left + deltaX, second, mMyPaint);
    360                 }
    361             }
    362 
    363 
    364             if (showInset) {
    365                 float iW = (float) (w * 0.2);
    366                 float iH = (float) (h * 0.2);
    367                 float iX = (float) (w * 0.7);
    368                 float iY = (float) (h * 0.1);
    369                 //x, y of inset
    370                 canvas.drawRect(iX, iY, iX + iW, iY + iH, mPaintInsetBackground);
    371                 canvas.drawRect(iX - 1, iY - 1, iX + iW + 2, iY + iH + 2, mPaintInsetBorder);
    372                 //paintInset
    373                 float iDeltaX = (float) iW / mInsetSize;
    374 
    375                 for (i = 0; i < mInsetSize; ++i) {
    376                     float top = iY + (float) ((valueMax - mInsetArray[i]) / valueRange) * iH;
    377                     float bottom = iY +
    378                             (float) ((valueMax - mInsetArray2[i]) / valueRange) * iH + 1;
    379                     float left = iX + i * iDeltaX;
    380                     canvas.drawRect(left, top, left + deltaX, bottom, mPaintInset);
    381                 }
    382 
    383                 if (mBigDataArray != null) {
    384                     //paint current region of zoom
    385                     int offsetSamples = getOffset();
    386                     double windowSamples = getWindowSamples();
    387                     int samples = mBigDataArray.length;
    388 
    389                     if (samples > 0) {
    390                         float x1 = (float) (iW * offsetSamples / samples);
    391                         float x2 = (float) (iW * (offsetSamples + windowSamples) / samples);
    392 
    393                         canvas.drawRect(iX + x1, iY, iX + x2, iY + iH, mPaintZoomBox);
    394                     }
    395                 }
    396             }
    397         }
    398         if (mScroller.computeScrollOffset()) {
    399             setOffset(mScroller.getCurrX(), false);
    400             refreshGraph();
    401         }
    402     }
    403 
    404 
    405     void resetArray() {
    406         Arrays.fill(mValuesArray, 0);
    407         Arrays.fill(mValuesArray2, 0);
    408     }
    409 
    410     void refreshView() {
    411         double maxZoom = getMaxZoomOut();
    412         setZoom(maxZoom);
    413         setOffset(0, false);
    414         computeInset();
    415         refreshGraph();
    416     }
    417 
    418     void computeInset() {
    419         if (mBigDataArray != null) {
    420             int sampleCount = mBigDataArray.length;
    421             double pointsPerSample = (double) mInsetSize / sampleCount;
    422 
    423             Arrays.fill(mInsetArray, 0);
    424             Arrays.fill(mInsetArray2, 0);
    425 
    426             double currentIndex = 0; //points.
    427             double max = -1.0;
    428             double min = 1.0;
    429             double maxAbs = 0.0;
    430             int index = 0;
    431 
    432             for (int i = 0; i < sampleCount; i++) {
    433                 double value = mBigDataArray[i];
    434                 if (value > max) {
    435                     max = value;
    436                 }
    437 
    438                 if (value < min) {
    439                     min = value;
    440                 }
    441 
    442                 int prevIndexInt = (int) currentIndex;
    443                 currentIndex += pointsPerSample;
    444                 if ((int) currentIndex > prevIndexInt) { //it switched, time to decide
    445                     mInsetArray[index] = max;
    446                     mInsetArray2[index] = min;
    447 
    448                     if (Math.abs(max) > maxAbs) maxAbs = Math.abs(max);
    449                     if (Math.abs(min) > maxAbs) maxAbs = Math.abs(min);
    450 
    451                     max = -1.0;
    452                     min = 1.0;
    453                     index++;
    454                 }
    455 
    456                 if (index >= mInsetSize)
    457                     break;
    458             }
    459 
    460             //now, normalize
    461             if (maxAbs > 0) {
    462                 for (int i = 0; i < mInsetSize; i++) {
    463                     mInsetArray[i] /= maxAbs;
    464                     mInsetArray2[i] /= maxAbs;
    465 
    466                 }
    467             }
    468 
    469         }
    470     }
    471 
    472 
    473     void computeViewArray(double zoomFactorX, int sampleOffset) {
    474         //zoom factor: how many samples per point. 1.0 = 1.0 samples per point
    475         // sample offset in samples.
    476         if (zoomFactorX < 1.0)
    477             zoomFactorX = 1.0;
    478 
    479         if (mBigDataArray != null) {
    480             int sampleCount = mBigDataArray.length;
    481             double samplesPerPoint = zoomFactorX;
    482             double pointsPerSample = 1.0 / samplesPerPoint;
    483 
    484             resetArray();
    485 
    486             double currentIndex = 0; //points.
    487             double max = -1.0;
    488             double min = 1.0;
    489             int index = 0;
    490 
    491             for (int i = sampleOffset; i < sampleCount; i++) {
    492 
    493                 double value = mBigDataArray[i];
    494                 if (value > max) {
    495                     max = value;
    496                 }
    497 
    498                 if (value < min) {
    499                     min = value;
    500                 }
    501 
    502                 int prevIndexInt = (int) currentIndex;
    503                 currentIndex += pointsPerSample;
    504                 if ((int) currentIndex > prevIndexInt) { //it switched, time to decide
    505                     mValuesArray[index] = max;
    506                     mValuesArray2[index] = min;
    507 
    508                     max = -1.0;
    509                     min = 1.0;
    510                     index++;
    511                 }
    512 
    513                 if (index >= mArraySize)
    514                     break;
    515             }
    516         } //big data array not null
    517 
    518         redraw();
    519     }
    520 
    521 
    522     void setData(double[] dataVector, int sampleRate) {
    523         if (sampleRate < 1)
    524             throw new IllegalArgumentException("sampleRate must be a positive integer");
    525 
    526         mSamplingRate = sampleRate;
    527         mBigDataArray = (dataVector != null ? dataVector : mDefaultDataVector);
    528 
    529         if (mHasDimensions) { // only refresh the view if it has been initialized already
    530             refreshView();
    531         }
    532     }
    533 
    534     void redraw() {
    535         invalidate();
    536     }
    537 
    538     @Override
    539     public boolean onTouchEvent(MotionEvent event) {
    540         mDetector.onTouchEvent(event);
    541         mSGDetector.onTouchEvent(event);
    542         //return super.onTouchEvent(event);
    543         return true;
    544     }
    545 
    546     class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
    547         private static final String DEBUG_TAG = "MyGestureListener";
    548         private boolean mInDrag = false;
    549 
    550         @Override
    551         public boolean onDown(MotionEvent event) {
    552             Log.d(DEBUG_TAG, "onDown: " + event.toString() + " " + TAG);
    553             if(!mScroller.isFinished()) {
    554                 mScroller.forceFinished(true);
    555                 refreshGraph();
    556             }
    557             return true;
    558         }
    559 
    560 
    561         @Override
    562         public boolean onFling(MotionEvent event1, MotionEvent event2,
    563                                float velocityX, float velocityY) {
    564             Log.d(DEBUG_TAG, "onFling: VelocityX: " + velocityX + "  velocityY:  " + velocityY);
    565 
    566             mScroller.fling(mCurrentOffset, 0,
    567                     (int) (-velocityX * getZoom()),
    568                     0, 0, mBigDataArray.length, 0, 0);
    569             refreshGraph();
    570             return true;
    571         }
    572 
    573 
    574         @Override
    575         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    576             setOffset((int) (distanceX * getZoom()), true);
    577             refreshGraph();
    578             return super.onScroll(e1, e2, distanceX, distanceY);
    579         }
    580 
    581         @Override
    582         public boolean onDoubleTap(MotionEvent event) {
    583             Log.d(DEBUG_TAG, "onDoubleTap: " + event.toString());
    584 
    585             int tappedSample = (int) (event.getX() * getZoom());
    586             setZoom(getZoom() / 2);
    587             setOffset(tappedSample / 2, true);
    588 
    589             refreshGraph();
    590             return true;
    591         }
    592 
    593         @Override
    594         public void onLongPress(MotionEvent e) {
    595             Vibrator vibe = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
    596             if (vibe.hasVibrator()) {
    597                 vibe.vibrate(20);
    598             }
    599             setZoom(getMaxZoomOut());
    600             setOffset(0, false);
    601             refreshGraph();
    602         }
    603     }
    604 
    605     private class MyScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    606         private static final String DEBUG_TAG = "MyScaleGestureListener";
    607         int focusSample = 0;
    608 
    609 
    610         @Override
    611         public boolean onScaleBegin(ScaleGestureDetector detector) {
    612             focusSample = (int) (detector.getFocusX() * getZoom()) + mCurrentOffset;
    613             return super.onScaleBegin(detector);
    614         }
    615 
    616         @Override
    617         public boolean onScale(ScaleGestureDetector detector) {
    618             setZoom(getZoom() / detector.getScaleFactor());
    619 
    620             int newFocusSample = (int) (detector.getFocusX() * getZoom()) + mCurrentOffset;
    621             int sampleDelta = (int) (focusSample - newFocusSample);
    622             setOffset(sampleDelta, true);
    623             refreshGraph();
    624             return true;
    625         }
    626     }
    627 
    628     private static void log(String msg) {
    629         Log.v(TAG, msg);
    630     }
    631 
    632 }
    633