Keun young Park | 80176b7 | 2011-12-20 16:31:19 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2011 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| 5 | * use this file except in compliance with the License. You may obtain a copy of |
| 6 | * 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, WITHOUT |
| 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 | * License for the specific language governing permissions and limitations under |
| 14 | * the License. |
| 15 | */ |
| 16 | |
| 17 | /* This test accepts a collection of N speech waveforms collected as |
| 18 | part of N recognition attempts. The waveforms are ordered by |
| 19 | increasing presentation level. The test determines the extent to |
| 20 | which the peak amplitudes in the waveforms track the change in |
| 21 | presentation level. Failure to track the presentation level within |
| 22 | some reasonable margin is an indication of clipping or of automatic |
| 23 | gain control in the signal path. |
| 24 | |
| 25 | RMS of each level is used as a parameter for deciding lienairy. |
| 26 | For each level, RMS is calculated, and a line fitting into RMS vs level |
| 27 | will be calculated. Then for each level, residual error of measurement |
| 28 | vs line fitting will be calculated, and the residual error is normalized |
| 29 | with each measurement. The test failes if residual error is bigger than |
| 30 | 2dB. |
| 31 | |
| 32 | This test will be robust to background noise as long as it is persistent. |
| 33 | But background noise which appears shortly with enough strength can |
| 34 | mess up the result. |
| 35 | */ |
| 36 | |
| 37 | #include <stdlib.h> |
| 38 | #include <stdio.h> |
| 39 | #include <math.h> |
| 40 | #include <cutils/log.h> |
| 41 | #include "LinearityTest.h" |
| 42 | |
| 43 | #define LOG_TAG "LinearityTestRms" |
| 44 | |
| 45 | // vectorDot, vectorNorm, and solveLeastSquares |
| 46 | // copied from frameworks/base/libs/ui/Input.cpp |
| 47 | |
| 48 | static inline float vectorDot(const float* a, const float* b, uint32_t m) { |
| 49 | float r = 0; |
| 50 | while (m--) { |
| 51 | r += *(a++) * *(b++); |
| 52 | } |
| 53 | return r; |
| 54 | } |
| 55 | |
| 56 | static inline float vectorNorm(const float* a, uint32_t m) { |
| 57 | float r = 0; |
| 58 | while (m--) { |
| 59 | float t = *(a++); |
| 60 | r += t * t; |
| 61 | } |
| 62 | return sqrtf(r); |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * Solves a linear least squares problem to obtain a N degree polynomial that fits |
| 67 | * the specified input data as nearly as possible. |
| 68 | * |
| 69 | * Returns true if a solution is found, false otherwise. |
| 70 | * |
| 71 | * The input consists of two vectors of data points X and Y with indices 0..m-1. |
| 72 | * The output is a vector B with indices 0..n-1 that describes a polynomial |
| 73 | * that fits the data, such the sum of abs(Y[i] - (B[0] + B[1] X[i] + B[2] X[i]^2 ... B[n] X[i]^n)) |
| 74 | * for all i between 0 and m-1 is minimized. |
| 75 | * |
| 76 | * That is to say, the function that generated the input data can be approximated |
| 77 | * by y(x) ~= B[0] + B[1] x + B[2] x^2 + ... + B[n] x^n. |
| 78 | * |
| 79 | * The coefficient of determination (R^2) is also returned to describe the goodness |
| 80 | * of fit of the model for the given data. It is a value between 0 and 1, where 1 |
| 81 | * indicates perfect correspondence. |
| 82 | * |
| 83 | * This function first expands the X vector to a m by n matrix A such that |
| 84 | * A[i][0] = 1, A[i][1] = X[i], A[i][2] = X[i]^2, ..., A[i][n] = X[i]^n. |
| 85 | * |
| 86 | * Then it calculates the QR decomposition of A yielding an m by m orthonormal matrix Q |
| 87 | * and an m by n upper triangular matrix R. Because R is upper triangular (lower |
| 88 | * part is all zeroes), we can simplify the decomposition into an m by n matrix |
| 89 | * Q1 and a n by n matrix R1 such that A = Q1 R1. |
| 90 | * |
| 91 | * Finally we solve the system of linear equations given by R1 B = (Qtranspose Y) |
| 92 | * to find B. |
| 93 | * |
| 94 | * For efficiency, we lay out A and Q column-wise in memory because we frequently |
| 95 | * operate on the column vectors. Conversely, we lay out R row-wise. |
| 96 | * |
| 97 | * http://en.wikipedia.org/wiki/Numerical_methods_for_linear_least_squares |
| 98 | * http://en.wikipedia.org/wiki/Gram-Schmidt |
| 99 | */ |
| 100 | static bool solveLeastSquares(const float* x, const float* y, uint32_t m, uint32_t n, |
| 101 | float* outB, float* outDet) { |
| 102 | #if DEBUG_LEAST_SQUARES |
| 103 | LOGD("solveLeastSquares: m=%d, n=%d, x=%s, y=%s", int(m), int(n), |
| 104 | vectorToString(x, m).string(), vectorToString(y, m).string()); |
| 105 | #endif |
| 106 | |
| 107 | // Expand the X vector to a matrix A. |
| 108 | float a[n][m]; // column-major order |
| 109 | for (uint32_t h = 0; h < m; h++) { |
| 110 | a[0][h] = 1; |
| 111 | for (uint32_t i = 1; i < n; i++) { |
| 112 | a[i][h] = a[i - 1][h] * x[h]; |
| 113 | } |
| 114 | } |
| 115 | #if DEBUG_LEAST_SQUARES |
| 116 | LOGD(" - a=%s", matrixToString(&a[0][0], m, n, false /*rowMajor*/).string()); |
| 117 | #endif |
| 118 | |
| 119 | // Apply the Gram-Schmidt process to A to obtain its QR decomposition. |
| 120 | float q[n][m]; // orthonormal basis, column-major order |
| 121 | float r[n][n]; // upper triangular matrix, row-major order |
| 122 | for (uint32_t j = 0; j < n; j++) { |
| 123 | for (uint32_t h = 0; h < m; h++) { |
| 124 | q[j][h] = a[j][h]; |
| 125 | } |
| 126 | for (uint32_t i = 0; i < j; i++) { |
| 127 | float dot = vectorDot(&q[j][0], &q[i][0], m); |
| 128 | for (uint32_t h = 0; h < m; h++) { |
| 129 | q[j][h] -= dot * q[i][h]; |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | float norm = vectorNorm(&q[j][0], m); |
| 134 | if (norm < 0.000001f) { |
| 135 | // vectors are linearly dependent or zero so no solution |
| 136 | #if DEBUG_LEAST_SQUARES |
| 137 | LOGD(" - no solution, norm=%f", norm); |
| 138 | #endif |
| 139 | return false; |
| 140 | } |
| 141 | |
| 142 | float invNorm = 1.0f / norm; |
| 143 | for (uint32_t h = 0; h < m; h++) { |
| 144 | q[j][h] *= invNorm; |
| 145 | } |
| 146 | for (uint32_t i = 0; i < n; i++) { |
| 147 | r[j][i] = i < j ? 0 : vectorDot(&q[j][0], &a[i][0], m); |
| 148 | } |
| 149 | } |
| 150 | #if DEBUG_LEAST_SQUARES |
| 151 | LOGD(" - q=%s", matrixToString(&q[0][0], m, n, false /*rowMajor*/).string()); |
| 152 | LOGD(" - r=%s", matrixToString(&r[0][0], n, n, true /*rowMajor*/).string()); |
| 153 | |
| 154 | // calculate QR, if we factored A correctly then QR should equal A |
| 155 | float qr[n][m]; |
| 156 | for (uint32_t h = 0; h < m; h++) { |
| 157 | for (uint32_t i = 0; i < n; i++) { |
| 158 | qr[i][h] = 0; |
| 159 | for (uint32_t j = 0; j < n; j++) { |
| 160 | qr[i][h] += q[j][h] * r[j][i]; |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | LOGD(" - qr=%s", matrixToString(&qr[0][0], m, n, false /*rowMajor*/).string()); |
| 165 | #endif |
| 166 | |
| 167 | // Solve R B = Qt Y to find B. This is easy because R is upper triangular. |
| 168 | // We just work from bottom-right to top-left calculating B's coefficients. |
| 169 | for (uint32_t i = n; i-- != 0; ) { |
| 170 | outB[i] = vectorDot(&q[i][0], y, m); |
| 171 | for (uint32_t j = n - 1; j > i; j--) { |
| 172 | outB[i] -= r[i][j] * outB[j]; |
| 173 | } |
| 174 | outB[i] /= r[i][i]; |
| 175 | } |
| 176 | #if DEBUG_LEAST_SQUARES |
| 177 | LOGD(" - b=%s", vectorToString(outB, n).string()); |
| 178 | #endif |
| 179 | |
| 180 | // Calculate the coefficient of determination as 1 - (SSerr / SStot) where |
| 181 | // SSerr is the residual sum of squares (squared variance of the error), |
| 182 | // and SStot is the total sum of squares (squared variance of the data). |
| 183 | float ymean = 0; |
| 184 | for (uint32_t h = 0; h < m; h++) { |
| 185 | ymean += y[h]; |
| 186 | } |
| 187 | ymean /= m; |
| 188 | |
| 189 | float sserr = 0; |
| 190 | float sstot = 0; |
| 191 | for (uint32_t h = 0; h < m; h++) { |
| 192 | float err = y[h] - outB[0]; |
| 193 | float term = 1; |
| 194 | for (uint32_t i = 1; i < n; i++) { |
| 195 | term *= x[h]; |
| 196 | err -= term * outB[i]; |
| 197 | } |
| 198 | sserr += err * err; |
| 199 | float var = y[h] - ymean; |
| 200 | sstot += var * var; |
| 201 | } |
| 202 | *outDet = sstot > 0.000001f ? 1.0f - (sserr / sstot) : 1; |
| 203 | #if DEBUG_LEAST_SQUARES |
| 204 | LOGD(" - sserr=%f", sserr); |
| 205 | LOGD(" - sstot=%f", sstot); |
| 206 | LOGD(" - det=%f", *outDet); |
| 207 | #endif |
| 208 | return true; |
| 209 | } |
| 210 | |
| 211 | /* calculate RMS of given sample with numSamples of length */ |
| 212 | float calcRms(short* pcm, int numSamples) |
| 213 | { |
| 214 | float energy = 0.0f; |
| 215 | for(int i = 0; i < numSamples; i++) { |
| 216 | float sample = (float)pcm[i]; |
| 217 | energy += (sample * sample); |
| 218 | } |
| 219 | return sqrtf(energy); |
| 220 | } |
| 221 | |
| 222 | /* There are numSignals int16 signals in pcms. sampleCounts is an |
| 223 | integer array of length numSignals containing their respective |
| 224 | lengths in samples. They are all sampled at sampleRate. The pcms |
| 225 | are ordered by increasing stimulus level. The level steps between |
| 226 | successive stimuli were of size dbStepSize dB. |
| 227 | The maximum deviation in linearity found |
| 228 | (in dB) is returned in maxDeviation. The function returns 1 if |
| 229 | the measurements could be made, or a negative number that |
| 230 | indicates the error, as defined in LinearityTest.h */ |
| 231 | int linearityTestRms(short** pcms, int* sampleCounts, int numSignals, |
| 232 | float sampleRate, float dbStepSize, |
| 233 | float* maxDeviation) { |
| 234 | if (!(pcms && sampleCounts)) { |
| 235 | return ERROR_INPUT_SIGNAL_MISSING; |
| 236 | } |
| 237 | if (numSignals < 2) { |
| 238 | return ERROR_INPUT_SIGNAL_NUMBERS; |
| 239 | } |
| 240 | if (sampleRate <= 4000.0) { |
| 241 | return ERROR_SAMPLE_RATE_TOO_LOW; |
| 242 | } |
| 243 | if (dbStepSize <= 0.0) { |
| 244 | return ERROR_NEGATIVE_STEP_SIZE; |
| 245 | } |
| 246 | |
| 247 | float* levels = new float[numSignals]; |
| 248 | levels[0] = 1.0f; |
| 249 | float stepInMag = powf(10.0f, dbStepSize/20.0f); |
| 250 | for(int i = 1; i < numSignals; i++) { |
| 251 | levels[i] = levels[i - 1] * stepInMag; |
| 252 | } |
| 253 | |
| 254 | float* rmsValues = new float[numSignals]; |
| 255 | for (int i = 0; i < numSignals; i++) { |
| 256 | rmsValues[i] = calcRms(pcms[i], sampleCounts[i]); |
| 257 | } |
| 258 | const int NO_COEFFS = 2; // only line fitting |
| 259 | float coeffs[NO_COEFFS]; |
| 260 | float outDet; |
| 261 | if(!solveLeastSquares(levels, rmsValues, numSignals, NO_COEFFS, |
| 262 | coeffs, &outDet)) { |
Steve Block | 11cd606 | 2012-01-04 20:04:35 +0000 | [diff] [blame^] | 263 | ALOGI(" solveLeastSquares fails with det %f", outDet); |
Keun young Park | 80176b7 | 2011-12-20 16:31:19 -0800 | [diff] [blame] | 264 | return ERROR_LINEAR_FITTING; |
| 265 | } |
Steve Block | 11cd606 | 2012-01-04 20:04:35 +0000 | [diff] [blame^] | 266 | ALOGI(" coeffs offset %f linear %f", coeffs[0], coeffs[1]); |
Keun young Park | 80176b7 | 2011-12-20 16:31:19 -0800 | [diff] [blame] | 267 | float maxDev = 0.0f; |
| 268 | for(int i = 0; i < numSignals; i++) { |
| 269 | float residue = coeffs[0] + coeffs[1] * levels[i] - rmsValues[i]; |
| 270 | // to make devInDb positive, add measured value itself |
| 271 | // then normalize |
| 272 | float devInDb = 20.0f * log10f((fabs(residue) + rmsValues[i]) |
| 273 | / rmsValues[i]); |
Steve Block | 11cd606 | 2012-01-04 20:04:35 +0000 | [diff] [blame^] | 274 | ALOGI(" %d-th residue %f dB", i, devInDb); |
Keun young Park | 80176b7 | 2011-12-20 16:31:19 -0800 | [diff] [blame] | 275 | if (devInDb > maxDev) { |
| 276 | maxDev = devInDb; |
| 277 | } |
| 278 | } |
| 279 | *maxDeviation = maxDev; |
| 280 | |
| 281 | delete[] levels; |
| 282 | delete[] rmsValues; |
| 283 | |
| 284 | return 1; |
| 285 | } |