• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
WavePlotView(Context context, AttributeSet attrs)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. */
initPaints()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 
getZoom()142     public double getZoom() {
143         return mZoomFactorX;
144     }
145 
146 
147     /** Return max zoom out value (> 1.0)/ */
getMaxZoomOut()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 
getMinZoomOut()160     public double getMinZoomOut() {
161         double minZoom = 1.0;
162         return minZoom;
163     }
164 
165 
getOffset()166     public int getOffset() {
167         return mCurrentOffset;
168     }
169 
170 
setZoom(double zoom)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 
setOffset(int sampleOffset, boolean relative)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 
getWindowSamples()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 
refreshGraph()228     public void refreshGraph() {
229         computeViewArray(mZoomFactorX, mCurrentOffset);
230     }
231 
232 
233     @Override
onSizeChanged(int w, int h, int oldw, int oldh)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 
initView()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
onDraw(Canvas canvas)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 
resetArray()405     private void resetArray() {
406         Arrays.fill(mValuesArray, 0);
407         Arrays.fill(mValuesArray2, 0);
408     }
409 
refreshView()410     private void refreshView() {
411         double maxZoom = getMaxZoomOut();
412         setZoom(maxZoom);
413         setOffset(0, false);
414         computeInset();
415         refreshGraph();
416     }
417 
computeInset()418     private 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 
computeViewArray(double zoomFactorX, int sampleOffset)473     private 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     // FIXME why not public?
setData(double[] dataVector, int sampleRate)523     void setData(double[] dataVector, int sampleRate) {
524         if (sampleRate < 1)
525             throw new IllegalArgumentException("sampleRate must be a positive integer");
526 
527         mSamplingRate = sampleRate;
528         mBigDataArray = (dataVector != null ? dataVector : mDefaultDataVector);
529 
530         if (mHasDimensions) { // only refresh the view if it has been initialized already
531             refreshView();
532         }
533     }
534 
535     // also called in LoopbackActivity
redraw()536     void redraw() {
537         invalidate();
538     }
539 
540     @Override
onTouchEvent(MotionEvent event)541     public boolean onTouchEvent(MotionEvent event) {
542         mDetector.onTouchEvent(event);
543         mSGDetector.onTouchEvent(event);
544         //return super.onTouchEvent(event);
545         return true;
546     }
547 
548     class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
549         private static final String DEBUG_TAG = "MyGestureListener";
550         private boolean mInDrag = false;
551 
552         @Override
onDown(MotionEvent event)553         public boolean onDown(MotionEvent event) {
554             Log.d(DEBUG_TAG, "onDown: " + event.toString() + " " + TAG);
555             if (!mScroller.isFinished()) {
556                 mScroller.forceFinished(true);
557                 refreshGraph();
558             }
559             return true;
560         }
561 
562 
563         @Override
onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)564         public boolean onFling(MotionEvent event1, MotionEvent event2,
565                                float velocityX, float velocityY) {
566             Log.d(DEBUG_TAG, "onFling: VelocityX: " + velocityX + "  velocityY:  " + velocityY);
567 
568             mScroller.fling(mCurrentOffset, 0,
569                     (int) (-velocityX * getZoom()),
570                     0, 0, mBigDataArray.length, 0, 0);
571             refreshGraph();
572             return true;
573         }
574 
575 
576         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)577         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
578             setOffset((int) (distanceX * getZoom()), true);
579             refreshGraph();
580             return super.onScroll(e1, e2, distanceX, distanceY);
581         }
582 
583         @Override
onDoubleTap(MotionEvent event)584         public boolean onDoubleTap(MotionEvent event) {
585             Log.d(DEBUG_TAG, "onDoubleTap: " + event.toString());
586 
587             int tappedSample = (int) (event.getX() * getZoom());
588             setZoom(getZoom() / 2);
589             setOffset(tappedSample / 2, true);
590 
591             refreshGraph();
592             return true;
593         }
594 
595         @Override
onLongPress(MotionEvent e)596         public void onLongPress(MotionEvent e) {
597             Vibrator vibe = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
598             if (vibe.hasVibrator()) {
599                 vibe.vibrate(20);
600             }
601             setZoom(getMaxZoomOut());
602             setOffset(0, false);
603             refreshGraph();
604         }
605 
606     }   // MyGestureListener
607 
608     private class MyScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
609         //private static final String DEBUG_TAG = "MyScaleGestureListener";
610         int focusSample = 0;
611 
612         @Override
onScaleBegin(ScaleGestureDetector detector)613         public boolean onScaleBegin(ScaleGestureDetector detector) {
614             focusSample = (int) (detector.getFocusX() * getZoom()) + mCurrentOffset;
615             return super.onScaleBegin(detector);
616         }
617 
618         @Override
onScale(ScaleGestureDetector detector)619         public boolean onScale(ScaleGestureDetector detector) {
620             setZoom(getZoom() / detector.getScaleFactor());
621 
622             int newFocusSample = (int) (detector.getFocusX() * getZoom()) + mCurrentOffset;
623             int sampleDelta = (int) (focusSample - newFocusSample);
624             setOffset(sampleDelta, true);
625             refreshGraph();
626             return true;
627         }
628 
629     }   // MyScaleGestureListener
630 
log(String msg)631     private static void log(String msg) {
632         Log.v(TAG, msg);
633     }
634 
635 }
636