package com.evsuite.abrp; import java.util.Locale; /** * What {@link SoundProbe} makes of Android's audio analyser: how loud a captured waveform is, how * strong a band of its spectrum, and whether a run of those looks like music the analyser can * hear. A failed read is null, never a silent 0. Android-free and unit-tested. */ final class SoundLevels { /** Below this the peak counts as silence (the 8-bit waveform's noise is about 1/128). */ static final float SILENT = 0.02f; private SoundLevels() {} /** The waveform's peak from 0 to 1. Its samples are unsigned 8-bit, with silence at 128. */ static Float peak(byte[] wave) { if (wave == null || wave.length == 0) return null; int max = 0; for (byte b : wave) max = Math.max(max, Math.abs((b & 0xFF) - 128)); return Math.min(1f, max / 128f); } /** * The mean strength from 0 to 1 of the bins between {@code fromHz} and {@code toHz}. The * analyser's FFT holds the DC and Nyquist values first, then each bin's real and imaginary * part, signed; a bin {@code k} is {@code k × rate / length} Hz. */ static Float band(byte[] fft, int rateHz, float fromHz, float toHz) { if (fft == null || fft.length < 4 || rateHz <= 0) return null; float sum = 0f; int bins = 0; for (int k = 1; k < fft.length / 2; k++) { float hz = k * (float) rateHz / fft.length; if (hz < fromHz || hz >= toHz) continue; sum += (float) Math.hypot(fft[2 * k], fft[2 * k + 1]) / 128f; bins++; } return bins == 0 ? null : Math.min(1f, sum / bins); } enum Verdict { /** The analyser gave no readings at all. */ NO_READING, /** Nothing was playing and the analyser heard nothing: says nothing about the source. */ NOTHING_PLAYING, /** Something played and the analyser heard silence: it cannot hear this source. */ DEAF, /** The level stays where it is: not music, or not real data. */ STEADY, /** The level moves: the pumpkin could follow it. */ HEARS } /** Readings gathered over a time. */ static final class Summary { private int samples, readings, bandReadings; private float peakMin = Float.MAX_VALUE, peakMax, bass, mid, treble; private Integer rms; /** One round of reads, each null when it failed. */ void add(Float peak, Float bassBand, Float midBand, Float trebleBand, Integer rmsMilliBel) { samples++; if (peak != null) { readings++; peakMin = Math.min(peakMin, peak); peakMax = Math.max(peakMax, peak); } if (bassBand != null && midBand != null && trebleBand != null) { bandReadings++; bass += bassBand; mid += midBand; treble += trebleBand; } if (rmsMilliBel != null) rms = rmsMilliBel; } Verdict verdict(boolean somethingPlays) { if (readings == 0) return Verdict.NO_READING; if (peakMax < SILENT) return somethingPlays ? Verdict.DEAF : Verdict.NOTHING_PLAYING; return peakMax - peakMin < SILENT ? Verdict.STEADY : Verdict.HEARS; } /** One line of the report; what could not be read is "—". */ String line() { return "peak " + (readings == 0 ? "—" : String.format(Locale.US, "%.2f–%.2f", peakMin, peakMax)) + ", bass " + mean(bass) + ", mid " + mean(mid) + ", treble " + mean(treble) + ", rms " + (rms == null ? "—" : rms + " mB") + " (" + readings + "/" + samples + ")"; } private String mean(float sum) { return bandReadings == 0 ? "—" : String.format(Locale.US, "%.3f", sum / bandReadings); } } /** The report's last line. */ static String words(Verdict verdict) { switch (verdict) { case HEARS: return "Result: the analyser hears it, and the level moves."; case STEADY: return "Result: the level does not move, so this is not usable music."; case DEAF: return "Result: something plays, but the analyser hears silence."; case NOTHING_PLAYING: return "Result: nothing was playing, so this says nothing. Play something and check again."; default: return "Result: the analyser gave no readings."; } } }