Back to project page audio-analyzer-for-android.
The source code is released under:
Apache License
If you think the Android project audio-analyzer-for-android listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* Copyright 2011 Google Inc. *// www. jav a 2 s. c om *Licensed under the Apache License, Version 2.0 (the "License"); *you may not use this file except in compliance with the License. *You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * *Unless required by applicable law or agreed to in writing, software *distributed under the License is distributed on an "AS IS" BASIS, *WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *See the License for the specific language governing permissions and *limitations under the License. * * @author Stephen Uhler * @author bewantbe@gmail.com */ package com.google.corp.productivity.specialprojects.android.samples.fft; import java.text.DecimalFormat; import java.util.Arrays; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.graphics.Typeface; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.view.View; /** * Custom view to draw the FFT graph */ public class AnalyzeView extends View { private final String TAG = "AnalyzeView::"; private Ready readyCallback = null; // callback to caller when rendering is complete static float DPRatio; private float cursorFreq, cursorDB; // cursor location private float xZoom, yZoom; // horizontal and vertical scaling private float xShift, yShift; // horizontal and vertical translation, in unit 1 unit private float minDB = -144f; // hard lower bound for dB private float maxDB = 12f; // hard upper bound for dB private RectF axisBounds; private double[] tmpSpectrum = new double[0]; private boolean showLines; private int canvasWidth, canvasHeight; // size of my canvas private Paint linePaint, backgroundPaint; private Paint cursorPaint; private Paint gridPaint, rulerBrightPaint; private Paint labelPaint; private Path path; private int[] myLocation = {0, 0}; // window location on screen private Matrix matrix = new Matrix(); private Matrix matrix0 = new Matrix(); private volatile static boolean isBusy = false; private float gridDensity; private double[][] gridPoints2 = new double[2][0]; private double[][] gridPoints2dB = new double[2][0]; private double[][] gridPoints2T = new double[2][0]; private StringBuilder[] gridPoints2Str = new StringBuilder[0]; private StringBuilder[] gridPoints2StrDB = new StringBuilder[0]; private StringBuilder[] gridPoints2StrT = new StringBuilder[0]; private char[][] gridPoints2st = new char[0][]; private char[][] gridPoints2stDB = new char[0][]; private char[][] gridPoints2stT = new char[0][]; public boolean isBusy() { return isBusy; } public AnalyzeView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setup(attrs, context); } public AnalyzeView(Context context, AttributeSet attrs) { super(context, attrs); setup(attrs, context); } public AnalyzeView(Context context) { super(context); setup(null, context); } public void setReady(Ready ready) { this.readyCallback = ready; } private void setup(AttributeSet attrs, Context context) { DPRatio = context.getResources().getDisplayMetrics().density; Log.v(TAG, "setup():"); matrix0.reset(); matrix0.setTranslate(0f, 0f); matrix0.postScale(1f, 1f); path = new Path(); linePaint = new Paint(); linePaint.setColor(Color.RED); linePaint.setStyle(Paint.Style.STROKE); linePaint.setStrokeWidth(0); cursorPaint = new Paint(linePaint); cursorPaint.setColor(Color.BLUE); gridPaint = new Paint(linePaint); gridPaint.setColor(Color.DKGRAY); rulerBrightPaint = new Paint(linePaint); rulerBrightPaint.setColor(Color.rgb(99, 99, 99)); // 99: between Color.DKGRAY and Color.GRAY labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); labelPaint.setColor(Color.GRAY); labelPaint.setTextSize(14.0f * DPRatio); labelPaint.setTypeface(Typeface.MONOSPACE); // or Typeface.SANS_SERIF backgroundPaint = new Paint(); backgroundPaint.setColor(Color.BLACK); cursorFreq = cursorDB = 0f; xZoom=1f; xShift=0f; yZoom=1f; yShift=0f; canvasWidth = canvasHeight = 0; axisBounds = new RectF(0.0f, 0.0f, 8000.0f, -120.0f); gridDensity = 1/85f; // every 85 pixel one grid line, on average Resources res = getResources(); minDB = Float.parseFloat(res.getString(R.string.max_DB_range)); } public void setBounds(RectF bounds) { this.axisBounds = bounds; } public void setBoundsBottom(float b) { this.axisBounds.bottom = b; } public void setLowerBound(double b) { this.dBLowerBound = b; } public RectF getBounds() { return new RectF(axisBounds); } public double getLowerBound() { return dBLowerBound; } public void setShowLines(boolean b) { showLines = b; } // return position of grid lines, there are roughly gridDensity lines for the bigger grid private void genLinearGridPoints(double[][] gridPointsArray, double startValue, double endValue, double gridDensity, int scale_mode) { if (startValue == endValue || Double.isInfinite(startValue+endValue) || Double.isNaN(startValue+endValue)) { Log.e(TAG, "genLinearGridPoints(): startValue == endValue or value invalid"); return; } if (startValue > endValue) { double t = endValue; endValue = startValue; startValue = t; } if (scale_mode == 0 || scale_mode == 2) { if (gridDensity < 3.2) { // 3.2 >= 2 * 5/sqrt(2*5), so that there are at least 2 bigger grid. // The constant here is because: if gridIntervalGuess = sqrt(2*5), then gridIntervalBig = 5 // i.e. grid size become larger by factor 5/sqrt(2*5). // By setting gridDensity = 3.2, we can make sure minimum gridDensity > 2 gridDensity = 3.2; } } else { if (gridDensity < 3.5) { // similar discussion as above gridDensity = 3.5; // 3.5 >= 2 * 3/sqrt(1*3) } } double intervalValue = endValue - startValue; double gridIntervalGuess = intervalValue / gridDensity; double gridIntervalBig; double gridIntervalSmall; // Determine a suitable grid interval from guess if (scale_mode == 0 || scale_mode == 2 || intervalValue <= 1) { // Linear scale (Hz, Time) double exponent = Math.pow(10, Math.floor(Math.log10(gridIntervalGuess))); double fraction = gridIntervalGuess / exponent; // grid interval is 1, 2, 5, 10, ... if (fraction < Math.sqrt(1*2)) { gridIntervalBig = 1; gridIntervalSmall = 0.2; } else if (fraction < Math.sqrt(2*5)) { gridIntervalBig = 2; gridIntervalSmall = 1.0; } else if (fraction < Math.sqrt(5*10)) { gridIntervalBig = 5; gridIntervalSmall = 1; } else { gridIntervalBig = 10; gridIntervalSmall = 2; } gridIntervalBig *= exponent; gridIntervalSmall *= exponent; } else { // dB scale if (gridIntervalGuess > Math.sqrt(36*12)) { gridIntervalBig = 36; gridIntervalSmall = 12; } else if (gridIntervalGuess > Math.sqrt(12*6)) { gridIntervalBig = 12; gridIntervalSmall = 2; } else if (gridIntervalGuess > Math.sqrt(6*3)) { gridIntervalBig = 6; gridIntervalSmall = 1; } else if (gridIntervalGuess > Math.sqrt(3*1)) { gridIntervalBig = 3; gridIntervalSmall = 1; } else { gridIntervalBig = 1; gridIntervalSmall = 1.0/6; } } if (gridPointsArray == null || gridPointsArray.length != 2) { Log.e(TAG, " genLinearGridPoints(): empty array!!"); return; } // Reallocate if number of grid lines are different // Then fill in the gird line coordinates. Assuming the grid lines starting from 0 double gridStartValueBig = Math.ceil(startValue / gridIntervalBig) * gridIntervalBig; int nGrid = (int)Math.floor((endValue - gridStartValueBig) / gridIntervalBig) + 1; if (nGrid != gridPointsArray[0].length) { gridPointsArray[0] = new double[nGrid]; } double[] bigGridPoints = gridPointsArray[0]; for (int i = 0; i < nGrid; i++) { bigGridPoints[i] = gridStartValueBig + i*gridIntervalBig; } double gridStartValueSmall = Math.ceil(startValue / gridIntervalSmall) * gridIntervalSmall; nGrid = (int)Math.floor((endValue - gridStartValueSmall) / gridIntervalSmall) + 1; if (nGrid != gridPointsArray[1].length) { // reallocate space when need gridPointsArray[1] = new double[nGrid]; } double[] smallGridPoints = gridPointsArray[1]; for (int i = 0; i < nGrid; i++) { smallGridPoints[i] = gridStartValueSmall + i*gridIntervalSmall; } } private double[][] oldGridPointBoundaryArray = new double[3][2]; private double[][][] gridPointsArray = {gridPoints2, gridPoints2dB, gridPoints2T}; private StringBuilder[][] gridPointsStrArray = new StringBuilder[3][0]; private char[][][] gridPointsStArray = new char[3][0][]; public enum GridScaleType { // java's enum type is inconvenient FREQ(0), DB(1), TIME(2); private final int value; private GridScaleType(int value) { this.value = value; } public int getValue() { return value; } } // It's so ugly to write these StringBuffer stuff -- in order to reduce garbage // Also, since there is no "pass by reference", modify array is also ugly... void updateGridLabels(double startValue, double endValue, double gridDensity, GridScaleType scale_mode) { int scale_mode_id = scale_mode.getValue(); double[][] gridPoints = gridPointsArray[scale_mode_id]; StringBuilder[] gridPointsStr = gridPointsStrArray[scale_mode_id]; char[][] gridPointsSt = gridPointsStArray[scale_mode_id]; double[] oldGridPointBoundary = oldGridPointBoundaryArray[scale_mode_id]; genLinearGridPoints(gridPoints, startValue, endValue, gridDensity, scale_mode_id); double[] gridPointsBig = gridPoints[0]; boolean needUpdate = false; if (gridPointsBig.length != gridPointsStr.length) { gridPointsStrArray[scale_mode_id] = new StringBuilder[gridPointsBig.length]; gridPointsStr = gridPointsStrArray[scale_mode_id]; for (int i = 0; i < gridPointsBig.length; i++) { gridPointsStr[i] = new StringBuilder(); } gridPointsStArray[scale_mode_id] = new char[gridPointsBig.length][]; // new array of two char array gridPointsSt = gridPointsStArray[scale_mode_id]; for (int i = 0; i < gridPointsBig.length; i++) { gridPointsSt[i] = new char[16]; } switch (scale_mode_id) { case 0: gridPoints2Str = gridPointsStr; gridPoints2st = gridPointsSt; break; case 1: gridPoints2StrDB = gridPointsStr; gridPoints2stDB = gridPointsSt; break; case 2: gridPoints2StrT = gridPointsStr; gridPoints2stT = gridPointsSt; break; } needUpdate = true; } if (gridPointsBig.length > 0 && (needUpdate || gridPointsBig[0] != oldGridPointBoundary[0] || gridPointsBig[gridPointsBig.length-1] != oldGridPointBoundary[1])) { oldGridPointBoundary[0] = gridPointsBig[0]; oldGridPointBoundary[1] = gridPointsBig[gridPointsBig.length-1]; for (int i = 0; i < gridPointsStr.length; i++) { gridPointsStr[i].setLength(0); if (gridPointsBig[1] - gridPointsBig[0] >= 1) { SBNumFormat.fillInNumFixedFrac(gridPointsStr[i], gridPointsBig[i], 7, 0); } else if (gridPointsBig[1] - gridPointsBig[0] >= 0.1) { SBNumFormat.fillInNumFixedFrac(gridPointsStr[i], gridPointsBig[i], 7, 1); } else { SBNumFormat.fillInNumFixedFrac(gridPointsStr[i], gridPointsBig[i], 7, 2); } gridPointsStr[i].getChars(0, gridPointsStr[i].length(), gridPointsSt[i], 0); } } } // Map a coordinate in frame of axis to frame of canvas c (in pixel unit) float canvasX4axis(float x) { return (x - axisBounds.left) / axisBounds.width() * canvasWidth; } float canvasY4axis(float y) { return (y - axisBounds.top) / axisBounds.height() * canvasHeight; } // Map a coordinate in frame of axis to frame of view id=plot (in pixel unit) float canvasViewX4axis(float x) { return ((x - axisBounds.left) / axisBounds.width() - xShift) * xZoom * canvasWidth; } float canvasViewY4axis(float y) { return ((y - axisBounds.top) / axisBounds.height() - yShift) * yZoom * canvasHeight; } float axisX4canvasView(float x) { return axisBounds.width() * (xShift + x / canvasWidth / xZoom) + axisBounds.left; } float axisY4canvasView(float y) { return axisBounds.height() * (yShift + y / canvasHeight / yZoom) + axisBounds.top; } private void drawGridLines(Canvas c, float nx, float ny) { updateGridLabels(getFreqMin(), getFreqMax(), nx, GridScaleType.FREQ); for(int i = 0; i < gridPoints2[0].length; i++) { float xPos = canvasViewX4axis((float)gridPoints2[0][i]); c.drawLine(xPos, 0, xPos, canvasHeight, gridPaint); } for(int i = 0; i < gridPoints2[1].length; i++) { float xPos = canvasViewX4axis((float)gridPoints2[1][i]); c.drawLine(xPos, 0, xPos, 0.02f * canvasHeight, gridPaint); } updateGridLabels(getMinY(), getMaxY(), ny, GridScaleType.DB); for(int i = 0; i < gridPoints2dB[0].length; i++) { float yPos = canvasViewY4axis((float)gridPoints2dB[0][i]); c.drawLine(0, yPos, canvasWidth, yPos, gridPaint); } for(int i = 0; i < gridPoints2dB[1].length; i++) { float yPos = canvasViewY4axis((float)gridPoints2dB[1][i]); c.drawLine(0, yPos, 0.02f * canvasWidth, yPos, gridPaint); } } private float getLabelBeginY() { float textHeigh = labelPaint.getFontMetrics(null); float labelLaegeLen = 0.5f * textHeigh; if (!showFreqAlongX && !bShowTimeAxis) { return canvasHeight; } else { return canvasHeight - 0.6f*labelLaegeLen - textHeigh; } } private float getLabelBeginX() { float textHeigh = labelPaint.getFontMetrics(null); float labelLaegeLen = 0.5f * textHeigh; if (showFreqAlongX) { if (bShowTimeAxis) { int j = 3; for (int i = 0; i < gridPoints2StrT.length; i++) { if (j < gridPoints2StrT[i].length()) { j = gridPoints2StrT[i].length(); } } return 0.6f*labelLaegeLen + j*0.5f*textHeigh; } else { return 0; } } else { return 0.6f*labelLaegeLen + 2.5f*textHeigh; } } static final String[] axisLabels = {"Hz", "dB", "Sec"}; // Draw axis, start from (labelBeginX, labelBeginY) in the canvas coordinate // drawOnXAxis == true : draw on X axis, otherwise Y axis private void drawAxis(Canvas c, float labelBeginX, float labelBeginY, float ng, boolean drawOnXAxis, float axisMin, float axisMax, GridScaleType scale_mode) { int scale_mode_id = scale_mode.getValue(); float canvasMin; float canvasMax; if (drawOnXAxis) { canvasMin = labelBeginX; canvasMax = canvasWidth; } else { canvasMin = labelBeginY; canvasMax = 0; } updateGridLabels(axisMin, axisMax, ng, scale_mode); String axisLabel = axisLabels[scale_mode_id]; double[][] gridPoints = gridPointsArray[scale_mode_id]; StringBuilder[] gridPointsStr = gridPointsStrArray[scale_mode_id]; char[][] gridPointsSt = gridPointsStArray[scale_mode_id]; // plot axis mark float posAlongAxis; float textHeigh = labelPaint.getFontMetrics(null); float labelLargeLen = 0.5f * textHeigh; float labelSmallLen = 0.6f*labelLargeLen; for(int i = 0; i < gridPoints[1].length; i++) { posAlongAxis =((float)gridPoints[1][i] - axisMin) / (axisMax-axisMin) * (canvasMax - canvasMin) + canvasMin; if (drawOnXAxis) { c.drawLine(posAlongAxis, labelBeginY, posAlongAxis, labelBeginY+labelSmallLen, gridPaint); } else { c.drawLine(labelBeginX-labelSmallLen, posAlongAxis, labelBeginX, posAlongAxis, gridPaint); } } for(int i = 0; i < gridPoints[0].length; i++) { posAlongAxis = ((float)gridPoints[0][i] - axisMin) / (axisMax-axisMin) * (canvasMax - canvasMin) + canvasMin; if (drawOnXAxis) { c.drawLine(posAlongAxis, labelBeginY, posAlongAxis, labelBeginY+labelLargeLen, rulerBrightPaint); } else { c.drawLine(labelBeginX-labelLargeLen, posAlongAxis, labelBeginX, posAlongAxis, rulerBrightPaint); } } if (drawOnXAxis) { c.drawLine(canvasMin, labelBeginY, canvasMax, labelBeginY, labelPaint); } else { c.drawLine(labelBeginX, canvasMin, labelBeginX, canvasMax, labelPaint); } // plot labels float widthDigit = labelPaint.measureText("0"); float posOffAxis = labelBeginY + 0.3f*labelLargeLen + textHeigh; for(int i = 0; i < gridPointsStr.length; i++) { posAlongAxis = ((float)gridPoints[0][i] - axisMin) / (axisMax-axisMin) * (canvasMax - canvasMin) + canvasMin; if (drawOnXAxis) { if (posAlongAxis + widthDigit * gridPointsStr[i].length() > canvasWidth - (axisLabel.length() + .3f)*widthDigit) { continue; } c.drawText(gridPointsSt[i], 0, gridPointsStr[i].length(), posAlongAxis, posOffAxis, labelPaint); } else { if (posAlongAxis - 0.5f*textHeigh < canvasMax + textHeigh) { continue; } c.drawText(gridPointsSt[i], 0, gridPointsStr[i].length(), labelBeginX - widthDigit * gridPointsStr[i].length() - 0.5f * labelLargeLen, posAlongAxis, labelPaint); } } if (drawOnXAxis) { c.drawText(axisLabel, canvasWidth - (axisLabel.length() +.3f)*widthDigit, posOffAxis, labelPaint); } else { c.drawText(axisLabel, labelBeginX - widthDigit * axisLabel.length() - 0.5f * labelLargeLen, canvasMax+textHeigh, labelPaint); } } // Draw frequency axis for spectrogram // Working in the original canvas frame // nx: number of grid lines on average private void drawFreqAxis(Canvas c, float labelBeginX, float labelBeginY, float nx, boolean drawOnXAxis) { drawAxis(c, labelBeginX, labelBeginY, nx, drawOnXAxis, getFreqMin(), getFreqMax(), GridScaleType.FREQ); } private float getTimeMin() { if (showMode == 0) { return 0; } if (showFreqAlongX) { return yShift * (float) timeWatch * timeMultiplier; } else { return xShift * (float) timeWatch * timeMultiplier; } } private float getTimeMax() { if (showMode == 0) { return 0; } if (showFreqAlongX) { return (yShift + 1/yZoom) * (float) timeWatch * timeMultiplier; } else { return (xShift + 1/xZoom) * (float) timeWatch * timeMultiplier; } } // Draw time axis for spectrogram // Working in the original canvas frame private void drawTimeAxis(Canvas c, float labelBeginX, float labelBeginY, float nt, boolean drawOnXAxis) { if (showFreqAlongX ^ (showModeSpectrogram == 0)) { drawAxis(c, labelBeginX, labelBeginY, nt, drawOnXAxis, getTimeMax(), getTimeMin(), GridScaleType.TIME); } else { drawAxis(c, labelBeginX, labelBeginY, nt, drawOnXAxis, getTimeMin(), getTimeMax(), GridScaleType.TIME); } } // The coordinate frame of this function is identical to its view (id=plot). private void drawGridLabels(Canvas c) { float textHeigh = labelPaint.getFontMetrics(null); float widthHz = labelPaint.measureText("Hz"); float widthDigit = labelPaint.measureText("0"); float xPos, yPos; yPos = textHeigh; for(int i = 0; i < gridPoints2Str.length; i++) { xPos = canvasViewX4axis((float)gridPoints2[0][i]); if (xPos + widthDigit*gridPoints2Str[i].length() + 1.5f*widthHz> canvasWidth) { continue; } c.drawText(gridPoints2st[i], 0, gridPoints2Str[i].length(), xPos, yPos, labelPaint); } c.drawLine(0, 0, canvasWidth, 0, labelPaint); c.drawText("Hz", canvasWidth - 1.3f*widthHz, yPos, labelPaint); xPos = 0.4f*widthHz; for(int i = 0; i < gridPoints2StrDB.length; i++) { yPos = canvasViewY4axis((float)gridPoints2dB[0][i]); if (yPos + 1.3f*widthHz > canvasHeight) continue; c.drawText(gridPoints2stDB[i], 0, gridPoints2StrDB[i].length(), xPos, yPos, labelPaint); } c.drawLine(0, 0, 0, canvasHeight, labelPaint); c.drawText("dB", xPos, canvasHeight - 0.4f*widthHz, labelPaint); } private float clampDB(float value) { if (value < minDB || Double.isNaN(value)) { value = minDB; } return value; } private void saveSpectrum(double[] db) { if (tmpSpectrum == null || tmpSpectrum.length != db.length) { tmpSpectrum = new double[db.length]; } System.arraycopy(db, 0, tmpSpectrum, 0, db.length); } /** * Re-plot the spectrum */ public void replotRawSpectrum(double[] db) { saveSpectrum(db); if (canvasHeight < 1) { return; } isBusy = true; // long t = SystemClock.uptimeMillis(); if (showMode == 0) { float minYcanvas = canvasY4axis(minDB); path.reset(); if (!showLines) { for (int i = 1; i < db.length; i++) { float x = (float) i / (db.length-1) * canvasWidth; float y = canvasY4axis(clampDB((float)db[i])); if (y != canvasHeight) { //path.moveTo(x, canvasHeight); path.moveTo(x, minYcanvas); path.lineTo(x, y); } } } else { // (0,0) is the upper left of the View, in pixel unit path.moveTo((float) 1 / (db.length-1) * canvasWidth, canvasY4axis(clampDB((float)db[1]))); for (int i = 1+1; i < db.length; i++) { float x = (float) i / (db.length-1) * canvasWidth; float y = canvasY4axis(clampDB((float)db[i])); path.lineTo(x, y); } } } else { //use pushRawSpectrum(db); } // Log.i(TAG, " replotRawSpectrum: dt = " + (SystemClock.uptimeMillis() - t) + " ms"); isBusy = false; } private boolean intersects(float x, float y) { getLocationOnScreen(myLocation); return x >= myLocation[0] && y >= myLocation[1] && x < myLocation[0] + getWidth() && y < myLocation[1] + getHeight(); } // return true if the coordinate (x,y) is inside graphView public boolean setCursor(float x, float y) { if (intersects(x, y)) { x = x - myLocation[0]; y = y - myLocation[1]; // Convert to coordinate in axis if (showMode == 0) { cursorFreq = axisX4canvasView(x); // frequency cursorDB = axisY4canvasView(y); // decibel } else { cursorDB = 0; // disabled if (showFreqAlongX) { cursorFreq = axisBounds.width() * (xShift + (x-labelBeginX)/(canvasWidth-labelBeginX)/xZoom); // frequency } else { cursorFreq = axisBounds.width() * (1 - yShift - y/labelBeginY/yZoom); // frequency } if (cursorFreq < 0) { cursorFreq = 0; } } return true; } else { return false; } } public float getCursorFreq() { return canvasWidth == 0 ? 0 : cursorFreq; } public float getCursorDB() { if (showMode == 0) { return canvasHeight == 0 ? 0 : cursorDB; } else { return 0; } } // In the original canvas view frame private void drawCursor(Canvas c) { float cX, cY; if (showMode == 0) { cX = canvasViewX4axis(cursorFreq); cY = canvasViewY4axis(cursorDB); if (cursorFreq != 0) { c.drawLine(cX, 0, cX, canvasHeight, cursorPaint); } if (cursorDB != 0) { c.drawLine(0, cY, canvasWidth, cY, cursorPaint); } } else { // Show only the frequency cursor if (showFreqAlongX) { cX = (cursorFreq / axisBounds.width() - xShift) * xZoom * (canvasWidth-labelBeginX) + labelBeginX; if (cursorFreq != 0) { c.drawLine(cX, 0, cX, labelBeginY, cursorPaint); } } else { cY = (1 - yShift - cursorFreq / axisBounds.width()) * yZoom * labelBeginY; if (cursorFreq != 0) { c.drawLine(labelBeginX, cY, canvasWidth, cY, cursorPaint); } } } } // In axis frame public float getMaxY() { return canvasHeight == 0 ? 0 : axisBounds.height() * yShift; } public float getMinY() { return canvasHeight == 0 ? 0 : axisBounds.height() * (yShift + 1 / yZoom); } public float getFreqMax() { if (showMode == 0 || showFreqAlongX) { return axisBounds.width() * (xShift + 1 / xZoom); } else { return axisBounds.width() * (1 - yShift); } } public float getFreqMin() { if (showMode == 0 || showFreqAlongX) { return axisBounds.width() * xShift; } else { return axisBounds.width() * (1 - yShift - 1/yZoom); } } public float getXZoom() { return xZoom; } public float getYZoom() { return yZoom; } public float getXShift() { return xShift; } public float getYShift() { return yShift; } public float getCanvasWidth() { if (showMode == 0) { return canvasWidth; } else { return canvasWidth - labelBeginX; } } public float getCanvasHeight() { if (showMode == 0) { return canvasHeight; } else { return labelBeginY; } } private float clamp(float x, float min, float max) { if (x > max) { return max; } else if (x < min) { return min; } else { return x; } } private float clampXShift(float offset) { return clamp(offset, 0f, 1 - 1 / xZoom); } private float clampYShift(float offset) { if (showMode == 0) { // limit to minDB ~ maxDB return clamp(offset, (maxDB - axisBounds.top) / axisBounds.height(), (minDB - axisBounds.top) / axisBounds.height() - 1 / yZoom); } else { // strict restrict, y can be frequency or time. // - 0.25f/canvasHeight so we can see "0" for sure return clamp(offset, 0f, 1 - (1 - 0.25f/canvasHeight) / yZoom); } } public void setXShift(float offset) { xShift = clampXShift(offset); } public void setYShift(float offset) { yShift = clampYShift(offset); } public void resetViewScale() { xShift = 0; xZoom = 1; yShift = 0; yZoom = 1; } private float xMidOld = 100; private float xDiffOld = 100; private float xZoomOld = 1; private float xShiftOld = 0; private float yMidOld = 100; private float yDiffOld = 100; private float yZoomOld = 1; private float yShiftOld = 0; // record the coordinate frame state when starting scaling public void setShiftScaleBegin(float x1, float y1, float x2, float y2) { xMidOld = (x1+x2)/2f; xDiffOld = Math.abs(x1-x2); xZoomOld = xZoom; xShiftOld = xShift; yMidOld = (y1+y2)/2f; yDiffOld = Math.abs(y1-y2); yZoomOld = yZoom; yShiftOld = yShift; } // Do the scaling according to the motion event getX() and getY() (getPointerCount()==2) public void setShiftScale(float x1, float y1, float x2, float y2) { float limitXZoom; float limitYZoom; if (showMode == 0) { limitXZoom = axisBounds.width()/200f; limitYZoom = -axisBounds.height()/6f; } else { if (showFreqAlongX) { limitXZoom = axisBounds.width()/200f; limitYZoom = nTimePoints>10 ? nTimePoints / 10 : 1; } else { limitXZoom = nTimePoints>10 ? nTimePoints / 10 : 1; limitYZoom = axisBounds.width()/200f; } } if (canvasWidth*0.13f < xDiffOld) { // if fingers are not very close in x direction, do scale in x direction // limit to 200Hz one screen xZoom = clamp(xZoomOld * Math.abs(x1-x2)/xDiffOld, 1f, limitXZoom); } xShift = clampXShift(xShiftOld + (xMidOld/xZoomOld - (x1+x2)/2f/xZoom) / canvasWidth); if (canvasHeight*0.13f < yDiffOld) { // if fingers are not very close in y direction, do scale in y direction // limit to 6dB one screen yZoom = clamp(yZoomOld * Math.abs(y1-y2)/yDiffOld, 1f, limitYZoom); } yShift = clampYShift(yShiftOld + (yMidOld/yZoomOld - (y1+y2)/2f/yZoom) / canvasHeight); } private void computeMatrix() { matrix.reset(); matrix.setTranslate(-xShift*canvasWidth, -yShift*canvasHeight); matrix.postScale(xZoom, yZoom); } @Override protected void onSizeChanged (int w, int h, int oldw, int oldh) { isBusy = true; this.canvasHeight = h; this.canvasWidth = w; Log.i(TAG, "onSizeChanged(): canvas (" + oldw + "," + oldh + ") -> (" + w + "," + h + ")"); if (h > 0 && readyCallback != null) { readyCallback.ready(); } if (showMode == 0 && tmpSpectrum != null && tmpSpectrum.length > 1) { replotRawSpectrum(tmpSpectrum); } isBusy = false; } private int[] spectrogramColors = new int[0]; // int:ARGB, nFreqPoints columns, nTimePoints rows private int[] spectrogramColorsShifting; // temporarily of spectrogramColors for shifting mode private int showMode = 0; // 0: Spectrum, 1:Spectrogram private int showModeSpectrogram = 1; // 0: moving (shifting) spectrogram, 1: overwriting in loop private boolean showFreqAlongX = false; private int nFreqPoints; private double timeWatch = 4.0; private volatile int timeMultiplier = 1; // should be accorded with nFFTAverage in AnalyzeActivity private boolean bShowTimeAxis = true; private int nTimePoints; private int spectrogramColorsPt; // pointer to the row to be filled (row major) private Matrix matrixSpectrogram = new Matrix(); private static final int[] cma = ColorMapArray.hot; private double dBLowerBound = -120; private Paint smoothBmpPaint; private float labelBeginX, labelBeginY; public int getShowMode() { return showMode; } public void setTimeMultiplier(int nAve) { timeMultiplier = nAve; } public void setShowTimeAxis(boolean bSTA) { bShowTimeAxis = bSTA; } public void setSpectrogramModeShifting(boolean b) { if (b) { showModeSpectrogram = 0; } else { showModeSpectrogram = 1; } } public void setShowFreqAlongX(boolean b) { if (showMode == 1 && showFreqAlongX != b) { // match zooming float t; if (showFreqAlongX) { t = xShift; xShift = yShift; yShift = 1 - t - 1/xZoom; } else { t = yShift; yShift = xShift; xShift = 1 - t - 1/yZoom; } t = xZoom; xZoom = yZoom; yZoom = t; } showFreqAlongX = b; } public void setSmoothRender(boolean b) { if (b) { smoothBmpPaint = new Paint(Paint.FILTER_BITMAP_FLAG); } else { smoothBmpPaint = null; } } float oldYShift = 0; float oldXShift = 0; float oldYZoom = 1; float oldXZoom = 1; public void switch2Spectrum() { Log.v(TAG, "switch2Spectrum()"); if (showMode == 0) { return; } // execute when switch from Spectrogram mode to Spectrum mode showMode = 0; if (showFreqAlongX) { //< the frequency range is the same } else { // get frequency range xShift = 1 - yShift - 1/yZoom; xZoom = yZoom; } yShift = oldYShift; yZoom = oldYZoom; if (tmpSpectrum != null && tmpSpectrum.length > 1) { replotRawSpectrum(tmpSpectrum); } } public void switch2Spectrogram(int sampleRate, int fftLen, double timeDurationE) { if (showMode == 0 && canvasHeight > 0) { // canvasHeight==0 means the program is just start oldXShift = xShift; oldXZoom = xZoom; oldYShift = yShift; oldYZoom = yZoom; if (showFreqAlongX) { //< no need to change x scaling yZoom = 1; yShift = 0; } else { yZoom = xZoom; yShift = 1 - 1/yZoom - xShift; xZoom = 1; xShift = 0; } } setupSpectrogram(sampleRate, fftLen, timeDurationE); showMode = 1; } public void setupSpectrogram(int sampleRate, int fftLen, double timeDurationE) { timeWatch = timeDurationE; double timeInc = fftLen / 2.0 / sampleRate; // time of each slice. /2.0 due to overlap window synchronized (this) { boolean bNeedClean = nFreqPoints != fftLen / 2; nFreqPoints = fftLen / 2; // no direct current term nTimePoints = (int)Math.ceil(timeWatch / timeInc); if (spectrogramColors == null || spectrogramColors.length != nFreqPoints * nTimePoints) { spectrogramColors = new int[nFreqPoints * nTimePoints]; spectrogramColorsShifting = new int[nFreqPoints * nTimePoints]; bNeedClean = true; } if (bNeedClean) { spectrogramColorsPt = 0; Arrays.fill(spectrogramColors, 0); } if (spectrogramColorsPt >= nTimePoints) { Log.w(TAG, "setupSpectrogram(): Should not happend!!"); spectrogramColorsPt = 0; } } Log.i(TAG, "setupSpectrogram() is ready"+ "\n sampleRate = " + sampleRate + "\n fftLen = " + fftLen + "\n timeDurationE = " + timeDurationE); } public int colorFromDB(double d) { if (d >= 0) { return cma[0]; } if (d <= dBLowerBound || Double.isInfinite(d) || Double.isNaN(d)) { return cma[cma.length-1]; } return cma[(int)(cma.length * d / dBLowerBound)]; } public void pushRawSpectrum(double[] db) { isBusy = true; saveSpectrum(db); synchronized (this) { int c; int pRef; double d; pRef = spectrogramColorsPt*nFreqPoints - 1; for (int i = 1; i < db.length; i++) { // no direct current term d = db[i]; if (d >= 0) { c = cma[0]; } else if (d <= dBLowerBound || Double.isInfinite(d) || Double.isNaN(d)) { c = cma[cma.length-1]; } else { c = cma[(int)(cma.length * d / dBLowerBound)]; } spectrogramColors[pRef + i] = c; } spectrogramColorsPt++; if (spectrogramColorsPt >= nTimePoints) { spectrogramColorsPt = 0; } } isBusy = false; } // FramesPerSecondCounter fpsCounter = new FramesPerSecondCounter("View"); // long t_old; @Override protected void onDraw(Canvas c) { // fpsCounter.inc(); // Log.i(TAG, " onDraw last call dt = " + (t - t_old)); // t_old = t; isBusy = true; c.concat(matrix0); c.save(); if (showMode == 0) { drawGridLines(c, canvasWidth * gridDensity / DPRatio, canvasHeight * gridDensity / DPRatio); computeMatrix(); c.concat(matrix); c.drawPath(path, linePaint); c.restore(); drawCursor(c); drawGridLabels(c); } else { labelBeginX = getLabelBeginX(); // this seems will make the scaling gesture inaccurate labelBeginY = getLabelBeginY(); // show Spectrogram float halfFreqResolutionShift; // move the color patch to match the center frequency matrixSpectrogram.reset(); if (showFreqAlongX) { // when xZoom== 1: nFreqPoints -> canvasWidth; 0 -> labelBeginX matrixSpectrogram.postScale(xZoom*(canvasWidth-labelBeginX)/nFreqPoints, yZoom*labelBeginY/nTimePoints); halfFreqResolutionShift = xZoom*(canvasWidth-labelBeginX)/nFreqPoints/2; matrixSpectrogram.postTranslate(labelBeginX - xShift*xZoom*(canvasWidth-labelBeginX) + halfFreqResolutionShift, -yShift*yZoom*labelBeginY); } else { // postRotate() will make c.drawBitmap about 20% slower, don't know why matrixSpectrogram.postRotate(-90); matrixSpectrogram.postScale(xZoom*(canvasWidth-labelBeginX)/nTimePoints, yZoom*labelBeginY/nFreqPoints); // (1-yShift) is relative position of shift (after rotation) // yZoom*labelBeginY is canvas length in frequency direction in pixel unit halfFreqResolutionShift = yZoom*labelBeginY/nFreqPoints/2; matrixSpectrogram.postTranslate(labelBeginX - xShift*xZoom*(canvasWidth-labelBeginX), (1-yShift)*yZoom*labelBeginY - halfFreqResolutionShift); } c.concat(matrixSpectrogram); // public void drawBitmap (int[] colors, int offset, int stride, float x, float y, // int width, int height, boolean hasAlpha, Paint paint) // long t = SystemClock.uptimeMillis(); synchronized (this) { if (showModeSpectrogram == 0) { System.arraycopy(spectrogramColors, 0, spectrogramColorsShifting, (nTimePoints-spectrogramColorsPt)*nFreqPoints, spectrogramColorsPt*nFreqPoints); System.arraycopy(spectrogramColors, spectrogramColorsPt*nFreqPoints, spectrogramColorsShifting, 0, (nTimePoints-spectrogramColorsPt)*nFreqPoints); c.drawBitmap(spectrogramColorsShifting, 0, nFreqPoints, 0, 0, nFreqPoints, nTimePoints, false, smoothBmpPaint); } else { c.drawBitmap(spectrogramColors, 0, nFreqPoints, 0, 0, nFreqPoints, nTimePoints, false, smoothBmpPaint); } } // Log.i(TAG, " onDraw: dt = " + (SystemClock.uptimeMillis() - t) + " ms"); if (showModeSpectrogram == 1) { c.drawLine(0, spectrogramColorsPt, nFreqPoints, spectrogramColorsPt, cursorPaint); } c.restore(); drawCursor(c); if (showFreqAlongX) { c.drawRect(0, labelBeginY, canvasWidth, canvasHeight, backgroundPaint); drawFreqAxis(c, labelBeginX, labelBeginY, canvasWidth * gridDensity / DPRatio, showFreqAlongX); if (labelBeginX > 0) { c.drawRect(0, 0, labelBeginX, labelBeginY, backgroundPaint); drawTimeAxis(c, labelBeginX, labelBeginY, canvasHeight * gridDensity / DPRatio, !showFreqAlongX); } } else { c.drawRect(0, 0, labelBeginX, labelBeginY, backgroundPaint); drawFreqAxis(c, labelBeginX, labelBeginY, canvasHeight * gridDensity / DPRatio, showFreqAlongX); if (labelBeginY != canvasHeight) { c.drawRect(0, labelBeginY, canvasWidth, canvasHeight, backgroundPaint); drawTimeAxis(c, labelBeginX, labelBeginY, canvasWidth * gridDensity / DPRatio, !showFreqAlongX); } } } isBusy = false; } /* * Save the labels, cursors, and bounds */ @Override protected Parcelable onSaveInstanceState() { Parcelable parentState = super.onSaveInstanceState(); State state = new State(parentState); state.cx = cursorFreq; state.cy = cursorDB; state.xZ = xZoom; state.yZ = yZoom; state.OyZ = oldYZoom; state.xS = xShift; state.yS = yShift; state.OyS = oldYShift; state.bounds = axisBounds; state.nfq = tmpSpectrum.length; state.tmpS = tmpSpectrum; state.nsc = spectrogramColors.length; state.nFP = nFreqPoints; state.nSCP = spectrogramColorsPt; state.tmpSC = spectrogramColors; Log.i("onSaveInstanceState()", "xShift = " + xShift + " xZoom = " + xZoom + " yShift = " + yShift + " yZoom = " + yZoom); return state; } // maybe we could save the whole view in main activity @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof State) { State s = (State) state; super.onRestoreInstanceState(s.getSuperState()); this.cursorFreq = s.cx; this.cursorDB = s.cy; this.xZoom = s.xZ; this.yZoom = s.yZ; this.oldYZoom = s.OyZ; this.xShift = s.xS; this.yShift = s.yS; this.oldYShift = s.OyS; this.axisBounds = s.bounds; this.tmpSpectrum = s.tmpS; this.nFreqPoints = s.nFP; this.spectrogramColorsPt = s.nSCP; this.spectrogramColors = s.tmpSC; this.spectrogramColorsShifting = new int[this.spectrogramColors.length]; Log.i("onRestoreInstanceState()", "xShift = " + xShift + " xZoom = " + xZoom + " yShift = " + yShift + " yZoom = " + yZoom); } else { super.onRestoreInstanceState(state); } } public static interface Ready { public void ready(); } public static class State extends BaseSavedState { float cx, cy; float xZ, yZ, OyZ; float xS, yS, OyS; RectF bounds; int nfq; double[] tmpS; int nsc; // size of tmpSC int nFP; int nSCP; int[] tmpSC; State(Parcelable state) { super(state); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeFloat(cx); out.writeFloat(cy); out.writeFloat(xZ); out.writeFloat(yZ); out.writeFloat(OyZ); out.writeFloat(xS); out.writeFloat(yS); out.writeFloat(OyS); bounds.writeToParcel(out, flags); out.writeInt(nfq); out.writeDoubleArray(tmpS); out.writeInt(nsc); out.writeInt(nFP); out.writeInt(nSCP); out.writeIntArray(tmpSC); } public static final Parcelable.Creator<State> CREATOR = new Parcelable.Creator<State>() { @Override public State createFromParcel(Parcel in) { return new State(in); } @Override public State[] newArray(int size) { return new State[size]; } }; private State(Parcel in) { super(in); cx = in.readFloat(); cy = in.readFloat(); xZ = in.readFloat(); yZ = in.readFloat(); OyZ = in.readFloat(); xS = in.readFloat(); yS = in.readFloat(); OyS = in.readFloat(); bounds = RectF.CREATOR.createFromParcel(in); nfq = in.readInt(); tmpS = new double[nfq]; in.readDoubleArray(tmpS); nsc = in.readInt(); nFP = in.readInt(); nSCP = in.readInt(); tmpSC = new int[nsc]; in.readIntArray(tmpSC); } } }