| /* |
| * Copyright 2014 The Android Open Source Project |
| * |
| * 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. |
| */ |
| package android.cts.util; |
| |
| import android.content.Context; |
| import android.content.res.AssetFileDescriptor; |
| import android.media.MediaCodec; |
| import android.media.MediaCodecInfo; |
| import android.media.MediaCodecInfo.CodecCapabilities; |
| import android.media.MediaCodecInfo.VideoCapabilities; |
| import android.media.MediaCodecList; |
| import android.media.MediaExtractor; |
| import android.media.MediaFormat; |
| import android.net.Uri; |
| import android.util.Log; |
| import android.util.Range; |
| |
| import com.android.compatibility.common.util.DeviceReportLog; |
| import com.android.compatibility.common.util.ResultType; |
| import com.android.compatibility.common.util.ResultUnit; |
| |
| import java.lang.reflect.Method; |
| import static java.lang.reflect.Modifier.isPublic; |
| import static java.lang.reflect.Modifier.isStatic; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Map; |
| |
| import static junit.framework.Assert.assertTrue; |
| |
| import java.io.IOException; |
| |
| public class MediaUtils { |
| private static final String TAG = "MediaUtils"; |
| |
| /* |
| * ----------------------- HELPER METHODS FOR SKIPPING TESTS ----------------------- |
| */ |
| private static final int ALL_AV_TRACKS = -1; |
| |
| private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS); |
| |
| /** |
| * Returns the test name (heuristically). |
| * |
| * Since it uses heuristics, this method has only been verified for media |
| * tests. This centralizes the way to signal errors during a test. |
| */ |
| public static String getTestName() { |
| return getTestName(false /* withClass */); |
| } |
| |
| /** |
| * Returns the test name with the full class (heuristically). |
| * |
| * Since it uses heuristics, this method has only been verified for media |
| * tests. This centralizes the way to signal errors during a test. |
| */ |
| public static String getTestNameWithClass() { |
| return getTestName(true /* withClass */); |
| } |
| |
| private static String getTestName(boolean withClass) { |
| int bestScore = -1; |
| String testName = "test???"; |
| Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces(); |
| for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) { |
| StackTraceElement[] stack = entry.getValue(); |
| for (int index = 0; index < stack.length; ++index) { |
| // method name must start with "test" |
| String methodName = stack[index].getMethodName(); |
| if (!methodName.startsWith("test")) { |
| continue; |
| } |
| |
| int score = 0; |
| // see if there is a public non-static void method that takes no argument |
| Class<?> clazz; |
| try { |
| clazz = Class.forName(stack[index].getClassName()); |
| ++score; |
| for (final Method method : clazz.getDeclaredMethods()) { |
| if (method.getName().equals(methodName) |
| && isPublic(method.getModifiers()) |
| && !isStatic(method.getModifiers()) |
| && method.getParameterTypes().length == 0 |
| && method.getReturnType().equals(Void.TYPE)) { |
| ++score; |
| break; |
| } |
| } |
| if (score == 1) { |
| // if we could read the class, but method is not public void, it is |
| // not a candidate |
| continue; |
| } |
| } catch (ClassNotFoundException e) { |
| } |
| |
| // even if we cannot verify the method signature, there are signals in the stack |
| |
| // usually test method is invoked by reflection |
| int depth = 1; |
| while (index + depth < stack.length |
| && stack[index + depth].getMethodName().equals("invoke") |
| && stack[index + depth].getClassName().equals( |
| "java.lang.reflect.Method")) { |
| ++depth; |
| } |
| if (depth > 1) { |
| ++score; |
| // and usually test method is run by runMethod method in android.test package |
| if (index + depth < stack.length) { |
| if (stack[index + depth].getClassName().startsWith("android.test.")) { |
| ++score; |
| } |
| if (stack[index + depth].getMethodName().equals("runMethod")) { |
| ++score; |
| } |
| } |
| } |
| |
| if (score > bestScore) { |
| bestScore = score; |
| testName = methodName; |
| if (withClass) { |
| testName = stack[index].getClassName() + "." + testName; |
| } |
| } |
| } |
| } |
| return testName; |
| } |
| |
| /** |
| * Finds test name (heuristically) and prints out standard skip message. |
| * |
| * Since it uses heuristics, this method has only been verified for media |
| * tests. This centralizes the way to signal a skipped test. |
| */ |
| public static void skipTest(String tag, String reason) { |
| Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason); |
| DeviceReportLog log = new DeviceReportLog("CtsMediaSkippedTests", "test_skipped"); |
| try { |
| log.addValue("reason", reason, ResultType.NEUTRAL, ResultUnit.NONE); |
| log.addValue( |
| "test", getTestNameWithClass(), ResultType.NEUTRAL, ResultUnit.NONE); |
| log.submit(); |
| } catch (NullPointerException e) { } |
| } |
| |
| /** |
| * Finds test name (heuristically) and prints out standard skip message. |
| * |
| * Since it uses heuristics, this method has only been verified for media |
| * tests. This centralizes the way to signal a skipped test. |
| */ |
| public static void skipTest(String reason) { |
| skipTest(TAG, reason); |
| } |
| |
| public static boolean check(boolean result, String message) { |
| if (!result) { |
| skipTest(message); |
| } |
| return result; |
| } |
| |
| /* |
| * ------------------- HELPER METHODS FOR CHECKING CODEC SUPPORT ------------------- |
| */ |
| |
| // returns the list of codecs that support any one of the formats |
| private static String[] getCodecNames( |
| boolean isEncoder, Boolean isGoog, MediaFormat... formats) { |
| MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); |
| ArrayList<String> result = new ArrayList<>(); |
| for (MediaCodecInfo info : mcl.getCodecInfos()) { |
| if (info.isEncoder() != isEncoder) { |
| continue; |
| } |
| if (isGoog != null |
| && info.getName().toLowerCase().startsWith("omx.google.") != isGoog) { |
| continue; |
| } |
| |
| for (MediaFormat format : formats) { |
| String mime = format.getString(MediaFormat.KEY_MIME); |
| |
| CodecCapabilities caps = null; |
| try { |
| caps = info.getCapabilitiesForType(mime); |
| } catch (IllegalArgumentException e) { // mime is not supported |
| continue; |
| } |
| if (caps.isFormatSupported(format)) { |
| result.add(info.getName()); |
| break; |
| } |
| } |
| } |
| return result.toArray(new String[result.size()]); |
| } |
| |
| /* Use isGoog = null to query all decoders */ |
| public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { |
| return getCodecNames(false /* isEncoder */, isGoog, formats); |
| } |
| |
| public static String[] getDecoderNames(MediaFormat... formats) { |
| return getCodecNames(false /* isEncoder */, null /* isGoog */, formats); |
| } |
| |
| /* Use isGoog = null to query all decoders */ |
| public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { |
| return getCodecNames(true /* isEncoder */, isGoog, formats); |
| } |
| |
| public static String[] getEncoderNames(MediaFormat... formats) { |
| return getCodecNames(true /* isEncoder */, null /* isGoog */, formats); |
| } |
| |
| public static void verifyNumCodecs( |
| int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats) { |
| String desc = (isEncoder ? "encoders" : "decoders") + " for " |
| + (formats.length == 1 ? formats[0].toString() : Arrays.toString(formats)); |
| if (isGoog != null) { |
| desc = (isGoog ? "Google " : "non-Google ") + desc; |
| } |
| |
| String[] codecs = getCodecNames(isEncoder, isGoog, formats); |
| assertTrue("test can only verify " + count + " " + desc + "; found " + codecs.length + ": " |
| + Arrays.toString(codecs), codecs.length <= count); |
| } |
| |
| public static MediaCodec getDecoder(MediaFormat format) { |
| String decoder = sMCL.findDecoderForFormat(format); |
| if (decoder != null) { |
| try { |
| return MediaCodec.createByCodecName(decoder); |
| } catch (IOException e) { |
| } |
| } |
| return null; |
| } |
| |
| public static boolean canEncode(MediaFormat format) { |
| if (sMCL.findEncoderForFormat(format) == null) { |
| Log.i(TAG, "no encoder for " + format); |
| return false; |
| } |
| return true; |
| } |
| |
| public static boolean canDecode(MediaFormat format) { |
| if (sMCL.findDecoderForFormat(format) == null) { |
| Log.i(TAG, "no decoder for " + format); |
| return false; |
| } |
| return true; |
| } |
| |
| public static boolean supports(String codecName, String mime, int w, int h) { |
| // While this could be simply written as such, give more graceful feedback. |
| // MediaFormat format = MediaFormat.createVideoFormat(mime, w, h); |
| // return supports(codecName, format); |
| |
| VideoCapabilities vidCap = getVideoCapabilities(codecName, mime); |
| if (vidCap == null) { |
| return false; |
| } else if (vidCap.isSizeSupported(w, h)) { |
| return true; |
| } |
| |
| Log.w(TAG, "unsupported size " + w + "x" + h); |
| return false; |
| } |
| |
| public static boolean supports(String codecName, MediaFormat format) { |
| MediaCodec codec; |
| try { |
| codec = MediaCodec.createByCodecName(codecName); |
| } catch (IOException e) { |
| Log.w(TAG, "codec not found: " + codecName); |
| return false; |
| } |
| |
| String mime = format.getString(MediaFormat.KEY_MIME); |
| CodecCapabilities cap = null; |
| try { |
| cap = codec.getCodecInfo().getCapabilitiesForType(mime); |
| return cap.isFormatSupported(format); |
| } catch (IllegalArgumentException e) { |
| Log.w(TAG, "not supported mime: " + mime); |
| return false; |
| } finally { |
| codec.release(); |
| } |
| } |
| |
| public static boolean hasCodecForTrack(MediaExtractor ex, int track) { |
| int count = ex.getTrackCount(); |
| if (track < 0 || track >= count) { |
| throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]"); |
| } |
| return canDecode(ex.getTrackFormat(track)); |
| } |
| |
| /** |
| * return true iff all audio and video tracks are supported |
| */ |
| public static boolean hasCodecsForMedia(MediaExtractor ex) { |
| for (int i = 0; i < ex.getTrackCount(); ++i) { |
| MediaFormat format = ex.getTrackFormat(i); |
| // only check for audio and video codecs |
| String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase(); |
| if (!mime.startsWith("audio/") && !mime.startsWith("video/")) { |
| continue; |
| } |
| if (!canDecode(format)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * return true iff any track starting with mimePrefix is supported |
| */ |
| public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) { |
| mimePrefix = mimePrefix.toLowerCase(); |
| for (int i = 0; i < ex.getTrackCount(); ++i) { |
| MediaFormat format = ex.getTrackFormat(i); |
| String mime = format.getString(MediaFormat.KEY_MIME); |
| if (mime.toLowerCase().startsWith(mimePrefix)) { |
| if (canDecode(format)) { |
| return true; |
| } |
| Log.i(TAG, "no decoder for " + format); |
| } |
| } |
| return false; |
| } |
| |
| private static boolean hasCodecsForResourceCombo( |
| Context context, int resourceId, int track, String mimePrefix) { |
| try { |
| AssetFileDescriptor afd = null; |
| MediaExtractor ex = null; |
| try { |
| afd = context.getResources().openRawResourceFd(resourceId); |
| ex = new MediaExtractor(); |
| ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); |
| if (mimePrefix != null) { |
| return hasCodecForMediaAndDomain(ex, mimePrefix); |
| } else if (track == ALL_AV_TRACKS) { |
| return hasCodecsForMedia(ex); |
| } else { |
| return hasCodecForTrack(ex, track); |
| } |
| } finally { |
| if (ex != null) { |
| ex.release(); |
| } |
| if (afd != null) { |
| afd.close(); |
| } |
| } |
| } catch (IOException e) { |
| Log.i(TAG, "could not open resource"); |
| } |
| return false; |
| } |
| |
| /** |
| * return true iff all audio and video tracks are supported |
| */ |
| public static boolean hasCodecsForResource(Context context, int resourceId) { |
| return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */); |
| } |
| |
| public static boolean checkCodecsForResource(Context context, int resourceId) { |
| return check(hasCodecsForResource(context, resourceId), "no decoder found"); |
| } |
| |
| /** |
| * return true iff track is supported. |
| */ |
| public static boolean hasCodecForResource(Context context, int resourceId, int track) { |
| return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */); |
| } |
| |
| public static boolean checkCodecForResource(Context context, int resourceId, int track) { |
| return check(hasCodecForResource(context, resourceId, track), "no decoder found"); |
| } |
| |
| /** |
| * return true iff any track starting with mimePrefix is supported |
| */ |
| public static boolean hasCodecForResourceAndDomain( |
| Context context, int resourceId, String mimePrefix) { |
| return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix); |
| } |
| |
| /** |
| * return true iff all audio and video tracks are supported |
| */ |
| public static boolean hasCodecsForPath(Context context, String path) { |
| MediaExtractor ex = null; |
| try { |
| ex = new MediaExtractor(); |
| Uri uri = Uri.parse(path); |
| String scheme = uri.getScheme(); |
| if (scheme == null) { // file |
| ex.setDataSource(path); |
| } else if (scheme.equalsIgnoreCase("file")) { |
| ex.setDataSource(uri.getPath()); |
| } else { |
| ex.setDataSource(context, uri, null); |
| } |
| return hasCodecsForMedia(ex); |
| } catch (IOException e) { |
| Log.i(TAG, "could not open path " + path); |
| } finally { |
| if (ex != null) { |
| ex.release(); |
| } |
| } |
| return true; |
| } |
| |
| public static boolean checkCodecsForPath(Context context, String path) { |
| return check(hasCodecsForPath(context, path), "no decoder found"); |
| } |
| |
| public static boolean hasCodecForDomain(boolean encoder, String domain) { |
| for (MediaCodecInfo info : sMCL.getCodecInfos()) { |
| if (encoder != info.isEncoder()) { |
| continue; |
| } |
| |
| for (String type : info.getSupportedTypes()) { |
| if (type.toLowerCase().startsWith(domain.toLowerCase() + "/")) { |
| Log.i(TAG, "found codec " + info.getName() + " for mime " + type); |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| public static boolean checkCodecForDomain(boolean encoder, String domain) { |
| return check(hasCodecForDomain(encoder, domain), |
| "no " + domain + (encoder ? " encoder" : " decoder") + " found"); |
| } |
| |
| private static boolean hasCodecForMime(boolean encoder, String mime) { |
| for (MediaCodecInfo info : sMCL.getCodecInfos()) { |
| if (encoder != info.isEncoder()) { |
| continue; |
| } |
| |
| for (String type : info.getSupportedTypes()) { |
| if (type.equalsIgnoreCase(mime)) { |
| Log.i(TAG, "found codec " + info.getName() + " for mime " + mime); |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private static boolean hasCodecForMimes(boolean encoder, String[] mimes) { |
| for (String mime : mimes) { |
| if (!hasCodecForMime(encoder, mime)) { |
| Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| |
| public static boolean hasEncoder(String... mimes) { |
| return hasCodecForMimes(true /* encoder */, mimes); |
| } |
| |
| public static boolean hasDecoder(String... mimes) { |
| return hasCodecForMimes(false /* encoder */, mimes); |
| } |
| |
| public static boolean checkDecoder(String... mimes) { |
| return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found"); |
| } |
| |
| public static boolean checkEncoder(String... mimes) { |
| return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found"); |
| } |
| |
| public static boolean canDecodeVideo(String mime, int width, int height, float rate) { |
| MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); |
| format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); |
| return canDecode(format); |
| } |
| |
| public static boolean canDecodeVideo( |
| String mime, int width, int height, float rate, |
| Integer profile, Integer level, Integer bitrate) { |
| MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); |
| format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); |
| if (profile != null) { |
| format.setInteger(MediaFormat.KEY_PROFILE, profile); |
| if (level != null) { |
| format.setInteger(MediaFormat.KEY_LEVEL, level); |
| } |
| } |
| if (bitrate != null) { |
| format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); |
| } |
| return canDecode(format); |
| } |
| |
| public static boolean checkEncoderForFormat(MediaFormat format) { |
| return check(canEncode(format), "no encoder for " + format); |
| } |
| |
| public static boolean checkDecoderForFormat(MediaFormat format) { |
| return check(canDecode(format), "no decoder for " + format); |
| } |
| |
| /* |
| * ----------------------- HELPER METHODS FOR MEDIA HANDLING ----------------------- |
| */ |
| |
| public static VideoCapabilities getVideoCapabilities(String codecName, String mime) { |
| for (MediaCodecInfo info : sMCL.getCodecInfos()) { |
| if (!info.getName().equalsIgnoreCase(codecName)) { |
| continue; |
| } |
| CodecCapabilities caps; |
| try { |
| caps = info.getCapabilitiesForType(mime); |
| } catch (IllegalArgumentException e) { |
| // mime is not supported |
| Log.w(TAG, "not supported mime: " + mime); |
| return null; |
| } |
| VideoCapabilities vidCaps = caps.getVideoCapabilities(); |
| if (vidCaps == null) { |
| Log.w(TAG, "not a video codec: " + codecName); |
| } |
| return vidCaps; |
| } |
| Log.w(TAG, "codec not found: " + codecName); |
| return null; |
| } |
| |
| public static MediaFormat getTrackFormatForResource( |
| Context context, int resourceId, String mimeTypePrefix) |
| throws IOException { |
| MediaFormat format = null; |
| MediaExtractor extractor = new MediaExtractor(); |
| AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId); |
| try { |
| extractor.setDataSource( |
| afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); |
| } finally { |
| afd.close(); |
| } |
| int trackIndex; |
| for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) { |
| MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex); |
| if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) { |
| format = trackMediaFormat; |
| break; |
| } |
| } |
| extractor.release(); |
| afd.close(); |
| if (format == null) { |
| throw new RuntimeException("couldn't get a track for " + mimeTypePrefix); |
| } |
| |
| return format; |
| } |
| |
| public static MediaExtractor createMediaExtractorForMimeType( |
| Context context, int resourceId, String mimeTypePrefix) |
| throws IOException { |
| MediaExtractor extractor = new MediaExtractor(); |
| AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId); |
| try { |
| extractor.setDataSource( |
| afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); |
| } finally { |
| afd.close(); |
| } |
| int trackIndex; |
| for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) { |
| MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex); |
| if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) { |
| extractor.selectTrack(trackIndex); |
| break; |
| } |
| } |
| if (trackIndex == extractor.getTrackCount()) { |
| extractor.release(); |
| throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix); |
| } |
| |
| return extractor; |
| } |
| |
| /* |
| * ---------------------- HELPER METHODS FOR CODEC CONFIGURATION |
| */ |
| |
| /** Format must contain mime, width and height. |
| * Throws Exception if encoder does not support this width and height */ |
| public static void setMaxEncoderFrameAndBitrates( |
| MediaCodec encoder, MediaFormat format, int maxFps) { |
| String mime = format.getString(MediaFormat.KEY_MIME); |
| |
| VideoCapabilities vidCaps = |
| encoder.getCodecInfo().getCapabilitiesForType(mime).getVideoCapabilities(); |
| setMaxEncoderFrameAndBitrates(vidCaps, format, maxFps); |
| } |
| |
| public static void setMaxEncoderFrameAndBitrates( |
| VideoCapabilities vidCaps, MediaFormat format, int maxFps) { |
| int width = format.getInteger(MediaFormat.KEY_WIDTH); |
| int height = format.getInteger(MediaFormat.KEY_HEIGHT); |
| |
| int maxWidth = vidCaps.getSupportedWidths().getUpper(); |
| int maxHeight = vidCaps.getSupportedHeightsFor(maxWidth).getUpper(); |
| int frameRate = Math.min( |
| maxFps, vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue()); |
| format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); |
| |
| int bitrate = vidCaps.getBitrateRange().clamp( |
| (int)(vidCaps.getBitrateRange().getUpper() / |
| Math.sqrt((double)maxWidth * maxHeight / width / height))); |
| format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); |
| } |
| |
| /* |
| * ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------ |
| */ |
| |
| // TODO: migrate this into com.android.compatibility.common.util.Stat |
| public static class Stats { |
| /** does not support NaN or Inf in |data| */ |
| public Stats(double[] data) { |
| mData = data; |
| if (mData != null) { |
| mNum = mData.length; |
| } |
| } |
| |
| public int getNum() { |
| return mNum; |
| } |
| |
| /** calculate mSumX and mSumXX */ |
| private void analyze() { |
| if (mAnalyzed) { |
| return; |
| } |
| |
| if (mData != null) { |
| for (double x : mData) { |
| if (!(x >= mMinX)) { // mMinX may be NaN |
| mMinX = x; |
| } |
| if (!(x <= mMaxX)) { // mMaxX may be NaN |
| mMaxX = x; |
| } |
| mSumX += x; |
| mSumXX += x * x; |
| } |
| } |
| mAnalyzed = true; |
| } |
| |
| /** returns the maximum or NaN if it does not exist */ |
| public double getMin() { |
| analyze(); |
| return mMinX; |
| } |
| |
| /** returns the minimum or NaN if it does not exist */ |
| public double getMax() { |
| analyze(); |
| return mMaxX; |
| } |
| |
| /** returns the average or NaN if it does not exist. */ |
| public double getAverage() { |
| analyze(); |
| if (mNum == 0) { |
| return Double.NaN; |
| } else { |
| return mSumX / mNum; |
| } |
| } |
| |
| /** returns the standard deviation or NaN if it does not exist. */ |
| public double getStdev() { |
| analyze(); |
| if (mNum == 0) { |
| return Double.NaN; |
| } else { |
| double average = mSumX / mNum; |
| return Math.sqrt(mSumXX / mNum - average * average); |
| } |
| } |
| |
| /** returns the statistics for the moving average over n values */ |
| public Stats movingAverage(int n) { |
| if (n < 1 || mNum < n) { |
| return new Stats(null); |
| } else if (n == 1) { |
| return this; |
| } |
| |
| double[] avgs = new double[mNum - n + 1]; |
| double sum = 0; |
| for (int i = 0; i < mNum; ++i) { |
| sum += mData[i]; |
| if (i >= n - 1) { |
| avgs[i - n + 1] = sum / n; |
| sum -= mData[i - n + 1]; |
| } |
| } |
| return new Stats(avgs); |
| } |
| |
| /** returns the statistics for the moving average over a window over the |
| * cumulative sum. Basically, moves a window from: [0, window] to |
| * [sum - window, sum] over the cumulative sum, over ((sum - window) / average) |
| * steps, and returns the average value over each window. |
| * This method is used to average time-diff data over a window of a constant time. |
| */ |
| public Stats movingAverageOverSum(double window) { |
| if (window <= 0 || mNum < 1) { |
| return new Stats(null); |
| } |
| |
| analyze(); |
| double average = mSumX / mNum; |
| if (window >= mSumX) { |
| return new Stats(new double[] { average }); |
| } |
| int samples = (int)Math.ceil((mSumX - window) / average); |
| double[] avgs = new double[samples]; |
| |
| // A somewhat brute force approach to calculating the moving average. |
| // TODO: add support for weights in Stats, so we can do a more refined approach. |
| double sum = 0; // sum of elements in the window |
| int num = 0; // number of elements in the moving window |
| int bi = 0; // index of the first element in the moving window |
| int ei = 0; // index of the last element in the moving window |
| double space = window; // space at the end of the window |
| double foot = 0; // space at the beginning of the window |
| |
| // invariants: foot + sum + space == window |
| // bi + num == ei |
| // |
| // window: |-------------------------------| |
| // | <-----sum------> | |
| // <foot> <---space--> |
| // | | |
| // intervals: |-----------|-------|-------|--------------------|--------| |
| // ^bi ^ei |
| |
| int ix = 0; // index in the result |
| while (ix < samples) { |
| // add intervals while there is space in the window |
| while (ei < mData.length && mData[ei] <= space) { |
| space -= mData[ei]; |
| sum += mData[ei]; |
| num++; |
| ei++; |
| } |
| |
| // calculate average over window and deal with odds and ends (e.g. if there are no |
| // intervals in the current window: pick whichever element overlaps the window |
| // most. |
| if (num > 0) { |
| avgs[ix++] = sum / num; |
| } else if (bi > 0 && foot > space) { |
| // consider previous |
| avgs[ix++] = mData[bi - 1]; |
| } else if (ei == mData.length) { |
| break; |
| } else { |
| avgs[ix++] = mData[ei]; |
| } |
| |
| // move the window to the next position |
| foot -= average; |
| space += average; |
| |
| // remove intervals that are now partially or wholly outside of the window |
| while (bi < ei && foot < 0) { |
| foot += mData[bi]; |
| sum -= mData[bi]; |
| num--; |
| bi++; |
| } |
| } |
| return new Stats(Arrays.copyOf(avgs, ix)); |
| } |
| |
| /** calculate mSortedData */ |
| private void sort() { |
| if (mSorted || mNum == 0) { |
| return; |
| } |
| mSortedData = Arrays.copyOf(mData, mNum); |
| Arrays.sort(mSortedData); |
| mSorted = true; |
| } |
| |
| /** returns an array of percentiles for the points using nearest rank */ |
| public double[] getPercentiles(double... points) { |
| sort(); |
| double[] res = new double[points.length]; |
| for (int i = 0; i < points.length; ++i) { |
| if (mNum < 1 || points[i] < 0 || points[i] > 100) { |
| res[i] = Double.NaN; |
| } else { |
| res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))]; |
| } |
| } |
| return res; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (o instanceof Stats) { |
| Stats other = (Stats)o; |
| if (other.mNum != mNum) { |
| return false; |
| } else if (mNum == 0) { |
| return true; |
| } |
| return Arrays.equals(mData, other.mData); |
| } |
| return false; |
| } |
| |
| private double[] mData; |
| private double mSumX = 0; |
| private double mSumXX = 0; |
| private double mMinX = Double.NaN; |
| private double mMaxX = Double.NaN; |
| private int mNum = 0; |
| private boolean mAnalyzed = false; |
| private double[] mSortedData; |
| private boolean mSorted = false; |
| } |
| |
| /* |
| * -------------------------------------- END -------------------------------------- |
| */ |
| } |