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